mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-05-11 05:55:12 +00:00
NEW PROVIDER: NETCUP (DNS) (#718)
* Add support for netcup DNS api. * Add documentation page. * Update reference to new version path. * Add OWNERS entry for netcup. * Add credentials for integration test. Netcup does not support PTRs. Fix parsing/formating of SRV records. * Skip integration tests that are not supported. * Use single quotes in JS code.
This commit is contained in:
1
OWNERS
1
OWNERS
@ -12,6 +12,7 @@ providers/internetbs @pragmaton
|
||||
providers/linode @koesie10
|
||||
providers/namecheap @captncraig
|
||||
# providers/namedotcom
|
||||
providers/netcup @kordianbruck
|
||||
providers/ns1 @captncraig
|
||||
# providers/route53
|
||||
# providers/softlayer
|
||||
|
38
docs/_providers/netcup.md
Normal file
38
docs/_providers/netcup.md
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Netcup
|
||||
title: Netcup Provider
|
||||
layout: default
|
||||
jsId: Netcup
|
||||
---
|
||||
# Netcup Provider
|
||||
|
||||
## Configuration
|
||||
In your credentials file, you must provide your [api key, password and your customer number](https://www.netcup-wiki.de/wiki/CCP_API#Authentifizierung).
|
||||
|
||||
{% highlight json %}
|
||||
{
|
||||
"netcup": {
|
||||
"api-key": "abc12345",
|
||||
"api-password": "abc12345",
|
||||
"customer-number": "123456"
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
## Usage
|
||||
Example Javascript:
|
||||
|
||||
{% highlight js %}
|
||||
var REG_NONE = NewRegistrar('none', 'NONE')
|
||||
var NETCUP = NewDnsProvider('netcup' 'NETCUP');
|
||||
|
||||
D('example.tld', REG_NONE, DnsProvider(NETCUP),
|
||||
A('test','1.2.3.4')
|
||||
);
|
||||
{%endhighlight%}
|
||||
|
||||
|
||||
## Caveats
|
||||
Netcup does not allow any TTLs to be set for individual records. Thus in
|
||||
the diff/preview it will always show a TTL of 0. `NS` records are also
|
||||
not currently supported.
|
@ -80,6 +80,7 @@ Maintainers of contributed providers:
|
||||
* `INTERNETBS` @pragmaton
|
||||
* `LINODE` @koesie10
|
||||
* `NAMECHEAP` @captncraig
|
||||
* `NETCUP` @kordianbruck
|
||||
* `NS1` @captncraig
|
||||
* `OCTODNS` @TomOnTime
|
||||
* `OPENSRS` @pierre-emmanuelJ
|
||||
|
@ -610,14 +610,15 @@ func makeTests(t *testing.T) []*TestGroup {
|
||||
),
|
||||
|
||||
testgroup("Null MX",
|
||||
not("AZURE_DNS", "GANDI_V5", "NAMEDOTCOM", "DIGITALOCEAN"), // These providers don't support RFC 7505
|
||||
not("AZURE_DNS", "GANDI_V5", "NAMEDOTCOM", "DIGITALOCEAN", "NETCUP"), // These providers don't support RFC 7505
|
||||
tc("Null MX", mx("@", 0, ".")),
|
||||
),
|
||||
|
||||
testgroup("NS",
|
||||
not("DNSIMPLE", "EXOSCALE"),
|
||||
not("DNSIMPLE", "EXOSCALE", "NETCUP"),
|
||||
// DNSIMPLE: Does not support NS records nor subdomains.
|
||||
// EXOSCALE: FILL IN
|
||||
// Netcup: NS records not currently supported.
|
||||
tc("NS for subdomain", ns("xyz", "ns2.foo.com.")),
|
||||
tc("Dual NS for subdomain", ns("xyz", "ns2.foo.com."), ns("xyz", "ns1.foo.com.")),
|
||||
tc("NS Record pointing to @", ns("foo", "**current-domain**")),
|
||||
@ -648,7 +649,8 @@ func makeTests(t *testing.T) []*TestGroup {
|
||||
tc("Change a TXT with ws at end", txt("foo", "with space at end ")),
|
||||
),
|
||||
|
||||
testgroup("empty TXT", not("DNSIMPLE", "CLOUDFLAREAPI"),
|
||||
testgroup("empty TXT",
|
||||
not("DNSIMPLE", "CLOUDFLAREAPI", "NETCUP"),
|
||||
tc("TXT with empty str", txt("foo1", "")),
|
||||
// https://github.com/StackExchange/dnscontrol/issues/598
|
||||
// We decided that permitting the TXT target to be an empty
|
||||
|
@ -77,6 +77,12 @@
|
||||
"apiuser": "$NAMEDOTCOM_USER",
|
||||
"domain": "$NAMEDOTCOM_DOMAIN"
|
||||
},
|
||||
"NETCUP": {
|
||||
"api-key": "$NETCUP_KEY",
|
||||
"api-password": "$NETCUP_PASSWORD",
|
||||
"customer-number": "$NETCUP_CUSTOMER_NUMBER",
|
||||
"domain": "$NETCUP_DOMAIN"
|
||||
},
|
||||
"NS1": {
|
||||
"api_token": "$NS1_TOKEN",
|
||||
"domain": "$NS1_DOMAIN"
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
_ "github.com/StackExchange/dnscontrol/v3/providers/linode"
|
||||
_ "github.com/StackExchange/dnscontrol/v3/providers/namecheap"
|
||||
_ "github.com/StackExchange/dnscontrol/v3/providers/namedotcom"
|
||||
_ "github.com/StackExchange/dnscontrol/v3/providers/netcup"
|
||||
_ "github.com/StackExchange/dnscontrol/v3/providers/ns1"
|
||||
_ "github.com/StackExchange/dnscontrol/v3/providers/octodns"
|
||||
_ "github.com/StackExchange/dnscontrol/v3/providers/opensrs"
|
||||
|
160
providers/netcup/api.go
Normal file
160
providers/netcup/api.go
Normal file
@ -0,0 +1,160 @@
|
||||
package netcup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
endpoint = "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON"
|
||||
)
|
||||
|
||||
type api struct {
|
||||
domainIndex map[string]string
|
||||
nameserversNames []string
|
||||
credentials struct {
|
||||
apikey string
|
||||
customernumber string
|
||||
sessionId string
|
||||
}
|
||||
}
|
||||
|
||||
func (api *api) createRecord(domain string, rec *record) error {
|
||||
rec.Delete = false
|
||||
data := paramUpdateRecords{
|
||||
Key: api.credentials.apikey,
|
||||
SessionId: api.credentials.sessionId,
|
||||
CustomerNumber: api.credentials.customernumber,
|
||||
DomainName: domain,
|
||||
RecordSet: records{Records: []record{
|
||||
*rec,
|
||||
}},
|
||||
}
|
||||
_, err := api.get("updateDnsRecords", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while trying to create a record: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *api) deleteRecord(domain string, rec *record) error {
|
||||
rec.Delete = true
|
||||
data := paramUpdateRecords{
|
||||
Key: api.credentials.apikey,
|
||||
SessionId: api.credentials.sessionId,
|
||||
CustomerNumber: api.credentials.customernumber,
|
||||
DomainName: domain,
|
||||
RecordSet: records{Records: []record{
|
||||
*rec,
|
||||
}},
|
||||
}
|
||||
_, err := api.get("updateDnsRecords", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while trying to delete a record: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *api) modifyRecord(domain string, rec *record) error {
|
||||
rec.Delete = false
|
||||
data := paramUpdateRecords{
|
||||
Key: api.credentials.apikey,
|
||||
SessionId: api.credentials.sessionId,
|
||||
CustomerNumber: api.credentials.customernumber,
|
||||
DomainName: domain,
|
||||
RecordSet: records{Records: []record{
|
||||
*rec,
|
||||
}},
|
||||
}
|
||||
_, err := api.get("updateDnsRecords", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while trying to modify a record: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *api) getRecords(domain string) ([]record, error) {
|
||||
data := paramGetRecords{
|
||||
Key: api.credentials.apikey,
|
||||
SessionId: api.credentials.sessionId,
|
||||
CustomerNumber: api.credentials.customernumber,
|
||||
DomainName: domain,
|
||||
}
|
||||
rawJson, err := api.get("infoDnsRecords", data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error while trying to login to netcup: %s", err)
|
||||
}
|
||||
|
||||
resp := &records{}
|
||||
json.Unmarshal(rawJson, &resp)
|
||||
return resp.Records, nil
|
||||
}
|
||||
|
||||
func (api *api) login(apikey, password, customernumber string) error {
|
||||
data := paramLogin{
|
||||
Key: apikey,
|
||||
Password: password,
|
||||
CustomerNumber: customernumber,
|
||||
}
|
||||
rawJson, err := api.get("login", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error while trying to login to netcup: %s", err)
|
||||
}
|
||||
|
||||
resp := &responseLogin{}
|
||||
json.Unmarshal(rawJson, &resp)
|
||||
api.credentials.apikey = apikey
|
||||
api.credentials.customernumber = customernumber
|
||||
api.credentials.sessionId = resp.SessionId
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *api) logout() error {
|
||||
data := paramLogout{
|
||||
Key: api.credentials.apikey,
|
||||
SessionId: api.credentials.sessionId,
|
||||
CustomerNumber: api.credentials.customernumber,
|
||||
}
|
||||
_, err := api.get("logout", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error while trying to logout from netcup: %s", err)
|
||||
}
|
||||
api.credentials.apikey, api.credentials.sessionId, api.credentials.customernumber = "", "", ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *api) get(action string, params interface{}) (json.RawMessage, error) {
|
||||
reqParam := request{
|
||||
Action: action,
|
||||
Param: params,
|
||||
}
|
||||
reqJson, _ := json.Marshal(reqParam)
|
||||
|
||||
client := &http.Client{}
|
||||
req, _ := http.NewRequest("POST", endpoint, bytes.NewBuffer(reqJson))
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bodyString, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
respData := &response{}
|
||||
err = json.Unmarshal(bodyString, &respData)
|
||||
|
||||
// Yeah, netcup implemented an empty recordset as an error - don't ask.
|
||||
if action == "infoDnsRecords" && respData.StatusCode == 5029 {
|
||||
emptyRecords, _ := json.Marshal(records{})
|
||||
return emptyRecords, nil
|
||||
}
|
||||
|
||||
// Check for any errors and log them
|
||||
if respData.StatusCode != 2000 && (action == "") {
|
||||
return nil, fmt.Errorf("Netcup API error: %v\n%v\n", reqParam, respData)
|
||||
}
|
||||
|
||||
return respData.Data, nil
|
||||
}
|
136
providers/netcup/netcupProvider.go
Normal file
136
providers/netcup/netcupProvider.go
Normal file
@ -0,0 +1,136 @@
|
||||
package netcup
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
||||
"github.com/StackExchange/dnscontrol/v3/providers"
|
||||
)
|
||||
|
||||
var features = providers.DocumentationNotes{
|
||||
providers.DocCreateDomains: providers.Cannot(),
|
||||
providers.DocDualHost: providers.Cannot(),
|
||||
providers.DocOfficiallySupported: providers.Cannot(),
|
||||
providers.CanUsePTR: providers.Cannot(),
|
||||
providers.CanUseSRV: providers.Can(),
|
||||
providers.CanUseCAA: providers.Can(),
|
||||
providers.CanUseTXTMulti: providers.Can(),
|
||||
providers.CanGetZones: providers.Cannot(),
|
||||
}
|
||||
|
||||
func init() {
|
||||
providers.RegisterDomainServiceProviderType("NETCUP", New, features)
|
||||
}
|
||||
|
||||
func New(settings map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
|
||||
if settings["api-key"] == "" || settings["api-password"] == "" || settings["customer-number"] == "" {
|
||||
return nil, fmt.Errorf("missing netcup login parameters")
|
||||
}
|
||||
|
||||
api := &api{}
|
||||
err := api.login(settings["api-key"], settings["api-password"], settings["customer-number"])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("login to netcup DNS failed, please check your credentials: %v", err)
|
||||
}
|
||||
return api, nil
|
||||
}
|
||||
|
||||
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
||||
func (api *api) GetZoneRecords(domain string) (models.Records, error) {
|
||||
records, err := api.getRecords(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
existingRecords := make([]*models.RecordConfig, len(records))
|
||||
for i := range records {
|
||||
existingRecords[i] = toRecordConfig(domain, &records[i])
|
||||
}
|
||||
return existingRecords, nil
|
||||
}
|
||||
|
||||
// GetNameservers returns the nameservers for a domain.
|
||||
// As netcup doesn't support setting nameservers over this API, these are static.
|
||||
// Domains not managed by netcup DNS will return an error
|
||||
func (api *api) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
||||
return models.ToNameservers([]string{
|
||||
"root-dns.netcup.net",
|
||||
"second-dns.netcup.net",
|
||||
"third-dns.netcup.net",
|
||||
})
|
||||
}
|
||||
|
||||
// GetDomainCorrections returns the corrections for a domain.
|
||||
func (api *api) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
dc, err := dc.Copy()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dc.Punycode()
|
||||
domain := dc.Name
|
||||
|
||||
// Setting the TTL is not supported for netcup
|
||||
for _, r := range dc.Records {
|
||||
r.TTL = 0
|
||||
}
|
||||
|
||||
// Filter out types we can't modify (like NS)
|
||||
newRecords := models.Records{}
|
||||
for _, r := range dc.Records {
|
||||
if r.Type != "NS" {
|
||||
newRecords = append(newRecords, r)
|
||||
}
|
||||
}
|
||||
dc.Records = newRecords
|
||||
|
||||
// Check existing set
|
||||
existingRecords, err := api.GetZoneRecords(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Normalize
|
||||
models.PostProcessRecords(existingRecords)
|
||||
differ := diff.New(dc)
|
||||
_, create, del, modify := differ.IncrementalDiff(existingRecords)
|
||||
|
||||
var corrections []*models.Correction
|
||||
|
||||
// Deletes first so changing type works etc.
|
||||
for _, m := range del {
|
||||
req := m.Existing.Original.(*record)
|
||||
corr := &models.Correction{
|
||||
Msg: fmt.Sprintf("%s, Netcup ID: %s", m.String(), req.Id),
|
||||
F: func() error {
|
||||
return api.deleteRecord(domain, req)
|
||||
},
|
||||
}
|
||||
corrections = append(corrections, corr)
|
||||
}
|
||||
|
||||
for _, m := range create {
|
||||
req := fromRecordConfig(m.Desired)
|
||||
corr := &models.Correction{
|
||||
Msg: m.String(),
|
||||
F: func() error {
|
||||
return api.createRecord(domain, req)
|
||||
},
|
||||
}
|
||||
corrections = append(corrections, corr)
|
||||
}
|
||||
for _, m := range modify {
|
||||
id := m.Existing.Original.(*record).Id
|
||||
req := fromRecordConfig(m.Desired)
|
||||
req.Id = id
|
||||
corr := &models.Correction{
|
||||
Msg: fmt.Sprintf("%s, Netcup ID: %s: ", m.String(), id),
|
||||
F: func() error {
|
||||
return api.modifyRecord(domain, req)
|
||||
},
|
||||
}
|
||||
corrections = append(corrections, corr)
|
||||
}
|
||||
|
||||
return corrections, nil
|
||||
}
|
147
providers/netcup/types.go
Normal file
147
providers/netcup/types.go
Normal file
@ -0,0 +1,147 @@
|
||||
package netcup
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type request struct {
|
||||
Action string `json:"action"`
|
||||
Param interface{} `json:"param"`
|
||||
}
|
||||
|
||||
type paramLogin struct {
|
||||
Key string `json:"apikey"`
|
||||
Password string `json:"apipassword"`
|
||||
CustomerNumber string `json:"customernumber"`
|
||||
}
|
||||
|
||||
type paramLogout struct {
|
||||
Key string `json:"apikey"`
|
||||
SessionId string `json:"apisessionid"`
|
||||
CustomerNumber string `json:"customernumber"`
|
||||
}
|
||||
|
||||
type paramGetRecords struct {
|
||||
Key string `json:"apikey"`
|
||||
SessionId string `json:"apisessionid"`
|
||||
CustomerNumber string `json:"customernumber"`
|
||||
DomainName string `json:"domainname"`
|
||||
}
|
||||
|
||||
type paramUpdateRecords struct {
|
||||
Key string `json:"apikey"`
|
||||
SessionId string `json:"apisessionid"`
|
||||
CustomerNumber string `json:"customernumber"`
|
||||
DomainName string `json:"domainname"`
|
||||
RecordSet records `json:"dnsrecordset"`
|
||||
}
|
||||
|
||||
type records struct {
|
||||
Records []record `json:"dnsrecords"`
|
||||
}
|
||||
|
||||
type record struct {
|
||||
Id string `json:"id"`
|
||||
Hostname string `json:"hostname"`
|
||||
Type string `json:"type"`
|
||||
Priority string `json:"priority"`
|
||||
Destination string `json:"destination"`
|
||||
Delete bool `json:"deleterecord"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type response struct {
|
||||
ServerRequestId string `json:"serverrequestid"`
|
||||
ClientRequestId string `json:"clientrequestid"`
|
||||
Action string `json:"action"`
|
||||
Status string `json:"status"`
|
||||
StatusCode int `json:"statuscode"`
|
||||
ShortMessage string `json:"shortmessage"`
|
||||
LongMessage string `json:"longmessage"`
|
||||
Data json.RawMessage `json:"responsedata"`
|
||||
}
|
||||
|
||||
type responseLogin struct {
|
||||
SessionId string `json:"apisessionid"`
|
||||
}
|
||||
|
||||
func toRecordConfig(domain string, r *record) *models.RecordConfig {
|
||||
priority, _ := strconv.ParseUint(r.Priority, 10, 32)
|
||||
|
||||
rc := &models.RecordConfig{
|
||||
Type: r.Type,
|
||||
TTL: uint32(0),
|
||||
MxPreference: uint16(priority),
|
||||
SrvPriority: uint16(priority),
|
||||
SrvWeight: uint16(0),
|
||||
SrvPort: uint16(0),
|
||||
Original: r,
|
||||
}
|
||||
rc.SetLabel(r.Hostname, domain)
|
||||
|
||||
switch rtype := r.Type; rtype { // #rtype_variations
|
||||
case "TXT":
|
||||
_ = rc.SetTargetTXT(r.Destination)
|
||||
case "NS", "ALIAS", "CNAME", "MX":
|
||||
_ = rc.SetTarget(dnsutil.AddOrigin(r.Destination+".", domain))
|
||||
case "SRV":
|
||||
parts := strings.Split(r.Destination, " ")
|
||||
priority, _ := strconv.ParseUint(parts[0], 10, 16)
|
||||
weight, _ := strconv.ParseUint(parts[1], 10, 16)
|
||||
port, _ := strconv.ParseUint(parts[2], 10, 16)
|
||||
rc.SrvPriority = uint16(priority)
|
||||
rc.SrvWeight = uint16(weight)
|
||||
rc.SrvPort = uint16(port)
|
||||
_ = rc.SetTarget(parts[3])
|
||||
case "CAA":
|
||||
parts := strings.Split(r.Destination, " ")
|
||||
caaFlag, _ := strconv.ParseUint(parts[0], 10, 32)
|
||||
rc.CaaFlag = uint8(caaFlag)
|
||||
rc.CaaTag = parts[1]
|
||||
_ = rc.SetTarget(strings.Trim(parts[2], "\""))
|
||||
default:
|
||||
_ = rc.SetTarget(r.Destination)
|
||||
}
|
||||
|
||||
return rc
|
||||
}
|
||||
|
||||
func fromRecordConfig(in *models.RecordConfig) *record {
|
||||
rc := &record{
|
||||
Hostname: in.GetLabel(),
|
||||
Type: in.Type,
|
||||
Destination: in.GetTargetField(),
|
||||
Delete: false,
|
||||
State: "",
|
||||
}
|
||||
|
||||
switch rc.Type { // #rtype_variations
|
||||
case "A", "AAAA", "PTR", "TXT", "SOA", "ALIAS":
|
||||
// Nothing special.
|
||||
case "CNAME":
|
||||
rc.Destination = strings.TrimSuffix(in.GetTargetField(), ".")
|
||||
case "NS":
|
||||
return nil // API ignores NS records
|
||||
case "MX":
|
||||
rc.Destination = strings.TrimSuffix(in.GetTargetField(), ".")
|
||||
rc.Priority = strconv.Itoa(int(in.MxPreference))
|
||||
case "SRV":
|
||||
rc.Destination = strconv.Itoa(int(in.SrvPriority)) + " " + strconv.Itoa(int(in.SrvWeight)) + " " + strconv.Itoa(int(in.SrvPort)) + " " + in.Target
|
||||
case "CAA":
|
||||
rc.Destination = strconv.Itoa(int(in.CaaFlag)) + " " + in.CaaTag + " \"" + in.GetTargetField() + "\""
|
||||
case "TLSA":
|
||||
rc.Destination = strconv.Itoa(int(in.TlsaUsage)) + " " + strconv.Itoa(int(in.TlsaSelector)) + " " + strconv.Itoa(int(in.TlsaMatchingType))
|
||||
case "SSHFP":
|
||||
rc.Destination = strconv.Itoa(int(in.SshfpAlgorithm)) + " " + strconv.Itoa(int(in.SshfpFingerprint))
|
||||
default:
|
||||
msg := fmt.Sprintf("ClouDNS.toReq rtype %v unimplemented", rc.Type)
|
||||
panic(msg)
|
||||
// We panic so that we quickly find any switch statements
|
||||
}
|
||||
return rc
|
||||
}
|
Reference in New Issue
Block a user