Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 54 additions & 30 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ import (
"github.com/libdns/libdns"
)

// nameClient extends the namedotcom api and request handler to the provider..
// nameClient extends the namedotcom api and request handler to the provider.
type nameClient struct {
client *nameDotCom
mutex sync.Mutex
}

// getClient initiates a new nameClient and assigns it to the provider..
func (p *Provider) getClient(ctx context.Context) error {
newNameclient, err := NewNameDotComClient(ctx, p.Token, p.User, p.Server)
newNameClient, err := NewNameDotComClient(ctx, p.Token, p.User, p.Server)
if err != nil {
return err
}
p.client = newNameclient
p.client = newNameClient
return nil
}

Expand Down Expand Up @@ -71,12 +71,32 @@ func (p *Provider) listZones(ctx context.Context) ([]libdns.Zone, error) {
return zones, nil
}

// listAllRecords returns all records for the given zone .. GET /v4/domains/{ domainName }/records
func (p *Provider) listAllRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
func (p *Provider) getRecordId(ctx context.Context, zone string, record libdns.Record) (int32, error) {
records, err := p.listAllRecords(ctx, zone)
if err != nil {
return 0, err
}

name := libdns.AbsoluteName(record.RR().Name, zone)
for _, rec := range records {
if rec.Type == record.RR().Type && rec.Fqdn == name && rec.Answer == record.RR().Data {
return rec.ID, nil
}
}

return 0, fmt.Errorf("could not find record with name %s", record.RR().Name)
}

func (p *Provider) listAllRecordsLocked(ctx context.Context, zone string) ([]nameDotComRecord, error) {
p.mutex.Lock()
defer p.mutex.Unlock()
return p.listAllRecords(ctx, zone)
}

