mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-01-10 17:38:13 +08:00
NEW PROVIDER: DNS Made Easy (#1093)
* implement DNS Made Easy provider * fix sandbox instructions in DNS Made Easy provider docs * remove unnecessary blank lines and fix golint warnings * remove unused deleteRecord method from DNSME api * remove trailing comma in providers.json * implement check for TXT records with double quotes for DNSME provider * implement changing apex NS records * rename DNSME to DNSMADEEASY Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
parent
bcad7c738b
commit
517b0458d6
11 changed files with 945 additions and 0 deletions
1
OWNERS
1
OWNERS
|
@ -8,6 +8,7 @@ providers/desec @D3luxee
|
||||||
providers/doh @mikenz
|
providers/doh @mikenz
|
||||||
providers/digitalocean @Deraen
|
providers/digitalocean @Deraen
|
||||||
providers/dnsimple @aeden
|
providers/dnsimple @aeden
|
||||||
|
providers/dnsmadeeasy @vojtad
|
||||||
providers/gandi_v5 @TomOnTime
|
providers/gandi_v5 @TomOnTime
|
||||||
# providers/gcloud
|
# providers/gcloud
|
||||||
providers/hedns @rblenkinsopp
|
providers/hedns @rblenkinsopp
|
||||||
|
|
|
@ -23,6 +23,7 @@ Currently supported DNS providers:
|
||||||
- ClouDNS
|
- ClouDNS
|
||||||
- Cloudflare
|
- Cloudflare
|
||||||
- DNSOVERHTTPS
|
- DNSOVERHTTPS
|
||||||
|
- DNS Made Easy
|
||||||
- DNSimple
|
- DNSimple
|
||||||
- DigitalOcean
|
- DigitalOcean
|
||||||
- Exoscale
|
- Exoscale
|
||||||
|
|
60
docs/_providers/dnsmadeeasy.md
Normal file
60
docs/_providers/dnsmadeeasy.md
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
name: DNSMADEEASY
|
||||||
|
title: DNS Made Simple Provider
|
||||||
|
layout: default
|
||||||
|
jsId: DNSMADEEASY
|
||||||
|
---
|
||||||
|
# DNS Made Simple Provider
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
In your credentials file, you must provide your `api_key` and `secret_key`. More info about authentication can be found in [DNS Made Easy API docs](https://api-docs.dnsmadeeasy.com/).
|
||||||
|
|
||||||
|
{% highlight json %}
|
||||||
|
{
|
||||||
|
"dnsmadeeasy": {
|
||||||
|
"api_key": "1c1a3c91-4770-4ce7-96f4-54c0eb0e457a",
|
||||||
|
"secret_key": "e2268cde-2ccd-4668-a518-8aa8757a65a0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
## Records
|
||||||
|
|
||||||
|
ALIAS/ANAME records are supported.
|
||||||
|
|
||||||
|
This provider does not support HTTPRED records.
|
||||||
|
|
||||||
|
SPF records are ignored by this provider. Use TXT records instead.
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
This provider does not recognize any special metadata fields unique to DNS Made Easy.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Example Javascript:
|
||||||
|
|
||||||
|
{% highlight js %}
|
||||||
|
var REG_NONE = NewRegistrar('none', 'NONE')
|
||||||
|
var DNSMADEEASY = NewDnsProvider("dnsmadeeasy", "DNSMADEEASY");
|
||||||
|
|
||||||
|
D("example.tld", REG_NONE, DnsProvider(DNSMADEEASY),
|
||||||
|
A("test","1.2.3.4")
|
||||||
|
);
|
||||||
|
{%endhighlight%}
|
||||||
|
|
||||||
|
## Activation
|
||||||
|
You can generate your `api_key` and `secret_key` in [Control Panel](https://cp.dnsmadeeasy.com/) in Account Information in Config menu.
|
||||||
|
|
||||||
|
API is only available for Business plan and higher plans.
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
### Global Traffic Director
|
||||||
|
Global Traffic Director feature is not supported.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
Set `DNSMADEEASY_DEBUG_HTTP` environment variable to dump all API calls made by this provider.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
Set `sandbox` key to any non-empty value in credentials JSON alongside `api_key` and `secret_key` to make all API calls against DNS Made Easy sandbox environment.
|
|
@ -78,6 +78,7 @@ Maintainers of contributed providers:
|
||||||
* `DIGITALOCEAN` @Deraen
|
* `DIGITALOCEAN` @Deraen
|
||||||
* `DNSOVERHTTPS` @mikenz
|
* `DNSOVERHTTPS` @mikenz
|
||||||
* `DNSIMPLE` @aeden
|
* `DNSIMPLE` @aeden
|
||||||
|
* `DNSMADEEASY` @vojtad
|
||||||
* `EXOSCALE` @pierre-emmanuelJ
|
* `EXOSCALE` @pierre-emmanuelJ
|
||||||
* `GANDI_V5` @TomOnTime
|
* `GANDI_V5` @TomOnTime
|
||||||
* `HEDNS` @rblenkinsopp
|
* `HEDNS` @rblenkinsopp
|
||||||
|
|
|
@ -48,6 +48,12 @@
|
||||||
"domain": "$DNSIMPLE_DOMAIN",
|
"domain": "$DNSIMPLE_DOMAIN",
|
||||||
"token": "$DNSIMPLE_TOKEN"
|
"token": "$DNSIMPLE_TOKEN"
|
||||||
},
|
},
|
||||||
|
"DNSMADEEASY": {
|
||||||
|
"domain": "$DNSMADEEASY_DOMAIN",
|
||||||
|
"sandbox": "true",
|
||||||
|
"api_key": "$DNSMADEEASY_API_KEY",
|
||||||
|
"secret_key": "$DNSMADEEASY_SECRET_KEY"
|
||||||
|
},
|
||||||
"EXOSCALE": {
|
"EXOSCALE": {
|
||||||
"apikey": "$EXOSCALE_API_KEY",
|
"apikey": "$EXOSCALE_API_KEY",
|
||||||
"dns-endpoint": "https://api.exoscale.ch/dns",
|
"dns-endpoint": "https://api.exoscale.ch/dns",
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
_ "github.com/StackExchange/dnscontrol/v3/providers/desec"
|
_ "github.com/StackExchange/dnscontrol/v3/providers/desec"
|
||||||
_ "github.com/StackExchange/dnscontrol/v3/providers/digitalocean"
|
_ "github.com/StackExchange/dnscontrol/v3/providers/digitalocean"
|
||||||
_ "github.com/StackExchange/dnscontrol/v3/providers/dnsimple"
|
_ "github.com/StackExchange/dnscontrol/v3/providers/dnsimple"
|
||||||
|
_ "github.com/StackExchange/dnscontrol/v3/providers/dnsmadeeasy"
|
||||||
_ "github.com/StackExchange/dnscontrol/v3/providers/doh"
|
_ "github.com/StackExchange/dnscontrol/v3/providers/doh"
|
||||||
_ "github.com/StackExchange/dnscontrol/v3/providers/exoscale"
|
_ "github.com/StackExchange/dnscontrol/v3/providers/exoscale"
|
||||||
_ "github.com/StackExchange/dnscontrol/v3/providers/gandiv5"
|
_ "github.com/StackExchange/dnscontrol/v3/providers/gandiv5"
|
||||||
|
|
155
providers/dnsmadeeasy/api.go
Normal file
155
providers/dnsmadeeasy/api.go
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
package dnsmadeeasy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dnsMadeEasyProvider struct {
|
||||||
|
restAPI *dnsMadeEasyRestAPI
|
||||||
|
domains map[string]multiDomainResponseDataEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProvider(apiKey string, secretKey string, sandbox bool, debug bool) *dnsMadeEasyProvider {
|
||||||
|
fmt.Println("creating DNSMADEEASY provider for sandbox")
|
||||||
|
|
||||||
|
baseURL := baseURLV2_0
|
||||||
|
if sandbox {
|
||||||
|
baseURL = sandboxBaseURLV2_0
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dnsMadeEasyProvider{
|
||||||
|
restAPI: &dnsMadeEasyRestAPI{
|
||||||
|
apiKey: apiKey,
|
||||||
|
secretKey: secretKey,
|
||||||
|
baseURL: baseURL,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
dumpHTTPRequest: debug,
|
||||||
|
dumpHTTPResponse: debug,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *dnsMadeEasyProvider) loadDomains() error {
|
||||||
|
if api.domains != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
domains := map[string]multiDomainResponseDataEntry{}
|
||||||
|
|
||||||
|
res, err := api.restAPI.multiDomainGet()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fetching domains from DNSMADEEASY failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, domain := range res.Data {
|
||||||
|
if domain.GtdEnabled {
|
||||||
|
return fmt.Errorf("fetching domains from DNSMADEEASY failed: domains with GTD enabled are not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
domains[domain.Name] = domain
|
||||||
|
}
|
||||||
|
|
||||||
|
api.domains = domains
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *dnsMadeEasyProvider) domainExists(name string) (bool, error) {
|
||||||
|
if err := api.loadDomains(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := api.domains[name]
|
||||||
|
|
||||||
|
return ok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *dnsMadeEasyProvider) findDomain(name string) (*multiDomainResponseDataEntry, error) {
|
||||||
|
if err := api.loadDomains(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, ok := api.domains[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("domain not found on this DNSMADEEASY account: %q", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &domain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *dnsMadeEasyProvider) fetchDomainRecords(domainName string) ([]recordResponseDataEntry, error) {
|
||||||
|
domain, err := api.findDomain(domainName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := api.restAPI.recordGet(domain.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetching records failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
records := make([]recordResponseDataEntry, 0)
|
||||||
|
for _, record := range res.Data {
|
||||||
|
if record.GtdLocation != "DEFAULT" {
|
||||||
|
return nil, fmt.Errorf("fetching records from DNSMADEEASY failed: only records with DEFAULT GTD location are supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *dnsMadeEasyProvider) fetchDomainNameServers(domainName string) ([]string, error) {
|
||||||
|
domain, err := api.findDomain(domainName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := api.restAPI.singleDomainGet(domain.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetching domain from DNSMADEEASY failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameServers []string
|
||||||
|
for i := range res.NameServers {
|
||||||
|
nameServers = append(nameServers, res.NameServers[i].Fqdn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nameServers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *dnsMadeEasyProvider) createDomain(domain string) error {
|
||||||
|
_, err := api.restAPI.singleDomainCreate(singleDomainRequestData{Name: domain})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset cached domains after adding a new one, they will be refetched when needed
|
||||||
|
api.domains = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *dnsMadeEasyProvider) deleteRecords(domainID int, recordIds []int) error {
|
||||||
|
err := api.restAPI.multiRecordDelete(domainID, recordIds)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *dnsMadeEasyProvider) updateRecords(domainID int, records []recordRequestData) error {
|
||||||
|
err := api.restAPI.multiRecordUpdate(domainID, records)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *dnsMadeEasyProvider) createRecords(domainID int, records []recordRequestData) error {
|
||||||
|
_, err := api.restAPI.multiRecordCreate(domainID, records)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
17
providers/dnsmadeeasy/auditrecords.go
Normal file
17
providers/dnsmadeeasy/auditrecords.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package dnsmadeeasy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/models"
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/pkg/recordaudit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuditRecords returns an error if any records are not
|
||||||
|
// supportable by this provider.
|
||||||
|
func AuditRecords(records []*models.RecordConfig) error {
|
||||||
|
if err := recordaudit.TxtNoDoubleQuotes(records); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Still needed as of 2021-03-11
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
235
providers/dnsmadeeasy/dnsMadeEasyProvider.go
Normal file
235
providers/dnsmadeeasy/dnsMadeEasyProvider.go
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
package dnsmadeeasy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/models"
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/providers"
|
||||||
|
)
|
||||||
|
|
||||||
|
var features = providers.DocumentationNotes{
|
||||||
|
providers.CanUseAlias: providers.Can(),
|
||||||
|
providers.CanUsePTR: providers.Can(),
|
||||||
|
providers.CanUseCAA: providers.Can(),
|
||||||
|
providers.CanUseSRV: providers.Can(),
|
||||||
|
providers.CanUseTLSA: providers.Cannot(),
|
||||||
|
providers.CanUseSSHFP: providers.Cannot(),
|
||||||
|
providers.CanUseDS: providers.Cannot(),
|
||||||
|
providers.CanUseDSForChildren: providers.Cannot(),
|
||||||
|
providers.DocCreateDomains: providers.Can(),
|
||||||
|
providers.DocDualHost: providers.Can("System NS records cannot be edited. Custom apex NS records can be added/changed/deleted."),
|
||||||
|
providers.DocOfficiallySupported: providers.Cannot(),
|
||||||
|
providers.CanGetZones: providers.Can(),
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
fns := providers.DspFuncs{
|
||||||
|
Initializer: New,
|
||||||
|
RecordAuditor: AuditRecords,
|
||||||
|
}
|
||||||
|
|
||||||
|
providers.RegisterDomainServiceProviderType("DNSMADEEASY", fns, features)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new API handle.
|
||||||
|
func New(settings map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
|
||||||
|
if settings["api_key"] == "" {
|
||||||
|
return nil, fmt.Errorf("missing DNSMADEEASY api_key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings["secret_key"] == "" {
|
||||||
|
return nil, fmt.Errorf("missing DNSMADEEASY secret_key")
|
||||||
|
}
|
||||||
|
|
||||||
|
sandbox := false
|
||||||
|
if settings["sandbox"] != "" {
|
||||||
|
sandbox = true
|
||||||
|
}
|
||||||
|
|
||||||
|
debug := false
|
||||||
|
if os.Getenv("DNSMADEEASY_DEBUG_HTTP") == "1" {
|
||||||
|
debug = true
|
||||||
|
}
|
||||||
|
|
||||||
|
api := newProvider(settings["api_key"], settings["secret_key"], sandbox, debug)
|
||||||
|
|
||||||
|
return api, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDomainCorrections returns the corrections for a domain.
|
||||||
|
func (api *dnsMadeEasyProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||||
|
dc, err := dc.Copy()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dc.Punycode()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ALIAS is called ANAME on DNS Made Easy
|
||||||
|
for _, rec := range dc.Records {
|
||||||
|
if rec.Type == "ALIAS" {
|
||||||
|
rec.Type = "ANAME"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
domainName := dc.Name
|
||||||
|
|
||||||
|
domain, err := api.findDomain(domainName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get existing records
|
||||||
|
existingRecords, err := api.GetZoneRecords(domainName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize
|
||||||
|
models.PostProcessRecords(existingRecords)
|
||||||
|
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
|
||||||
|
|
||||||
|
differ := diff.New(dc)
|
||||||
|
_, create, del, modify, err := differ.IncrementalDiff(existingRecords)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var corrections []*models.Correction
|
||||||
|
|
||||||
|
var deleteRecordIds []int
|
||||||
|
deleteDescription := []string{"Batch deletion of records:"}
|
||||||
|
for _, m := range del {
|
||||||
|
originalRecordID := m.Existing.Original.(*recordResponseDataEntry).ID
|
||||||
|
deleteRecordIds = append(deleteRecordIds, originalRecordID)
|
||||||
|
deleteDescription = append(deleteDescription, m.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(deleteRecordIds) > 0 {
|
||||||
|
corr := &models.Correction{
|
||||||
|
Msg: strings.Join(deleteDescription, "\n\t"),
|
||||||
|
F: func() error {
|
||||||
|
return api.deleteRecords(domain.ID, deleteRecordIds)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
corrections = append(corrections, corr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var createRecords []recordRequestData
|
||||||
|
createDescription := []string{"Batch creation of records:"}
|
||||||
|
for _, m := range create {
|
||||||
|
record := fromRecordConfig(m.Desired)
|
||||||
|
createRecords = append(createRecords, *record)
|
||||||
|
createDescription = append(createDescription, m.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(createRecords) > 0 {
|
||||||
|
corr := &models.Correction{
|
||||||
|
Msg: strings.Join(createDescription, "\n\t"),
|
||||||
|
F: func() error {
|
||||||
|
return api.createRecords(domain.ID, createRecords)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
corrections = append(corrections, corr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var modifyRecords []recordRequestData
|
||||||
|
modifyDescription := []string{"Batch modification of records:"}
|
||||||
|
for _, m := range modify {
|
||||||
|
originalRecord := m.Existing.Original.(*recordResponseDataEntry)
|
||||||
|
|
||||||
|
record := fromRecordConfig(m.Desired)
|
||||||
|
record.ID = originalRecord.ID
|
||||||
|
record.GtdLocation = originalRecord.GtdLocation
|
||||||
|
|
||||||
|
modifyRecords = append(modifyRecords, *record)
|
||||||
|
modifyDescription = append(modifyDescription, m.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(modifyRecords) > 0 {
|
||||||
|
corr := &models.Correction{
|
||||||
|
Msg: strings.Join(modifyDescription, "\n\t"),
|
||||||
|
F: func() error {
|
||||||
|
return api.updateRecords(domain.ID, modifyRecords)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
corrections = append(corrections, corr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return corrections, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureDomainExists returns an error if domain doesn't exist.
|
||||||
|
func (api *dnsMadeEasyProvider) EnsureDomainExists(domainName string) error {
|
||||||
|
exists, err := api.domainExists(domainName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// domain already exists
|
||||||
|
if exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.createDomain(domainName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNameservers returns the nameservers for a domain.
|
||||||
|
func (api *dnsMadeEasyProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
||||||
|
nameServers, err := api.fetchDomainNameServers(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.ToNameservers(nameServers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
||||||
|
func (api *dnsMadeEasyProvider) GetZoneRecords(domain string) (models.Records, error) {
|
||||||
|
records, err := api.fetchDomainRecords(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nameServers, err := api.fetchDomainNameServers(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
existingRecords := make([]*models.RecordConfig, 0, len(records))
|
||||||
|
for i := range records {
|
||||||
|
// Ignore HTTPRED and SPF records
|
||||||
|
if records[i].Type == "HTTPRED" || records[i].Type == "SPF" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existingRecords = append(existingRecords, toRecordConfig(domain, &records[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range nameServers {
|
||||||
|
existingRecords = append(existingRecords, systemNameServerToRecordConfig(domain, nameServers[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingRecords, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListZones lists the zones on this account.
|
||||||
|
func (api *dnsMadeEasyProvider) ListZones() ([]string, error) {
|
||||||
|
if err := api.loadDomains(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var zones []string
|
||||||
|
for i := range api.domains {
|
||||||
|
zones = append(zones, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return zones, nil
|
||||||
|
}
|
275
providers/dnsmadeeasy/restApi.go
Normal file
275
providers/dnsmadeeasy/restApi.go
Normal file
|
@ -0,0 +1,275 @@
|
||||||
|
package dnsmadeeasy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
baseURLV2_0 = "https://api.dnsmadeeasy.com/V2.0/"
|
||||||
|
sandboxBaseURLV2_0 = "https://api.sandbox.dnsmadeeasy.com/V2.0/"
|
||||||
|
requestDateHeaderLayout = "Mon, 2 Jan 2006 15:04:05 MST"
|
||||||
|
initialBackoff = time.Second * 10 // First backoff delay duration
|
||||||
|
maxBackoff = time.Minute * 3 // Maximum backoff delay
|
||||||
|
)
|
||||||
|
|
||||||
|
type dnsMadeEasyRestAPI struct {
|
||||||
|
baseURL string
|
||||||
|
httpClient *http.Client
|
||||||
|
|
||||||
|
apiKey string
|
||||||
|
secretKey string
|
||||||
|
|
||||||
|
dumpHTTPRequest bool
|
||||||
|
dumpHTTPResponse bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiErrorResponse struct {
|
||||||
|
Error []string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiEmptyResponse struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiRequest struct {
|
||||||
|
method string
|
||||||
|
endpoint string
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (restApi *dnsMadeEasyRestAPI) singleDomainGet(domainID int) (*singleDomainResponse, error) {
|
||||||
|
req := &apiRequest{
|
||||||
|
method: "GET",
|
||||||
|
endpoint: fmt.Sprintf("dns/managed/%d", domainID),
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &singleDomainResponse{}
|
||||||
|
_, err := restApi.sendRequest(req, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (restApi *dnsMadeEasyRestAPI) multiDomainGet() (*multiDomainResponse, error) {
|
||||||
|
req := &apiRequest{
|
||||||
|
method: "GET",
|
||||||
|
endpoint: "dns/managed/",
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &multiDomainResponse{}
|
||||||
|
_, err := restApi.sendRequest(req, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (restApi *dnsMadeEasyRestAPI) recordGet(domainID int) (*recordResponse, error) {
|
||||||
|
req := &apiRequest{
|
||||||
|
method: "GET",
|
||||||
|
endpoint: fmt.Sprintf("dns/managed/%d/records", domainID),
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &recordResponse{}
|
||||||
|
_, err := restApi.sendRequest(req, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (restApi *dnsMadeEasyRestAPI) singleDomainCreate(data singleDomainRequestData) (*singleDomainResponse, error) {
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &apiRequest{
|
||||||
|
method: "POST",
|
||||||
|
endpoint: fmt.Sprintf("dns/managed/"),
|
||||||
|
data: jsonData,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &singleDomainResponse{}
|
||||||
|
_, err = restApi.sendRequest(req, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (restApi *dnsMadeEasyRestAPI) multiRecordCreate(domainID int, data []recordRequestData) (*[]recordResponseDataEntry, error) {
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &apiRequest{
|
||||||
|
method: "POST",
|
||||||
|
endpoint: fmt.Sprintf("dns/managed/%d/records/createMulti", domainID),
|
||||||
|
data: jsonData,
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &[]recordResponseDataEntry{}
|
||||||
|
_, err = restApi.sendRequest(req, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (restApi *dnsMadeEasyRestAPI) multiRecordUpdate(domainID int, data []recordRequestData) error {
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &apiRequest{
|
||||||
|
method: "PUT",
|
||||||
|
endpoint: fmt.Sprintf("dns/managed/%d/records/updateMulti", domainID),
|
||||||
|
data: jsonData,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = restApi.sendRequest(req, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (restApi *dnsMadeEasyRestAPI) multiRecordDelete(domainID int, recordIDs []int) error {
|
||||||
|
params := []string{}
|
||||||
|
for i := range recordIDs {
|
||||||
|
params = append(params, fmt.Sprintf("ids=%d", recordIDs[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &apiRequest{
|
||||||
|
method: "DELETE",
|
||||||
|
endpoint: fmt.Sprintf("dns/managed/%d/records?%s", domainID, strings.Join(params, "&")),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := restApi.sendRequest(req, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (restApi *dnsMadeEasyRestAPI) createRequestAuthHeaders() (string, string) {
|
||||||
|
t := time.Now()
|
||||||
|
requestDate := t.UTC().Format(requestDateHeaderLayout)
|
||||||
|
|
||||||
|
mac := hmac.New(sha1.New, []byte(restApi.secretKey))
|
||||||
|
mac.Write([]byte(requestDate))
|
||||||
|
macStr := hex.EncodeToString(mac.Sum(nil))
|
||||||
|
|
||||||
|
return requestDate, macStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (restApi *dnsMadeEasyRestAPI) createRequest(request *apiRequest) (*http.Request, error) {
|
||||||
|
url := restApi.baseURL + request.endpoint
|
||||||
|
var req *http.Request
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if request.method == "PUT" || request.method == "POST" {
|
||||||
|
req, err = http.NewRequest(request.method, url, bytes.NewBuffer([]byte(request.data)))
|
||||||
|
} else if request.method == "GET" || request.method == "DELETE" {
|
||||||
|
req, err = http.NewRequest(request.method, url, nil)
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("unknown API request method in DNSMADEEASY REST API: %s", request.method)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
requestDate, hmac := restApi.createRequestAuthHeaders()
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("x-dnsme-apiKey", restApi.apiKey)
|
||||||
|
req.Header.Set("x-dnsme-hmac", hmac)
|
||||||
|
req.Header.Set("x-dnsme-requestDate", requestDate)
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS Made Simple only allows 150 request / 5 minutes
|
||||||
|
// backoff is the amount of time to sleep if a "Rate limit exceeded" error is received
|
||||||
|
// It is increased up to maxBackoff after each use
|
||||||
|
// It is reset after successful request
|
||||||
|
var backoff = initialBackoff
|
||||||
|
|
||||||
|
func (restApi *dnsMadeEasyRestAPI) sendRequest(request *apiRequest, response interface{}) (int, error) {
|
||||||
|
retry:
|
||||||
|
req, err := restApi.createRequest(request)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if restApi.dumpHTTPRequest {
|
||||||
|
dump, _ := httputil.DumpRequest(req, true)
|
||||||
|
fmt.Println(string(dump))
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := restApi.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if restApi.dumpHTTPResponse {
|
||||||
|
dump, _ := httputil.DumpResponse(res, true)
|
||||||
|
fmt.Println(string(dump))
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
|
||||||
|
var apiErr apiErrorResponse
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&apiErr)
|
||||||
|
if err != nil {
|
||||||
|
return res.StatusCode, fmt.Errorf("DNSMADEEASY API unknown error, status code: %d", res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(apiErr.Error) == 1 && apiErr.Error[0] == "Rate limit exceeded" {
|
||||||
|
fmt.Printf("pausing DNSMADEEASY due to ratelimit: %v seconds\n", backoff)
|
||||||
|
|
||||||
|
time.Sleep(backoff)
|
||||||
|
|
||||||
|
backoff = backoff + (backoff / 2)
|
||||||
|
if backoff > maxBackoff {
|
||||||
|
backoff = maxBackoff
|
||||||
|
}
|
||||||
|
|
||||||
|
goto retry
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.StatusCode, fmt.Errorf("DNSMADEEASY API error: %s", strings.Join(apiErr.Error, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
backoff = initialBackoff
|
||||||
|
|
||||||
|
if response != nil {
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&response)
|
||||||
|
if err != nil {
|
||||||
|
return res.StatusCode, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.StatusCode, nil
|
||||||
|
}
|
193
providers/dnsmadeeasy/types.go
Normal file
193
providers/dnsmadeeasy/types.go
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
package dnsmadeeasy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type singleDomainResponse struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DelegateNameServers []string `json:"delegateNameServers"`
|
||||||
|
NameServers []singleDomainResponseNameServer `json:"nameServers"`
|
||||||
|
ProcessMulti bool `json:"processMulti"`
|
||||||
|
ActiveThirdParties []interface{} `json:"activeThirdParties"`
|
||||||
|
PendingActionID int `json:"pendingActionId"`
|
||||||
|
GtdEnabled bool `json:"gtdEnabled"`
|
||||||
|
Created int64 `json:"created"`
|
||||||
|
Updated int64 `json:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type singleDomainResponseNameServer struct {
|
||||||
|
Fqdn string `json:"fqdn"`
|
||||||
|
Ipv4 string `json:"ipv4"`
|
||||||
|
Ipv6 string `json:"ipv6"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type singleDomainRequestData struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type multiDomainResponse struct {
|
||||||
|
TotalRecords int `json:"totalRecords"`
|
||||||
|
TotalPages int `json:"totalPages"`
|
||||||
|
Data []multiDomainResponseDataEntry `json:"data"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type multiDomainResponseDataEntry struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
FolderID int `json:"folderId"`
|
||||||
|
GtdEnabled bool `json:"gtdEnabled"`
|
||||||
|
ProcessMulti bool `json:"processMulti"`
|
||||||
|
ActiveThirdParties []interface{} `json:"activeThirdParties"`
|
||||||
|
PendingActionID int `json:"pendingActionId"`
|
||||||
|
VanityID int `json:"vanityId,omitempty"`
|
||||||
|
Created int64 `json:"created"`
|
||||||
|
Updated int64 `json:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type recordResponse struct {
|
||||||
|
TotalRecords int `json:"totalRecords"`
|
||||||
|
TotalPages int `json:"totalPages"`
|
||||||
|
Data []recordResponseDataEntry `json:"data"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type recordResponseDataEntry struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
|
||||||
|
Source int `json:"source"`
|
||||||
|
SourceID int `json:"sourceId"`
|
||||||
|
|
||||||
|
DynamicDNS bool `json:"dynamicDns"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
|
||||||
|
// A records
|
||||||
|
Monitor bool `json:"monitor"`
|
||||||
|
Failover bool `json:"failover"`
|
||||||
|
Failed bool `json:"failed"`
|
||||||
|
|
||||||
|
// Global Traffic Director
|
||||||
|
GtdLocation string `json:"gtdLocation"`
|
||||||
|
|
||||||
|
// HTTPRED records
|
||||||
|
Description string `json:"description"`
|
||||||
|
Keywords string `json:"keywords"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
RedirectType string `json:"redirectType"`
|
||||||
|
HardLink bool `json:"hardLink"`
|
||||||
|
|
||||||
|
// MX records
|
||||||
|
MxLevel int `json:"mxLevel"`
|
||||||
|
|
||||||
|
// SRV records
|
||||||
|
Weight int `json:"weight"`
|
||||||
|
Priority int `json:"Priority"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
|
||||||
|
// CAA records
|
||||||
|
CaaType string `json:"caaType"`
|
||||||
|
IssuerCritical int `json:"issuerCritical"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type recordRequestData struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
|
||||||
|
// Global Traffic Director
|
||||||
|
GtdLocation string `json:"gtdLocation"`
|
||||||
|
|
||||||
|
// MX records
|
||||||
|
MxLevel int `json:"mxLevel"`
|
||||||
|
|
||||||
|
// SRV records
|
||||||
|
Weight int `json:"weight,omitempty"`
|
||||||
|
Priority int `json:"priority,omitempty"`
|
||||||
|
Port int `json:"port,omitempty"`
|
||||||
|
|
||||||
|
// CAA records
|
||||||
|
CaaType string `json:"caaType"`
|
||||||
|
IssuerCritical int `json:"issuerCritical"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toRecordConfig(domain string, record *recordResponseDataEntry) *models.RecordConfig {
|
||||||
|
rc := &models.RecordConfig{
|
||||||
|
Type: record.Type,
|
||||||
|
TTL: uint32(record.TTL),
|
||||||
|
Original: record,
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.SetLabel(record.Name, domain)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if record.Type == "MX" {
|
||||||
|
err = rc.SetTargetMX(uint16(record.MxLevel), record.Value)
|
||||||
|
} else if record.Type == "SRV" {
|
||||||
|
err = rc.SetTargetSRV(uint16(record.Priority), uint16(record.Weight), uint16(record.Port), record.Value)
|
||||||
|
} else if record.Type == "CAA" {
|
||||||
|
value, err := strconv.Unquote(record.Value)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = rc.SetTargetCAA(uint8(record.IssuerCritical), record.CaaType, value)
|
||||||
|
} else {
|
||||||
|
err = rc.PopulateFromString(record.Type, record.Value, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromRecordConfig(rc *models.RecordConfig) *recordRequestData {
|
||||||
|
label := rc.GetLabel()
|
||||||
|
if label == "@" {
|
||||||
|
label = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
record := &recordRequestData{
|
||||||
|
Type: rc.Type,
|
||||||
|
TTL: int(rc.TTL),
|
||||||
|
GtdLocation: "DEFAULT",
|
||||||
|
Name: label,
|
||||||
|
Value: rc.GetTargetCombined(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.Type == "MX" {
|
||||||
|
record.MxLevel = int(rc.MxPreference)
|
||||||
|
record.Value = rc.GetTargetField()
|
||||||
|
} else if record.Type == "SRV" {
|
||||||
|
target := rc.GetTargetField()
|
||||||
|
if target == "." {
|
||||||
|
target += "."
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Priority = int(rc.SrvPriority)
|
||||||
|
record.Weight = int(rc.SrvWeight)
|
||||||
|
record.Port = int(rc.SrvPort)
|
||||||
|
record.Value = target
|
||||||
|
} else if record.Type == "CAA" {
|
||||||
|
record.IssuerCritical = int(rc.CaaFlag)
|
||||||
|
record.CaaType = rc.CaaTag
|
||||||
|
record.Value = rc.GetTargetField()
|
||||||
|
}
|
||||||
|
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemNameServerToRecordConfig(domain string, nameServer string) *models.RecordConfig {
|
||||||
|
target := nameServer + "."
|
||||||
|
return toRecordConfig(domain, &recordResponseDataEntry{Type: "NS", Value: target, TTL: int(models.DefaultTTL)})
|
||||||
|
}
|
Loading…
Reference in a new issue