mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-09-30 08:54:26 +08:00
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:
parent
18933436cf
commit
c883c1ac68
12 changed files with 772 additions and 0 deletions
1
OWNERS
1
OWNERS
|
@ -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
|
||||
|
|
|
@ -30,6 +30,7 @@ Currently supported DNS providers:
|
|||
- Google DNS
|
||||
- Hetzner
|
||||
- HEXONET
|
||||
- hosting.de
|
||||
- Hurricane Electric DNS
|
||||
- INWX
|
||||
- Internet.bs
|
||||
|
|
|
@ -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'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>
|
||||
|
|
40
docs/_providers/hostingde.md
Normal file
40
docs/_providers/hostingde.md
Normal 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 %}
|
|
@ -83,6 +83,7 @@ Maintainers of contributed providers:
|
|||
* `HEDNS` @rblenkinsopp
|
||||
* `HETZNER` @das7pad
|
||||
* `HEXONET` @papakai
|
||||
* `HOSTINGDE` @membero
|
||||
* `INTERNETBS` @pragmaton
|
||||
* `INWX` @svenpeter42
|
||||
* `LINODE` @koesie10
|
||||
|
|
|
@ -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)...),
|
||||
|
|
|
@ -86,6 +86,10 @@
|
|||
"domain": "$HEXONET_DOMAIN",
|
||||
"ipaddress": "$HEXONET_IP"
|
||||
},
|
||||
"HOSTINGDE": {
|
||||
"authToken": "$HOSTINGDE_AUTHTOKEN",
|
||||
"domain": "$HOSTINGDE_DOMAIN"
|
||||
},
|
||||
"INWX": {
|
||||
"domain": "$INWX_DOMAIN",
|
||||
"password": "$INWX_PASSWORD",
|
||||
|
|
|
@ -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
273
providers/hostingde/api.go
Normal 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
|
||||
}
|
11
providers/hostingde/auditrecords.go
Normal file
11
providers/hostingde/auditrecords.go
Normal 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
|
||||
}
|
201
providers/hostingde/hostingdeProvider.go
Normal file
201
providers/hostingde/hostingdeProvider.go
Normal 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
|
||||
}
|
182
providers/hostingde/types.go
Normal file
182
providers/hostingde/types.go
Normal 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
|
||||
}
|
Loading…
Add table
Reference in a new issue