2020-04-29 02:40:58 +08:00
package desec
import (
"bytes"
"encoding/json"
"fmt"
2020-07-01 17:55:20 +08:00
"sort"
2020-04-29 02:40:58 +08:00
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
2021-07-08 22:06:54 +08:00
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
2020-04-29 02:40:58 +08:00
"github.com/StackExchange/dnscontrol/v3/providers"
"github.com/miekg/dns/dnsutil"
)
/ *
desec API DNS provider :
Info required in ` creds.json ` :
- auth - token
* /
// NewDeSec creates the provider.
func NewDeSec ( m map [ string ] string , metadata json . RawMessage ) ( providers . DNSServiceProvider , error ) {
2020-10-26 21:25:30 +08:00
c := & desecProvider { }
2020-04-29 02:40:58 +08:00
c . creds . token = m [ "auth-token" ]
if c . creds . token == "" {
return nil , fmt . Errorf ( "missing deSEC auth-token" )
}
2021-07-08 22:06:54 +08:00
if err := c . authenticate ( ) ; err != nil {
return nil , fmt . Errorf ( "authentication failed" )
2020-04-29 02:40:58 +08:00
}
2021-07-21 23:44:10 +08:00
//DomainIndex is used for corrections (minttl) and domain creation
if err := c . initializeDomainIndex ( ) ; err != nil {
return nil , err
}
2020-04-29 02:40:58 +08:00
return c , nil
}
var features = providers . DocumentationNotes {
2022-03-03 00:19:15 +08:00
providers . CanAutoDNSSEC : providers . Can ( "deSEC always signs all records. When trying to disable, a notice is printed." ) ,
providers . CanGetZones : providers . Can ( ) ,
2021-05-18 03:45:24 +08:00
providers . CanUseAlias : providers . Unimplemented ( "Apex aliasing is supported via new SVCB and HTTPS record types. For details, check the deSEC docs." ) ,
2022-03-03 00:19:15 +08:00
providers . CanUseCAA : providers . Can ( ) ,
2020-05-30 22:40:21 +08:00
providers . CanUseDS : providers . Can ( ) ,
2022-03-03 00:19:15 +08:00
providers . CanUseNAPTR : providers . Can ( ) ,
providers . CanUsePTR : providers . Can ( ) ,
providers . CanUseSRV : providers . Can ( ) ,
2020-04-29 02:40:58 +08:00
providers . CanUseSSHFP : providers . Can ( ) ,
providers . CanUseTLSA : providers . Can ( ) ,
2022-03-03 00:19:15 +08:00
providers . DocCreateDomains : providers . Can ( ) ,
providers . DocDualHost : providers . Unimplemented ( ) ,
providers . DocOfficiallySupported : providers . Cannot ( ) ,
2020-04-29 02:40:58 +08:00
}
var defaultNameServerNames = [ ] string {
"ns1.desec.io" ,
"ns2.desec.org" ,
}
func init ( ) {
2021-03-08 02:19:22 +08:00
fns := providers . DspFuncs {
2021-05-05 02:15:31 +08:00
Initializer : NewDeSec ,
2021-03-09 09:14:30 +08:00
RecordAuditor : AuditRecords ,
2021-03-08 02:19:22 +08:00
}
providers . RegisterDomainServiceProviderType ( "DESEC" , fns , features )
2020-04-29 02:40:58 +08:00
}
// GetNameservers returns the nameservers for a domain.
2020-10-26 21:25:30 +08:00
func ( c * desecProvider ) GetNameservers ( domain string ) ( [ ] * models . Nameserver , error ) {
2020-04-29 02:40:58 +08:00
return models . ToNameservers ( defaultNameServerNames )
}
2020-10-26 21:25:30 +08:00
func ( c * desecProvider ) GetDomainCorrections ( dc * models . DomainConfig ) ( [ ] * models . Correction , error ) {
2021-05-18 03:45:24 +08:00
if dc . AutoDNSSEC == "off" {
fmt . Printf ( "Notice: DNSSEC signing was not requested, but cannot be turned off. (deSEC always signs all records.)\n" )
}
2020-04-29 02:40:58 +08:00
existing , err := c . GetZoneRecords ( dc . Name )
if err != nil {
return nil , err
}
models . PostProcessRecords ( existing )
clean := PrepFoundRecords ( existing )
2020-06-18 21:37:57 +08:00
var minTTL uint32
2021-07-21 23:44:10 +08:00
c . mutex . Lock ( )
2020-04-29 02:40:58 +08:00
if ttl , ok := c . domainIndex [ dc . Name ] ; ! ok {
2020-06-18 21:37:57 +08:00
minTTL = 3600
2020-04-29 02:40:58 +08:00
} else {
2020-06-18 21:37:57 +08:00
minTTL = ttl
2020-04-29 02:40:58 +08:00
}
2021-07-21 23:44:10 +08:00
c . mutex . Unlock ( )
2020-06-18 21:37:57 +08:00
PrepDesiredRecords ( dc , minTTL )
2020-04-29 02:40:58 +08:00
return c . GenerateDomainCorrections ( dc , clean )
}
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
2020-10-26 21:25:30 +08:00
func ( c * desecProvider ) GetZoneRecords ( domain string ) ( models . Records , error ) {
2020-04-29 02:40:58 +08:00
records , err := c . getRecords ( domain )
if err != nil {
return nil , err
}
// Convert them to DNScontrol's native format:
existingRecords := [ ] * models . RecordConfig { }
2021-07-08 22:06:54 +08:00
//spew.Dump(records)
2020-04-29 02:40:58 +08:00
for _ , rr := range records {
existingRecords = append ( existingRecords , nativeToRecords ( rr , domain ) ... )
}
return existingRecords , nil
}
// EnsureDomainExists returns an error if domain doesn't exist.
2020-10-26 21:25:30 +08:00
func ( c * desecProvider ) EnsureDomainExists ( domain string ) error {
2020-04-29 02:40:58 +08:00
// domain already exists
2021-07-21 23:44:10 +08:00
c . mutex . Lock ( )
defer c . mutex . Unlock ( )
2020-04-29 02:40:58 +08:00
if _ , ok := c . domainIndex [ domain ] ; ok {
return nil
}
return c . createDomain ( domain )
}
// PrepFoundRecords munges any records to make them compatible with
// this provider. Usually this is a no-op.
func PrepFoundRecords ( recs models . Records ) models . Records {
// If there are records that need to be modified, removed, etc. we
// do it here. Usually this is a no-op.
return recs
}
// PrepDesiredRecords munges any records to best suit this provider.
2020-06-18 21:37:57 +08:00
func PrepDesiredRecords ( dc * models . DomainConfig , minTTL uint32 ) {
2020-04-29 02:40:58 +08:00
// Sort through the dc.Records, eliminate any that can't be
// supported; modify any that need adjustments to work with the
// provider. We try to do minimal changes otherwise it gets
// confusing.
dc . Punycode ( )
2021-07-08 22:06:54 +08:00
txtutil . SplitSingleLongTxt ( dc . Records )
2020-04-29 02:40:58 +08:00
recordsToKeep := make ( [ ] * models . RecordConfig , 0 , len ( dc . Records ) )
for _ , rec := range dc . Records {
if rec . Type == "ALIAS" {
// deSEC does not permit ALIAS records, just ignore it
printer . Warnf ( "deSEC does not support alias records\n" )
continue
}
2020-06-18 21:37:57 +08:00
if rec . TTL < minTTL {
2020-04-29 02:40:58 +08:00
if rec . Type != "NS" {
2021-05-18 03:45:24 +08:00
printer . Warnf ( "Please contact support@desec.io if you need TTLs < %d. Setting TTL of %s type %s from %d to %d\n" , minTTL , rec . GetLabelFQDN ( ) , rec . Type , rec . TTL , minTTL )
2020-04-29 02:40:58 +08:00
}
2020-06-18 21:37:57 +08:00
rec . TTL = minTTL
2020-04-29 02:40:58 +08:00
}
recordsToKeep = append ( recordsToKeep , rec )
}
dc . Records = recordsToKeep
}
// GenerateDomainCorrections takes the desired and existing records
// and produces a Correction list. The correction list is simply
// a list of functions to call to actually make the desired
// correction, and a message to output to the user when the change is
// made.
2020-10-26 21:25:30 +08:00
func ( c * desecProvider ) GenerateDomainCorrections ( dc * models . DomainConfig , existing models . Records ) ( [ ] * models . Correction , error ) {
2020-04-29 02:40:58 +08:00
var corrections = [ ] * models . Correction { }
// diff existing vs. current.
differ := diff . New ( dc )
2020-08-21 03:49:00 +08:00
keysToUpdate , err := differ . ChangedGroups ( existing )
if err != nil {
return nil , err
}
2020-04-29 02:40:58 +08:00
if len ( keysToUpdate ) == 0 {
return nil , nil
}
desiredRecords := dc . Records . GroupedByKey ( )
var rrs [ ] resourceRecord
buf := & bytes . Buffer { }
// For any key with an update, delete or replace those records.
for label := range keysToUpdate {
if _ , ok := desiredRecords [ label ] ; ! ok {
//we could not find this RecordKey in the desiredRecords
//this means it must be deleted
for i , msg := range keysToUpdate [ label ] {
if i == 0 {
rc := resourceRecord { }
rc . Type = label . Type
rc . Records = make ( [ ] string , 0 ) // empty array of records should delete this rrset
rc . TTL = 3600
shortname := dnsutil . TrimDomainName ( label . NameFQDN , dc . Name )
if shortname == "@" {
shortname = ""
}
rc . Subname = shortname
fmt . Fprintln ( buf , msg )
rrs = append ( rrs , rc )
} else {
//just add the message
fmt . Fprintln ( buf , msg )
}
}
} else {
//it must be an update or create, both can be done with the same api call.
ns := recordsToNative ( desiredRecords [ label ] , dc . Name )
if len ( ns ) > 1 {
panic ( "we got more than one resource record to create / modify" )
}
for i , msg := range keysToUpdate [ label ] {
if i == 0 {
rrs = append ( rrs , ns [ 0 ] )
fmt . Fprintln ( buf , msg )
} else {
//noop just for printing the additional messages
fmt . Fprintln ( buf , msg )
}
}
}
}
2020-08-31 08:38:08 +08:00
msg := fmt . Sprintf ( "Changes:\n%s" , buf )
2020-04-29 02:40:58 +08:00
corrections = append ( corrections ,
& models . Correction {
Msg : msg ,
F : func ( ) error {
rc := rrs
2020-06-18 21:37:57 +08:00
err := c . upsertRR ( rc , dc . Name )
2020-04-29 02:40:58 +08:00
if err != nil {
return err
}
return nil
} ,
} )
2020-07-01 17:55:20 +08:00
// NB(tlim): This sort is just to make updates look pretty. It is
// cosmetic. The risk here is that there may be some updates that
// require a specific order (for example a delete before an add).
// However the code doesn't seem to have such situation. All tests
// pass. That said, if this breaks anything, the easiest fix might
// be to just remove the sort.
sort . Slice ( corrections , func ( i , j int ) bool { return diff . CorrectionLess ( corrections , i , j ) } )
2020-04-29 02:40:58 +08:00
return corrections , nil
}