2017-09-13 11:49:15 -04:00
package ns1
import (
"encoding/json"
2022-08-09 22:29:54 +02:00
"errors"
2017-09-13 11:49:15 -04:00
"fmt"
"net/http"
2022-02-11 18:05:32 +01:00
"strconv"
2022-02-11 11:14:19 -05:00
"strings"
2017-09-13 11:49:15 -04:00
2020-04-14 16:47:30 -04:00
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/providers"
2022-08-14 20:46:56 -04:00
"gopkg.in/ns1/ns1-go.v2/rest"
"gopkg.in/ns1/ns1-go.v2/rest/model/dns"
"gopkg.in/ns1/ns1-go.v2/rest/model/filter"
2017-09-13 11:49:15 -04:00
)
2017-09-14 16:13:17 -04:00
var docNotes = providers . DocumentationNotes {
2022-03-02 11:19:15 -05:00
providers . CanGetZones : providers . Can ( ) ,
2020-07-06 20:29:25 -04:00
providers . CanUseAlias : providers . Can ( ) ,
2022-06-18 14:56:04 +02:00
providers . CanAutoDNSSEC : providers . Can ( ) ,
2021-05-11 22:35:28 +02:00
providers . CanUseCAA : providers . Can ( ) ,
2022-06-18 14:56:04 +02:00
providers . CanUseDS : providers . Can ( ) ,
providers . CanUseDSForChildren : providers . Can ( ) ,
2022-02-11 18:05:32 +01:00
providers . CanUseNAPTR : providers . Can ( ) ,
2022-03-02 11:19:15 -05:00
providers . CanUsePTR : providers . Can ( ) ,
2021-05-14 20:39:09 +02:00
providers . DocCreateDomains : providers . Can ( ) ,
2017-09-14 16:13:17 -04:00
providers . DocDualHost : providers . Can ( ) ,
2021-05-11 22:35:28 +02:00
providers . DocOfficiallySupported : providers . Cannot ( ) ,
2017-09-14 16:13:17 -04:00
}
2017-09-13 11:49:15 -04:00
func init ( ) {
2021-03-07 13:19:22 -05:00
fns := providers . DspFuncs {
2021-05-04 14:15:31 -04:00
Initializer : newProvider ,
2021-03-08 20:14:30 -05:00
RecordAuditor : AuditRecords ,
2021-03-07 13:19:22 -05:00
}
providers . RegisterDomainServiceProviderType ( "NS1" , fns , providers . CanUseSRV , docNotes )
2021-02-10 16:57:15 +00:00
providers . RegisterCustomRecordType ( "NS1_URLFWD" , "NS1" , "URLFWD" )
2017-09-13 11:49:15 -04:00
}
type nsone struct {
* rest . Client
}
func newProvider ( creds map [ string ] string , meta json . RawMessage ) ( providers . DNSServiceProvider , error ) {
if creds [ "api_token" ] == "" {
Switch to Go 1.13 error wrapping (#604)
* Replaced errors.Wrap with fmt.Errorf (#589)
* Find: errors\.Wrap\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Replaced errors.Wrapf with fmt.Errorf (#589)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])\)
Replace: fmt.Errorf($2: %w$3, $1)
* Find: errors\.Wrapf\(([^,]+),\s+(["`][^"`]*)(["`])(,[^)]+)\)
* Replace: fmt.Errorf($2: %w$3$4, $1)
* Replaced errors.Errorf with fmt.Errorf (#589)
* Find: errors\.Errorf
Replace: fmt.Errorf
* Cleaned up remaining imports
* Cleanup
* Regenerate provider support matrix
This was broken by #533 ... and it's now the third time this has been missed.
2020-01-28 11:06:56 -05:00
return nil , fmt . Errorf ( "api_token required for ns1" )
2017-09-13 11:49:15 -04:00
}
return & nsone { rest . NewClient ( http . DefaultClient , rest . SetAPIKey ( creds [ "api_token" ] ) ) } , nil
}
2021-05-14 20:39:09 +02:00
func ( n * nsone ) EnsureDomainExists ( domain string ) error {
// This enables the create-domains subcommand
zone := dns . NewZone ( domain )
_ , err := n . Zones . Create ( zone )
if err == rest . ErrZoneExists {
// if domain exists already, just return nil, nothing to do here.
return nil
}
2022-08-09 22:29:54 +02:00
newZoneExistsError := errors . New ( "invalid: FQDN already exists in the view" )
if errors . As ( err , & newZoneExistsError ) {
// XXX: FIX: This is an ugly workaround for https://github.com/ns1/ns1-go/issues/163. Remove when resolved.
return nil
}
2021-05-14 20:39:09 +02:00
return err
}
2017-09-13 11:49:15 -04:00
func ( n * nsone ) GetNameservers ( domain string ) ( [ ] * models . Nameserver , error ) {
z , _ , err := n . Zones . Get ( domain )
if err != nil {
return nil , err
}
2020-03-26 09:59:59 -04:00
return models . ToNameservers ( z . DNSServers )
2017-09-13 11:49:15 -04:00
}
2020-02-18 08:59:18 -05:00
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
2020-06-18 09:37:57 -04:00
func ( n * nsone ) GetZoneRecords ( domain string ) ( models . Records , error ) {
2022-01-19 19:58:21 +01:00
z , _ , err := n . Zones . Get ( domain )
2017-09-13 11:49:15 -04:00
if err != nil {
return nil , err
}
found := models . Records { }
for _ , r := range z . Records {
2022-01-19 19:58:21 +01:00
zrs , err := convert ( r , domain )
2017-09-13 11:49:15 -04:00
if err != nil {
return nil , err
}
found = append ( found , zrs ... )
}
2022-01-19 19:58:21 +01:00
return found , nil
}
2022-03-11 17:12:45 +02:00
// GetZoneDNSSEC gets DNSSEC status for zone. Returns true for enabled, false for disabled
// a domain in NS1 can be in 3 states:
2022-08-14 20:49:57 -04:00
// 1. DNSSEC is enabled (returns true)
// 2. DNSSEC is disabled (returns false)
// 3. some error state (return false plus the error)
2022-03-11 17:12:45 +02:00
func ( n * nsone ) GetZoneDNSSEC ( domain string ) ( bool , error ) {
_ , _ , err := n . DNSSEC . Get ( domain )
// rest.ErrDNSECNotEnabled is our "disabled" state
if err != nil && err == rest . ErrDNSECNotEnabled {
return false , nil
}
// any other errors not expected, let's surface them
if err != nil {
return false , err
}
// no errors returned, we assume DNSSEC is enabled
return true , nil
}
// getDomainCorrectionsDNSSEC creates DNSSEC zone corrections based on current state and preference
2022-06-18 14:56:04 +02:00
func ( n * nsone ) getDomainCorrectionsDNSSEC ( domain , toggleDNSSEC string ) * models . Correction {
2022-03-11 17:12:45 +02:00
// get dnssec status from NS1 for domain
// if errors are returned, we bail out without any DNSSEC corrections
status , err := n . GetZoneDNSSEC ( domain )
if err != nil {
return nil
}
if toggleDNSSEC == "on" && ! status {
// disabled, but prefer it on, let's enable DNSSEC
return & models . Correction {
Msg : "ENABLE DNSSEC" ,
F : func ( ) error { return n . configureDNSSEC ( domain , true ) } ,
}
} else if toggleDNSSEC == "off" && status {
// enabled, but prefer it off, let's disable DNSSEC
return & models . Correction {
Msg : "DISABLE DNSSEC" ,
F : func ( ) error { return n . configureDNSSEC ( domain , false ) } ,
}
}
return nil
}
2022-01-19 19:58:21 +01:00
func ( n * nsone ) GetDomainCorrections ( dc * models . DomainConfig ) ( [ ] * models . Correction , error ) {
dc . Punycode ( )
//dc.CombineMXs()
domain := dc . Name
// Get existing records
existingRecords , err := n . GetZoneRecords ( domain )
if err != nil {
return nil , err
}
existingGrouped := existingRecords . GroupedByKey ( )
2020-01-20 14:13:32 -05:00
desiredGrouped := dc . Records . GroupedByKey ( )
2017-09-13 11:49:15 -04:00
2017-11-07 14:12:17 -08:00
// Normalize
2022-01-19 19:58:21 +01:00
models . PostProcessRecords ( existingRecords )
2017-11-07 14:12:17 -08:00
2017-09-13 11:49:15 -04:00
differ := diff . New ( dc )
2022-01-19 19:58:21 +01:00
changedGroups , err := differ . ChangedGroups ( existingRecords )
2020-08-21 07:49:00 +12:00
if err != nil {
return nil , err
}
2022-03-11 17:12:45 +02:00
2017-09-13 11:49:15 -04:00
corrections := [ ] * models . Correction { }
2022-03-11 17:12:45 +02:00
2022-06-18 14:56:04 +02:00
if dnssecCorrections := n . getDomainCorrectionsDNSSEC ( domain , dc . AutoDNSSEC ) ; dnssecCorrections != nil {
2022-03-11 17:12:45 +02:00
corrections = append ( corrections , dnssecCorrections )
}
2017-09-13 11:49:15 -04:00
// each name/type is given to the api as a unit.
for k , descs := range changedGroups {
key := k
2021-05-11 21:52:27 +02:00
2017-09-13 11:49:15 -04:00
desc := strings . Join ( descs , "\n" )
2022-01-19 19:58:21 +01:00
_ , current := existingGrouped [ k ]
2017-09-13 11:49:15 -04:00
recs , wanted := desiredGrouped [ k ]
if wanted && ! current {
// pure addition
corrections = append ( corrections , & models . Correction {
Msg : desc ,
F : func ( ) error { return n . add ( recs , dc . Name ) } ,
} )
} else if current && ! wanted {
// pure deletion
corrections = append ( corrections , & models . Correction {
Msg : desc ,
F : func ( ) error { return n . remove ( key , dc . Name ) } ,
} )
} else {
// modification
corrections = append ( corrections , & models . Correction {
Msg : desc ,
F : func ( ) error { return n . modify ( recs , dc . Name ) } ,
} )
}
}
return corrections , nil
}
func ( n * nsone ) add ( recs models . Records , domain string ) error {
_ , err := n . Records . Create ( buildRecord ( recs , domain , "" ) )
return err
}
func ( n * nsone ) remove ( key models . RecordKey , domain string ) error {
2018-09-07 13:46:44 -04:00
_ , err := n . Records . Delete ( domain , key . NameFQDN , key . Type )
2017-09-13 11:49:15 -04:00
return err
}
func ( n * nsone ) modify ( recs models . Records , domain string ) error {
_ , err := n . Records . Update ( buildRecord ( recs , domain , "" ) )
return err
}
2022-03-11 17:12:45 +02:00
// configureDNSSEC configures DNSSEC for a zone. Set 'enabled' to true to enable, false to disable.
// There's a cornercase, in which DNSSEC is globally disabled for the account.
// In that situation, enabling DNSSEC will always fail with:
//
2022-08-14 20:49:57 -04:00
// #1: ENABLE DNSSEC
// FAILURE! POST https://api.nsone.net/v1/zones/example.com: 400 DNSSEC support is not enabled for this account. Please contact support@ns1.com to enable it
2022-03-11 17:12:45 +02:00
//
2022-08-14 20:49:57 -04:00
// Unfortunately this is not detectable otherwise, so given that we have a nice error message, we just let this through.
2022-03-11 17:12:45 +02:00
func ( n * nsone ) configureDNSSEC ( domain string , enabled bool ) error {
z , _ , err := n . Zones . Get ( domain )
if err != nil {
return err
}
z . DNSSEC = & enabled
_ , err = n . Zones . Update ( z )
return err
}
2017-09-13 11:49:15 -04:00
func buildRecord ( recs models . Records , domain string , id string ) * dns . Record {
r := recs [ 0 ]
rec := & dns . Record {
2021-02-10 16:57:15 +00:00
Domain : r . GetLabelFQDN ( ) ,
Type : r . Type ,
ID : id ,
TTL : int ( r . TTL ) ,
Zone : domain ,
Filters : [ ] * filter . Filter { } , // Work through a bug in the NS1 API library that causes 400 Input validation failed (Value None for field '<obj>.filters' is not of type array)
2017-09-13 11:49:15 -04:00
}
for _ , r := range recs {
2020-07-14 08:33:21 -04:00
if r . Type == "MX" {
rec . AddAnswer ( & dns . Answer { Rdata : strings . Split ( fmt . Sprintf ( "%d %v" , r . MxPreference , r . GetTargetField ( ) ) , " " ) } )
} else if r . Type == "TXT" {
2018-02-15 12:02:50 -05:00
rec . AddAnswer ( & dns . Answer { Rdata : r . TxtStrings } )
2021-05-11 22:35:28 +02:00
} else if r . Type == "CAA" {
2022-02-10 20:22:59 +01:00
rec . AddAnswer ( & dns . Answer {
Rdata : [ ] string {
fmt . Sprintf ( "%v" , r . CaaFlag ) ,
r . CaaTag ,
2022-02-11 14:30:45 -05:00
r . GetTargetField ( ) ,
} } )
2017-11-29 20:57:35 +08:00
} else if r . Type == "SRV" {
2018-02-15 12:02:50 -05:00
rec . AddAnswer ( & dns . Answer { Rdata : strings . Split ( fmt . Sprintf ( "%d %d %d %v" , r . SrvPriority , r . SrvWeight , r . SrvPort , r . GetTargetField ( ) ) , " " ) } )
2022-02-11 18:05:32 +01:00
} else if r . Type == "NAPTR" {
rec . AddAnswer ( & dns . Answer { Rdata : [ ] string {
strconv . Itoa ( int ( r . NaptrOrder ) ) ,
strconv . Itoa ( int ( r . NaptrPreference ) ) ,
r . NaptrFlags ,
r . NaptrService ,
r . NaptrRegexp ,
r . GetTargetField ( ) } } )
2022-03-07 17:31:55 +01:00
} else if r . Type == "DS" {
rec . AddAnswer ( & dns . Answer { Rdata : [ ] string {
strconv . Itoa ( int ( r . DsKeyTag ) ) ,
strconv . Itoa ( int ( r . DsAlgorithm ) ) ,
strconv . Itoa ( int ( r . DsDigestType ) ) ,
r . DsDigest } } )
2017-11-20 08:53:44 -05:00
} else {
2018-02-15 12:02:50 -05:00
rec . AddAnswer ( & dns . Answer { Rdata : strings . Split ( r . GetTargetField ( ) , " " ) } )
2017-09-13 11:49:15 -04:00
}
}
return rec
}
func convert ( zr * dns . ZoneRecord , domain string ) ( [ ] * models . RecordConfig , error ) {
found := [ ] * models . RecordConfig { }
for _ , ans := range zr . ShortAns {
rec := & models . RecordConfig {
TTL : uint32 ( zr . TTL ) ,
Original : zr ,
}
2018-02-15 12:02:50 -05:00
rec . SetLabelFromFQDN ( zr . Domain , domain )
switch rtype := zr . Type ; rtype {
2022-03-07 16:43:48 +01:00
case "DNSKEY" , "RRSIG" :
// if a zone is enabled for DNSSEC, NS1 autoconfigures DNSKEY & RRSIG records.
// these entries are not modifiable via the API though, so we have to ignore them while converting.
// ie. API returns "405 Operation on DNSSEC record is not allowed" on such operations
continue
2020-07-06 20:29:25 -04:00
case "ALIAS" :
2021-05-11 21:52:27 +02:00
rec . Type = rtype
if err := rec . SetTarget ( ans ) ; err != nil {
2022-02-10 20:22:59 +01:00
return nil , fmt . Errorf ( "unparsable %s record received from ns1: %w" , rtype , err )
2021-05-11 21:52:27 +02:00
}
2021-02-10 16:57:15 +00:00
case "URLFWD" :
2020-07-06 20:29:25 -04:00
rec . Type = rtype
if err := rec . SetTarget ( ans ) ; err != nil {
2022-02-10 20:22:59 +01:00
return nil , fmt . Errorf ( "unparsable %s record received from ns1: %w" , rtype , err )
}
case "CAA" :
//dnscontrol expects quotes around multivalue CAA entries, API doesn't add them
2022-02-11 14:30:45 -05:00
xAns := strings . SplitN ( ans , " " , 3 )
if err := rec . SetTargetCAAStrings ( xAns [ 0 ] , xAns [ 1 ] , xAns [ 2 ] ) ; err != nil {
2022-02-10 20:22:59 +01:00
return nil , fmt . Errorf ( "unparsable %s record received from ns1: %w" , rtype , err )
2020-07-06 20:29:25 -04:00
}
2018-02-15 12:02:50 -05:00
default :
if err := rec . PopulateFromString ( rtype , ans , domain ) ; err != nil {
2022-02-10 20:22:59 +01:00
return nil , fmt . Errorf ( "unparsable record received from ns1: %w" , err )
2018-02-15 12:02:50 -05:00
}
2017-09-13 11:49:15 -04:00
}
found = append ( found , rec )
}
return found , nil
}