2020-04-28 20:40:58 +02:00
package desec
import (
"bytes"
"encoding/json"
"fmt"
2023-05-20 19:21:45 +02:00
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
"github.com/StackExchange/dnscontrol/v4/providers"
2020-04-28 20:40:58 +02:00
"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 09:25:30 -04:00
c := & desecProvider { }
2020-04-28 20:40:58 +02:00
c . creds . token = m [ "auth-token" ]
if c . creds . token == "" {
return nil , fmt . Errorf ( "missing deSEC auth-token" )
}
2021-07-08 16:06:54 +02:00
if err := c . authenticate ( ) ; err != nil {
return nil , fmt . Errorf ( "authentication failed" )
2020-04-28 20:40:58 +02:00
}
2021-07-21 17:44:10 +02:00
//DomainIndex is used for corrections (minttl) and domain creation
if err := c . initializeDomainIndex ( ) ; err != nil {
return nil , err
}
2020-04-28 20:40:58 +02:00
return c , nil
}
var features = providers . DocumentationNotes {
2022-03-02 11:19:15 -05:00
providers . CanAutoDNSSEC : providers . Can ( "deSEC always signs all records. When trying to disable, a notice is printed." ) ,
providers . CanGetZones : providers . Can ( ) ,
2021-05-17 21:45:24 +02:00
providers . CanUseAlias : providers . Unimplemented ( "Apex aliasing is supported via new SVCB and HTTPS record types. For details, check the deSEC docs." ) ,
2022-03-02 11:19:15 -05:00
providers . CanUseCAA : providers . Can ( ) ,
2020-05-30 10:40:21 -04:00
providers . CanUseDS : providers . Can ( ) ,
2023-03-16 19:59:44 -04:00
providers . CanUseLOC : providers . Unimplemented ( ) ,
2022-03-02 11:19:15 -05:00
providers . CanUseNAPTR : providers . Can ( ) ,
providers . CanUsePTR : providers . Can ( ) ,
providers . CanUseSRV : providers . Can ( ) ,
2020-04-28 20:40:58 +02:00
providers . CanUseSSHFP : providers . Can ( ) ,
providers . CanUseTLSA : providers . Can ( ) ,
2022-03-02 11:19:15 -05:00
providers . DocCreateDomains : providers . Can ( ) ,
providers . DocDualHost : providers . Unimplemented ( ) ,
providers . DocOfficiallySupported : providers . Cannot ( ) ,
2020-04-28 20:40:58 +02:00
}
var defaultNameServerNames = [ ] string {
"ns1.desec.io" ,
"ns2.desec.org" ,
}
func init ( ) {
2021-03-07 13:19:22 -05:00
fns := providers . DspFuncs {
2021-05-04 14:15:31 -04:00
Initializer : NewDeSec ,
2021-03-08 20:14:30 -05:00
RecordAuditor : AuditRecords ,
2021-03-07 13:19:22 -05:00
}
providers . RegisterDomainServiceProviderType ( "DESEC" , fns , features )
2020-04-28 20:40:58 +02:00
}
// GetNameservers returns the nameservers for a domain.
2020-10-26 09:25:30 -04:00
func ( c * desecProvider ) GetNameservers ( domain string ) ( [ ] * models . Nameserver , error ) {
2020-04-28 20:40:58 +02:00
return models . ToNameservers ( defaultNameServerNames )
}
2023-04-14 15:22:23 -04:00
// func (c *desecProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
// if dc.AutoDNSSEC == "off" {
// printer.Printf("Notice: DNSSEC signing was not requested, but cannot be turned off. (deSEC always signs all records.)\n")
// }
// existing, err := c.GetZoneRecords(dc.Name)
// if err != nil {
// return nil, err
// }
// models.PostProcessRecords(existing)
// clean := PrepFoundRecords(existing)
// var minTTL uint32
// c.mutex.Lock()
// if ttl, ok := c.domainIndex[dc.Name]; !ok {
// minTTL = 3600
// } else {
// minTTL = ttl
// }
// c.mutex.Unlock()
// PrepDesiredRecords(dc, minTTL)
// return c.GetZoneRecordsCorrections(dc, clean)
// }
2020-04-28 20:40:58 +02:00
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
2023-05-02 13:04:59 -04:00
func ( c * desecProvider ) GetZoneRecords ( domain string , meta map [ string ] string ) ( models . Records , error ) {
2020-04-28 20:40:58 +02: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 16:06:54 +02:00
//spew.Dump(records)
2020-04-28 20:40:58 +02:00
for _ , rr := range records {
existingRecords = append ( existingRecords , nativeToRecords ( rr , domain ) ... )
}
2023-04-14 15:22:23 -04:00
2020-04-28 20:40:58 +02:00
return existingRecords , nil
}
2023-02-07 17:52:49 +05:30
// EnsureZoneExists creates a zone if it does not exist
func ( c * desecProvider ) EnsureZoneExists ( domain string ) error {
2021-07-21 17:44:10 +02:00
c . mutex . Lock ( )
defer c . mutex . Unlock ( )
2020-04-28 20:40:58 +02:00
if _ , ok := c . domainIndex [ domain ] ; ok {
return nil
}
return c . createDomain ( domain )
}
// PrepDesiredRecords munges any records to best suit this provider.
2020-06-18 09:37:57 -04:00
func PrepDesiredRecords ( dc * models . DomainConfig , minTTL uint32 ) {
2020-04-28 20:40:58 +02: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.
2023-04-14 15:22:23 -04:00
//dc.Punycode()
2020-04-28 20:40:58 +02: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 09:37:57 -04:00
if rec . TTL < minTTL {
2020-04-28 20:40:58 +02:00
if rec . Type != "NS" {
2021-05-17 21:45:24 +02: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-28 20:40:58 +02:00
}
2020-06-18 09:37:57 -04:00
rec . TTL = minTTL
2020-04-28 20:40:58 +02:00
}
recordsToKeep = append ( recordsToKeep , rec )
}
dc . Records = recordsToKeep
}
2023-04-14 15:22:23 -04:00
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
func ( c * desecProvider ) GetZoneRecordsCorrections ( dc * models . DomainConfig , existing models . Records ) ( [ ] * models . Correction , error ) {
var minTTL uint32
c . mutex . Lock ( )
if ttl , ok := c . domainIndex [ dc . Name ] ; ! ok {
minTTL = 3600
} else {
minTTL = ttl
}
c . mutex . Unlock ( )
PrepDesiredRecords ( dc , minTTL )
2020-04-28 20:40:58 +02:00
2023-10-22 13:56:13 -04:00
keysToUpdate , toReport , err := diff . NewCompat ( dc ) . ChangedGroups ( existing )
2023-02-02 09:27:30 -05:00
if err != nil {
return nil , err
}
2023-10-22 13:56:13 -04:00
// Start corrections with the reports
corrections := diff . GenerateMessageCorrections ( toReport )
2020-04-28 20:40:58 +02:00
2023-10-22 13:56:13 -04:00
if len ( corrections ) == 0 && len ( keysToUpdate ) == 0 {
2023-02-02 09:27:30 -05:00
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 = ""
2020-04-28 20:40:58 +02:00
}
2023-02-02 09:27:30 -05:00
rc . Subname = shortname
fmt . Fprintln ( buf , msg )
rrs = append ( rrs , rc )
} else {
//just add the message
fmt . Fprintln ( buf , msg )
2020-04-28 20:40:58 +02:00
}
2023-02-02 09:27:30 -05:00
}
} 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-04-28 20:40:58 +02:00
}
}
}
}
2023-02-02 09:27:30 -05:00
msg := fmt . Sprintf ( "Changes:\n%s" , buf )
corrections = append ( corrections ,
& models . Correction {
Msg : msg ,
F : func ( ) error {
rc := rrs
err := c . upsertRR ( rc , dc . Name )
if err != nil {
return err
}
return nil
} ,
} )
// 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.
2023-11-19 13:44:49 -05:00
//sort.Slice(corrections, func(i, j int) bool { return diff.CorrectionLess(corrections, i, j) })
2022-12-11 15:02:58 -05:00
2020-04-28 20:40:58 +02:00
return corrections , nil
}
2023-05-21 01:20:37 +08:00
// ListZones return all the zones in the account
func ( c * desecProvider ) ListZones ( ) ( [ ] string , error ) {
var domains [ ] string
for domain := range c . domainIndex {
domains = append ( domains , domain )
}
return domains , nil
}