diff --git a/providers/autodns/api.go b/providers/autodns/api.go index ec8e11ebc..5aec90261 100644 --- a/providers/autodns/api.go +++ b/providers/autodns/api.go @@ -4,9 +4,11 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "io" "math" "net/http" + "os" "sort" "time" @@ -62,7 +64,8 @@ func (api *autoDNSProvider) request(method string, requestPath string, data inte responseText, _ := io.ReadAll(response.Body) - if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusTooManyRequests { + // FUTUREWORK: 202 starts a long-running task. Should we instead poll here until task is completed? + if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusAccepted && response.StatusCode != http.StatusTooManyRequests { return nil, errors.New("Request to " + requestURL.Path + " failed: " + string(responseText)) } @@ -99,9 +102,12 @@ func (api *autoDNSProvider) findZoneSystemNameServer(domain string) (*models.Nam } var responseObject JSONResponseDataZone - _ = json.Unmarshal(responseData, &responseObject) + if err := json.Unmarshal(responseData, &responseObject); err != nil { + return nil, err + } + if len(responseObject.Data) != 1 { - return nil, errors.New("Domain " + domain + " could not be found in AutoDNS") + return nil, fmt.Errorf("Zone "+domain+" could not be found in AutoDNS: %w", os.ErrNotExist) } systemNameServer := &models.Nameserver{Name: responseObject.Data[0].SystemNameServer} @@ -109,6 +115,42 @@ func (api *autoDNSProvider) findZoneSystemNameServer(domain string) (*models.Nam return systemNameServer, nil } +func (api *autoDNSProvider) createZone(domain string, zone *Zone) (*Zone, error) { + responseData, err := api.request("POST", "zone", zone) + if err != nil { + return nil, err + } + + var responseObject JSONResponseDataZone + if err := json.Unmarshal(responseData, &responseObject); err != nil { + return nil, err + } + + if len(responseObject.Data) != 1 { + return nil, errors.New("Zone " + domain + " not returned") + } + + return responseObject.Data[0], nil +} + +func (api *autoDNSProvider) getDomain(domain string) (*Domain, error) { + responseData, err := api.request("GET", "domain/"+domain, nil) + if err != nil { + return nil, err + } + + var responseObject JSONResponseDataDomain + if err := json.Unmarshal(responseData, &responseObject); err != nil { + return nil, err + } + + if len(responseObject.Data) != 1 { + return nil, fmt.Errorf("Domain "+domain+" could not be found in AutoDNS: %w", os.ErrNotExist) + } + + return responseObject.Data[0], nil +} + func (api *autoDNSProvider) getZone(domain string) (*Zone, error) { systemNameServer, err := api.findZoneSystemNameServer(domain) if err != nil { @@ -124,14 +166,32 @@ func (api *autoDNSProvider) getZone(domain string) (*Zone, error) { var responseObject JSONResponseDataZone // make sure that the response is valid, the zone is in AutoDNS but we're not sure the returned data meets our expectation - unmErr := json.Unmarshal(responseData, &responseObject) - if unmErr != nil { - return nil, unmErr + if err := json.Unmarshal(responseData, &responseObject); err != nil { + return nil, err } return responseObject.Data[0], nil } +func (api *autoDNSProvider) getZones() ([]string, error) { + responseData, err := api.request("POST", "zone/_search", nil) + if err != nil { + return nil, err + } + + var responseObject JSONResponseDataZone + if err := json.Unmarshal(responseData, &responseObject); err != nil { + return nil, err + } + + names := make([]string, 0, len(responseObject.Data)) + for _, zone := range responseObject.Data { + names = append(names, zone.Origin) + } + + return names, nil +} + func (api *autoDNSProvider) updateZone(domain string, resourceRecords []*ResourceRecord, nameServers []*models.Nameserver, zoneTTL uint32) error { systemNameServer, err := api.findZoneSystemNameServer(domain) if err != nil { @@ -172,3 +232,12 @@ func (api *autoDNSProvider) updateZone(domain string, resourceRecords []*Resourc return nil } + +func (api *autoDNSProvider) updateDomain(name string, domain *Domain) error { + _, err := api.request("PUT", "domain/"+name, domain) + if err != nil { + return err + } + + return nil +} diff --git a/providers/autodns/auditrecords.go b/providers/autodns/auditrecords.go index 3f6cc1143..6d8f28ec9 100644 --- a/providers/autodns/auditrecords.go +++ b/providers/autodns/auditrecords.go @@ -11,7 +11,8 @@ import ( func AuditRecords(records []*models.RecordConfig) []error { a := rejectif.Auditor{} - a.Add("MX", rejectif.MxNull) // Last verified 2022-03-25 + a.Add("MX", rejectif.MxNull) // Last verified 2022-03-25 + a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2025-05-13 return a.Audit(records) } diff --git a/providers/autodns/autoDnsProvider.go b/providers/autodns/autoDnsProvider.go index 79c0feec8..49b273a66 100644 --- a/providers/autodns/autoDnsProvider.go +++ b/providers/autodns/autoDnsProvider.go @@ -6,13 +6,16 @@ import ( "fmt" "net/http" "net/url" + "os" "regexp" + "sort" "strconv" "strings" "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff2" "github.com/StackExchange/dnscontrol/v4/providers" + "github.com/StackExchange/dnscontrol/v4/providers/bind" ) var features = providers.DocumentationNotes{ @@ -41,15 +44,19 @@ func init() { const providerName = "AUTODNS" const providerMaintainer = "@arnoschoon" fns := providers.DspFuncs{ - Initializer: New, + Initializer: func(settings map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) { + return new(settings), nil + }, RecordAuditor: AuditRecords, } + providers.RegisterRegistrarType(providerName, func(settings map[string]string) (providers.Registrar, error) { + return new(settings), nil + }, features) providers.RegisterDomainServiceProviderType(providerName, fns, features) providers.RegisterMaintainer(providerName, providerMaintainer) } -// New creates a new API handle. -func New(settings map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) { +func new(settings map[string]string) *autoDNSProvider { api := &autoDNSProvider{} api.baseURL = url.URL{ @@ -68,7 +75,7 @@ func New(settings map[string]string, _ json.RawMessage) (providers.DNSServicePro "X-Domainrobot-Context": []string{settings["context"]}, } - return api, nil + return api } // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. @@ -243,6 +250,74 @@ func (api *autoDNSProvider) GetZoneRecords(domain string, meta map[string]string return existingRecords, nil } +func (api *autoDNSProvider) EnsureZoneExists(domain string) error { + // try to get zone + _, err := api.getZone(domain) + + if !errors.Is(err, os.ErrNotExist) { + return err + } + + _, err = api.createZone(domain, &Zone{ + Origin: domain, + NameServers: []*models.Nameserver{ + {Name: "a.ns14.net"}, {Name: "b.ns14.net"}, + {Name: "c.ns14.net"}, {Name: "d.ns14.net"}, + }, + Soa: &bind.SoaDefaults{ + Expire: 1209600, + Refresh: 43200, + Retry: 7200, + TTL: 86400, + }, + }) + + return err +} + +func (api *autoDNSProvider) ListZones() ([]string, error) { + return api.getZones() +} + +func (api *autoDNSProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { + domain, err := api.getDomain(dc.Name) + if err != nil { + return nil, err + } + + existingNs := make([]string, 0, len(domain.NameServers)) + for _, ns := range domain.NameServers { + existingNs = append(existingNs, ns.Name) + } + sort.Strings(existingNs) + existing := strings.Join(existingNs, ",") + + desiredNs := models.NameserversToStrings(dc.Nameservers) + sort.Strings(desiredNs) + desired := strings.Join(desiredNs, ",") + + if existing != desired { + return []*models.Correction{ + { + Msg: fmt.Sprintf("Change Nameservers from '%s' to '%s'", existing, desired), + F: func() error { + nameservers := make([]*NameServer, 0, len(desiredNs)) + for _, name := range desiredNs { + nameservers = append(nameservers, &NameServer{ + Name: name, + }) + } + return api.updateDomain(dc.Name, &Domain{ + NameServers: nameservers, + }) + }, + }, + }, nil + } + + return nil, nil +} + func toRecordConfig(domain string, record *ResourceRecord) (*models.RecordConfig, error) { rc := &models.RecordConfig{ Type: record.Type, diff --git a/providers/autodns/types.go b/providers/autodns/types.go index 220b5bc7d..f86131396 100644 --- a/providers/autodns/types.go +++ b/providers/autodns/types.go @@ -61,8 +61,31 @@ type Zone struct { SystemNameServer string `json:"virtualNameServer,omitempty"` } +// Domain represents the Domain in API calls. +// These are only present for domains where AUTODNS also is a registrar. +type Domain struct { + Name string `json:"name,omitempty"` + + NameServers []*NameServer `json:"nameServers"` + Zone *Zone `json:"zone,omitempty"` +} + +type NameServer struct { + // Host name of the nameserver written as a Fully-Qualified-Domain-Name (FQDN). + Name string `json:"name"` + // Time-to-live value of the nameservers in seconds + TTL uint64 `json:"ttl,omitempty"` + // IPv4 and IPv6 addresses of the name server. For GLUE records only; optional. The values for the IP addresses are only relevant for domain operations and are only used there in the case of glue name servers. + IPAddresses []string `json:"ipAddresses,omitempty"` +} + // JSONResponseDataZone represents the response to the DataZone call. type JSONResponseDataZone struct { // The data for the response. The type of the objects are depending on the request and are also specified in the responseObject value of the response. Data []*Zone `json:"data"` } + +type JSONResponseDataDomain struct { + // The data for the response. The type of the objects are depending on the request and are also specified in the responseObject value of the response. + Data []*Domain `json:"data"` +}