diff --git a/docs/_includes/matrix.html b/docs/_includes/matrix.html index a2a965dd0..e92331641 100644 --- a/docs/_includes/matrix.html +++ b/docs/_includes/matrix.html @@ -12,6 +12,7 @@
BIND
CLOUDFLAREAPI
CLOUDNS
+
CSCGLOBAL
DESEC
DIGITALOCEAN
DNSIMPLE
@@ -148,6 +149,9 @@ + + + @@ -238,6 +242,9 @@ + + + @@ -324,6 +331,7 @@ + @@ -388,6 +396,7 @@ + @@ -442,6 +451,7 @@ + @@ -516,6 +526,7 @@ + @@ -590,6 +601,7 @@ + @@ -638,6 +650,7 @@ + @@ -718,6 +731,7 @@ + @@ -778,6 +792,7 @@ + @@ -840,6 +855,7 @@ + @@ -966,6 +982,7 @@ + @@ -1018,6 +1035,7 @@ + @@ -1092,6 +1110,9 @@ + + + @@ -1272,6 +1293,7 @@ + diff --git a/docs/_providers/cscglobal.md b/docs/_providers/cscglobal.md new file mode 100644 index 000000000..e20124a7a --- /dev/null +++ b/docs/_providers/cscglobal.md @@ -0,0 +1,37 @@ +--- +name: CSC Global +title: CSC Global Provider +layout: default +jsId: CSCGLOBAL +--- +# CSC Global Provider + +DNSControl's CSC Global provider supports being a Registrar. Support for being a DNS Provider is not included, although CSC Global's API does provide for this so it could be implemented in the future. + +## Configuration +In your `creds.json` file, you must provide your API key and user/client token. You can optionally provide an comma separated list of email addresses to have CSC Global send updates to. + +{% highlight json %} +{ + "cscglobal": { + "api-key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "user-token": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy", + "notification_emails": "test@exmaple.tld,hostmaster@example.tld" + } +} +{% endhighlight %} + +## Usage +Example Javascript for `example.tld` and delegated to Route53: + +{% highlight js %} +var REG_CSCGLOBAL = NewRegistrar('cscglobal', 'CSCGLOBAL'); +var R53 = NewDnsProvider('r53_main', 'ROUTE53'); + +D("example.tld", REG_CSCGLOBAL, DnsProvider(R53), + A('test','1.2.3.4') +); +{% endhighlight %} + +## Activation +To get access to the [CSC Global API](https://www.cscglobal.com/cscglobal/docs/dbs/domainmanager/api-v2/) contact your account manager. diff --git a/docs/provider-list.md b/docs/provider-list.md index 79391c57d..37eafb1ec 100644 --- a/docs/provider-list.md +++ b/docs/provider-list.md @@ -73,6 +73,7 @@ Maintainers of contributed providers: * `AXFRDDNS` @hnrgrgr * `CLOUDNS` @pragmaton +* `CSCGLOBAL` @Air-New-Zealand * `DESEC` @D3luxee * `DIGITALOCEAN` @Deraen * `DNSOVERHTTPS` @mikenz diff --git a/providers/_all/all.go b/providers/_all/all.go index 8a9800a4a..2ce06459a 100644 --- a/providers/_all/all.go +++ b/providers/_all/all.go @@ -9,6 +9,7 @@ import ( _ "github.com/StackExchange/dnscontrol/v3/providers/bind" _ "github.com/StackExchange/dnscontrol/v3/providers/cloudflare" _ "github.com/StackExchange/dnscontrol/v3/providers/cloudns" + _ "github.com/StackExchange/dnscontrol/v3/providers/cscglobal" _ "github.com/StackExchange/dnscontrol/v3/providers/desec" _ "github.com/StackExchange/dnscontrol/v3/providers/digitalocean" _ "github.com/StackExchange/dnscontrol/v3/providers/dnsimple" diff --git a/providers/cscglobal/api.go b/providers/cscglobal/api.go new file mode 100644 index 000000000..9ffc243e8 --- /dev/null +++ b/providers/cscglobal/api.go @@ -0,0 +1,175 @@ +package cscglobal + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "sort" +) + +const apiBase = "https://apis.cscglobal.com/dbs/api/v2" + +// Api layer for CSC Global + +type api struct { + key string + token string + notifyEmails []string +} + +type requestParams map[string]string + +type errorResponse struct { + Code string `json:"code"` + Description string `json:"description"` + Value string `json:"value,omitempty"` +} + +type nsModRequest struct { + Domain string `json:"qualifiedDomainName"` + NameServers []string `json:"nameServers"` + DNSType string `json:"dnsType,omitempty"` + Notifications struct { + Enabled bool `json:"enabled,omitempty"` + Emails []string `json:"additionalNotificationEmails,omitempty"` + } `json:"notifications"` + ShowPrice bool `json:"showPrice,omitempty"` + CustomFields []string `json:"customFields,omitempty"` +} + +type nsModRequestResult struct { + Result struct { + Domain string `json:"qualifiedDomainName"` + Status struct { + Code string `json:"code"` + Message string `json:"message"` + AdditionalInformation string `json:"additionalInformation"` + UUID string `json:"uuid"` + } `json:"status"` + } `json:"result"` +} + +type domainRecord struct { + Nameserver []string `json:"nameservers"` +} + +func (c *api) getNameservers(domain string) ([]string, error) { + var bodyString, err = c.get("/domains/" + domain) + if err != nil { + return nil, err + } + + var dr domainRecord + json.Unmarshal(bodyString, &dr) + ns := []string{} + for _, nameserver := range dr.Nameserver { + ns = append(ns, nameserver) + } + sort.Strings(ns) + return ns, nil +} + +func (c *api) updateNameservers(ns []string, domain string) error { + req := nsModRequest{ + Domain: domain, + NameServers: ns, + DNSType: "OTHER_DNS", + ShowPrice: false, + } + if c.notifyEmails != nil { + req.Notifications.Enabled = true + req.Notifications.Emails = c.notifyEmails + } + req.CustomFields = []string{} + + requestBody, err := json.MarshalIndent(req, "", " ") + if err != nil { + return err + } + + bodyString, err := c.put("/domains/nsmodification", requestBody) + if err != nil { + return fmt.Errorf("CSC Global: Error update NS : %w", err) + } + + var res nsModRequestResult + json.Unmarshal(bodyString, &res) + if res.Result.Status.Code != "SUBMITTED" { + return fmt.Errorf("CSC Global: Error update NS Code: %s Message: %s AdditionalInfo: %s", res.Result.Status.Code, res.Result.Status.Message, res.Result.Status.AdditionalInformation) + } + + return nil +} + +func (c *api) put(endpoint string, requestBody []byte) ([]byte, error) { + client := &http.Client{} + req, _ := http.NewRequest("PUT", apiBase+endpoint, bytes.NewReader(requestBody)) + + // Add headers + req.Header.Add("apikey", c.key) + req.Header.Add("Authorization", "Bearer "+c.token) + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + bodyString, _ := ioutil.ReadAll(resp.Body) + if resp.StatusCode == 200 { + return bodyString, nil + } + + // Got a error response from API, see if it's json format + var errResp errorResponse + err = json.Unmarshal(bodyString, &errResp) + if err != nil { + // Some error messages are plain text + return nil, fmt.Errorf("CSC Global API error: %s URL: %s%s", + bodyString, + req.Host, req.URL.RequestURI()) + } + return nil, fmt.Errorf("CSC Global API error code: %s description: %s URL: %s%s", + errResp.Code, errResp.Description, + req.Host, req.URL.RequestURI()) +} + +func (c *api) get(endpoint string) ([]byte, error) { + client := &http.Client{} + req, _ := http.NewRequest("GET", apiBase+endpoint, nil) + + // Add headers + req.Header.Add("apikey", c.key) + req.Header.Add("Authorization", "Bearer "+c.token) + req.Header.Add("Accept", "application/json") + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + bodyString, _ := ioutil.ReadAll(resp.Body) + if resp.StatusCode == 200 { + return bodyString, nil + } + + if resp.StatusCode == 400 { + // 400, error message is in the body as plain text + return nil, fmt.Errorf("CSC Global API error: %w URL: %s%s", + bodyString, + req.Host, req.URL.RequestURI()) + } + + // Got a json error response from API + var errResp errorResponse + err = json.Unmarshal(bodyString, &errResp) + if err != nil { + return nil, err + } + return nil, fmt.Errorf("CSC Global API error code: %s description: %s URL: %s%s", + errResp.Code, errResp.Description, + req.Host, req.URL.RequestURI()) +} diff --git a/providers/cscglobal/cscglobalProvider.go b/providers/cscglobal/cscglobalProvider.go new file mode 100644 index 000000000..186215cfe --- /dev/null +++ b/providers/cscglobal/cscglobalProvider.go @@ -0,0 +1,72 @@ +package cscglobal + +import ( + "fmt" + "sort" + "strings" + + "github.com/StackExchange/dnscontrol/v3/models" + "github.com/StackExchange/dnscontrol/v3/providers" +) + +/* + +CSC Global Registrar: + +Info required in `creds.json`: + - api-key Api Key + - user-token User Token + - notification_emails (optional) Comma seperated list of email addresses to send notifications to +*/ + +func init() { + providers.RegisterRegistrarType("CSCGLOBAL", newCscGlobal) +} + +func newCscGlobal(m map[string]string) (providers.Registrar, error) { + api := &api{} + + api.key, api.token = m["api-key"], m["user-token"] + if api.key == "" || api.token == "" { + return nil, fmt.Errorf("missing CSC Global api-key and/or user-token") + } + + if m["notification_emails"] != "" { + api.notifyEmails = strings.Split(m["notification_emails"], ",") + } + + return api, nil +} + +// GetRegistrarCorrections gathers corrections that would being n to match dc. +func (c *api) 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 { + if ns.Name[len(ns.Name)-1] == '.' { + // When this code was written ns.Name never included a single trailing dot. + // If that changes, the code should change too. + return nil, fmt.Errorf("Name server includes a trailing dot, has the API changed?") + } + expected = append(expected, ns.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 +}