Domainnameshop provider (#1625)

* Added basic structure for domain name shop

* Finished proof of concept for domainnameshop

* Fixed handeling of IDNA for CNAME records

* Updated documentation notes

* Added docs

* Ran linter and vet

* Removed proxy config used for debugging

* Ran go generate

* Fixed issue with TTLs being restricted to a multiple of 60

* Ran tests, vet and linting and fixed flaws

* Fixed typo in docs

* Improved code based on feedback

* Fixed issues with TXT records not working properly

* Refactored according to new file layout proposed

* Updated documentation matrix

* Suggestions and corrections

* Corrected according to suggestions

Co-authored-by: Simen Bai <git@simenbai.no>
Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
Simen Bai 2022-08-01 18:01:37 +02:00 committed by GitHub
parent befb52be86
commit e9510da434
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 688 additions and 0 deletions

1
OWNERS
View file

@ -12,6 +12,7 @@ providers/digitalocean @Deraen
providers/dnsimple @onlyhavecans
providers/dnsmadeeasy @vojtad
providers/doh @mikenz
providers/domainnameshop @SimenBai
providers/easyname @tresni
providers/exoscale @pierre-emmanuelJ
providers/gandi_v5 @TomOnTime

View file

@ -28,6 +28,7 @@ Currently supported DNS providers:
- DNS Made Easy
- DNSimple
- DigitalOcean
- DomainNameShop (domeneshop)
- Exoscale
- Gandi
- Google DNS

View file

@ -19,6 +19,7 @@
<th class="rotate"><div><span>DNSIMPLE</span></div></th>
<th class="rotate"><div><span>DNSMADEEASY</span></div></th>
<th class="rotate"><div><span>DNSOVERHTTPS</span></div></th>
<th class="rotate"><div><span>DOMAINNAMESHOP</span></div></th>
<th class="rotate"><div><span>EASYNAME</span></div></th>
<th class="rotate"><div><span>EXOSCALE</span></div></th>
<th class="rotate"><div><span>GANDI_V5</span></div></th>
@ -101,6 +102,9 @@
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
@ -215,6 +219,9 @@
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
@ -338,6 +345,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
@ -451,6 +461,9 @@
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
@ -538,6 +551,9 @@
</td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
@ -619,6 +635,9 @@
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
@ -722,6 +741,9 @@
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
@ -817,6 +839,9 @@
</td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
@ -880,6 +905,9 @@
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
@ -949,6 +977,9 @@
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success" data-toggle="tooltip" data-container="body" data-placement="top" title="SRV records with empty targets are not supported">
<i class="fa has-tooltip fa-check text-success" aria-hidden="true"></i>
@ -1056,6 +1087,9 @@
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
@ -1141,6 +1175,9 @@
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
@ -1240,6 +1277,7 @@
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
</tr>
<tr>
<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="Provider supports Route 53 limited ALIAS">R53_ALIAS</th>
@ -1263,6 +1301,7 @@
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Using ALIAS is possible through our extended DNS (X-DNS) service. Feel free to get in touch with us.">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
</td>
@ -1315,6 +1354,7 @@
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
@ -1367,6 +1407,9 @@
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Only supports DS records at the apex">
@ -1458,6 +1501,7 @@
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
</tr>
<tr>
<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="This provider is recommended for use in &#39;dual hosting&#39; scenarios. Usually this means the provider allows full control over the apex NS records">dual host</th>
@ -1497,6 +1541,9 @@
<i class="fa has-tooltip fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Exoscale does not allow sufficient control over the apex NS records">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
@ -1606,6 +1653,9 @@
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
@ -1735,6 +1785,9 @@
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
@ -1850,6 +1903,9 @@
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>

View file

@ -0,0 +1,46 @@
---
name: DomainNameShop
title: DomainNameShop Provider
layout: default
jsId: DOMAINNAMESHOP
---
# DOMAINNAMESHOP Provider
## Configuration
To use this provider, add an entry to `creds.json` with `TYPE` set to `DOMAINNAMESHOP`
along with your [DomainNameShop Token and Secret](https://www.domeneshop.no/admin?view=api).
Example:
```json
{
"mydomainnameshop": {
"TYPE": "DOMAINNAMESHOP",
"token": "your-domainnameshop-token",
"secret": "your-domainnameshop-secret"
}
}
```
## Metadata
This provider does not recognize any special metadata fields unique to DomainNameShop.
## Usage
An example `dnsconfig.js` configuration:
```js
var REG_NONE = NewRegistrar("none");
var DSP_DOMAINNAMESHOP = NewDnsProvider("mydomainnameshop");
D("example.tld", REG_NONE, DnsProvider(DSP_DOMAINNAMESHOP),
A("test", "1.2.3.4")
);
```
## Activation
[Create API Token and secret](https://www.domeneshop.no/admin?view=api)
## Limitations
- DomainNameShop DNS only supports TTLs which are a multiple of 60.

View file

@ -82,6 +82,7 @@ Providers in this category and their maintainers are:
* `DNSOVERHTTPS` @mikenz
* `DNSIMPLE` @onlyhavecans
* `DNSMADEEASY` @vojtad
* `DOMAINNAMESHOP` @SimenBai
* `EASYNAME` @tresni
* `EXOSCALE` @pierre-emmanuelJ
* `GANDI_V5` @TomOnTime

View file

@ -204,5 +204,10 @@
"TRANSIP": {
"AccessToken": "$TRANSIP_ACCESS_TOKEN",
"domain": "$TRANSIP_DOMAIN"
},
"DOMAINNAMESHOP": {
"token": "$DOMAINNAMESHOP_TOKEN",
"secret": "$DOMAINNAMESHOP_SECRET",
"domain": "$DOMAINNAMESHOP_DOMAIN"
}
}

View file

@ -17,6 +17,7 @@ import (
_ "github.com/StackExchange/dnscontrol/v3/providers/dnsimple"
_ "github.com/StackExchange/dnscontrol/v3/providers/dnsmadeeasy"
_ "github.com/StackExchange/dnscontrol/v3/providers/doh"
_ "github.com/StackExchange/dnscontrol/v3/providers/domainnameshop"
_ "github.com/StackExchange/dnscontrol/v3/providers/easyname"
_ "github.com/StackExchange/dnscontrol/v3/providers/exoscale"
_ "github.com/StackExchange/dnscontrol/v3/providers/gandiv5"

View file

@ -0,0 +1,223 @@
package domainnameshop
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"golang.org/x/net/idna"
)
var rootAPIURI = "https://api.domeneshop.no/v0"
func (api *domainNameShopProvider) getDomains(domainName string) ([]domainResponse, error) {
client := &http.Client{}
req, err := http.NewRequest(http.MethodGet, rootAPIURI+"/domains?domain="+domainName, nil)
if err != nil {
return nil, err
}
req.SetBasicAuth(api.Token, api.Secret)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var domainResp []domainResponse
err = json.NewDecoder(resp.Body).Decode(&domainResp)
if err != nil {
return nil, err
}
if domainName != "" && domainName != domainResp[0].Domain {
return nil, fmt.Errorf("invalid domain name: %q != %q", domainName, domainResp[0].Domain)
}
return domainResp, nil
}
func (api *domainNameShopProvider) getDomainID(domainName string) (string, error) {
domainResp, err := api.getDomains(domainName)
if err != nil {
return "", err
}
return strconv.Itoa(domainResp[0].ID), nil
}
func (api *domainNameShopProvider) getNS(domainName string) ([]string, error) {
domainResp, err := api.getDomains(domainName)
if err != nil {
return nil, err
}
return domainResp[0].Nameservers, nil
}
func (api *domainNameShopProvider) getDNS(domainName string) ([]domainNameShopRecord, error) {
domainID, err := api.getDomainID(domainName)
if err != nil {
return nil, err
}
client := &http.Client{}
req, err := http.NewRequest(http.MethodGet, rootAPIURI+"/domains/"+domainID+"/dns", nil)
if err != nil {
return nil, err
}
req.SetBasicAuth(api.Token, api.Secret)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var domainResponse []domainNameShopRecord
err = json.NewDecoder(resp.Body).Decode(&domainResponse)
if err != nil {
return nil, err
}
// Post processing of the data received. Converting to correct types and setting default values.
for i := range domainResponse {
// Convert priority from string to Uint, defaulting to 0
record := &domainResponse[i]
priority, err := strconv.ParseUint(record.Priority, 10, 16)
if err != nil {
record.ActualPriority = 0
}
record.ActualPriority = uint16(priority)
// Convert port from string to Uint, defaulting to 0
port, err := strconv.ParseUint(record.Port, 10, 16)
if err != nil {
record.ActualPort = 0
}
record.ActualPort = uint16(port)
// Convert weight from string ti Uint, defaulting to 0
weight, err := strconv.ParseUint(record.Weight, 10, 16)
if err != nil {
record.ActualWeight = 0
}
record.ActualWeight = uint16(weight)
// Converting the CAA flag from string to correct value
if record.Type == "CAA" {
CaaFlag, err := strconv.ParseUint(record.ActualCAAFlag, 10, 8)
if err != nil {
record.CAAFlag = 0
}
record.CAAFlag = CaaFlag
}
// Transform data field to punycode if CNAME
if record.Type == "CNAME" {
punycodeData, err := idna.ToASCII(record.Data)
if err != nil {
return nil, err
}
record.Data = punycodeData
if !strings.HasSuffix(record.Data, ".") {
record.Data += "."
}
}
// Normalize the TTL.
record.TTL = uint16(fixTTL(uint32(record.TTL)))
// Add domain id
(&domainResponse[i]).DomainID = domainID
}
ns, err := api.getNS(domainName)
if err != nil {
return nil, err
}
// Adds NS as records
for _, nameserver := range ns {
domainResponse = append(domainResponse, domainNameShopRecord{
ID: 0,
Host: "@",
TTL: 300,
Type: "NS",
Data: nameserver + ".",
DomainID: domainID,
})
}
return domainResponse, nil
}
func (api *domainNameShopProvider) deleteRecord(domainID string, recordID string) error {
return api.sendChangeRequest(http.MethodDelete, rootAPIURI+"/domains/"+domainID+"/dns/"+recordID, nil)
}
func (api *domainNameShopProvider) CreateRecord(domainName string, dnsR *domainNameShopRecord) error {
domainID, err := api.getDomainID(domainName)
if err != nil {
return err
}
payloadBuf := new(bytes.Buffer)
err = json.NewEncoder(payloadBuf).Encode(&dnsR)
if err != nil {
return err
}
return api.sendChangeRequest(http.MethodPost, rootAPIURI+"/domains/"+domainID+"/dns", payloadBuf)
}
func (api *domainNameShopProvider) UpdateRecord(dnsR *domainNameShopRecord) error {
domainID := dnsR.DomainID
recordID := strconv.Itoa(dnsR.ID)
payloadBuf := new(bytes.Buffer)
json.NewEncoder(payloadBuf).Encode(&dnsR)
return api.sendChangeRequest(http.MethodPut, rootAPIURI+"/domains/"+domainID+"/dns/"+recordID, payloadBuf)
}
func (api *domainNameShopProvider) sendChangeRequest(method string, uri string, payload *bytes.Buffer) error {
client := &http.Client{}
var req *http.Request
var err error
if payload != nil {
req, err = http.NewRequest(method, uri, payload)
} else {
req, err = http.NewRequest(method, uri, nil)
}
if err != nil {
return err
}
req.SetBasicAuth(api.Token, api.Secret)
resp, err := client.Do(req)
if err != nil {
return err
}
switch resp.StatusCode {
case 201:
// Record is deleted
return nil
case 204:
//Update successful
return nil
case 400:
return fmt.Errorf("DNS record failed validation")
case 403:
return fmt.Errorf("not authorized")
case 404:
return fmt.Errorf("does not exist")
case 409:
return fmt.Errorf("collision")
default:
return fmt.Errorf("unknown statuscode: %v", resp.StatusCode)
}
}

View file

@ -0,0 +1,95 @@
package domainnameshop
import (
"strconv"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/miekg/dns/dnsutil"
)
func toRecordConfig(domain string, currentRecord *domainNameShopRecord) *models.RecordConfig {
name := dnsutil.AddOrigin(currentRecord.Host, domain)
target := currentRecord.Data
t := &models.RecordConfig{
Type: currentRecord.Type,
TTL: fixTTL(uint32(currentRecord.TTL)),
MxPreference: uint16(currentRecord.ActualPriority),
SrvPriority: uint16(currentRecord.ActualPriority),
SrvWeight: uint16(currentRecord.ActualWeight),
SrvPort: uint16(currentRecord.ActualPort),
Original: currentRecord,
CaaTag: currentRecord.CAATag,
CaaFlag: uint8(currentRecord.CAAFlag),
}
t.SetTarget(target)
t.SetLabelFromFQDN(name, domain)
switch rtype := currentRecord.Type; rtype {
case "TXT":
t.SetTargetTXT(target)
case "CAA":
if currentRecord.CAATag == "0" {
t.CaaTag = "issue"
} else if currentRecord.CAATag == "1" {
t.CaaTag = "issuewild"
} else {
t.CaaTag = "iodef"
}
default:
// nothing additional required
}
return t
}
func (api *domainNameShopProvider) fromRecordConfig(domainName string, rc *models.RecordConfig) (*domainNameShopRecord, error) {
domainID, err := api.getDomainID(domainName)
if err != nil {
return nil, err
}
data := ""
if rc.Type == "TXT" {
data = rc.GetTargetTXTJoined()
} else {
data = rc.GetTargetField()
}
dnsR := &domainNameShopRecord{
ID: 0,
Host: rc.GetLabel(),
TTL: uint16(fixTTL(rc.TTL)),
Type: rc.Type,
Data: data,
Weight: strconv.Itoa(int(rc.SrvWeight)),
Port: strconv.Itoa(int(rc.SrvPort)),
ActualWeight: rc.SrvWeight,
ActualPort: rc.SrvPort,
CAAFlag: uint64(int(rc.CaaFlag)),
ActualCAAFlag: strconv.Itoa(int(rc.CaaFlag)),
DomainID: domainID,
}
switch rc.Type {
case "CAA":
// Actual CAA FLAG
switch rc.CaaTag {
case "issue":
dnsR.CAATag = "0"
case "issuewild":
dnsR.CAATag = "1"
case "iodef":
dnsR.CAATag = "2"
}
case "MX":
dnsR.Priority = strconv.Itoa(int(rc.MxPreference))
case "SRV":
dnsR.Priority = strconv.Itoa(int(rc.SrvPriority))
default:
// pass through
}
return dnsR, nil
}

View file

@ -0,0 +1,131 @@
package domainnameshop
import (
"fmt"
"strconv"
"strings"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
)
func (api *domainNameShopProvider) GetZoneRecords(domain string) (models.Records, error) {
records, err := api.getDNS(domain)
if err != nil {
return nil, err
}
var existingRecords []*models.RecordConfig
for i := range records {
rC := toRecordConfig(domain, &records[i])
existingRecords = append(existingRecords, rC)
}
return existingRecords, nil
}
func (api *domainNameShopProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
existingRecords, err := api.GetZoneRecords(dc.Name)
if err != nil {
return nil, err
}
// Normalize
models.PostProcessRecords(existingRecords)
// Merge TXT strings to one string
for _, rc := range dc.Records {
if rc.HasFormatIdenticalToTXT() {
rc.SetTargetTXT(strings.Join(rc.TxtStrings, ""))
}
}
// Domainnameshop doesn't allow arbitrary TTLs they must be a multiple of 60.
for _, record := range dc.Records {
record.TTL = fixTTL(record.TTL)
}
differ := diff.New(dc)
_, create, delete, modify, err := differ.IncrementalDiff(existingRecords)
if err != nil {
return nil, err
}
var corrections = []*models.Correction{}
// Delete record
for _, r := range delete {
domainID := r.Existing.Original.(*domainNameShopRecord).DomainID
recordID := strconv.Itoa(r.Existing.Original.(*domainNameShopRecord).ID)
corr := &models.Correction{
Msg: fmt.Sprintf("%s, record id: %s", r.String(), recordID),
F: func() error { return api.deleteRecord(domainID, recordID) },
}
corrections = append(corrections, corr)
}
// Create records
for _, r := range create {
// Retrieve the domain name that is targeted. I.e. example.com instead of sub.example.com
domainName := strings.Replace(r.Desired.GetLabelFQDN(), r.Desired.GetLabel()+".", "", -1)
dnsR, err := api.fromRecordConfig(domainName, r.Desired)
if err != nil {
return nil, err
}
corr := &models.Correction{
Msg: r.String(),
F: func() error { return api.CreateRecord(domainName, dnsR) },
}
corrections = append(corrections, corr)
}
for _, r := range modify {
domainName := strings.Replace(r.Desired.GetLabelFQDN(), r.Desired.GetLabel()+".", "", -1)
dnsR, err := api.fromRecordConfig(domainName, r.Desired)
if err != nil {
return nil, err
}
dnsR.ID = r.Existing.Original.(*domainNameShopRecord).ID
corr := &models.Correction{
Msg: r.String(),
F: func() error { return api.UpdateRecord(dnsR) },
}
corrections = append(corrections, corr)
}
return corrections, nil
}
func (api *domainNameShopProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
ns, err := api.getNS(domain)
if err != nil {
return nil, err
}
return models.ToNameservers(ns)
}
const minAllowedTTL = 60
const maxAllowedTTL = 604800
const multiplierTTL = 60
func fixTTL(ttl uint32) uint32 {
// if the TTL is larger than the largest allowed value, return the largest allowed value
if ttl > maxAllowedTTL {
return maxAllowedTTL
} else if ttl < 60 {
return minAllowedTTL
}
// Return closest rounded down possible
return (ttl / multiplierTTL) * multiplierTTL
}

View file

@ -0,0 +1,27 @@
package domainnameshop
import (
"testing"
)
func TestFixTTL(t *testing.T) {
for i, test := range []struct {
given, expected uint32
}{
{1, minAllowedTTL},
{multiplierTTL*5 - 1, multiplierTTL * 4},
{maxAllowedTTL + 1, maxAllowedTTL},
{0, 60},
{59, 60},
{60, 60},
{61, 60},
{119, 60},
{120, 120},
{121, 120},
} {
found := fixTTL(test.given)
if found != test.expected {
t.Errorf("Test %d: Expected %d, but was %d", i, test.expected, found)
}
}
}

View file

@ -0,0 +1,101 @@
package domainnameshop
import (
"encoding/json"
"fmt"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/providers"
)
/**
DomainNameShop Provider
Info required in 'creds.json':
- token API Token
- secret API Secret
*/
type domainNameShopProvider struct {
Token string // The API token
Secret string // The API secret
}
var features = providers.DocumentationNotes{
providers.CanAutoDNSSEC: providers.Cannot(), // Maybe there is support for it
providers.CanGetZones: providers.Unimplemented(), //
providers.CanUseAlias: providers.Unimplemented(), // Can possibly be implemented, needs further research
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Unimplemented(), // Seems to support but needs to be implemented
providers.CanUseDSForChildren: providers.Unimplemented(), // Seems to support but needs to be implemented
providers.CanUseNAPTR: providers.Cannot(), // Does not seem to support it
providers.CanUsePTR: providers.Unimplemented(), // Seems to support but needs to be implemented
providers.CanUseSOA: providers.Cannot(), // Does not seem to support it
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Cannot(), // Does not seem to support it
providers.CanUseTLSA: providers.Unimplemented(), // Seems to support but needs to be implemented
providers.DocCreateDomains: providers.Unimplemented(), // Not tested
providers.DocDualHost: providers.Unimplemented(), // Not tested
providers.DocOfficiallySupported: providers.Cannot(),
}
// Register with the dnscontrol system.
// This establishes the name (all caps), and the function to call to initialize it.
func init() {
fns := providers.DspFuncs{
Initializer: newDomainNameShopProvider,
RecordAuditor: auditRecords,
}
providers.RegisterDomainServiceProviderType("DOMAINNAMESHOP", fns, features)
}
// newDomainNameShopProvider creates a DomainNameShop specific DNS provider.
func newDomainNameShopProvider(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
if conf["token"] == "" {
return nil, fmt.Errorf("no DomainNameShop token provided")
} else if conf["secret"] == "" {
return nil, fmt.Errorf("no DomainNameShop secret provided")
}
api := &domainNameShopProvider{
Token: conf["token"],
Secret: conf["secret"],
}
// Consider testing if creds work
return api, nil
}
func auditRecords(records []*models.RecordConfig) error {
return nil
}
type domainResponse struct {
ID int `json:"id"`
Domain string `json:"domain"`
Nameservers []string `json:"nameservers"`
}
// The Actual fields are the values in the right format according to what is needed for RecordConfig.
// While the values without Actual are the values directly as received from the DomainNameShop API.
// This is done to make it easier to use the values at later points.
type domainNameShopRecord struct {
ID int `json:"id"`
Host string `json:"host"`
TTL uint16 `json:"ttl,omitempty"`
Type string `json:"type"`
Data string `json:"data"`
Priority string `json:"priority,omitempty"`
ActualPriority uint16
Weight string `json:"weight,omitempty"`
ActualWeight uint16
Port string `json:"port,omitempty"`
ActualPort uint16
CAATag string `json:"tag,omitempty"`
ActualCAAFlag string `json:"flags,omitempty"`
CAAFlag uint64
DomainID string
}