diff --git a/.goreleaser.yml b/.goreleaser.yml index dbf10e4e8..ac5568685 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -33,7 +33,7 @@ changelog: regexp: "(?i)^.*(major|new provider|feature)[(\\w)]*:+.*$" order: 1 - title: 'Provider-specific changes:' - regexp: "(?i)((akamaiedge|autodns|axfrd|azure|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|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|route53|rwth|softlayer|transip|vultr).*:)+.*" + regexp: "(?i)((akamaiedge|autodns|axfrd|azure|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).*:)+.*" order: 2 - title: 'Deprecation warnings:' regexp: "(?i)^.*Deprecate[(\\w)]*:+.*$" diff --git a/OWNERS b/OWNERS index c57767493..b1b524718 100644 --- a/OWNERS +++ b/OWNERS @@ -27,6 +27,7 @@ providers/linode @koesie10 providers/loopia @systemcrash providers/luadns @riku22 providers/msdns @tlimoncelli +providers/mythicbeasts @tomfitzhenry providers/namecheap @willpower232 # providers/namedotcom NEEDS VOLUNTEER providers/netcup @kordianbruck diff --git a/README.md b/README.md index f2d29ba01..1b658f66b 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Currently supported DNS providers: - Loopia - LuaDNS - Microsoft Windows Server DNS Server +- Mythic Beasts - Namecheap - Name.com - Netcup diff --git a/documentation/SUMMARY.md b/documentation/SUMMARY.md index 2ab2aeb2a..cecf11e51 100644 --- a/documentation/SUMMARY.md +++ b/documentation/SUMMARY.md @@ -124,6 +124,7 @@ * [Loopia](providers/loopia.md) * [LuaDNS](providers/luadns.md) * [Microsoft DNS Server on Microsoft Windows Server](providers/msdns.md) + * [Mythic Beasts](providers/mythicbeasts.md) * [Namecheap](providers/namecheap.md) * [Name.com](providers/namedotcom.md) * [Netcup](providers/netcup.md) diff --git a/documentation/providers.md b/documentation/providers.md index 7bc3ab87a..5a1eafece 100644 --- a/documentation/providers.md +++ b/documentation/providers.md @@ -43,6 +43,7 @@ If a feature is definitively not supported for whatever reason, we would also li | [`LOOPIA`](providers/loopia.md) | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | | [`LUADNS`](providers/luadns.md) | ❌ | ✅ | ❌ | ✅ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ✅ | | [`MSDNS`](providers/msdns.md) | ✅ | ✅ | ❌ | ❌ | ❌ | ❔ | ❌ | ✅ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❌ | ❌ | ✅ | ✅ | +| [`MYTHICBEASTS`](providers/mythicbeasts.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ❔ | ✅ | ❔ | ✅ | ❌ | ✅ | ✅ | | [`NAMECHEAP`](providers/namecheap.md) | ❌ | ✅ | ✅ | ✅ | ✅ | ❔ | ❌ | ❔ | ❌ | ❔ | ❌ | ❔ | ❌ | ❔ | ❌ | ❌ | ❌ | ✅ | | [`NAMEDOTCOM`](providers/namedotcom.md) | ❌ | ✅ | ✅ | ✅ | ❔ | ❔ | ❌ | ❔ | ❌ | ❔ | ✅ | ❔ | ❔ | ❔ | ✅ | ❌ | ✅ | ✅ | | [`NETCUP`](providers/netcup.md) | ❌ | ✅ | ❌ | ❔ | ✅ | ❔ | ❌ | ❔ | ❌ | ❔ | ✅ | ❔ | ❔ | ❔ | ❌ | ❌ | ✅ | ❌ | diff --git a/documentation/providers/mythicbeasts.md b/documentation/providers/mythicbeasts.md new file mode 100644 index 000000000..f239722ce --- /dev/null +++ b/documentation/providers/mythicbeasts.md @@ -0,0 +1,39 @@ +This is the provider for [Mythic Beasts](https://www.mythic-beasts.com/) using its [Primary DNS API v2](https://www.mythic-beasts.com/support/api/dnsv2). + +## Configuration + +To use this provider, add an entry to `creds.json` with `TYPE` set to `MYTHICBEASTS` along with a Mythic Beasts API key ID and secret. + +Example: + +{% code title="creds.json" %} +```json +{ + "mythicbeasts": { + "TYPE": "MYTHICBEASTS", + "keyID": "xxxxxxx", + "secret": "xxxxxx" + } +} +``` +{% endcode %} + +## Usage + +For each domain: + +* Domains must be added in the [web UI](https://www.mythic-beasts.com/customer/domains), and have DNS enabled. +* In Mythic Beasts' DNS management web UI, new domains will have set a default DNS template of "Mythic Beasts nameservers only". You must set this to "None". + +An example configuration: + +{% code title="dnsconfig.js" %} +```javascript +var REG_NONE = NewRegistrar("none"); +var DSP_MYTHIC = NewDnsProvider("mythicbeasts"); + +D("example.com", REG_NONE, DnsProvider(DSP_MYTHIC), + A("test", "1.2.3.4") +); +``` +{% endcode %} diff --git a/integrationTest/providers.json b/integrationTest/providers.json index 5bc22885c..019876b8d 100644 --- a/integrationTest/providers.json +++ b/integrationTest/providers.json @@ -179,6 +179,12 @@ "domain": "$MSDNS_DOMAIN", "pssession": "$MSDNS_PSSESSION" }, + "MYTHICBEASTS": { + "TYPE": "MYTHICBEASTS", + "keyID": "$MYTHICBEASTS_KEYID", + "secret": "$MYTHICBEASTS_SECRET", + "domain": "$MYTHICBEASTS_DOMAIN" + }, "NAMECHEAP": { "BaseURL": "$NAMECHEAP_BASEURL", "TYPE": "NAMECHEAP", diff --git a/providers/_all/all.go b/providers/_all/all.go index 9fcbaa7f4..9f6ab843b 100644 --- a/providers/_all/all.go +++ b/providers/_all/all.go @@ -32,6 +32,7 @@ import ( _ "github.com/StackExchange/dnscontrol/v4/providers/loopia" _ "github.com/StackExchange/dnscontrol/v4/providers/luadns" _ "github.com/StackExchange/dnscontrol/v4/providers/msdns" + _ "github.com/StackExchange/dnscontrol/v4/providers/mythicbeasts" _ "github.com/StackExchange/dnscontrol/v4/providers/namecheap" _ "github.com/StackExchange/dnscontrol/v4/providers/namedotcom" _ "github.com/StackExchange/dnscontrol/v4/providers/netcup" diff --git a/providers/mythicbeasts/auditrecords.go b/providers/mythicbeasts/auditrecords.go new file mode 100644 index 000000000..7f7d242cb --- /dev/null +++ b/providers/mythicbeasts/auditrecords.go @@ -0,0 +1,15 @@ +package mythicbeasts + +import ( + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/rejectif" +) + +// AuditRecords returns a list of errors corresponding to the records +// that aren't supported by this provider. If all records are +// supported, an empty list is returned. +func AuditRecords(records []*models.RecordConfig) []error { + a := rejectif.Auditor{} + a.Add("TXT", rejectif.TxtHasDoubleQuotes) + return nil +} diff --git a/providers/mythicbeasts/mythicbeastsProvider.go b/providers/mythicbeasts/mythicbeastsProvider.go new file mode 100644 index 000000000..69f300988 --- /dev/null +++ b/providers/mythicbeasts/mythicbeastsProvider.go @@ -0,0 +1,147 @@ +// Package mythicbeasts provides a provider for managing zones in Mythic Beasts. +// +// This package uses the Primary DNS API v2, as described in https://www.mythic-beasts.com/support/api/dnsv2 +package mythicbeasts + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "strings" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/diff2" + "github.com/StackExchange/dnscontrol/v4/providers" + "github.com/miekg/dns" +) + +// mythicBeastsDefaultNS lists the default nameservers, per https://www.mythic-beasts.com/support/domains/nameservers. +var mythicBeastsDefaultNS = []string{ + "ns1.mythic-beasts.com", + "ns2.mythic-beasts.com", +} + +// mythicBeastsProvider is the handle for this provider. +type mythicBeastsProvider struct { + secret string + keyID string + client *http.Client +} + +var features = providers.DocumentationNotes{ + providers.CanGetZones: providers.Can(), + providers.CanUseAlias: providers.Cannot(), + providers.CanUseCAA: providers.Can(), + providers.CanUseLOC: providers.Cannot(), + providers.CanUsePTR: providers.Can(), + providers.CanUseSRV: providers.Can(), + providers.CanUseTLSA: providers.Can(), + providers.DocCreateDomains: providers.Cannot("Requires domain registered through Web UI"), + providers.DocDualHost: providers.Can(), + providers.DocOfficiallySupported: providers.Cannot(), +} + +func init() { + fns := providers.DspFuncs{ + Initializer: newDsp, + RecordAuditor: AuditRecords, + } + providers.RegisterDomainServiceProviderType("MYTHICBEASTS", fns, features) +} + +func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { + if conf["keyID"] == "" { + return nil, fmt.Errorf("missing Mythic Beasts auth keyID") + } + if conf["secret"] == "" { + return nil, fmt.Errorf("missing Mythic Beasts auth secret") + } + return &mythicBeastsProvider{ + keyID: conf["keyID"], + secret: conf["secret"], + client: &http.Client{}, + }, nil +} + +func (n *mythicBeastsProvider) httpRequest(method, url string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest(method, "https://api.mythic-beasts.com/dns/v2"+url, body) + if err != nil { + return nil, err + } + // https://www.mythic-beasts.com/support/api/auth + req.SetBasicAuth(n.keyID, n.secret) + req.Header.Add("Content-Type", "text/dns") + req.Header.Add("Accept", "text/dns") + return n.client.Do(req) +} + +// GetZoneRecords gets the records of a zone and returns them in RecordConfig format. +func (n *mythicBeastsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) { + resp, err := n.httpRequest("GET", "/zones/"+domain+"/records", nil) + if err != nil { + return nil, err + } + if got, want := resp.StatusCode, 200; got != want { + body, _ := ioutil.ReadAll(resp.Body) + return nil, fmt.Errorf("got HTTP %v, want %v: %v", got, want, string(body)) + } + return zoneFileToRecords(resp.Body, domain) +} + +func zoneFileToRecords(r io.Reader, origin string) (models.Records, error) { + zp := dns.NewZoneParser(r, origin, origin) + var records []*models.RecordConfig + for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { + rec, err := models.RRtoRC(rr, origin) + if err != nil { + return nil, err + } + records = append(records, &rec) + } + + if err := zp.Err(); err != nil { + return nil, fmt.Errorf("parsing zone for %v: %w", origin, err) + } + return records, nil +} + +// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. +func (n *mythicBeastsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) { + msgs, changes, err := diff2.ByZone(actual, dc, nil) + if err != nil { + return nil, err + } + + var corrections []*models.Correction + if changes { + corrections = append(corrections, + &models.Correction{ + Msg: strings.Join(msgs, "\n"), + F: func() error { + var b strings.Builder + for _, rr := range dc.Records { + fmt.Fprintf(&b, "%v\n", rr.ToRR().String()) + } + + resp, err := n.httpRequest("PUT", "/zones/"+dc.Name+"/records", strings.NewReader(b.String())) + if err != nil { + return err + } + if got, want := resp.StatusCode, 200; got != want { + body, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("got HTTP %v, want %v: %v", got, want, string(body)) + } + return nil + }, + }) + } + + return corrections, nil +} + +// GetNameservers returns the nameservers for a domain. +func (n *mythicBeastsProvider) GetNameservers(domainName string) ([]*models.Nameserver, error) { + return models.ToNameservers(mythicBeastsDefaultNS) +}