New provider and new registrar: hosting.de (#1041)

* Add http.net provider

* Rename httpnetProvider

* Add SSHFP capability

* Add paging for records

* Sort documentation notes alphabetically

* Add custom base URL

* Extend documentation for custom base URL

* - renamed to hosting.de
- Fix EnsureDomainExists
- GetNameservers read from NS Records

* Replaced http.net with hosting.de
Contributor Support from hosting.de

* baseURL for hosting.de in documentation
replaced %v with %w for errors
special handling for txt records using .TxtStrings

* removed last references to rc.Target
fixed Trim of last dot

* Re-engineer TXT records for simplicity and better compliance (#1063)

Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
Co-authored-by: Oliver Dick <o.dick@hosting.de>
Co-authored-by: Oliver Dick <31733320+membero@users.noreply.github.com>
This commit is contained in:
Julius Rickert 2021-03-09 01:25:55 +01:00 committed by GitHub
parent 18933436cf
commit c883c1ac68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 772 additions and 0 deletions

1
OWNERS
View file

@ -13,6 +13,7 @@ providers/gandi_v5 @TomOnTime
providers/hedns @rblenkinsopp
providers/hetzner @das7pad
providers/hexonet @papakai
providers/hostingde @juliusrickert
providers/internetbs @pragmaton
providers/inwx @svenpeter42
providers/msdns @tlimoncelli

View file

@ -30,6 +30,7 @@ Currently supported DNS providers:
- Google DNS
- Hetzner
- HEXONET
- hosting.de
- Hurricane Electric DNS
- INWX
- Internet.bs

View file

@ -23,6 +23,7 @@
<th class="rotate"><div><span>HEDNS</span></div></th>
<th class="rotate"><div><span>HETZNER</span></div></th>
<th class="rotate"><div><span>HEXONET</span></div></th>
<th class="rotate"><div><span>HOSTINGDE</span></div></th>
<th class="rotate"><div><span>INTERNETBS</span></div></th>
<th class="rotate"><div><span>INWX</span></div></th>
<th class="rotate"><div><span>LINODE</span></div></th>
@ -104,6 +105,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>
@ -197,6 +201,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>
@ -305,6 +312,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>
@ -389,6 +399,9 @@
<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>
<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="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="INWX does not support the ALIAS or ANAME record type.">
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
@ -455,6 +468,9 @@
</td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td class="info" data-toggle="tooltip" data-container="body" data-placement="top" title="Supported but not implemented yet.">
<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" data-toggle="tooltip" data-container="body" data-placement="top" title="Supported by INWX but not implemented yet.">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
@ -525,6 +541,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><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
@ -607,6 +626,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><i class="fa fa-minus dim"></i></td>
<td class="success" data-toggle="tooltip" data-container="body" data-placement="top" title="PTR records with empty targets are not supported">
<i class="fa has-tooltip fa-check text-success" aria-hidden="true"></i>
@ -677,6 +699,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 class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
@ -749,6 +774,9 @@
<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>
</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>
@ -833,6 +861,9 @@
<i class="fa fa-times text-danger" 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>
@ -905,6 +936,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><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
@ -971,6 +1005,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>
@ -993,6 +1028,9 @@
<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>
<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>
@ -1033,6 +1071,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 class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
@ -1081,6 +1122,9 @@
<i class="fa fa-times text-danger" 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="info" data-toggle="tooltip" data-container="body" data-placement="top" title="DS records are only supported at the apex and require a different API call that hasn&#39;t been implemented yet.">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
@ -1149,6 +1193,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><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
@ -1243,6 +1290,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>
@ -1357,6 +1407,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>
@ -1443,6 +1496,9 @@
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></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>

View file

@ -0,0 +1,40 @@
---
name: hosting.de
title: hosting.de Provider
layout: default
jsId: hostingde
---
# hosting.de Provider
## Configuration
In your credentials file, you must provide your [`authToken` and optionally an `ownerAccountId`](https://www.hosting.de/api/#requests-and-authentication).
**If you want to use this provider with http.net or a demo system you need to provide a custom `baseURL`.**
* hosting.de (default): `https://secure.hosting.de`
* http.net: `https://partner.http.net`
* Demo: `https://demo.routing.net`
{% highlight json %}
{
"hosting.de": {
"authToken": "YOUR_API_KEY"
},
"http.net": {
"authToken": "YOUR_API_KEY",
"baseURL": "https://partner.http.net"
}
}
{% endhighlight %}
## Usage
Example JavaScript:
{% highlight js %}
var REG_HOSTINGDE = NewRegistrar('hosting.de', 'HOSTINGDE')
var DNS_HOSTINGDE = NewDnsProvider('hosting.de' 'HOSTINGDE');
D('example.tld', REG_HOSTINGDE, DnsProvider(DNS_HOSTINGDE),
A('test', '1.2.3.4')
);
{% endhighlight %}

View file

@ -83,6 +83,7 @@ Maintainers of contributed providers:
* `HEDNS` @rblenkinsopp
* `HETZNER` @das7pad
* `HEXONET` @papakai
* `HOSTINGDE` @membero
* `INTERNETBS` @pragmaton
* `INWX` @svenpeter42
* `LINODE` @koesie10

View file

@ -926,6 +926,7 @@ func makeTests(t *testing.T) []*TestGroup {
//"MSDNS", // No paging done. No need to test.
//"AZURE_DNS", // Currently failing. See https://github.com/StackExchange/dnscontrol/issues/770
"HEXONET",
"HOSTINGDE",
//"ROUTE53", // Currently failing. See https://github.com/StackExchange/dnscontrol/issues/908
),
tc("1200 records", manyA("rec%04d", "1.2.3.4", 1200)...),

View file

@ -86,6 +86,10 @@
"domain": "$HEXONET_DOMAIN",
"ipaddress": "$HEXONET_IP"
},
"HOSTINGDE": {
"authToken": "$HOSTINGDE_AUTHTOKEN",
"domain": "$HOSTINGDE_DOMAIN"
},
"INWX": {
"domain": "$INWX_DOMAIN",
"password": "$INWX_PASSWORD",

View file

@ -20,6 +20,7 @@ import (
_ "github.com/StackExchange/dnscontrol/v3/providers/hedns"
_ "github.com/StackExchange/dnscontrol/v3/providers/hetzner"
_ "github.com/StackExchange/dnscontrol/v3/providers/hexonet"
_ "github.com/StackExchange/dnscontrol/v3/providers/hostingde"
_ "github.com/StackExchange/dnscontrol/v3/providers/internetbs"
_ "github.com/StackExchange/dnscontrol/v3/providers/inwx"
_ "github.com/StackExchange/dnscontrol/v3/providers/linode"

273
providers/hostingde/api.go Normal file
View file

@ -0,0 +1,273 @@
package hostingde
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
"golang.org/x/net/idna"
)
const endpoint = "%s/api/%s/v1/json/%s"
type hostingdeProvider struct {
authToken string
ownerAccountID string
baseURL string
}
func (hp *hostingdeProvider) getDomainConfig(domain string) (*domainConfig, error) {
zc, err := hp.getZoneConfig(domain)
if err != nil {
return nil, fmt.Errorf("error getting zone config: %w", err)
}
params := request{
Filter: filter{
Field: "domainName",
Value: zc.Name,
},
}
resp, err := hp.get("domain", "domainsFind", params)
if err != nil {
return nil, fmt.Errorf("error getting domain info: %w", err)
}
domainConf := []*domainConfig{}
if err := json.Unmarshal(resp.Data, &domainConf); err != nil {
return nil, fmt.Errorf("error parsing response: %w", err)
}
if len(domainConf) == 0 {
return nil, fmt.Errorf("could not get domain config: %s", domain)
}
return domainConf[0], nil
}
func (hp *hostingdeProvider) createZone(domain string) error {
t, err := idna.ToASCII(domain)
if err != nil {
return err
}
records := []*record{}
for _, ns := range defaultNameservers {
records = append(records, &record{
Name: domain,
Type: "NS",
Content: ns,
TTL: 86400,
})
}
params := request{
ZoneConfig: &zoneConfig{
Name: t,
Type: "NATIVE",
},
Records: records,
}
_, err = hp.get("dns", "zoneCreate", params)
if err != nil {
return fmt.Errorf("error creating zone: %w", err)
}
return nil
}
func (hp *hostingdeProvider) getNameservers(domain string) ([]string, error) {
t, err := idna.ToASCII(domain)
if err != nil {
return nil, err
}
domainConf, err := hp.getDomainConfig(t)
if err != nil {
return nil, fmt.Errorf("error getting domain config: %w", err)
}
nss := []string{}
for _, ns := range domainConf.Nameservers {
// Currently does not support glued IP addresses
if len(ns.IPs) > 0 {
return nil, fmt.Errorf("domain %s has glued IP addresses which are not supported", domain)
}
nss = append(nss, ns.Name)
}
return nss, nil
}
func (hp *hostingdeProvider) updateNameservers(nss []string, domain string) func() error {
return func() error {
domainConf, err := hp.getDomainConfig(domain)
if err != nil {
return err
}
nameservers := []nameserver{}
for _, ns := range nss {
nameservers = append(nameservers, nameserver{Name: ns})
}
domainConf.Nameservers = nameservers
params := request{
Domain: domainConf,
}
if _, err := hp.get("domain", "domainUpdate", params); err != nil {
return err
}
return nil
}
}
func (hp *hostingdeProvider) getRecords(domain string) ([]*record, error) {
zc, err := hp.getZoneConfig(domain)
if err != nil {
return nil, err
}
records := []*record{}
page := uint(1)
for {
params := request{
Filter: filter{
Field: "ZoneConfigId",
Value: zc.ID,
},
Limit: 1000,
Page: page,
}
resp, err := hp.get("dns", "recordsFind", params)
if err != nil {
return nil, err
}
newRecords := []*record{}
if err := json.Unmarshal(resp.Data, &newRecords); err != nil {
return nil, err
}
records = append(records, newRecords...)
if page >= resp.TotalPages {
break
}
page++
}
return records, nil
}
func (hp *hostingdeProvider) updateRecords(domain string, create, del, mod diff.Changeset) error {
zc, err := hp.getZoneConfig(domain)
if err != nil {
return err
}
toAdd := []*record{}
for _, c := range create {
r := recordToNative(c.Desired)
toAdd = append(toAdd, r)
}
toDelete := []*record{}
for _, d := range del {
r := recordToNative(d.Existing)
r.ID = d.Existing.Original.(*record).ID
toDelete = append(toDelete, r)
}
toModify := []*record{}
for _, m := range mod {
r := recordToNative(m.Desired)
r.ID = m.Existing.Original.(*record).ID
toModify = append(toModify, r)
}
params := request{
ZoneConfig: zc,
RecordsToAdd: toAdd,
RecordsToDelete: toDelete,
RecordsToModify: toModify,
}
_, err = hp.get("dns", "zoneUpdate", params)
if err != nil {
return err
}
return nil
}
func (hp *hostingdeProvider) getZoneConfig(domain string) (*zoneConfig, error) {
t, err := idna.ToASCII(domain)
if err != nil {
return nil, err
}
params := request{
Filter: filter{
Field: "ZoneName",
Value: t,
},
}
resp, err := hp.get("dns", "zoneConfigsFind", params)
if err != nil {
return nil, fmt.Errorf("could not get zone config: %w", err)
}
zc := []*zoneConfig{}
if err := json.Unmarshal(resp.Data, &zc); err != nil {
return nil, fmt.Errorf("could not parse response: %w", err)
}
if len(zc) == 0 {
return nil, errZoneNotFound
}
return zc[0], nil
}
func (hp *hostingdeProvider) get(service, method string, params request) (*responseData, error) {
params.AuthToken = hp.authToken
params.OwnerAccountID = hp.ownerAccountID
reqBody, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("could not marshal request body: %w", err)
}
url := fmt.Sprintf(endpoint, hp.baseURL, service, method)
resp, err := http.Post(url, "application/json", bytes.NewBuffer(reqBody))
if err != nil {
return nil, fmt.Errorf("could not carry out request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("error occurred: %s", resp.Status)
}
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("could not read response body: %w", err)
}
respData := &response{}
if err := json.Unmarshal(bodyBytes, &respData); err != nil {
return nil, fmt.Errorf("could not unmarshal response body: %w", err)
}
if len(respData.Errors) > 0 && respData.Status == "error" {
return nil, fmt.Errorf("%+v", respData.Errors)
}
return respData.Response, nil
}

View file

@ -0,0 +1,11 @@
package hostingde
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View file

@ -0,0 +1,201 @@
package hostingde
import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/providers"
)
var defaultNameservers = []string{"ns1.hosting.de", "ns2.hosting.de", "ns3.hosting.de"}
var features = providers.DocumentationNotes{
providers.CanAutoDNSSEC: providers.Unimplemented("Supported but not implemented yet."),
providers.CanGetZones: providers.Can(),
providers.CanUseAlias: providers.Can(),
providers.CanUseAzureAlias: providers.Cannot(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Can(),
providers.CanUseNAPTR: providers.Cannot(),
providers.CanUsePTR: providers.Can(),
providers.CanUseRoute53Alias: providers.Cannot(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
providers.CanUseTLSA: providers.Can(),
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot(),
}
func init() {
providers.RegisterRegistrarType("HOSTINGDE", newHostingdeReg)
fns := providers.DspFuncs{
Initializer: newHostingdeDsp,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("HOSTINGDE", fns, features)
}
func newHostingde(m map[string]string) (*hostingdeProvider, error) {
authToken, ownerAccountID, baseURL := m["authToken"], m["ownerAccountId"], m["baseURL"]
if authToken == "" {
return nil, fmt.Errorf("hosting.de: authtoken must be provided")
}
if baseURL == "" {
baseURL = "https://secure.hosting.de"
}
baseURL = strings.TrimSuffix(baseURL, "/")
hp := &hostingdeProvider{
authToken: authToken,
ownerAccountID: ownerAccountID,
baseURL: baseURL,
}
return hp, nil
}
func newHostingdeDsp(m map[string]string, raw json.RawMessage) (providers.DNSServiceProvider, error) {
return newHostingde(m)
}
func newHostingdeReg(m map[string]string) (providers.Registrar, error) {
return newHostingde(m)
}
func (hp *hostingdeProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
src, err := hp.getRecords(domain)
if err != nil {
return nil, err
}
var nameservers []string
for _, record := range src {
if record.Type == "NS" {
nameservers = append(nameservers, record.Content)
}
}
return models.ToNameservers(nameservers)
}
func (hp *hostingdeProvider) GetZoneRecords(domain string) (models.Records, error) {
src, err := hp.getRecords(domain)
if err != nil {
return nil, err
}
records := []*models.RecordConfig{}
for _, r := range src {
if r.Type == "SOA" {
continue
}
records = append(records, r.nativeToRecord(domain))
}
return records, nil
}
func (hp *hostingdeProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
err := dc.Punycode()
if err != nil {
return nil, err
}
// TTL must be between (inclusive) 1m and 1y (in fact, a little bit more)
for _, r := range dc.Records {
if r.TTL < 60 {
r.TTL = 60
}
if r.TTL > 31556926 {
r.TTL = 31556926
}
}
records, err := hp.GetZoneRecords(dc.Name)
if err != nil {
return nil, err
}
differ := diff.New(dc)
_, create, del, mod, err := differ.IncrementalDiff(records)
if err != nil {
return nil, err
}
// NOPURGE
if dc.KeepUnknown {
del = []diff.Correlation{}
}
msg := []string{}
for _, c := range append(del, append(create, mod...)...) {
msg = append(msg, c.String())
}
if len(create) == 0 && len(del) == 0 && len(mod) == 0 {
return nil, nil
}
corrections := []*models.Correction{
{
Msg: fmt.Sprintf("\n%s", strings.Join(msg, "\n")),
F: func() error {
return hp.updateRecords(dc.Name, create, del, mod)
},
},
}
return corrections, nil
}
func (hp *hostingdeProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
err := dc.Punycode()
if err != nil {
return nil, err
}
found, err := hp.getNameservers(dc.Name)
if err != nil {
return nil, fmt.Errorf("error getting nameservers: %w", err)
}
sort.Strings(found)
foundNameservers := strings.Join(found, ",")
expected := []string{}
for _, ns := range dc.Nameservers {
expected = append(expected, ns.Name)
}
sort.Strings(expected)
expectedNameservers := strings.Join(expected, ",")
// We don't care about glued records because we disallowed them
if foundNameservers != expectedNameservers {
return []*models.Correction{
{
Msg: fmt.Sprintf("Update nameservers %s -> %s", foundNameservers, expectedNameservers),
F: hp.updateNameservers(expected, dc.Name),
},
}, nil
}
return nil, nil
// TODO: Handle AutoDNSSEC
}
func (hp *hostingdeProvider) EnsureDomainExists(domain string) error {
_, err := hp.getZoneConfig(domain)
if err == errZoneNotFound {
if err := hp.createZone(domain); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,182 @@
package hostingde
import (
"encoding/json"
"fmt"
"log"
"net"
"strings"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/pkg/errors"
)
var (
errZoneNotFound = errors.Errorf("zone not found")
)
type request struct {
AuthToken string `json:"authToken"`
OwnerAccountID string `json:"ownerAccountId,omitempty"`
Filter filter `json:"filter,omitempty"`
Limit uint `json:"limit,omitempty"`
Page uint `json:"page,omitempty"`
// Update Zone
ZoneConfig *zoneConfig `json:"zoneConfig"`
RecordsToAdd []*record `json:"recordsToAdd"`
RecordsToModify []*record `json:"recordsToModify"`
RecordsToDelete []*record `json:"recordsToDelete"`
// Create Zone
Records []*record `json:"records"`
// Domain
Domain *domainConfig `json:"domain"`
}
type filter struct {
Field string `json:"field"`
Value string `json:"value"`
Relation string `json:"relation,omitempty"`
}
type nameserver struct {
Name string `json:"name"`
IPs []net.IP `json:"ips"`
}
type domainConfig struct {
Name string `json:"name"`
Contacts json.RawMessage `json:"contacts"`
Nameservers []nameserver `json:"nameservers"`
TransferLockEnabled bool `json:"transferLockEnabled"`
}
type zoneConfig struct {
ID string `json:"id"`
DNSSECMode string `json:"dnsSecMode"`
EmailAddress string `json:"emailAddress,omitempty"`
MasterIP string `json:"masterIp"`
Name string `json:"name"` // Not required per docs, but required IRL
NameUnicode string `json:"nameUnicode"`
// SOAValues struct {
// Refresh uint32 `json:"refresh"`
// Retry uint32 `json:"retry"`
// Expire uint32 `json:"expire"`
// TTL uint32 `json:"ttl"`
// NegativeTTL uint32 `json:"negativeTtl"`
// } `json:"soaValues,omitempty"`
Type string `json:"type"`
ZoneTransferWhitelist []string `json:"zoneTransferWhitelist"`
}
type record struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
TTL uint32 `json:"ttl"`
Priority uint16 `json:"priority"`
}
type response struct {
Errors []apiError `json:"errors"`
Response *responseData `json:"response"`
Status string `json:"status"`
}
type apiError struct {
Code int `json:"code"`
ContextObject string `json:"contextObject"`
ContextPath string `json:"contextPath"`
Text string `json:"text"`
Value string `json:"value"`
}
type responseData struct {
Data json.RawMessage `json:"data"`
Type string `json:"type"`
Limit uint `json:"limit"`
Page uint `json:"page"`
TotalPages uint `json:"totalPages"`
}
func (r *record) nativeToRecord(domain string) *models.RecordConfig {
// normalize cname,mx,ns records with dots to be consistent with our config format.
if r.Type == "ALIAS" || r.Type == "CNAME" || r.Type == "MX" || r.Type == "NS" || r.Type == "SRV" {
if r.Content != "." {
r.Content = r.Content + "."
}
}
rc := &models.RecordConfig{
Type: "",
TTL: r.TTL,
MxPreference: r.Priority,
SrvPriority: r.Priority,
Original: r,
}
rc.SetLabelFromFQDN(r.Name, domain)
var err error
switch r.Type {
case "ALIAS":
rc.Type = r.Type
rc.SetTarget(r.Content)
case "NULLMX":
err = rc.PopulateFromString("MX", "0 .", domain)
case "MX":
err = rc.SetTargetMX(uint16(r.Priority), r.Content)
case "SRV":
err = rc.SetTargetSRVPriorityString(uint16(r.Priority), r.Content)
default:
if err := rc.PopulateFromString(r.Type, r.Content, domain); err != nil {
panic(err)
}
}
if err != nil {
panic(err)
}
return rc
}
func recordToNative(rc *models.RecordConfig) *record {
record := &record{
Name: rc.NameFQDN,
Type: rc.Type,
Content: strings.TrimSuffix(rc.GetTargetCombined(), "."),
TTL: rc.TTL,
}
switch rc.Type { // #rtype_variations
case "A", "AAAA", "ALIAS", "CAA", "CNAME", "DNSKEY", "DS", "NS", "NSEC", "NSEC3", "NSEC3PARAM", "PTR", "RRSIG", "SSHFP", "TSLA":
// Nothing special.
case "TXT":
if cap(rc.TxtStrings) == 1 {
record.Content = "\"" + rc.TxtStrings[0] + "\""
} else if cap(rc.TxtStrings) > 1 {
record.Content = ""
for _, str := range rc.TxtStrings {
record.Content = record.Content + " \"" + str + "\""
}
record.Content = record.Content[1:len(record.Content)]
}
case "MX":
record.Priority = rc.MxPreference
record.Content = strings.TrimSuffix(rc.GetTargetField(), ".")
if record.Content == "" {
record.Type = "NULLMX"
record.Priority = 10
}
case "SRV":
record.Priority = rc.SrvPriority
record.Content = fmt.Sprintf("%d %d %s", rc.SrvWeight, rc.SrvPort, strings.TrimSuffix(rc.GetTargetField(), "."))
default:
log.Printf("hosting.de rtype %v unimplemented", rc.Type)
}
return record
}