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
+}