mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-11-10 09:12:47 +08:00
372 lines
10 KiB
Go
372 lines
10 KiB
Go
package hostingde
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/models"
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
|
|
"github.com/StackExchange/dnscontrol/v4/providers"
|
|
)
|
|
|
|
var defaultNameservers = []string{"ns1.hosting.de", "ns2.hosting.de", "ns3.hosting.de"}
|
|
|
|
var features = providers.DocumentationNotes{
|
|
// The default for unlisted capabilities is 'Cannot'.
|
|
// See providers/capabilities.go for the entire list of capabilities.
|
|
providers.CanAutoDNSSEC: providers.Can(),
|
|
providers.CanGetZones: providers.Can(),
|
|
providers.CanConcur: providers.Cannot(),
|
|
providers.CanUseAlias: providers.Can(),
|
|
providers.CanUseCAA: providers.Can(),
|
|
providers.CanUseDS: providers.Can(),
|
|
providers.CanUseLOC: providers.Cannot(),
|
|
providers.CanUseNAPTR: providers.Cannot(),
|
|
providers.CanUsePTR: providers.Can(),
|
|
providers.CanUseSOA: providers.Can(),
|
|
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,
|
|
RecordAuditor: AuditRecords,
|
|
}
|
|
providers.RegisterDomainServiceProviderType("HOSTINGDE", fns, features)
|
|
}
|
|
|
|
type providerMeta struct {
|
|
DefaultNS []string `json:"default_ns"`
|
|
}
|
|
|
|
func newHostingde(m map[string]string, providermeta json.RawMessage) (*hostingdeProvider, error) {
|
|
authToken, ownerAccountID, filterAccountID, baseURL := m["authToken"], m["ownerAccountId"], m["filterAccountId"], 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,
|
|
filterAccountID: filterAccountID,
|
|
baseURL: baseURL,
|
|
nameservers: defaultNameservers,
|
|
}
|
|
|
|
if len(providermeta) > 0 {
|
|
var pm providerMeta
|
|
if err := json.Unmarshal(providermeta, &pm); err != nil {
|
|
return nil, fmt.Errorf("hosting.de: could not parse providermeta: %w", err)
|
|
}
|
|
|
|
if len(pm.DefaultNS) > 0 {
|
|
hp.nameservers = pm.DefaultNS
|
|
}
|
|
}
|
|
|
|
return hp, nil
|
|
}
|
|
|
|
func newHostingdeDsp(m map[string]string, providermeta json.RawMessage) (providers.DNSServiceProvider, error) {
|
|
return newHostingde(m, providermeta)
|
|
}
|
|
|
|
func newHostingdeReg(m map[string]string) (providers.Registrar, error) {
|
|
return newHostingde(m, json.RawMessage{})
|
|
}
|
|
|
|
func (hp *hostingdeProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
|
return models.ToNameservers(hp.nameservers)
|
|
}
|
|
|
|
func (hp *hostingdeProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
|
|
zone, err := hp.getZone(domain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return hp.APIRecordsToStandardRecordsModel(domain, zone.Records), nil
|
|
}
|
|
|
|
func (hp *hostingdeProvider) APIRecordsToStandardRecordsModel(domain string, src []record) models.Records {
|
|
records := []*models.RecordConfig{}
|
|
for _, r := range src {
|
|
if r.Type == "SOA" {
|
|
continue
|
|
}
|
|
records = append(records, r.nativeToRecord(domain))
|
|
}
|
|
|
|
return records
|
|
}
|
|
|
|
func soaToString(s soaValues) string {
|
|
return fmt.Sprintf("refresh=%d retry=%d expire=%d negativettl=%d ttl=%d", s.Refresh, s.Retry, s.Expire, s.NegativeTTL, s.TTL)
|
|
}
|
|
|
|
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
|
|
func (hp *hostingdeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {
|
|
var err error
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
zoneChanged := false
|
|
|
|
zone, err := hp.getZone(dc.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
toReport, create, del, mod, err := diff.NewCompat(dc).IncrementalDiff(records)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Start corrections with the reports
|
|
corrections := diff.GenerateMessageCorrections(toReport)
|
|
|
|
// NOPURGE
|
|
if dc.KeepUnknown {
|
|
del = nil
|
|
}
|
|
|
|
// remove SOA record from corrections as it is handled separately
|
|
for i, r := range create {
|
|
if r.Desired.Type == "SOA" {
|
|
create = append(create[:i], create[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
if len(create) != 0 || len(del) != 0 || len(mod) != 0 {
|
|
zoneChanged = true
|
|
}
|
|
|
|
msg := []string{}
|
|
for _, c := range append(del, append(create, mod...)...) {
|
|
msg = append(msg, c.String())
|
|
}
|
|
|
|
var desiredSoa *models.RecordConfig
|
|
for _, r := range dc.Records {
|
|
if r.Type == "SOA" && r.Name == "@" {
|
|
desiredSoa = r
|
|
break
|
|
}
|
|
}
|
|
if desiredSoa == nil {
|
|
desiredSoa = &models.RecordConfig{}
|
|
}
|
|
|
|
defaultSoa := &hp.defaultSoa
|
|
// Commented out because this can not happen:
|
|
// if defaultSoa == nil {
|
|
// defaultSoa = &soaValues{}
|
|
// }
|
|
|
|
newSOA := soaValues{
|
|
Refresh: firstNonZero(desiredSoa.SoaRefresh, defaultSoa.Refresh, 86400),
|
|
Retry: firstNonZero(desiredSoa.SoaRetry, defaultSoa.Retry, 7200),
|
|
Expire: firstNonZero(desiredSoa.SoaExpire, defaultSoa.Expire, 3600000),
|
|
NegativeTTL: firstNonZero(desiredSoa.SoaMinttl, defaultSoa.NegativeTTL, 900),
|
|
TTL: firstNonZero(desiredSoa.TTL, defaultSoa.TTL, 86400),
|
|
}
|
|
|
|
if zone.ZoneConfig.SOAValues != newSOA {
|
|
msg = append(msg, fmt.Sprintf("Updating SOARecord from (%s) to (%s)", soaToString(zone.ZoneConfig.SOAValues), soaToString(newSOA)))
|
|
zone.ZoneConfig.SOAValues = newSOA
|
|
zoneChanged = true
|
|
}
|
|
|
|
if desiredSoa.SoaMbox != "" {
|
|
desiredMail := ""
|
|
if desiredSoa.SoaMbox[len(desiredSoa.SoaMbox)-1] != '.' {
|
|
desiredMail = desiredSoa.SoaMbox + "@" + dc.Name
|
|
}
|
|
if desiredMail != "" && zone.ZoneConfig.EmailAddress != desiredMail {
|
|
msg = append(msg, fmt.Sprintf("Changing SOA Mail from %s to %s", zone.ZoneConfig.EmailAddress, desiredMail))
|
|
zone.ZoneConfig.EmailAddress = desiredMail
|
|
zoneChanged = true
|
|
}
|
|
}
|
|
|
|
existingAutoDNSSecEnabled := zone.ZoneConfig.DNSSECMode == "automatic"
|
|
desiredAutoDNSSecEnabled := dc.AutoDNSSEC == "on"
|
|
|
|
var DNSSecOptions *dnsSecOptions
|
|
var removeDNSSecEntries []dnsSecEntry
|
|
|
|
// ensure that publishKsk is set for domains with AutoDNSSec
|
|
if existingAutoDNSSecEnabled && desiredAutoDNSSecEnabled {
|
|
currentDNSSecOptions, err := hp.getDNSSECOptions(zone.ZoneConfig.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !currentDNSSecOptions.PublishKSK {
|
|
msg = append(msg, "Enabling publishKsk for AutoDNSSec")
|
|
DNSSecOptions = currentDNSSecOptions
|
|
DNSSecOptions.PublishKSK = true
|
|
zoneChanged = true
|
|
}
|
|
}
|
|
|
|
if !existingAutoDNSSecEnabled && desiredAutoDNSSecEnabled {
|
|
msg = append(msg, "Enable AutoDNSSEC")
|
|
DNSSecOptions = &dnsSecOptions{
|
|
NSECMode: "nsec3",
|
|
PublishKSK: true,
|
|
}
|
|
zone.ZoneConfig.DNSSECMode = "automatic"
|
|
zoneChanged = true
|
|
} else if existingAutoDNSSecEnabled && !desiredAutoDNSSecEnabled {
|
|
currentDNSSecOptions, err := hp.getDNSSECOptions(zone.ZoneConfig.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
msg = append(msg, "Disable AutoDNSSEC")
|
|
zone.ZoneConfig.DNSSECMode = "off"
|
|
|
|
// Remove auto dnssec keys from domain
|
|
DomainConfig, err := hp.getDomainConfig(dc.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, entry := range DomainConfig.DNSSecEntries {
|
|
for _, autoDNSKey := range currentDNSSecOptions.Keys {
|
|
if entry.KeyData.PublicKey == autoDNSKey.KeyData.PublicKey {
|
|
removeDNSSecEntries = append(removeDNSSecEntries, entry)
|
|
}
|
|
}
|
|
}
|
|
zoneChanged = true
|
|
}
|
|
|
|
if !zoneChanged {
|
|
return nil, nil
|
|
}
|
|
|
|
corrections = append(corrections, &models.Correction{
|
|
Msg: fmt.Sprintf("\n%s", strings.Join(msg, "\n")),
|
|
F: func() error {
|
|
for i := 0; i < 10; i++ {
|
|
err := hp.updateZone(&zone.ZoneConfig, DNSSecOptions, create, del, mod)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
// Code:10205 indicates the zone is currently blocked due to a running zone update.
|
|
if !strings.Contains(err.Error(), "Code:10205") {
|
|
return err
|
|
}
|
|
|
|
// Exponential back-off retry.
|
|
// Base of 1.8 seemed like a good trade-off, retrying for approximately 45 seconds.
|
|
time.Sleep(time.Duration(math.Pow(1.8, float64(i))) * 100 * time.Millisecond)
|
|
}
|
|
return fmt.Errorf("retry exhaustion: zone blocked for 10 attempts")
|
|
},
|
|
},
|
|
)
|
|
|
|
if removeDNSSecEntries != nil {
|
|
correction := &models.Correction{
|
|
Msg: "Removing AutoDNSSEC Keys from Domain",
|
|
F: func() error {
|
|
err := hp.dnsSecKeyModify(dc.Name, nil, removeDNSSecEntries)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
corrections = append(corrections, correction)
|
|
}
|
|
|
|
return corrections, nil
|
|
}
|
|
|
|
func firstNonZero(items ...uint32) uint32 {
|
|
for _, item := range items {
|
|
if item != 0 {
|
|
return item
|
|
}
|
|
}
|
|
return 999
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (hp *hostingdeProvider) EnsureZoneExists(domain string) error {
|
|
_, err := hp.getZoneConfig(domain)
|
|
if err == errZoneNotFound {
|
|
if err := hp.createZone(domain); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (hp *hostingdeProvider) ListZones() ([]string, error) {
|
|
zcs, err := hp.getAllZoneConfigs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
zones := make([]string, 0, len(zcs))
|
|
for _, zoneConfig := range zcs {
|
|
zones = append(zones, zoneConfig.Name)
|
|
}
|
|
return zones, nil
|
|
|
|
}
|