// listAllRecords returns all records for the given zone . GET /v4/domains/{ domainName }/records
func (p *Provider) listAllRecords(ctx context.Context, zone string) ([]nameDotComRecord, error) {
var (
records []libdns.Record
records []nameDotComRecord

/*** 'zone' args that are passed in using compliant zone formats have the FQDN '.' suffix qualifier
and in order to use the zone arg as a domainName reference to name.com's api we must remove the '.' suffix.
Expand All @@ -92,24 +112,24 @@ func (p *Provider) listAllRecords(ctx context.Context, zone string) ([]libdns.Re
)

if err = p.getClient(ctx); err != nil {
return []libdns.Record{}, err
return []nameDotComRecord{}, err
}

// handle pagination, in case domain has more records then the default of 1000 per page
// handle pagination, in case domain has more records than the default of 1000 per page
for reqPage > 0 {
if reqPage != 0 {
endpoint := fmt.Sprintf("/v4/domains/%s/records?page=%d", unFQDNzone, reqPage)

if body, err = p.client.doRequest(ctx, method, endpoint, nil); err != nil {
return []libdns.Record{}, fmt.Errorf("request failed: %w", err)
return []nameDotComRecord{}, fmt.Errorf("request failed: %w", err)
}

if err = json.NewDecoder(body).Decode(resp); err != nil {
return []libdns.Record{}, fmt.Errorf("could not decode name.com's response: %w", err)
return []nameDotComRecord{}, fmt.Errorf("could not decode name.com's response: %w", err)
}

for _, record := range resp.Records {
records = append(records, record.toLibDNSRecord())
records = append(records, record)
}

reqPage = int(resp.NextPage)
Expand All @@ -123,82 +143,86 @@ func (p *Provider) listAllRecords(ctx context.Context, zone string) ([]libdns.Re
func (p *Provider) deleteRecord(ctx context.Context, zone string, record libdns.Record) (libdns.Record, error) {
p.mutex.Lock()
defer p.mutex.Unlock()

recordId, err := p.getRecordId(ctx, zone, record)
if err != nil {
return libdns.RR{}, err
}

var (
shouldDelete nameDotComRecord
unFQDNzone = strings.TrimSuffix(zone, ".")

method = "DELETE"
endpoint = fmt.Sprintf("/v4/domains/%s/records/%s", unFQDNzone, record.ID)
endpoint = fmt.Sprintf("/v4/domains/%s/records/%d", unFQDNzone, recordId)
body io.Reader
post = &bytes.Buffer{}

err error
)

shouldDelete.fromLibDNSRecord(record, unFQDNzone)
shouldDelete.fromLibDNSRecord(recordId, record, unFQDNzone)

if err = p.getClient(ctx); err != nil {
return libdns.Record{}, err
return libdns.RR{}, err
}

if err = json.NewEncoder(post).Encode(shouldDelete); err != nil {
return libdns.Record{}, fmt.Errorf("could not encode form data for request: %w", err)
return libdns.RR{}, fmt.Errorf("could not encode form data for request: %w", err)
}

if body, err = p.client.doRequest(ctx, method, endpoint, post); err != nil {
return libdns.Record{}, fmt.Errorf("request to delete the record was not successful: %w", err)
return libdns.RR{}, fmt.Errorf("request to delete the record was not successful: %w", err)
}

if err = json.NewDecoder(body).Decode(&shouldDelete); err != nil {
return libdns.Record{}, fmt.Errorf("could not decode the response from name.com: %w", err)
return libdns.RR{}, fmt.Errorf("could not decode the response from name.com: %w", err)
}

return shouldDelete.toLibDNSRecord(), nil
return shouldDelete.toLibDNSRecord(unFQDNzone)
}

// upsertRecord PUT || POST /v4/domains/{ domainName }/records/{ record.ID }
func (p *Provider) upsertRecord(ctx context.Context, zone string, canidateRecord libdns.Record) (libdns.Record, error) {
p.mutex.Lock()
defer p.mutex.Unlock()

recordId, err := p.getRecordId(ctx, zone, canidateRecord)

var (
shouldUpsert nameDotComRecord
unFQDNzone = strings.TrimSuffix(zone, ".")

method = "PUT"
endpoint = fmt.Sprintf("/v4/domains/%s/records/%s", unFQDNzone, canidateRecord.ID)
endpoint = fmt.Sprintf("/v4/domains/%s/records/%d", unFQDNzone, recordId)
body io.Reader
post = &bytes.Buffer{}

err error
)

if canidateRecord.ID == "" {
if err != nil {
method = "POST"
endpoint = fmt.Sprintf("/v4/domains/%s/records", unFQDNzone)
}

shouldUpsert.fromLibDNSRecord(canidateRecord, unFQDNzone)
shouldUpsert.fromLibDNSRecord(recordId, canidateRecord, unFQDNzone)

if err = p.getClient(ctx); err != nil {
return libdns.Record{}, err
return libdns.RR{}, err
}

if err = json.NewEncoder(post).Encode(shouldUpsert); err != nil {
return libdns.Record{}, fmt.Errorf("could not encode the form data for the request: %w", err)
return libdns.RR{}, fmt.Errorf("could not encode the form data for the request: %w", err)
}

if body, err = p.client.doRequest(ctx, method, endpoint, post); err != nil {
if strings.Contains(err.Error(), "Duplicate Record") {
err = fmt.Errorf("name.com will not allow an update to a record that has identical values to an existing record: %w", err)
}

return libdns.Record{}, fmt.Errorf("request to update the record was not successful: %w", err)
return libdns.RR{}, fmt.Errorf("request to update the record was not successful: %w", err)
}

if err = json.NewDecoder(body).Decode(&shouldUpsert); err != nil {
return libdns.Record{}, fmt.Errorf("could not decode name.com's response: %w", err)
return libdns.RR{}, fmt.Errorf("could not decode name.com's response: %w", err)
}

return shouldUpsert.toLibDNSRecord(), nil
return shouldUpsert.toLibDNSRecord(unFQDNzone)
}
5 changes: 2 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
module github.com/libdns/namedotcom

go 1.16
go 1.25

require (
github.com/libdns/libdns v0.2.2
github.com/pkg/errors v0.9.1
github.com/libdns/libdns v1.1.1
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
105 changes: 84 additions & 21 deletions namedotcom.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ package namedotcom
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/netip"
"regexp"
"strconv"
"strings"
"time"

"github.com/libdns/libdns"
"github.com/pkg/errors"
)

// default timeout for the http request handler (seconds)
Expand Down Expand Up @@ -77,10 +78,10 @@ func (n *nameDotCom) errorResponse(resp *http.Response) error {
er := &errorResponse{}
err := json.NewDecoder(resp.Body).Decode(er)
if err != nil {
return errors.Wrap(err, "api returned unexpected response")
return fmt.Errorf("api returned unexpected response: %w", err)
}

return errors.WithStack(er)
return err
}

// doRequest is the base http request handler including a request context.
Expand All @@ -92,7 +93,6 @@ func (n *nameDotCom) doRequest(ctx context.Context, method, endpoint string, pos
}

req.SetBasicAuth(n.User, n.Token)

resp, err := n.client.Do(req)
if err != nil {
return nil, err
Expand All @@ -105,33 +105,96 @@ func (n *nameDotCom) doRequest(ctx context.Context, method, endpoint string, pos
}

// fromLibDNSRecord maps a name.com record from a libdns record
func (n *nameDotComRecord) fromLibDNSRecord(record libdns.Record, zone string) {
var id int64
if record.ID != "" {
id, _ = strconv.ParseInt(record.ID, 10, 32)
}
n.ID = int32(id)
n.Type = record.Type
func (n *nameDotComRecord) fromLibDNSRecord(id int32, record libdns.Record, zone string) {
n.ID = id
n.Type = record.RR().Type
n.Host = n.toSanitized(record, zone)
n.Answer = record.Value
n.TTL = uint32(record.TTL.Seconds())
n.Answer = record.RR().Data
n.TTL = uint32(record.RR().TTL.Seconds())
}

// toLibDNSRecord maps a name.com record to a libdns record
func (n *nameDotComRecord) toLibDNSRecord() libdns.Record {
return libdns.Record{
ID: fmt.Sprint(n.ID),
Type: n.Type,
Name: n.Host,
Value: n.Answer,
TTL: time.Duration(n.TTL) * time.Second,
func (record *nameDotComRecord) toLibDNSRecord(zone string) (libdns.Record, error) {
name := libdns.RelativeName(record.Fqdn, zone)
ttl := time.Duration(record.TTL) * time.Second

switch record.Type {
case "A", "AAAA":
ip, err := netip.ParseAddr(record.Answer)
if err != nil {
return libdns.Address{}, err
}
return libdns.Address{
Name: name,
TTL: ttl,
IP: ip,
}, nil
case "CAA":
contentParts := strings.SplitN(record.Answer, " ", 3)
flags, err := strconv.Atoi(contentParts[0])
if err != nil {
return libdns.CAA{}, err
}
tag := contentParts[1]
value := contentParts[2]
return libdns.CAA{
Name: name,
TTL: ttl,
Flags: uint8(flags),
Tag: tag,
Value: value,
}, nil
case "CNAME":
return libdns.CNAME{
Name: name,
TTL: ttl,
Target: record.Answer,
}, nil
case "SRV":
priority := record.Priority

nameParts := strings.SplitN(name, ".", 2)
if len(nameParts) < 2 {
return libdns.SRV{}, fmt.Errorf("name %v does not contain enough fields; expected format: '_service._proto'", name)
}
contentParts := strings.SplitN(record.Answer, " ", 3)
if len(contentParts) < 3 {
return libdns.SRV{}, fmt.Errorf("content %v does not contain enough fields; expected format: 'weight port target'", name)
}
weight, err := strconv.Atoi(contentParts[0])
if err != nil {
return libdns.SRV{}, fmt.Errorf("invalid value for weight %v; expected integer", record.Priority)
}
port, err := strconv.Atoi(contentParts[1])
if err != nil {
return libdns.SRV{}, fmt.Errorf("invalid value for port %v; expected integer", record.Priority)
}

return libdns.SRV{
Service: strings.TrimPrefix(nameParts[0], "_"),
Transport: strings.TrimPrefix(nameParts[1], "_"),
Name: zone,
TTL: ttl,
Priority: uint16(priority),
Weight: uint16(weight),
Port: uint16(port),
Target: contentParts[2],
}, nil
case "TXT":
return libdns.TXT{
Name: name,
TTL: ttl,
Text: record.Answer,
}, nil
default:
return libdns.RR{}, fmt.Errorf("Unsupported record type: %s", record.Type)
}
}

// name.com's api server expects the sub domain name to be relavtive and have no trailing period
// , e.g. "sub.zone." -> "sub"
func (n *nameDotComRecord) toSanitized(libdnsRecord libdns.Record, zone string) string {
return strings.TrimSuffix(strings.Replace(libdnsRecord.Name, zone, "", -1), ".")
return strings.TrimSuffix(strings.Replace(libdnsRecord.RR().Name, zone, "", -1), ".")
}

// NewNameDotComClient returns a new name.com client struct
Expand Down
Loading