diff --git a/.goreleaser.yml b/.goreleaser.yml index ca667aa4b..d1b1ffd2e 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -35,7 +35,7 @@ changelog: regexp: "(?i)^.*(major|new provider|feature)[(\\w)]*:+.*$" order: 1 - title: 'Provider-specific changes:' - regexp: "(?i)((akamaiedge|autodns|axfrd|azure|azure_private_dns|bind|cloudflare|cloudflareapi_old|cloudns|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|easyname|exoscale|gandi|gcloud|gcore|hedns|hetzner|hexonet|hostingde|inwx|linode|loopia|luadns|msdns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|route53|rwth|softlayer|transip|vultr).*:)+.*" + regexp: "(?i)((akamaiedge|autodns|axfrd|azure|azure_private_dns|bind|cloudflare|cloudflareapi_old|cloudns|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|gandi|gcloud|gcore|hedns|hetzner|hexonet|hostingde|inwx|linode|loopia|luadns|msdns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|route53|rwth|softlayer|transip|vultr).*:)+.*" order: 2 - title: 'Documentation:' regexp: "(?i)^.*(docs)[(\\w)]*:+.*$" diff --git a/OWNERS b/OWNERS index a43698425..973c35f90 100644 --- a/OWNERS +++ b/OWNERS @@ -13,6 +13,7 @@ providers/dnsimple @onlyhavecans providers/dnsmadeeasy @vojtad providers/doh @mikenz providers/domainnameshop @SimenBai +providers/dynadot @e-im providers/easyname @tresni providers/exoscale @pierre-emmanuelJ providers/gandiv5 @TomOnTime diff --git a/README.md b/README.md index 15a0b989c..071347b6d 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Currently supported Domain Registrars: - AWS Route 53 - CSC Global - DNSOVERHTTPS +- Dynadot - easyname - Gandi - HEXONET diff --git a/documentation/SUMMARY.md b/documentation/SUMMARY.md index 22b8d7cad..52e0ea22e 100644 --- a/documentation/SUMMARY.md +++ b/documentation/SUMMARY.md @@ -113,6 +113,7 @@ * [DNSimple](providers/dnsimple.md) * [DNS-over-HTTPS](providers/dnsoverhttps.md) * [DOMAINNAMESHOP](providers/domainnameshop.md) + * [Dynadot](providers/dynadot.md) * [easyname](providers/easyname.md) * [Exoscale](providers/exoscale.md) * [Gandi_v5](providers/gandi_v5.md) diff --git a/documentation/providers.md b/documentation/providers.md index 917baab15..603efcacf 100644 --- a/documentation/providers.md +++ b/documentation/providers.md @@ -29,6 +29,7 @@ If a feature is definitively not supported for whatever reason, we would also li | [`DNSMADEEASY`](providers/dnsmadeeasy.md) | ❌ | ✅ | ❌ | ✅ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ❌ | ❌ | ❌ | ❔ | ✅ | ✅ | ✅ | ✅ | | [`DNSOVERHTTPS`](providers/dnsoverhttps.md) | ❌ | ❌ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ✅ | ❔ | | [`DOMAINNAMESHOP`](providers/domainnameshop.md) | ❌ | ✅ | ❌ | ❔ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ❔ | +| [`DYNADOT`](providers/dynadot.md) | ❌ | ❌ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ✅ | ❔ | | [`EASYNAME`](providers/easyname.md) | ❌ | ❌ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ✅ | ❔ | | [`EXOSCALE`](providers/exoscale.md) | ❌ | ✅ | ❌ | ✅ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ❔ | ❌ | ❔ | ❔ | ❌ | ❌ | ✅ | ❔ | | [`GANDI_V5`](providers/gandi_v5.md) | ❌ | ✅ | ✅ | ✅ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ✅ | ✅ | ❌ | ❔ | ❔ | ❌ | ❌ | ✅ | diff --git a/documentation/providers/dynadot.md b/documentation/providers/dynadot.md new file mode 100644 index 000000000..19a88970e --- /dev/null +++ b/documentation/providers/dynadot.md @@ -0,0 +1,41 @@ +DNSControl's Dynadot provider supports being a Registrar. Support for being a DNS Provider is not included, but could be added in the future. + +## Configuration + +To use this provider, add an entry to `creds.json` with `TYPE` set to `DYNADOT` +along with `key` from the [Dynadot API](https://www.dynadot.com/account/domain/setting/api.html). + +Example: + +{% code title="creds.json" %} +```json +{ + "easyname": { + "TYPE": "DYNADOT", + "key": "API Key", + } +} +``` +{% endcode %} + +## Metadata +This provider does not recognize any special metadata fields unique to Dynadot. + +## Usage +An example configuration: + +{% code title="dnsconfig.js" %} +```javascript +var REG_DYNADOT = NewRegistrar("dynadot"); + +DOMAIN_ELSEWHERE("example.com", REG_DYNADOT, [ + "ns1.example.net.", + "ns2.example.net.", + "ns3.example.net.", +]); +``` +{% endcode %} + +## Activation + +You must [enable the Dynadot API](https://www.dynadot.com/account/domain/setting/api.html) for your account and whitelist the IP address of the machine that will run DNSControl. \ No newline at end of file diff --git a/providers/_all/all.go b/providers/_all/all.go index 48be689e9..2a04d320c 100644 --- a/providers/_all/all.go +++ b/providers/_all/all.go @@ -18,6 +18,7 @@ import ( _ "github.com/StackExchange/dnscontrol/v4/providers/dnsmadeeasy" _ "github.com/StackExchange/dnscontrol/v4/providers/doh" _ "github.com/StackExchange/dnscontrol/v4/providers/domainnameshop" + _ "github.com/StackExchange/dnscontrol/v4/providers/dynadot" _ "github.com/StackExchange/dnscontrol/v4/providers/easyname" _ "github.com/StackExchange/dnscontrol/v4/providers/exoscale" _ "github.com/StackExchange/dnscontrol/v4/providers/gandiv5" diff --git a/providers/dynadot/api.go b/providers/dynadot/api.go new file mode 100644 index 000000000..232fa2f3e --- /dev/null +++ b/providers/dynadot/api.go @@ -0,0 +1,135 @@ +package dynadot + +import ( + "encoding/xml" + "fmt" + "io" + "net/http" + "strings" +) + +// API layer for Dynadot + +type dynadotProvider struct { + key string +} + +type requestParams map[string]string + +type header struct { + SuccessCode int `xml:"SuccessCode"` + Status string `xml:"Status"` + Error string `xml:"Error,omitempty"` +} + +type addNsResponse struct { + XMLName xml.Name `xml:"AddNsResponse"` + AddNsHeader header `xml:"AddNsHeader"` +} + +type setNsResponse struct { + XMLName xml.Name `xml:"SetNsResponse"` + SetNsHeader header `xml:"SetNsHeader"` +} + +type getNsResponse struct { + XMLName xml.Name `xml:"GetNsResponse"` + NsContent nsContent `xml:"NsContent"` + GetNsHeader header `xml:"GetNsHeader"` +} + +type nsContent struct { + Host []string `xml:"Host"` + NsName string `xml:"NsName"` +} + +func (c *dynadotProvider) getNameservers(domain string) ([]string, error) { + var bodyString, err = c.get("get_ns", requestParams{"domain": domain}) + if err != nil { + return []string{}, fmt.Errorf("failed NS list (Dynadot): %s", err) + } + var ns getNsResponse + xml.Unmarshal(bodyString, &ns) + + if ns.GetNsHeader.SuccessCode != 0 { + return []string{}, fmt.Errorf("failed NS list (Dynadot): %s", ns.GetNsHeader.Error) + } + + hosts := []string{} + hosts = append(hosts, ns.NsContent.Host...) + return hosts, nil +} + +func (c *dynadotProvider) updateNameservers(ns []string, domain string) error { + if len(ns) > 13 { + return fmt.Errorf("failed NS update (Dynadot): only up to 13 nameservers are supported") + } + + // Nameservers must first be added to the Dynadot account + for _, host := range ns { + b, err := c.get("add_ns", requestParams{"host": host}) + if err != nil { + return fmt.Errorf("failed NS add (Dynadot): %s", err) + } + var resp addNsResponse + err = xml.Unmarshal(b, &resp) + if err != nil { + return fmt.Errorf("failed NS add (Dynadot): %s", err) + } + + if resp.AddNsHeader.SuccessCode != 0 { + // No apparent way to get all existing entries on an account, so filter + if strings.Contains(resp.AddNsHeader.Error, "already exists") { + continue + } + return fmt.Errorf("failed NS add (Dynadot): %s", resp.AddNsHeader.Error) + + } + } + + rec := requestParams{} + rec["domain"] = domain + // supported prams: ns0 - ns12 + for i, h := range ns { + rec[fmt.Sprintf("%s%d", "ns", i)] = h + } + + b, err := c.get("set_ns", rec) + if err != nil { + return fmt.Errorf("failed NS set (Dynadot): %s", err) + } + + var resp setNsResponse + err = xml.Unmarshal(b, &resp) + if err != nil { + return fmt.Errorf("failed NS add (Dynadot): %s", err) + } + + if resp.SetNsHeader.SuccessCode != 0 { + return fmt.Errorf("failed NS add (Dynadot): %s", resp.SetNsHeader.Error) + } + + return nil +} + +func (c *dynadotProvider) get(command string, params requestParams) ([]byte, error) { + client := &http.Client{} + req, _ := http.NewRequest("GET", "https://api.dynadot.com/api3.xml", nil) + q := req.URL.Query() + + q.Add("key", c.key) + q.Add("command", command) + + for pName, pValue := range params { + q.Add(pName, pValue) + } + + req.URL.RawQuery = q.Encode() + + resp, err := client.Do(req) + if err != nil { + return []byte{}, err + } + + return io.ReadAll(resp.Body) +} diff --git a/providers/dynadot/dynadotProvider.go b/providers/dynadot/dynadotProvider.go new file mode 100644 index 000000000..48d0c622f --- /dev/null +++ b/providers/dynadot/dynadotProvider.go @@ -0,0 +1,62 @@ +package dynadot + +import ( + "fmt" + "sort" + "strings" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/providers" +) + +/* + +Dynadot Registrator: + +Info required in `creds.json`: + - key API Key + +*/ + +func init() { + providers.RegisterRegistrarType("DYNADOT", newDynadot) +} + +func newDynadot(m map[string]string) (providers.Registrar, error) { + d := &dynadotProvider{} + + d.key = m["key"] + if d.key == "" { + return nil, fmt.Errorf("missing Dynadot key") + } + + return d, nil +} + +func (c *dynadotProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { + nss, err := c.getNameservers(dc.Name) + if err != nil { + return nil, err + } + foundNameservers := strings.Join(nss, ",") + + expected := []string{} + for _, ns := range dc.Nameservers { + name := strings.TrimRight(ns.Name, ".") + expected = append(expected, name) + } + sort.Strings(expected) + expectedNameservers := strings.Join(expected, ",") + + if foundNameservers != expectedNameservers { + return []*models.Correction{ + { + Msg: fmt.Sprintf("Update nameservers (%s) -> (%s)", foundNameservers, expectedNameservers), + F: func() error { + return c.updateNameservers(expected, dc.Name) + }, + }, + }, nil + } + return nil, nil +}