mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-01-10 17:38:13 +08:00
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:
parent
befb52be86
commit
e9510da434
12 changed files with 688 additions and 0 deletions
1
OWNERS
1
OWNERS
|
@ -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
|
||||
|
|
|
@ -28,6 +28,7 @@ Currently supported DNS providers:
|
|||
- DNS Made Easy
|
||||
- DNSimple
|
||||
- DigitalOcean
|
||||
- DomainNameShop (domeneshop)
|
||||
- Exoscale
|
||||
- Gandi
|
||||
- Google DNS
|
||||
|
|
|
@ -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 'dual hosting' 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>
|
||||
|
|
46
docs/_providers/domainnameshop.md
Normal file
46
docs/_providers/domainnameshop.md
Normal 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.
|
|
@ -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
|
||||
|
|
|
@ -204,5 +204,10 @@
|
|||
"TRANSIP": {
|
||||
"AccessToken": "$TRANSIP_ACCESS_TOKEN",
|
||||
"domain": "$TRANSIP_DOMAIN"
|
||||
},
|
||||
"DOMAINNAMESHOP": {
|
||||
"token": "$DOMAINNAMESHOP_TOKEN",
|
||||
"secret": "$DOMAINNAMESHOP_SECRET",
|
||||
"domain": "$DOMAINNAMESHOP_DOMAIN"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
223
providers/domainnameshop/api.go
Normal file
223
providers/domainnameshop/api.go
Normal 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)
|
||||
}
|
||||
}
|
95
providers/domainnameshop/convert.go
Normal file
95
providers/domainnameshop/convert.go
Normal 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
|
||||
}
|
131
providers/domainnameshop/dns.go
Normal file
131
providers/domainnameshop/dns.go
Normal 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
|
||||
}
|
27
providers/domainnameshop/dns_test.go
Normal file
27
providers/domainnameshop/dns_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
101
providers/domainnameshop/domainnameshopProvider.go
Normal file
101
providers/domainnameshop/domainnameshopProvider.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue