1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

NEW PROVIDER: CSCGLOBAL as DNS Service Provider (#1516)

* Move the registrar features to a separate file

* Prepare the testing framework

* Roughed out functions

* Fix up structs

* WIP!

* First tests pass

* wip!

* Flesh out remaining rTypes, get nameservers, etc

* Fix TXT records

* Clean up code

* More cleanups. Fix CAA/SRV

* Linting

* Cleanups/linting

* Fix CAA [more] and more cleanups

* CSC does not like very long txt records

* Use timer only when interactive

* Disable CAA for now

* Update docs

* Remove debug printf

* add go-isatty

* cleanups
This commit is contained in:
Tom Limoncelli
2022-06-12 16:01:08 -04:00
committed by GitHub
parent 60324bc4f5
commit 752e25471d
17 changed files with 1093 additions and 69 deletions

View File

@@ -6,19 +6,18 @@ import (
"fmt"
"io/ioutil"
"net/http"
"os"
"sort"
"strings"
"time"
"github.com/mattn/go-isatty"
)
const apiBase = "https://apis.cscglobal.com/dbs/api/v2"
// Api layer for CSC Global
type cscglobalProvider struct {
key string
token string
notifyEmails []string
}
type requestParams map[string]string
type errorResponse struct {
@@ -55,8 +54,154 @@ type domainRecord struct {
Nameserver []string `json:"nameservers"`
}
func (c *cscglobalProvider) getNameservers(domain string) ([]string, error) {
var bodyString, err = c.get("/domains/" + domain)
// Get zone
type nativeRecordA = struct {
ID string `json:"id"`
Key string `json:"key"`
Value string `json:"value"`
TTL uint32 `json:"ttl"`
Status string `json:"status"`
}
type nativeRecordCNAME = struct {
ID string `json:"id"`
Key string `json:"key"`
Value string `json:"value"`
TTL uint32 `json:"ttl"`
Status string `json:"status"`
}
type nativeRecordAAAA = struct {
ID string `json:"id"`
Key string `json:"key"`
Value string `json:"value"`
TTL uint32 `json:"ttl"`
Status string `json:"status"`
}
type nativeRecordTXT = struct {
ID string `json:"id"`
Key string `json:"key"`
Value string `json:"value"`
TTL uint32 `json:"ttl"`
Status string `json:"status"`
}
type nativeRecordMX = struct {
ID string `json:"id"`
Key string `json:"key"`
Value string `json:"value"`
TTL uint32 `json:"ttl"`
Status string `json:"status"`
Priority uint16 `json:"priority"`
}
type nativeRecordNS = struct {
ID string `json:"id"`
Key string `json:"key"`
Value string `json:"value"`
TTL uint32 `json:"ttl"`
Status string `json:"status"`
Priority int `json:"priority"`
}
type nativeRecordSRV = struct {
ID string `json:"id"`
Key string `json:"key"`
Value string `json:"value"`
TTL uint32 `json:"ttl"`
Status string `json:"status"`
Priority uint16 `json:"priority"`
Weight uint16 `json:"weight"`
Port uint16 `json:"port"`
}
type nativeRecordCAA = struct {
ID string `json:"id"`
Key string `json:"key"`
Value string `json:"value"`
TTL uint32 `json:"ttl"`
Status string `json:"status"`
Tag string `json:"tag"`
Flag uint8 `json:"flag"`
}
type nativeRecordSOA = struct {
Serial int `json:"serial"`
Refresh int `json:"refresh"`
Retry int `json:"retry"`
Expire int `json:"expire"`
TTL uint32 `json:"ttlMin"`
TTLNeg int `json:"ttlNeg"`
TTLZone int `json:"ttlZone"`
TechEmail string `json:"techEmail"`
MasterHost string `json:"masterHost"`
}
type zoneResponse struct {
ZoneName string `json:"zoneName"`
HostingType string `json:"hostingType"`
A []nativeRecordA `json:"a"`
Cname []nativeRecordCNAME `json:"cname"`
Aaaa []nativeRecordAAAA `json:"aaaa"`
Txt []nativeRecordTXT `json:"txt"`
Mx []nativeRecordMX `json:"mx"`
Ns []nativeRecordNS `json:"ns"`
Srv []nativeRecordSRV `json:"srv"`
Caa []nativeRecordCAA `json:"caa"`
Soa nativeRecordSOA `json:"soa"`
}
// Zone edits
type zoneResourceRecordEdit = struct {
Action string `json:"action"`
RecordType string `json:"recordType"`
CurrentKey string `json:"currentKey,omitempty"`
CurrentValue string `json:"currentValue,omitempty"`
NewKey string `json:"newKey,omitempty"`
NewValue string `json:"newValue,omitempty"`
NewTTL uint32 `json:"newTtl,omitempty"`
// MX and SRV:
NewPriority uint16 `json:"newPriority,omitempty"`
// SRV:
NewWeight uint16 `json:"newWeight,omitempty"`
NewPort uint16 `json:"newPort,omitempty"`
// CAA:
// These are pointers so that we can display the zero-value on demand. If
// they were not pointers, the zero-value ("" and 0) would result in no JSON
// output for those fields. Sometimes we want to generate fields with
// zero-values, such as `"newTag":""`. Thus we make these pointers. The
// zero-value is now "nil". If we want the field to appear in the JSON, we
// set the pointer to a value. It is no longer nil, and will be output even
// if the value at the pointer is zero-value.
// See: https://emretanriverdi.medium.com/json-serialization-in-go-a27aeeb968de
CurrentTag *string `json:"currentTag,omitempty"`
NewTag *string `json:"newTag,omitempty"` // "" needs to be sent explicitly.
NewFlag *uint8 `json:"newFlag,omitempty"` // 0 needs to be sent explictly.
}
type zoneEditRequest = struct {
ZoneName string `json:"zoneName"`
Edits *[]zoneResourceRecordEdit `json:"edits"`
}
type zoneEditRequestResultZoneEditRequestResult struct {
Content struct {
Status string `json:"status"`
Message string `json:"message"`
} `json:"content"`
Links struct {
Self string `json:"self"`
Status string `json:"status"`
} `json:"links"`
}
type zoneEditStatusResultZoneEditStatusResult struct {
Content struct {
Status string `json:"status"`
ErrorDescription string `json:"errorDescription"`
} `json:"content"`
Links struct {
Cancel string `json:"cancel"`
} `json:"links"`
}
func (client *providerClient) getNameservers(domain string) ([]string, error) {
var bodyString, err = client.get("/domains/" + domain)
if err != nil {
return nil, err
}
@@ -69,16 +214,16 @@ func (c *cscglobalProvider) getNameservers(domain string) ([]string, error) {
return ns, nil
}
func (c *cscglobalProvider) updateNameservers(ns []string, domain string) error {
func (client *providerClient) updateNameservers(ns []string, domain string) error {
req := nsModRequest{
Domain: domain,
NameServers: ns,
DNSType: "OTHER_DNS",
ShowPrice: false,
}
if c.notifyEmails != nil {
if client.notifyEmails != nil {
req.Notifications.Enabled = true
req.Notifications.Emails = c.notifyEmails
req.Notifications.Emails = client.notifyEmails
}
req.CustomFields = []string{}
@@ -87,7 +232,7 @@ func (c *cscglobalProvider) updateNameservers(ns []string, domain string) error
return err
}
bodyString, err := c.put("/domains/nsmodification", requestBody)
bodyString, err := client.put("/domains/nsmodification", requestBody)
if err != nil {
return fmt.Errorf("CSC Global: Error update NS : %w", err)
}
@@ -101,17 +246,273 @@ func (c *cscglobalProvider) updateNameservers(ns []string, domain string) error
return nil
}
func (c *cscglobalProvider) put(endpoint string, requestBody []byte) ([]byte, error) {
client := &http.Client{}
// domainsResult is the JSON returned by "/domains". Fields we don't
// use are commented out.
type domainsResult struct {
Meta struct {
NumResults int `json:"numResults"`
Pages int `json:"pages"`
} `json:"meta"`
Domains []struct {
QualifiedDomainName string `json:"qualifiedDomainName"`
// Domain string `json:"domain"`
// Idn string `json:"idn"`
// Extension string `json:"extension"`
// NewGtld bool `json:"newGtld"`
// ManagedStatus string `json:"managedStatus"`
// RegistrationDate string `json:"registrationDate"`
// RegistryExpiryDate string `json:"registryExpiryDate"`
// PaidThroughDate string `json:"paidThroughDate"`
// CountryCode string `json:"countryCode"`
// ServerDeleteProhibited bool `json:"serverDeleteProhibited"`
// ServerTransferProhibited bool `json:"serverTransferProhibited"`
// ServerUpdateProhibited bool `json:"serverUpdateProhibited"`
// DNSType string `json:"dnsType"`
// WhoisPrivacy bool `json:"whoisPrivacy"`
// LocalAgent bool `json:"localAgent"`
// DnssecActivated string `json:"dnssecActivated"`
// CriticalDomain bool `json:"criticalDomain"`
// BusinessUnit string `json:"businessUnit"`
// BrandName string `json:"brandName"`
// IdnReferenceName string `json:"idnReferenceName"`
// CustomFields []interface{} `json:"customFields"`
// Account struct {
// AccountNumber string `json:"accountNumber"`
// AccountName string `json:"accountName"`
// } `json:"account"`
// Urlf struct {
// RedirectType string `json:"redirectType"`
// URLForwarding bool `json:"urlForwarding"`
// } `json:"urlf"`
// NameServers []string `json:"nameServers"`
// WhoisContacts []struct {
// ContactType string `json:"contactType"`
// FirstName string `json:"firstName"`
// LastName string `json:"lastName"`
// Organization string `json:"organization"`
// Street1 string `json:"street1"`
// Street2 string `json:"street2"`
// City string `json:"city"`
// StateProvince string `json:"stateProvince"`
// Country string `json:"country"`
// PostalCode string `json:"postalCode"`
// Email string `json:"email"`
// Phone string `json:"phone"`
// PhoneExtn string `json:"phoneExtn"`
// Fax string `json:"fax"`
// } `json:"whoisContacts"`
// LastModifiedDate string `json:"lastModifiedDate"`
// LastModifiedReason string `json:"lastModifiedReason"`
// LastModifiedDescription string `json:"lastModifiedDescription"`
} `json:"domains"`
// Links struct {
// Self string `json:"self"`
// } `json:"links"`
}
func (client *providerClient) getDomains() ([]string, error) {
var bodyString, err = client.get("/domains")
if err != nil {
return nil, err
}
//fmt.Printf("------------------\n")
//fmt.Printf("DEBUG: GETDOMAINS bodystring = %s\n", bodyString)
//fmt.Printf("------------------\n")
var dr domainsResult
json.Unmarshal(bodyString, &dr)
if dr.Meta.Pages > 1 {
return nil, fmt.Errorf("cscglobal getDomains: unimplemented paganation")
}
var r []string
for _, d := range dr.Domains {
r = append(r, d.QualifiedDomainName)
}
//fmt.Printf("------------------\n")
//fmt.Printf("DEBUG: GETDOMAINS dr = %+v\n", dr)
//fmt.Printf("------------------\n")
return r, nil
}
func (client *providerClient) getZoneRecordsAll(zone string) (*zoneResponse, error) {
var bodyString, err = client.get("/zones/" + zone)
if err != nil {
return nil, err
}
if cscDebug {
fmt.Printf("------------------\n")
fmt.Printf("DEBUG: ZONE RESPONSE = %s\n", bodyString)
fmt.Printf("------------------\n")
}
var dr zoneResponse
json.Unmarshal(bodyString, &dr)
return &dr, nil
}
func (client *providerClient) sendZoneEditRequest(domainname string, edits []zoneResourceRecordEdit) error {
req := zoneEditRequest{
ZoneName: domainname,
Edits: &edits,
}
requestBody, err := json.Marshal(req)
if err != nil {
return err
}
if cscDebug {
fmt.Printf("DEBUG: edit request = %s\n", requestBody)
}
responseBody, err := client.post("/zones/edits", requestBody)
if err != nil {
return err
}
var errResp zoneEditRequestResultZoneEditRequestResult
err = json.Unmarshal(responseBody, &errResp)
if err != nil {
return fmt.Errorf("CSC Global API error: %s DATA: %q", err, errResp)
}
if errResp.Content.Status != "SUCCESS" {
return fmt.Errorf("CSC Global API error: %s DATA: %q", errResp.Content.Status, errResp.Content.Message)
}
// The request was successfully submitted. Now query the status link until the request is complete.
statusURL := errResp.Links.Status
return client.waitRequestURL(statusURL)
}
func (client *providerClient) waitRequest(reqID string) error {
return client.waitRequestURL(apiBase + "/zones/edits/status/" + reqID)
}
func (client *providerClient) waitRequestURL(statusURL string) error {
t1 := time.Now()
for {
statusBody, err := client.geturl(statusURL)
if err != nil {
fmt.Println()
return fmt.Errorf("CSC Global API error: %s DATA: %q", err, statusBody)
}
var statusResp zoneEditStatusResultZoneEditStatusResult
err = json.Unmarshal(statusBody, &statusResp)
if err != nil {
fmt.Println()
return fmt.Errorf("CSC Global API error: %s DATA: %q", err, statusBody)
}
status, msg := statusResp.Content.Status, statusResp.Content.ErrorDescription
if isatty.IsTerminal(os.Stdout.Fd()) {
dur := time.Since(t1).Round(time.Second)
if msg == "" {
fmt.Printf("WAITING: % 6s STATUS=%s \r", dur, status)
} else {
fmt.Printf("WAITING: % 6s STATUS=%s MSG=%q \r", dur, status, msg)
}
}
if status == "FAILED" {
fmt.Println()
parts := strings.Split(statusResp.Links.Cancel, "/")
client.cancelRequest(parts[len(parts)-1])
return fmt.Errorf("update failed: %s %s", msg, statusURL)
}
if status == "COMPLETED" {
fmt.Println()
break
}
time.Sleep(1 * time.Second)
}
return nil
// Response looks like:
//{
// "content": {
// "status": "SUCCESS",
// "message": "The publish request was successfully enqueued."
// },
// "links": {
// "self": "https://apis.cscglobal.com/dbs/api/v2/zones/edits/9e139e34-a2a1-462e-88ab-3645833a55d4",
// "status": "https://apis.cscglobal.com/dbs/api/v2/zones/edits/status/9e139e34-a2a1-462e-88ab-3645833a55d4"
// }
// }
}
// Cancel pending/stuck edits
type pagedZoneEditResponsePagedZoneEditResponse struct {
Meta struct {
NumResults int `json:"numResults"`
Pages int `json:"pages"`
} `json:"meta"`
ZoneEdits []struct {
ZoneName string `json:"zoneName"`
ID string `json:"id"`
Status string `json:"status"`
} `json:"zoneEdits"`
}
func (client *providerClient) clearRequests(domain string) error {
var bodyString, err = client.get("/zones/edits?filter=zoneName==" + domain)
if err != nil {
return err
}
var dr pagedZoneEditResponsePagedZoneEditResponse
json.Unmarshal(bodyString, &dr)
// TODO(tlim): Properly handle paganation.
if dr.Meta.Pages != 1 {
return fmt.Errorf("cancelPendingEdits failed: Pages=%d", dr.Meta.Pages)
}
for i, ze := range dr.ZoneEdits {
if cscDebug {
if ze.Status != "COMPLETED" && ze.Status != "CANCELED" {
fmt.Printf("REQUEST %d: %s %s\n", i, ze.ID, ze.Status)
}
}
switch ze.Status {
case "PROPAGATING":
fmt.Printf("INFO: Waiting for id=%s status=%s\n", ze.ID, ze.Status)
client.waitRequest(ze.ID)
case "FAILED":
fmt.Printf("INFO: Deleting request status=%s id=%s\n", ze.Status, ze.ID)
client.cancelRequest(ze.ID)
case "COMPLETED", "CANCELED":
continue
default:
return fmt.Errorf("cscglobal ClearRequests: unimplemented status: %q", ze.Status)
}
}
return nil
}
func (client *providerClient) cancelRequest(reqID string) error {
_, err := client.delete("/zones/edits/" + reqID)
return err
}
func (client *providerClient) put(endpoint string, requestBody []byte) ([]byte, error) {
hclient := &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("apikey", client.key)
req.Header.Add("Authorization", "Bearer "+client.token)
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
resp, err := hclient.Do(req)
if err != nil {
return nil, err
}
@@ -135,16 +536,95 @@ func (c *cscglobalProvider) put(endpoint string, requestBody []byte) ([]byte, er
req.Host, req.URL.RequestURI())
}
func (c *cscglobalProvider) get(endpoint string) ([]byte, error) {
client := &http.Client{}
req, _ := http.NewRequest("GET", apiBase+endpoint, nil)
func (client *providerClient) delete(endpoint string) ([]byte, error) {
hclient := &http.Client{}
fmt.Printf("DEBUG: delete endpoint: %q\n", apiBase+endpoint)
req, _ := http.NewRequest("DELETE", apiBase+endpoint, nil)
// Add headers
req.Header.Add("apikey", c.key)
req.Header.Add("Authorization", "Bearer "+c.token)
req.Header.Add("apikey", client.key)
req.Header.Add("Authorization", "Bearer "+client.token)
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
resp, err := hclient.Do(req)
if err != nil {
return nil, err
}
bodyString, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode == 200 {
fmt.Printf("DEBUG: Delete successful (200)\n")
return bodyString, nil
}
fmt.Printf("DEBUG: Delete failed (%d)\n", resp.StatusCode)
// 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 (client *providerClient) post(endpoint string, requestBody []byte) ([]byte, error) {
hclient := &http.Client{}
req, _ := http.NewRequest("POST", apiBase+endpoint, bytes.NewBuffer(requestBody))
// Add headers
req.Header.Add("apikey", client.key)
req.Header.Add("Authorization", "Bearer "+client.token)
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
resp, err := hclient.Do(req)
if err != nil {
return nil, err
}
bodyString, _ := ioutil.ReadAll(resp.Body)
//fmt.Printf("------------------\n")
//fmt.Printf("DEBUG: resp.StatusCode == %d\n", resp.StatusCode)
//fmt.Printf("POST RESPONSE = %s\n", bodyString)
//fmt.Printf("------------------\n")
if resp.StatusCode == 201 {
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 (client *providerClient) get(endpoint string) ([]byte, error) {
return client.geturl(apiBase + endpoint)
}
func (client *providerClient) geturl(url string) ([]byte, error) {
hclient := &http.Client{}
req, _ := http.NewRequest("GET", url, nil)
// Add headers
req.Header.Add("apikey", client.key)
req.Header.Add("Authorization", "Bearer "+client.token)
req.Header.Add("Accept", "application/json")
resp, err := client.Do(req)
resp, err := hclient.Do(req)
if err != nil {
return nil, err
}

View File

@@ -2,10 +2,39 @@ package cscglobal
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 {
// Each test should be encapsulated in a function that can be tested
// individually. If the test is of general use, add it to the
// recordaudit module.
// Each test should document the last time we verified the test was
// still needed. Sometimes companies change their API.
if err := recordaudit.TxtNoDoubleQuotes(records); err != nil {
return err
} // Needed as of 2022-06-10
if err := recordaudit.TxtNoLen255(records); err != nil {
return err
} // Needed as of 2022-06-10
if err := recordaudit.TxtNoMultipleStrings(records); err != nil {
return err
} // Needed as of 2022-06-10
if err := recordaudit.TxtNoTrailingSpace(records); err != nil {
return err
} // Needed as of 2022-06-10
if err := recordaudit.TxtNotEmpty(records); err != nil {
return err
} // Needed as of 2022-06-10
return nil
}

View File

@@ -0,0 +1,129 @@
package cscglobal
// Convert the provider's native record description to models.RecordConfig.
import (
"net"
"github.com/StackExchange/dnscontrol/v3/models"
)
// nativeToRecordA takes an A record from DNS and returns a native RecordConfig struct.
func nativeToRecordA(nr nativeRecordA, origin string, defaultTTL uint32) *models.RecordConfig {
ttl := nr.TTL
if ttl == 0 {
ttl = defaultTTL
}
rc := &models.RecordConfig{
Type: "A",
TTL: ttl,
}
rc.SetLabel(nr.Key, origin)
rc.SetTargetIP(net.ParseIP(nr.Value).To4())
return rc
}
// nativeToRecordCNAME takes a CNAME record from DNS and returns a native RecordConfig struct.
func nativeToRecordCNAME(nr nativeRecordCNAME, origin string, defaultTTL uint32) *models.RecordConfig {
ttl := nr.TTL
if ttl == 0 {
ttl = defaultTTL
}
rc := &models.RecordConfig{
Type: "CNAME",
TTL: ttl,
}
rc.SetLabel(nr.Key, origin)
rc.SetTarget(nr.Value)
return rc
}
// nativeToRecordA takes an AAAA record from DNS and returns a native RecordConfig struct.
func nativeToRecordAAAA(nr nativeRecordAAAA, origin string, defaultTTL uint32) *models.RecordConfig {
ttl := nr.TTL
if ttl == 0 {
ttl = defaultTTL
}
rc := &models.RecordConfig{
Type: "AAAA",
TTL: ttl,
}
rc.SetLabel(nr.Key, origin)
rc.SetTargetIP(net.ParseIP(nr.Value).To16())
return rc
}
// nativeToRecordTXT takes a TXT record from DNS and returns a native RecordConfig struct.
func nativeToRecordTXT(nr nativeRecordTXT, origin string, defaultTTL uint32) *models.RecordConfig {
ttl := nr.TTL
if ttl == 0 {
ttl = defaultTTL
}
rc := &models.RecordConfig{
Type: "TXT",
TTL: ttl,
}
rc.SetLabel(nr.Key, origin)
rc.SetTargetTXT(nr.Value)
return rc
}
// nativeToRecordMX takes a MX record from DNS and returns a native RecordConfig struct.
func nativeToRecordMX(nr nativeRecordMX, origin string, defaultTTL uint32) *models.RecordConfig {
ttl := nr.TTL
if ttl == 0 {
ttl = defaultTTL
}
rc := &models.RecordConfig{
Type: "MX",
TTL: ttl,
}
rc.SetLabel(nr.Key, origin)
rc.SetTargetMX(nr.Priority, nr.Value)
return rc
}
// nativeToRecordNS takes a NS record from DNS and returns a native RecordConfig struct.
func nativeToRecordNS(nr nativeRecordNS, origin string, defaultTTL uint32) *models.RecordConfig {
ttl := nr.TTL
if ttl == 0 {
ttl = defaultTTL
}
rc := &models.RecordConfig{
Type: "NS",
TTL: ttl,
}
rc.SetLabel(nr.Key, origin)
rc.SetTarget(nr.Value)
return rc
}
// nativeToRecordSRV takes a SRV record from DNS and returns a native RecordConfig struct.
func nativeToRecordSRV(nr nativeRecordSRV, origin string, defaultTTL uint32) *models.RecordConfig {
ttl := nr.TTL
if ttl == 0 {
ttl = defaultTTL
}
rc := &models.RecordConfig{
Type: "SRV",
TTL: ttl,
}
rc.SetLabel(nr.Key, origin)
rc.SetTargetSRV(nr.Priority, nr.Weight, nr.Port, nr.Value)
return rc
}
// nativeToRecordCAA takes a CAA record from DNS and returns a native RecordConfig struct.
func nativeToRecordCAA(nr nativeRecordCAA, origin string, defaultTTL uint32) *models.RecordConfig {
ttl := nr.TTL
if ttl == 0 {
ttl = defaultTTL
}
rc := &models.RecordConfig{
Type: "CAA",
TTL: ttl,
}
rc.SetLabel(nr.Key, origin)
rc.SetTargetCAA(nr.Flag, nr.Tag, nr.Value)
return rc
}

View File

@@ -1,11 +1,10 @@
package cscglobal
import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/providers"
)
@@ -19,12 +18,32 @@ Info required in `creds.json`:
- notification_emails (optional) Comma separated list of email addresses to send notifications to
*/
func init() {
providers.RegisterRegistrarType("CSCGLOBAL", newCscGlobal)
type providerClient struct {
key string
token string
notifyEmails []string
}
func newCscGlobal(m map[string]string) (providers.Registrar, error) {
api := &cscglobalProvider{}
var features = providers.DocumentationNotes{
providers.CanGetZones: providers.Can(),
//providers.CanUseCAA: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.DocOfficiallySupported: providers.Can(),
}
// Set cscDebug to true if you want to see the JSON of important API requests and responses.
var cscDebug = false
func newReg(conf map[string]string) (providers.Registrar, error) {
return newProvider(conf)
}
func newDsp(conf map[string]string, meta json.RawMessage) (providers.DNSServiceProvider, error) {
return newProvider(conf)
}
func newProvider(m map[string]string) (*providerClient, error) {
api := &providerClient{}
api.key, api.token = m["api-key"], m["user-token"]
if api.key == "" || api.token == "" {
@@ -38,35 +57,12 @@ func newCscGlobal(m map[string]string) (providers.Registrar, error) {
return api, nil
}
// GetRegistrarCorrections gathers corrections that would being n to match dc.
func (c *cscglobalProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
nss, err := c.getNameservers(dc.Name)
if err != nil {
return nil, err
}
foundNameservers := strings.Join(nss, ",")
func init() {
providers.RegisterRegistrarType("CSCGLOBAL", newReg)
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)
fns := providers.DspFuncs{
Initializer: newDsp,
RecordAuditor: AuditRecords,
}
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
providers.RegisterDomainServiceProviderType("CSCGLOBAL", fns, features)
}

314
providers/cscglobal/dns.go Normal file
View File

@@ -0,0 +1,314 @@
package cscglobal
import (
"fmt"
"strings"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
)
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
func (client *providerClient) GetZoneRecords(domain string) (models.Records, error) {
records, err := client.getZoneRecordsAll(domain)
if err != nil {
return nil, err
}
// Convert them to DNScontrol's native format:
existingRecords := []*models.RecordConfig{}
// Option 1: One long list. If your provider returns one long list,
// convert each one to RecordType like this:
// for _, rr := range records {
// existingRecords = append(existingRecords, nativeToRecord(rr, domain))
//}
// Option 2: Grouped records. Sometimes the provider returns one item per
// label. Each item contains a list of all the records at that label.
// You'll need to split them out into one RecordConfig for each record. An
// example of this is the ROUTE53 provider.
// for _, rg := range records {
// for _, rr := range rg {
// existingRecords = append(existingRecords, nativeToRecords(rg, rr, domain)...)
// }
// }
// Option 3: Something else. In this case, we get a big massive structure
// which needs to be broken up. Still, we're generating a list of
// RecordConfig structures.
defaultTTL := records.Soa.TTL
for _, rr := range records.A {
existingRecords = append(existingRecords, nativeToRecordA(rr, domain, defaultTTL))
}
for _, rr := range records.Cname {
existingRecords = append(existingRecords, nativeToRecordCNAME(rr, domain, defaultTTL))
}
for _, rr := range records.Aaaa {
existingRecords = append(existingRecords, nativeToRecordAAAA(rr, domain, defaultTTL))
}
for _, rr := range records.Txt {
existingRecords = append(existingRecords, nativeToRecordTXT(rr, domain, defaultTTL))
}
for _, rr := range records.Mx {
existingRecords = append(existingRecords, nativeToRecordMX(rr, domain, defaultTTL))
}
for _, rr := range records.Ns {
existingRecords = append(existingRecords, nativeToRecordNS(rr, domain, defaultTTL))
}
for _, rr := range records.Srv {
existingRecords = append(existingRecords, nativeToRecordSRV(rr, domain, defaultTTL))
}
for _, rr := range records.Caa {
existingRecords = append(existingRecords, nativeToRecordCAA(rr, domain, defaultTTL))
}
return existingRecords, nil
}
func (client *providerClient) GetNameservers(domain string) ([]*models.Nameserver, error) {
nss, err := client.getNameservers(domain)
if err != nil {
return nil, err
}
return models.ToNameservers(nss)
}
// GetDomainCorrections get the current and existing records,
// post-process them, and generate corrections.
// NB(tlim): This function should be exactly the same in all DNS providers. Once
// all providers do this, we can eliminate it and use a Go interface instead.
func (client *providerClient) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
existing, err := client.GetZoneRecords(dc.Name)
if err != nil {
return nil, err
}
models.PostProcessRecords(existing)
//txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
clean := PrepFoundRecords(existing)
PrepDesiredRecords(dc)
return client.GenerateDomainCorrections(dc, clean)
}
// PrepFoundRecords munges any records to make them compatible with
// this provider. Usually this is a no-op.
func PrepFoundRecords(recs models.Records) models.Records {
// If there are records that need to be modified, removed, etc. we
// do it here. Usually this is a no-op.
return recs
}
// PrepDesiredRecords munges any records to best suit this provider.
func PrepDesiredRecords(dc *models.DomainConfig) {
// Sort through the dc.Records, eliminate any that can't be
// supported; modify any that need adjustments to work with the
// provider. We try to do minimal changes otherwise it gets
// confusing.
dc.Punycode()
}
// GetDomainCorrections gets existing records, diffs them against existing, and returns corrections.
func (client *providerClient) GenerateDomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
// Read foundRecords:
foundRecords, err := client.GetZoneRecords(dc.Name)
if err != nil {
return nil, fmt.Errorf("c.GetDNSZoneRecords(%v) failed: %v", dc.Name, err)
}
// Normalize
models.PostProcessRecords(foundRecords)
//txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
differ := diff.New(dc)
_, creates, dels, modifications, err := differ.IncrementalDiff(foundRecords)
if err != nil {
return nil, err
}
// How to generate corrections?
// (1) Most providers take individual deletes, creates, and
// modifications:
// // Generate changes.
// corrections := []*models.Correction{}
// for _, del := range dels {
// corrections = append(corrections, client.deleteRec(client.dnsserver, dc.Name, del))
// }
// for _, cre := range creates {
// corrections = append(corrections, client.createRec(client.dnsserver, dc.Name, cre)...)
// }
// for _, m := range modifications {
// corrections = append(corrections, client.modifyRec(client.dnsserver, dc.Name, m))
// }
// return corrections, nil
// (2) Some providers upload the entire zone every time. Look at
// GetDomainCorrections for BIND and NAMECHEAP for inspiration.
// (3) Others do something entirely different. Like CSCGlobal:
// CSCGlobal has a unique API. A list of edits is sent in one API
// call. Edits aren't permitted if an existing edit is being
// processed. Therefore, before we do an edit we block until the
// previous edit is done executing.
var edits []zoneResourceRecordEdit
var descriptions []string
for _, del := range dels {
edits = append(edits, makePurge(dc.Name, del))
descriptions = append(descriptions, del.String())
}
for _, cre := range creates {
edits = append(edits, makeAdd(dc.Name, cre))
descriptions = append(descriptions, cre.String())
}
for _, m := range modifications {
edits = append(edits, makeEdit(dc.Name, m))
descriptions = append(descriptions, m.String())
}
corrections := []*models.Correction{}
if len(edits) > 0 {
c := &models.Correction{
Msg: "\t" + strings.Join(descriptions, "\n\t"),
F: func() error {
// CSCGlobal's API only permits one pending update at a time.
// Therefore we block until any outstanding updates are done.
// We also clear out any failures, since (and I can't believe
// I'm writing this) any time something fails, the failure has
// to be cleared out with an additional API call.
err := client.clearRequests(dc.Name)
if err != nil {
return err
}
return client.sendZoneEditRequest(dc.Name, edits)
},
}
corrections = append(corrections, c)
}
return corrections, nil
}
func makePurge(domainname string, cor diff.Correlation) zoneResourceRecordEdit {
var existingTarget string
switch cor.Existing.Type {
case "TXT":
existingTarget = strings.Join(cor.Existing.TxtStrings, "")
default:
existingTarget = cor.Existing.GetTargetField()
}
zer := zoneResourceRecordEdit{
Action: "PURGE",
RecordType: cor.Existing.Type,
CurrentKey: cor.Existing.Name,
CurrentValue: existingTarget,
}
if cor.Existing.Type == "CAA" {
var tagValue = cor.Existing.CaaTag
//fmt.Printf("DEBUG: CAA TAG = %q\n", tagValue)
zer.CurrentTag = &tagValue
}
return zer
}
func makeAdd(domainname string, cre diff.Correlation) zoneResourceRecordEdit {
rec := cre.Desired
var recTarget string
switch rec.Type {
case "TXT":
recTarget = strings.Join(rec.TxtStrings, "")
default:
recTarget = rec.GetTargetField()
}
zer := zoneResourceRecordEdit{
Action: "ADD",
RecordType: rec.Type,
NewKey: rec.Name,
NewValue: recTarget,
NewTTL: rec.TTL,
}
switch rec.Type {
case "CAA":
var tagValue = rec.CaaTag
var flagValue = rec.CaaFlag
zer.NewTag = &tagValue
zer.NewFlag = &flagValue
case "MX":
zer.NewPriority = rec.MxPreference
case "SRV":
zer.NewPriority = rec.SrvPriority
zer.NewWeight = rec.SrvWeight
zer.NewPort = rec.SrvPort
case "TXT":
zer.NewValue = strings.Join(rec.TxtStrings, "")
default: // "A", "CNAME", "NS"
// Nothing to do.
}
return zer
}
func makeEdit(domainname string, m diff.Correlation) zoneResourceRecordEdit {
old, rec := m.Existing, m.Desired
// TODO: Assert that old.Type == rec.Type
// TODO: Assert that old.Name == rec.Name
var oldTarget, recTarget string
switch old.Type {
case "TXT":
oldTarget = strings.Join(old.TxtStrings, "")
recTarget = strings.Join(rec.TxtStrings, "")
default:
oldTarget = old.GetTargetField()
recTarget = rec.GetTargetField()
}
zer := zoneResourceRecordEdit{
Action: "EDIT",
RecordType: old.Type,
CurrentKey: old.Name,
CurrentValue: oldTarget,
}
if oldTarget != recTarget {
zer.NewValue = recTarget
}
if old.TTL != rec.TTL {
zer.NewTTL = rec.TTL
}
switch old.Type {
case "CAA":
var tagValue = old.CaaTag
zer.CurrentTag = &tagValue
if old.CaaTag != rec.CaaTag {
zer.NewTag = &(rec.CaaTag)
}
if old.CaaFlag != rec.CaaFlag {
zer.NewFlag = &(rec.CaaFlag)
}
case "MX":
if old.MxPreference != rec.MxPreference {
zer.NewPriority = rec.MxPreference
}
case "SRV":
zer.NewWeight = rec.SrvWeight
zer.NewPort = rec.SrvPort
zer.NewPriority = rec.SrvPriority
default: // "A", "CNAME", "NS", "TXT"
// Nothing to do.
}
return zer
}

View File

@@ -0,0 +1,7 @@
package cscglobal
// ListZones returns all the zones in an account
func (client *providerClient) ListZones() ([]string, error) {
return client.getDomains()
}

View File

@@ -0,0 +1,42 @@
package cscglobal
import (
"fmt"
"sort"
"strings"
"github.com/StackExchange/dnscontrol/v3/models"
)
// GetRegistrarCorrections gathers corrections that would being n to match dc.
func (client *providerClient) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
nss, err := client.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 client.updateNameservers(expected, dc.Name)
},
},
}, nil
}
return nil, nil
}

View File

@@ -82,7 +82,7 @@ func CreateRegistrar(rType string, config map[string]string) (Registrar, error)
initer, ok := RegistrarTypes[rType]
if !ok {
return nil, fmt.Errorf("No such registrar type: %q", rType)
return nil, fmt.Errorf("no such registrar type: %q", rType)
}
return initer(config)
}
@@ -97,7 +97,7 @@ func CreateDNSProvider(providerTypeName string, config map[string]string, meta j
p, ok := DNSProviderTypes[providerTypeName]
if !ok {
return nil, fmt.Errorf("No such DNS service provider: %q", providerTypeName)
return nil, fmt.Errorf("no such DNS service provider: %q", providerTypeName)
}
return p.Initializer(config, meta)
}
@@ -135,7 +135,7 @@ func beCompatible(n string, config map[string]string) (string, error) {
func AuditRecords(dType string, rcs models.Records) error {
p, ok := DNSProviderTypes[dType]
if !ok {
return fmt.Errorf("Unknown DNS service provider type: %q", dType)
return fmt.Errorf("unknown DNS service provider type: %q", dType)
}
if p.RecordAuditor == nil {
return fmt.Errorf("DNS service provider type %q has no RecordAuditor", dType)