mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-05-11 05:55:12 +00:00
NEW DNS PROVIDER: Realtime Register (REALTIMEREGISTER) (#2741)
Co-authored-by: pieterjan.eilers <pieterjan.eilers@realtimeregister.com> Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
@ -36,7 +36,7 @@ changelog:
|
|||||||
regexp: "(?i)^.*(major|new provider|feature)[(\\w)]*:+.*$"
|
regexp: "(?i)^.*(major|new provider|feature)[(\\w)]*:+.*$"
|
||||||
order: 1
|
order: 1
|
||||||
- title: 'Provider-specific changes:'
|
- 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
|
order: 2
|
||||||
- title: 'Documentation:'
|
- title: 'Documentation:'
|
||||||
regexp: "(?i)^.*(docs)[(\\w)]*:+.*$"
|
regexp: "(?i)^.*(docs)[(\\w)]*:+.*$"
|
||||||
|
1
OWNERS
1
OWNERS
@ -42,6 +42,7 @@ providers/ovh @masterzen
|
|||||||
providers/packetframe @hamptonmoore
|
providers/packetframe @hamptonmoore
|
||||||
providers/porkbun @imlonghao
|
providers/porkbun @imlonghao
|
||||||
providers/powerdns @jpbede
|
providers/powerdns @jpbede
|
||||||
|
providers/realtimeregister @PJEilers
|
||||||
providers/route53 @tresni
|
providers/route53 @tresni
|
||||||
providers/rwth @mistererwin
|
providers/rwth @mistererwin
|
||||||
# providers/softlayer NEEDS VOLUNTEER
|
# providers/softlayer NEEDS VOLUNTEER
|
||||||
|
@ -55,6 +55,7 @@ Currently supported DNS providers:
|
|||||||
- Packetframe
|
- Packetframe
|
||||||
- Porkbun
|
- Porkbun
|
||||||
- PowerDNS
|
- PowerDNS
|
||||||
|
- Realtime Register
|
||||||
- RWTH DNS-Admin
|
- RWTH DNS-Admin
|
||||||
- SoftLayer
|
- SoftLayer
|
||||||
- TransIP
|
- TransIP
|
||||||
@ -76,6 +77,7 @@ Currently supported Domain Registrars:
|
|||||||
- Name.com
|
- Name.com
|
||||||
- OpenSRS
|
- OpenSRS
|
||||||
- OVH
|
- OVH
|
||||||
|
- Realtime Register
|
||||||
|
|
||||||
At Stack Overflow, we use this system to manage hundreds of domains
|
At Stack Overflow, we use this system to manage hundreds of domains
|
||||||
and subdomains across multiple registrars and DNS providers.
|
and subdomains across multiple registrars and DNS providers.
|
||||||
|
@ -142,6 +142,7 @@
|
|||||||
* [Packetframe](providers/packetframe.md)
|
* [Packetframe](providers/packetframe.md)
|
||||||
* [Porkbun](providers/porkbun.md)
|
* [Porkbun](providers/porkbun.md)
|
||||||
* [PowerDNS](providers/powerdns.md)
|
* [PowerDNS](providers/powerdns.md)
|
||||||
|
* [Realtime Register](providers/realtimeregister.md)
|
||||||
* [RWTH DNS-Admin](providers/rwth.md)
|
* [RWTH DNS-Admin](providers/rwth.md)
|
||||||
* [SoftLayer DNS](providers/softlayer.md)
|
* [SoftLayer DNS](providers/softlayer.md)
|
||||||
* [TransIP](providers/transip.md)
|
* [TransIP](providers/transip.md)
|
||||||
|
@ -58,6 +58,7 @@ If a feature is definitively not supported for whatever reason, we would also li
|
|||||||
| [`PACKETFRAME`](providers/packetframe.md) | ❌ | ✅ | ❌ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❌ | ❌ | ✅ | ❔ |
|
| [`PACKETFRAME`](providers/packetframe.md) | ❌ | ✅ | ❌ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❌ | ❌ | ✅ | ❔ |
|
||||||
| [`PORKBUN`](providers/porkbun.md) | ❌ | ✅ | ✅ | ✅ | ❔ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❔ | ❌ | ❌ | ✅ | ✅ |
|
| [`PORKBUN`](providers/porkbun.md) | ❌ | ✅ | ✅ | ✅ | ❔ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❔ | ❌ | ❌ | ✅ | ✅ |
|
||||||
| [`POWERDNS`](providers/powerdns.md) | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ✅ |
|
| [`POWERDNS`](providers/powerdns.md) | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| [`REALTIMEREGISTER`](providers/realtimeregister.md) | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
|
||||||
| [`ROUTE53`](providers/route53.md) | ✅ | ✅ | ✅ | ❌ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ | ✅ |
|
| [`ROUTE53`](providers/route53.md) | ✅ | ✅ | ✅ | ❌ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [`RWTH`](providers/rwth.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ❔ | ❌ | ❌ | ✅ | ❔ | ✅ | ✅ | ❌ | ❔ | ❔ | ❌ | ❌ | ✅ | ✅ |
|
| [`RWTH`](providers/rwth.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ❔ | ❌ | ❌ | ✅ | ❔ | ✅ | ✅ | ❌ | ❔ | ❔ | ❌ | ❌ | ✅ | ✅ |
|
||||||
| [`SOFTLAYER`](providers/softlayer.md) | ❌ | ✅ | ❌ | ❔ | ❔ | ❔ | ❌ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ✅ | ❔ |
|
| [`SOFTLAYER`](providers/softlayer.md) | ❌ | ✅ | ❌ | ❔ | ❔ | ❔ | ❌ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ✅ | ❔ |
|
||||||
@ -143,6 +144,7 @@ Providers in this category and their maintainers are:
|
|||||||
|[`OVH`](providers/ovh.md)|@masterzen|
|
|[`OVH`](providers/ovh.md)|@masterzen|
|
||||||
|[`PACKETFRAME`](providers/packetframe.md)|@hamptonmoore|
|
|[`PACKETFRAME`](providers/packetframe.md)|@hamptonmoore|
|
||||||
|[`POWERDNS`](providers/powerdns.md)|@jpbede|
|
|[`POWERDNS`](providers/powerdns.md)|@jpbede|
|
||||||
|
|[`REALTIMEREGISTER`](providers/realtimeregister.md)|@PJEilers|
|
||||||
|[`ROUTE53`](providers/route53.md)|@tresni|
|
|[`ROUTE53`](providers/route53.md)|@tresni|
|
||||||
|[`RWTH`](providers/rwth.md)|@MisterErwin|
|
|[`RWTH`](providers/rwth.md)|@MisterErwin|
|
||||||
|[`SOFTLAYER`](providers/softlayer.md)|@jamielennox|
|
|[`SOFTLAYER`](providers/softlayer.md)|@jamielennox|
|
||||||
|
46
documentation/providers/realtimeregister.md
Normal file
46
documentation/providers/realtimeregister.md
Normal file
@ -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 %}
|
@ -258,6 +258,13 @@
|
|||||||
"domain": "$POWERDNS_DOMAIN",
|
"domain": "$POWERDNS_DOMAIN",
|
||||||
"serverName": "$POWERDNS_SERVERNAME"
|
"serverName": "$POWERDNS_SERVERNAME"
|
||||||
},
|
},
|
||||||
|
"REALTIMEREGISTER": {
|
||||||
|
"TYPE": "REALTIMEREGISTER",
|
||||||
|
"apikey": "$REALTIMEREGISTER_APIKEY",
|
||||||
|
"sandbox" : "$REALTIMEREGISTER_SANDBOX",
|
||||||
|
"domain": "$REALTIMEREGISTER_DOMAIN",
|
||||||
|
"premium": "$REALTIMEREGISTER_PREMIUM"
|
||||||
|
},
|
||||||
"ROUTE53": {
|
"ROUTE53": {
|
||||||
"KeyId": "$ROUTE53_KEY_ID",
|
"KeyId": "$ROUTE53_KEY_ID",
|
||||||
"SecretKey": "$ROUTE53_KEY",
|
"SecretKey": "$ROUTE53_KEY",
|
||||||
|
@ -47,6 +47,7 @@ import (
|
|||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/packetframe"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/packetframe"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/porkbun"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/porkbun"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/powerdns"
|
_ "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/route53"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/rwth"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/rwth"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/softlayer"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/softlayer"
|
||||||
|
221
providers/realtimeregister/api.go
Normal file
221
providers/realtimeregister/api.go
Normal file
@ -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
|
||||||
|
}
|
19
providers/realtimeregister/auditrecords.go
Normal file
19
providers/realtimeregister/auditrecords.go
Normal file
@ -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)
|
||||||
|
}
|
335
providers/realtimeregister/realtimeregisterProvider.go
Normal file
335
providers/realtimeregister/realtimeregisterProvider.go
Normal file
@ -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"
|
||||||
|
}
|
16
providers/realtimeregister/realtimeregisterProvider_test.go
Normal file
16
providers/realtimeregister/realtimeregisterProvider_test.go
Normal file
@ -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)
|
||||||
|
}
|
Reference in New Issue
Block a user