2017-09-13 11:49:15 -04:00
package ns1
import (
"encoding/json"
"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
2023-05-20 19:21:45 +02:00
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/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 {
2023-03-16 19:59:44 -04:00
providers . CanAutoDNSSEC : providers . Can ( ) ,
2022-03-02 11:19:15 -05:00
providers . CanGetZones : providers . Can ( ) ,
2020-07-06 20:29:25 -04:00
providers . CanUseAlias : 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 ( ) ,
2023-03-16 19:04:20 +01:00
providers . CanUseLOC : providers . Cannot ( ) ,
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
}
2023-09-15 15:30:55 -04:00
// clientRetries is the number of retries for API backend requests in case of StatusTooManyRequests responses
const clientRetries = 10
2023-08-17 08:37:36 -07: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 )
2023-04-17 15:21:26 -07:00
providers . RegisterCustomRecordType ( "NS1_URLFWD" , "NS1" , "" )
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
}
2023-08-17 08:37:36 -07:00
// Enable Sleep API Rate limit strategy - it will sleep until new tokens are available
// see https://help.ns1.com/hc/en-us/articles/360020250573-About-API-rate-limiting
// this strategy would imply the least sleep time for non-parallel client requests
return & nsone { rest . NewClient (
http . DefaultClient ,
rest . SetAPIKey ( creds [ "api_token" ] ) ,
func ( c * rest . Client ) {
c . RateLimitStrategySleep ( )
} ,
) } , nil
}
// A wrapper around rest.Client's Zones.Get() implementing retries
// no explicit sleep is needed, it is implemented in NS1 client's RateLimitStrategy we used
func ( n * nsone ) GetZone ( domain string ) ( * dns . Zone , error ) {
for rtr := 0 ; ; rtr ++ {
2023-08-18 08:25:36 -04:00
z , httpResp , err := n . Zones . Get ( domain , true )
2023-09-15 15:30:55 -04:00
if httpResp . StatusCode == http . StatusTooManyRequests && rtr < clientRetries {
2023-08-17 08:37:36 -07:00
continue
}
return z , err
}
2017-09-13 11:49:15 -04:00
}
2023-02-07 17:52:49 +05:30
func ( n * nsone ) EnsureZoneExists ( domain string ) error {
2021-05-14 20:39:09 +02:00
// This enables the create-domains subcommand
zone := dns . NewZone ( domain )
2023-08-17 08:37:36 -07:00
for rtr := 0 ; ; rtr ++ {
httpResp , err := n . Zones . Create ( zone )
if err == rest . ErrZoneExists {
// if domain exists already, just return nil, nothing to do here.
return nil
}
// too many requests - retry w/out waiting. We specified rate limit strategy creating the client
2023-09-15 15:30:55 -04:00
if httpResp . StatusCode == http . StatusTooManyRequests && rtr < clientRetries {
2023-08-17 08:37:36 -07:00
continue
}
return err
2021-05-14 20:39:09 +02:00
}
}
2017-09-13 11:49:15 -04:00
func ( n * nsone ) GetNameservers ( domain string ) ( [ ] * models . Nameserver , error ) {
2023-04-04 13:50:05 +02:00
var nservers [ ] string
2023-08-18 08:25:36 -04:00
z , _ , err := n . Zones . Get ( domain , true )
2017-09-13 11:49:15 -04:00
if err != nil {
return nil , err
}
2023-04-04 13:50:05 +02:00
// on newly-created domains NS1 may assign nameservers with or without a
// trailing dot. This is not reflected in the actual DNS records, that
// always have the trailing dots.
//
// Handle both scenarios by stripping dots where existing, before continuing.
for _ , ns := range z . DNSServers {
if strings . HasSuffix ( ns , "." ) {
nservers = append ( nservers , ns [ 0 : len ( ns ) - 1 ] )
} else {
nservers = append ( nservers , ns )
}
}
2023-05-06 11:46:07 -04:00
return models . ToNameservers ( nservers )
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.
2023-05-02 13:04:59 -04:00
func ( n * nsone ) GetZoneRecords ( domain string , meta map [ string ] string ) ( models . Records , error ) {
2023-08-17 18:19:16 +03:00
z , _ , err := n . Zones . Get ( domain , true )
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 ) {
2023-08-17 08:37:36 -07:00
for rtr := 0 ; ; rtr ++ {
_ , httpResp , err := n . DNSSEC . Get ( domain )
// rest.ErrDNSECNotEnabled is our "disabled" state
if err != nil && err == rest . ErrDNSECNotEnabled {
return false , nil
}
2023-09-15 15:30:55 -04:00
if httpResp . StatusCode == http . StatusTooManyRequests && rtr < clientRetries {
2023-08-17 08:37:36 -07:00
continue
}
// any other errors not expected, let's surface them
if err != nil {
return false , err
}
2022-03-11 17:12:45 +02:00
2023-08-17 08:37:36 -07:00
// no errors returned, we assume DNSSEC is enabled
return true , nil
2022-03-11 17:12:45 +02:00
}
}
// 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
}
2023-04-14 15:22:23 -04:00
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
func ( n * nsone ) GetZoneRecordsCorrections ( dc * models . DomainConfig , existingRecords models . Records ) ( [ ] * models . Correction , error ) {
2023-10-22 13:56:13 -04:00
var corrections [ ] * models . Correction
2023-04-14 15:22:23 -04:00
domain := dc . Name
2017-11-07 14:12:17 -08:00
2022-12-19 20:57:48 +01:00
// add DNSSEC-related corrections
if dnssecCorrections := n . getDomainCorrectionsDNSSEC ( domain , dc . AutoDNSSEC ) ; dnssecCorrections != nil {
corrections = append ( corrections , dnssecCorrections )
}
changes , err := diff2 . ByRecordSet ( existingRecords , dc , nil )
if err != nil {
return nil , err
}
2022-12-11 15:02:58 -05:00
2022-12-19 20:57:48 +01:00
for _ , change := range changes {
key := change . Key
recs := change . New
desc := strings . Join ( change . Msgs , "\n" )
2023-02-01 07:27:00 -05:00
switch change . Type {
case diff2 . REPORT :
corrections = append ( corrections , & models . Correction { Msg : change . MsgsJoined } )
case diff2 . CREATE :
2022-12-19 20:57:48 +01:00
corrections = append ( corrections , & models . Correction {
Msg : desc ,
F : func ( ) error { return n . add ( recs , dc . Name ) } ,
} )
2023-02-01 07:27:00 -05:00
case diff2 . CHANGE :
2022-12-19 20:57:48 +01:00
corrections = append ( corrections , & models . Correction {
Msg : desc ,
F : func ( ) error { return n . modify ( recs , dc . Name ) } ,
} )
2023-02-01 07:27:00 -05:00
case diff2 . DELETE :
2022-12-19 20:57:48 +01:00
corrections = append ( corrections , & models . Correction {
Msg : desc ,
F : func ( ) error { return n . remove ( key , dc . Name ) } ,
} )
2023-02-01 07:27:00 -05:00
default :
panic ( fmt . Sprintf ( "unhandled inst.Type %s" , change . Type ) )
2022-12-19 20:57:48 +01:00
}
2023-02-01 07:27:00 -05:00
2022-12-19 20:57:48 +01:00
}
2017-09-13 11:49:15 -04:00
return corrections , nil
}
func ( n * nsone ) add ( recs models . Records , domain string ) error {
2023-08-17 08:37:36 -07:00
for rtr := 0 ; ; rtr ++ {
httpResp , err := n . Records . Create ( buildRecord ( recs , domain , "" ) )
2023-09-15 15:30:55 -04:00
if httpResp . StatusCode == http . StatusTooManyRequests && rtr < clientRetries {
2023-08-17 08:37:36 -07:00
continue
}
return err
}
2017-09-13 11:49:15 -04:00
}
func ( n * nsone ) remove ( key models . RecordKey , domain string ) error {
2023-04-26 06:20:14 -07:00
if key . Type == "NS1_URLFWD" {
key . Type = "URLFWD"
}
2023-08-17 08:37:36 -07:00
for rtr := 0 ; ; rtr ++ {
httpResp , err := n . Records . Delete ( domain , key . NameFQDN , key . Type )
2023-09-15 15:30:55 -04:00
if httpResp . StatusCode == http . StatusTooManyRequests && rtr < clientRetries {
2023-08-17 08:37:36 -07:00
continue
}
return err
}
2017-09-13 11:49:15 -04:00
}
func ( n * nsone ) modify ( recs models . Records , domain string ) error {
2023-08-17 08:37:36 -07:00
for rtr := 0 ; ; rtr ++ {
httpResp , err := n . Records . Update ( buildRecord ( recs , domain , "" ) )
2023-09-15 15:30:55 -04:00
if httpResp . StatusCode == http . StatusTooManyRequests && rtr < clientRetries {
2023-08-17 08:37:36 -07:00
continue
}
return err
}
2017-09-13 11:49:15 -04:00
}
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 {
2023-08-17 18:19:16 +03:00
z , _ , err := n . Zones . Get ( domain , true )
2022-03-11 17:12:45 +02:00
if err != nil {
return err
}
z . DNSSEC = & enabled
2023-08-17 08:37:36 -07:00
for rtr := 0 ; ; rtr ++ {
httpResp , err := n . Zones . Update ( z )
2023-09-15 15:30:55 -04:00
if httpResp . StatusCode == http . StatusTooManyRequests && rtr < clientRetries {
2023-08-17 08:37:36 -07:00
continue
}
return err
}
2022-03-11 17:12:45 +02:00
}
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" {
2023-01-30 04:01:41 +01:00
rec . AddAnswer ( & dns . Answer { Rdata : strings . Fields ( fmt . Sprintf ( "%d %v" , r . MxPreference , r . GetTargetField ( ) ) ) } )
2020-07-14 08:33:21 -04:00
} 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" {
2023-01-30 04:01:41 +01:00
rec . AddAnswer ( & dns . Answer { Rdata : strings . Fields ( 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 } } )
2023-04-26 06:20:14 -07:00
} else if r . Type == "NS1_URLFWD" {
rec . Type = "URLFWD"
rec . AddAnswer ( & dns . Answer { Rdata : strings . Fields ( r . GetTargetField ( ) ) } )
2017-11-20 08:53:44 -05:00
} else {
2023-01-30 04:01:41 +01:00
rec . AddAnswer ( & dns . Answer { Rdata : strings . Fields ( 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" :
2023-04-17 15:21:26 -07:00
rec . Type = "NS1_URLFWD"
2020-07-06 20:29:25 -04:00
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
}