From 3d570ead31e177562150dc3a17c534af1f8abc82 Mon Sep 17 00:00:00 2001 From: PJEilers Date: Tue, 9 Jan 2024 16:45:59 +0100 Subject: [PATCH] NEW DNS PROVIDER: Realtime Register (REALTIMEREGISTER) (#2741) Co-authored-by: pieterjan.eilers Co-authored-by: Tom Limoncelli --- .goreleaser.yml | 2 +- OWNERS | 1 + README.md | 2 + documentation/SUMMARY.md | 1 + documentation/providers.md | 2 + documentation/providers/realtimeregister.md | 46 +++ integrationTest/providers.json | 7 + providers/_all/all.go | 1 + providers/realtimeregister/api.go | 221 ++++++++++++ providers/realtimeregister/auditrecords.go | 19 + .../realtimeregisterProvider.go | 335 ++++++++++++++++++ .../realtimeregisterProvider_test.go | 16 + 12 files changed, 652 insertions(+), 1 deletion(-) create mode 100644 documentation/providers/realtimeregister.md create mode 100644 providers/realtimeregister/api.go create mode 100644 providers/realtimeregister/auditrecords.go create mode 100644 providers/realtimeregister/realtimeregisterProvider.go create mode 100644 providers/realtimeregister/realtimeregisterProvider_test.go diff --git a/.goreleaser.yml b/.goreleaser.yml index 4761bf861..c596a98d8 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -36,7 +36,7 @@ changelog: regexp: "(?i)^.*(major|new provider|feature)[(\\w)]*:+.*$" order: 1 - title: 'Provider-specific changes:' - regexp: "(?i)((akamaiedge|autodns|axfrd|azure|azure_private_dns|bind|bunnydns|cloudflare|cloudflareapi_old|cloudns|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|gandi|gcloud|gcore|hedns|hetzner|hexonet|hostingde|inwx|linode|loopia|luadns|msdns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|route53|rwth|softlayer|transip|vultr).*:)+.*" + regexp: "(?i)((akamaiedge|autodns|axfrd|azure|azure_private_dns|bind|bunnydns|cloudflare|cloudflareapi_old|cloudns|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|gandi|gcloud|gcore|hedns|hetzner|hexonet|hostingde|inwx|linode|loopia|luadns|msdns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|realtimeregister|route53|rwth|softlayer|transip|vultr).*:)+.*" order: 2 - title: 'Documentation:' regexp: "(?i)^.*(docs)[(\\w)]*:+.*$" diff --git a/OWNERS b/OWNERS index 647b0b6e1..dd5797989 100644 --- a/OWNERS +++ b/OWNERS @@ -42,6 +42,7 @@ providers/ovh @masterzen providers/packetframe @hamptonmoore providers/porkbun @imlonghao providers/powerdns @jpbede +providers/realtimeregister @PJEilers providers/route53 @tresni providers/rwth @mistererwin # providers/softlayer NEEDS VOLUNTEER diff --git a/README.md b/README.md index 29a2a46fe..794c68c8c 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ Currently supported DNS providers: - Packetframe - Porkbun - PowerDNS +- Realtime Register - RWTH DNS-Admin - SoftLayer - TransIP @@ -76,6 +77,7 @@ Currently supported Domain Registrars: - Name.com - OpenSRS - OVH +- Realtime Register At Stack Overflow, we use this system to manage hundreds of domains and subdomains across multiple registrars and DNS providers. diff --git a/documentation/SUMMARY.md b/documentation/SUMMARY.md index 88dc47388..0329d777a 100644 --- a/documentation/SUMMARY.md +++ b/documentation/SUMMARY.md @@ -142,6 +142,7 @@ * [Packetframe](providers/packetframe.md) * [Porkbun](providers/porkbun.md) * [PowerDNS](providers/powerdns.md) + * [Realtime Register](providers/realtimeregister.md) * [RWTH DNS-Admin](providers/rwth.md) * [SoftLayer DNS](providers/softlayer.md) * [TransIP](providers/transip.md) diff --git a/documentation/providers.md b/documentation/providers.md index 74a8a17d9..9efd84de7 100644 --- a/documentation/providers.md +++ b/documentation/providers.md @@ -58,6 +58,7 @@ If a feature is definitively not supported for whatever reason, we would also li | [`PACKETFRAME`](providers/packetframe.md) | ❌ | ✅ | ❌ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❌ | ❌ | ✅ | ❔ | | [`PORKBUN`](providers/porkbun.md) | ❌ | ✅ | ✅ | ✅ | ❔ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❔ | ❌ | ❌ | ✅ | ✅ | | [`POWERDNS`](providers/powerdns.md) | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ✅ | +| [`REALTIMEREGISTER`](providers/realtimeregister.md) | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | | [`ROUTE53`](providers/route53.md) | ✅ | ✅ | ✅ | ❌ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ | ✅ | | [`RWTH`](providers/rwth.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ❔ | ❌ | ❌ | ✅ | ❔ | ✅ | ✅ | ❌ | ❔ | ❔ | ❌ | ❌ | ✅ | ✅ | | [`SOFTLAYER`](providers/softlayer.md) | ❌ | ✅ | ❌ | ❔ | ❔ | ❔ | ❌ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ✅ | ❔ | @@ -143,6 +144,7 @@ Providers in this category and their maintainers are: |[`OVH`](providers/ovh.md)|@masterzen| |[`PACKETFRAME`](providers/packetframe.md)|@hamptonmoore| |[`POWERDNS`](providers/powerdns.md)|@jpbede| +|[`REALTIMEREGISTER`](providers/realtimeregister.md)|@PJEilers| |[`ROUTE53`](providers/route53.md)|@tresni| |[`RWTH`](providers/rwth.md)|@MisterErwin| |[`SOFTLAYER`](providers/softlayer.md)|@jamielennox| diff --git a/documentation/providers/realtimeregister.md b/documentation/providers/realtimeregister.md new file mode 100644 index 000000000..12d27dbb9 --- /dev/null +++ b/documentation/providers/realtimeregister.md @@ -0,0 +1,46 @@ +[realtimeregister.com](https://realtimeregister.com) is a domain registrar based in the Netherlands. + +## Configuration + +To use this provider, add an entry to `creds.json` with `TYPE` set to `REALTIMEREGISTER` +along with your API-key. Further configuration includes a flag indicating BASIC or PREMIUM DNS-service and a flag +indicating the use of the sandbox environment + +**Example:** + +{% code title="creds.json" %} +```json +{ + "realtimeregister": { + "TYPE": "REALTIMEREGISTER", + "apikey": "abcdefghijklmnopqrstuvwxyz1234567890", + "sandbox" : "0", + "premium" : "0" + } +} +``` +{% endcode %} + +If sandbox is omitted or set to any other value than "1" the production API will be used. +If premium is set to "1", you will only be able to update zones using Premium DNS. If it is omitted or set to any other value, you +will only be able to update zones using Basic DNS. + +**Important Notes**: +* Anyone with access to this `creds.json` file will have *full* access to your RTR account and will be able to transfer or delete your domains + +## Metadata +This provider does not recognize any special metadata fields unique to Realtime Register. + +## Usage +An example `dnsconfig.js` configuration file + +{% code title="dnsconfig.js" %} +```javascript +var REG_RTR = NewRegistrar("realtimeregister"); +var DSP_RTR = NewDnsProvider("realtimeregister"); + +D("example.com", REG_RTR, DnsProvider(DSP_RTR), + A("test", "1.2.3.4") +); +``` +{% endcode %} diff --git a/integrationTest/providers.json b/integrationTest/providers.json index c8b9eed3f..e931b29f9 100644 --- a/integrationTest/providers.json +++ b/integrationTest/providers.json @@ -258,6 +258,13 @@ "domain": "$POWERDNS_DOMAIN", "serverName": "$POWERDNS_SERVERNAME" }, + "REALTIMEREGISTER": { + "TYPE": "REALTIMEREGISTER", + "apikey": "$REALTIMEREGISTER_APIKEY", + "sandbox" : "$REALTIMEREGISTER_SANDBOX", + "domain": "$REALTIMEREGISTER_DOMAIN", + "premium": "$REALTIMEREGISTER_PREMIUM" + }, "ROUTE53": { "KeyId": "$ROUTE53_KEY_ID", "SecretKey": "$ROUTE53_KEY", diff --git a/providers/_all/all.go b/providers/_all/all.go index 73013d339..c7689b6ae 100644 --- a/providers/_all/all.go +++ b/providers/_all/all.go @@ -47,6 +47,7 @@ import ( _ "github.com/StackExchange/dnscontrol/v4/providers/packetframe" _ "github.com/StackExchange/dnscontrol/v4/providers/porkbun" _ "github.com/StackExchange/dnscontrol/v4/providers/powerdns" + _ "github.com/StackExchange/dnscontrol/v4/providers/realtimeregister" _ "github.com/StackExchange/dnscontrol/v4/providers/route53" _ "github.com/StackExchange/dnscontrol/v4/providers/rwth" _ "github.com/StackExchange/dnscontrol/v4/providers/softlayer" diff --git a/providers/realtimeregister/api.go b/providers/realtimeregister/api.go new file mode 100644 index 000000000..38a7f9f36 --- /dev/null +++ b/providers/realtimeregister/api.go @@ -0,0 +1,221 @@ +package realtimeregister + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +type realtimeregisterAPI struct { + apikey string + endpoint string + Zones map[string]*Zone //cache + ServiceType string +} + +type Zones struct { + Entities []Zone `json:"entities"` +} + +type Domain struct { + Nameservers []string `json:"ns"` +} + +type Zone struct { + Name string `json:"name,omitempty"` + Service string `json:"service,omitempty"` + ID int `json:"id,omitempty"` + Records []Record `json:"records"` + Dnssec bool `json:"dnssec"` +} + +type Record struct { + Name string `json:"name"` + Type string `json:"type"` + Content string `json:"content"` + Priority int `json:"prio,omitempty"` + TTL int `json:"ttl"` +} + +const ( + endpoint = "https://api.yoursrs.com/v2" + endpointSandbox = "https://api.yoursrs-ote.com/v2" +) + +func (api *realtimeregisterAPI) request(method string, url string, body io.Reader) ([]byte, error) { + client := &http.Client{} + req, _ := http.NewRequest( + method, + url, + body, + ) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "ApiKey "+api.apikey) + + resp, err := client.Do(req) + + if err != nil { + return nil, err + } + + bodyString, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("realtime Register API error on request to %s: %d, %s", url, resp.StatusCode, + string(bodyString)) + } + + return bodyString, nil +} + +func (api *realtimeregisterAPI) getZone(domain string) (*Zone, error) { + zones, err := api.getDomainZones(domain) + if err != nil { + return nil, err + } + + if len(zones.Entities) == 0 { + return nil, fmt.Errorf("zone %s does not exist", domain) + } + + api.Zones[domain] = &zones.Entities[0] + + return &zones.Entities[0], nil +} + +func (api *realtimeregisterAPI) getDomainZones(domain string) (*Zones, error) { + + url := fmt.Sprintf(api.endpoint+"/dns/zones?name=%s&service=%s", domain, api.ServiceType) + + return api.getZones(url) +} + +func (api *realtimeregisterAPI) getAllZones() ([]string, error) { + url := fmt.Sprintf(api.endpoint+"/dns/zones?service=%s&export=true&fields=id,name", api.ServiceType) + + zones, err := api.getZones(url) + if err != nil { + return nil, err + } + + zoneNames := make([]string, len(zones.Entities)) + + for i, zone := range zones.Entities { + zoneNames[i] = zone.Name + } + + return zoneNames, nil +} + +func (api *realtimeregisterAPI) getZones(url string) (*Zones, error) { + bodyBytes, err := api.request( + "GET", + url, + nil, + ) + + if err != nil { + return nil, err + } + + respData := &Zones{} + err = json.Unmarshal(bodyBytes, &respData) + if err != nil { + return nil, err + } + + return respData, nil +} + +func (api *realtimeregisterAPI) createZone(domain string) error { + zone := &Zone{ + Records: []Record{}, + Name: domain, + Service: api.ServiceType, + } + + err := api.createOrUpdateZone(zone, api.endpoint+"/dns/zones") + if err != nil { + return err + } + + return nil +} + +func (api *realtimeregisterAPI) zoneExists(domain string) (bool, error) { + if api.Zones[domain] != nil { + return true, nil + } + zones, err := api.getDomainZones(domain) + if err != nil { + return false, err + } + return len(zones.Entities) > 0, nil +} + +func (api *realtimeregisterAPI) getDomainNameservers(domainName string) ([]string, error) { + respData, err := api.request( + "GET", + fmt.Sprintf(api.endpoint+"/domains/%s", domainName), + nil, + ) + if err != nil { + return nil, err + } + domain := &Domain{} + err = json.Unmarshal(respData, &domain) + if err != nil { + return nil, err + } + return domain.Nameservers, nil +} + +func (api *realtimeregisterAPI) updateZone(domain string, body *Zone) error { + return api.createOrUpdateZone( + body, + fmt.Sprintf(api.endpoint+"/dns/zones/%d/update", api.Zones[domain].ID), + ) +} + +func (api *realtimeregisterAPI) updateNameservers(domainName string, nameservers []string) error { + domain := &Domain{ + Nameservers: nameservers, + } + + bodyBytes, err := json.Marshal(domain) + if err != nil { + return err + } + + _, err = api.request( + "POST", + fmt.Sprintf(api.endpoint+"/domains/%s/update", domainName), + bytes.NewReader(bodyBytes), + ) + + if err != nil { + return err + } + return nil +} + +func (api *realtimeregisterAPI) createOrUpdateZone(body *Zone, url string) error { + bodyBytes, err := json.Marshal(body) + + if err != nil { + return err + } + + //Ugly hack for MX records with null target + requestBody := strings.Replace(string(bodyBytes), "\"prio\":-1", "\"prio\":0", -1) + + _, err = api.request("POST", url, strings.NewReader(requestBody)) + if err != nil { + return err + } + return nil +} diff --git a/providers/realtimeregister/auditrecords.go b/providers/realtimeregister/auditrecords.go new file mode 100644 index 000000000..e8f681ff9 --- /dev/null +++ b/providers/realtimeregister/auditrecords.go @@ -0,0 +1,19 @@ +package realtimeregister + +import ( + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/rejectif" +) + +// AuditRecords returns a list of errors corresponding to the records +// that aren't supported by this provider. If all records are +// supported, an empty list is returned. +func AuditRecords(records []*models.RecordConfig) []error { + auditor := rejectif.Auditor{} + + auditor.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2024-01-03 + + auditor.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2024-01-03 + + return auditor.Audit(records) +} diff --git a/providers/realtimeregister/realtimeregisterProvider.go b/providers/realtimeregister/realtimeregisterProvider.go new file mode 100644 index 000000000..dd49c2c08 --- /dev/null +++ b/providers/realtimeregister/realtimeregisterProvider.go @@ -0,0 +1,335 @@ +package realtimeregister + +import ( + "encoding/json" + "fmt" + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/diff2" + "github.com/StackExchange/dnscontrol/v4/providers" + "github.com/miekg/dns/dnsutil" + "golang.org/x/exp/slices" + "sort" + "strconv" + "strings" +) + +/* +Realtime Register DNS provider + +Info required in `creds.json`: + - apikey + - premium: (0 for BASIC or 1 for PREMIUM) + +Additional settings available in `creds.json`: + - sandbox (set to 1 to use the sandbox API from realtime register) +*/ + +var features = providers.DocumentationNotes{ + providers.CanAutoDNSSEC: providers.Can(), + providers.CanGetZones: providers.Can(), + providers.CanUseAlias: providers.Can(), + providers.CanUseCAA: providers.Can(), + providers.CanUseDHCID: providers.Cannot(), + providers.CanUseDS: providers.Cannot("Only for subdomains"), + providers.CanUseDSForChildren: providers.Can(), + providers.CanUseLOC: providers.Can(), + providers.CanUseNAPTR: providers.Can(), + providers.CanUsePTR: providers.Cannot(), + providers.CanUseSRV: providers.Can(), + providers.CanUseSSHFP: providers.Can(), + providers.CanUseSOA: providers.Cannot(), + providers.CanUseTLSA: providers.Can(), + providers.DocCreateDomains: providers.Can(), + providers.DocDualHost: providers.Cannot(), + providers.DocOfficiallySupported: providers.Cannot(), +} + +// init registers the domain service provider with dnscontrol. +func init() { + fns := providers.DspFuncs{ + Initializer: newRtrDsp, + RecordAuditor: AuditRecords, + } + providers.RegisterDomainServiceProviderType("REALTIMEREGISTER", fns, features) + providers.RegisterRegistrarType("REALTIMEREGISTER", newRtrReg) +} + +func newRtr(config map[string]string, metadata json.RawMessage) (*realtimeregisterAPI, error) { + apikey := config["apikey"] + sandbox := config["sandbox"] == "1" + + if apikey == "" { + return nil, fmt.Errorf("realtime register: apikey must be provided") + } + + api := &realtimeregisterAPI{ + apikey: apikey, + endpoint: getEndpoint(sandbox), + Zones: make(map[string]*Zone), + ServiceType: getServiceType(config["premium"] == "1"), + } + + return api, nil +} + +func newRtrDsp(config map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { + return newRtr(config, metadata) +} + +func newRtrReg(config map[string]string) (providers.Registrar, error) { + return newRtr(config, nil) +} + +// GetNameservers Default name servers should not be included in the update +func (api *realtimeregisterAPI) GetNameservers(domain string) ([]*models.Nameserver, error) { + return []*models.Nameserver{}, nil +} + +func (api *realtimeregisterAPI) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) { + response, err := api.getZone(domain) + if err != nil { + return nil, err + } + records := response.Records + recordConfigs := make([]*models.RecordConfig, len(records)) + for i := range records { + recordConfigs[i] = toRecordConfig(domain, &records[i]) + } + + return recordConfigs, nil +} + +func (api *realtimeregisterAPI) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) { + msgs, changes, err := diff2.ByZone(existing, dc, nil) + if err != nil { + return nil, err + } + + var corrections []*models.Correction + + if !changes { + return corrections, nil + } + + dnssec := api.Zones[dc.Name].Dnssec + + if api.Zones[dc.Name].Dnssec && dc.AutoDNSSEC == "off" { + dnssec = false + corrections = append(corrections, + &models.Correction{ + Msg: "Update DNSSEC on -> off", + F: func() error { + return nil + }, + }) + } + + if !api.Zones[dc.Name].Dnssec && dc.AutoDNSSEC == "on" { + dnssec = true + corrections = append(corrections, + &models.Correction{ + Msg: "Update DNSSEC off -> on", + F: func() error { + return nil + }, + }) + } + + if changes { + corrections = append(corrections, + &models.Correction{ + Msg: strings.Join(msgs, "\n"), + F: func() error { + records := make([]Record, len(dc.Records)) + for i, r := range dc.Records { + records[i] = toRecord(r) + } + zone := &Zone{Records: records, Dnssec: dnssec} + + err := api.updateZone(dc.Name, zone) + if err != nil { + return err + } + return nil + }, + }) + } + + return corrections, nil +} + +func (api *realtimeregisterAPI) ListZones() ([]string, error) { + zones, err := api.getAllZones() + if err != nil { + return nil, err + } + return zones, nil +} + +func (api *realtimeregisterAPI) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { + nameservers, err := api.getDomainNameservers(dc.Name) + if err != nil { + return nil, err + } + + expected := make([]string, len(dc.Nameservers)) + for i, ns := range dc.Nameservers { + expected[i] = removeTrailingDot(ns.Name) + } + + sort.Strings(nameservers) + sort.Strings(expected) + + if !slices.Equal(nameservers, expected) { + return []*models.Correction{ + { + Msg: fmt.Sprintf("Update nameservers %s -> %s", + strings.Join(nameservers, ","), strings.Join(expected, ",")), + F: func() error { return api.updateNameservers(dc.Name, expected) }, + }, + }, nil + } + + return nil, nil +} + +func toRecordConfig(domain string, record *Record) *models.RecordConfig { + + recordConfig := &models.RecordConfig{ + Type: record.Type, + TTL: uint32(record.TTL), + MxPreference: uint16(record.Priority), + SrvWeight: uint16(0), + SrvPort: uint16(0), + Original: record, + } + + recordConfig.SetLabelFromFQDN(record.Name, domain) + + switch rtype := record.Type; rtype { // #rtype_variations + case "TXT": + _ = recordConfig.SetTargetTXT(removeEscapeChars(record.Content)) + case "NS", "ALIAS", "CNAME": + _ = recordConfig.SetTarget(dnsutil.AddOrigin(addTrailingDot(record.Content), domain)) + case "MX": + content := record.Content + if content != "." { + content = addTrailingDot(content) + } + _ = recordConfig.SetTarget(dnsutil.AddOrigin(content, domain)) + case "NAPTR": + _ = recordConfig.SetTargetNAPTRString(record.Content) + case "SRV": + parts := strings.Fields(record.Content) + weight, _ := strconv.ParseUint(parts[0], 10, 16) + port, _ := strconv.ParseUint(parts[1], 10, 16) + content := parts[2] + if content != "." { + content = addTrailingDot(content) + } + _ = recordConfig.SetTargetSRV(uint16(record.Priority), uint16(weight), uint16(port), content) + case "CAA": + _ = recordConfig.SetTargetCAAString(record.Content) + case "SSHFP": + _ = recordConfig.SetTargetSSHFPString(record.Content) + case "TLSA": + _ = recordConfig.SetTargetTLSAString(record.Content) + case "DS": + _ = recordConfig.SetTargetDSString(record.Content) + case "LOC": + _ = recordConfig.SetTargetLOCString(domain, record.Content) + default: + _ = recordConfig.SetTarget(record.Content) + } + return recordConfig +} + +func toRecord(recordConfig *models.RecordConfig) Record { + record := &Record{ + Type: recordConfig.Type, + Name: recordConfig.NameFQDN, + Content: removeTrailingDot(recordConfig.GetTargetField()), + TTL: int(recordConfig.TTL), + } + + switch rtype := recordConfig.Type; rtype { + case "SRV": + if record.Content == "" { + record.Content = "." + } + record.Priority = int(recordConfig.SrvPriority) + record.Content = fmt.Sprintf("%d %d %s", recordConfig.SrvWeight, recordConfig.SrvPort, record.Content) + case "NAPTR", "SSHFP", "TLSA", "CAA": + record.Content = recordConfig.GetTargetCombined() + case "TXT": + record.Content = addEscapeChars(record.Content) + case "DS": + record.Content = fmt.Sprintf("%d %d %d %s", recordConfig.DsKeyTag, recordConfig.DsAlgorithm, + recordConfig.DsDigestType, strings.ToUpper(recordConfig.DsDigest)) + case "MX": + if record.Content == "" { + record.Content = "." + record.Priority = -1 + } else { + record.Priority = int(recordConfig.MxPreference) + } + case "LOC": + parts := strings.Fields(recordConfig.GetTargetCombined()) + degrees1, _ := strconv.ParseUint(parts[0], 10, 32) + minutes1, _ := strconv.ParseUint(parts[1], 10, 32) + degrees2, _ := strconv.ParseUint(parts[4], 10, 32) + minutes2, _ := strconv.ParseUint(parts[5], 10, 32) + altitude, _ := strconv.ParseFloat(strings.Split(parts[8], "m")[0], 64) + size, _ := strconv.ParseFloat(strings.Split(parts[9], "m")[0], 64) + hp, _ := strconv.ParseFloat(strings.Split(parts[10], "m")[0], 64) + vp, _ := strconv.ParseFloat(strings.Split(parts[11], "m")[0], 64) + record.Content = fmt.Sprintf("%d %d %s %s %d %d %s %s %.2fm %.2fm %.2fm %.2fm", + degrees1, minutes1, parts[2], parts[3], degrees2, minutes2, + parts[6], parts[7], altitude, size, hp, vp, + ) + } + + return *record +} + +func (api *realtimeregisterAPI) EnsureZoneExists(domain string) error { + exists, err := api.zoneExists(domain) + if err != nil { + return err + } + if exists { + return nil + } + + return api.createZone(domain) +} + +func removeTrailingDot(record string) string { + return strings.TrimSuffix(record, ".") +} + +func addTrailingDot(record string) string { + return record + "." +} + +func removeEscapeChars(name string) string { + return strings.Replace(strings.Replace(name, "\\\"", "\"", -1), "\\\\", "\\", -1) +} + +func addEscapeChars(name string) string { + return strings.Replace(strings.Replace(name, "\\", "\\\\", -1), "\"", "\\\"", -1) +} + +func getEndpoint(sandbox bool) string { + if sandbox { + return endpointSandbox + } + return endpoint +} + +func getServiceType(premium bool) string { + if premium { + return "PREMIUM" + } + return "BASIC" +} diff --git a/providers/realtimeregister/realtimeregisterProvider_test.go b/providers/realtimeregister/realtimeregisterProvider_test.go new file mode 100644 index 000000000..9fe6dc09d --- /dev/null +++ b/providers/realtimeregister/realtimeregisterProvider_test.go @@ -0,0 +1,16 @@ +package realtimeregister + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRemoveEscapeChars(t *testing.T) { + cleanedString := removeEscapeChars("\\\\\\\"") + assert.Equal(t, "\\\"", cleanedString) +} + +func TestAddEscapeChars(t *testing.T) { + addedString := addEscapeChars("\\\"") + assert.Equal(t, "\\\\\\\"", addedString) +}