2016-08-22 18:31:50 -06:00
package cloudflare
import (
"encoding/json"
"fmt"
"log"
"net"
"strings"
"time"
"github.com/StackExchange/dnscontrol/models"
2018-10-08 13:10:44 -07:00
"github.com/StackExchange/dnscontrol/pkg/printer"
2017-05-25 14:25:39 -04:00
"github.com/StackExchange/dnscontrol/pkg/transform"
2016-08-22 18:31:50 -06:00
"github.com/StackExchange/dnscontrol/providers"
"github.com/StackExchange/dnscontrol/providers/diff"
2016-12-16 13:10:27 -07:00
"github.com/miekg/dns/dnsutil"
2018-02-05 16:17:20 -05:00
"github.com/pkg/errors"
2016-08-22 18:31:50 -06:00
)
/ *
2017-04-10 20:26:11 -05:00
Cloudflare API DNS provider :
2016-08-22 18:31:50 -06:00
Info required in ` creds.json ` :
- apikey
- apiuser
2018-12-19 15:48:27 +01:00
- accountid ( optional )
- accountname ( optional )
2016-08-22 18:31:50 -06:00
2017-04-10 20:26:11 -05:00
Record level metadata available :
- cloudflare_proxy ( "on" , "off" , or "full" )
2016-08-22 18:31:50 -06:00
2017-04-10 20:26:11 -05:00
Domain level metadata available :
- cloudflare_proxy_default ( "on" , "off" , or "full" )
2016-08-22 18:31:50 -06:00
2017-04-10 20:26:11 -05:00
Provider level metadata available :
2016-08-22 18:31:50 -06:00
- ip_conversions
* /
2018-01-04 19:19:35 -05:00
var features = providers . DocumentationNotes {
providers . CanUseAlias : providers . Can ( "CF automatically flattens CNAME records into A records dynamically" ) ,
2019-05-21 04:32:39 +02:00
providers . CanUsePTR : providers . Cannot ( ) ,
2018-01-04 19:19:35 -05:00
providers . CanUseCAA : providers . Can ( ) ,
providers . CanUseSRV : providers . Can ( ) ,
2019-05-21 04:32:39 +02:00
providers . CanUseTLSA : providers . Can ( ) ,
providers . CanUseSSHFP : providers . Can ( ) ,
2017-09-14 16:13:17 -04:00
providers . DocCreateDomains : providers . Can ( ) ,
2018-01-04 19:19:35 -05:00
providers . DocDualHost : providers . Cannot ( "Cloudflare will not work well in situations where it is not the only DNS server" ) ,
2017-09-14 16:13:17 -04:00
providers . DocOfficiallySupported : providers . Can ( ) ,
}
2017-05-19 14:15:57 -04:00
func init ( ) {
2018-01-04 19:19:35 -05:00
providers . RegisterDomainServiceProviderType ( "CLOUDFLAREAPI" , newCloudflare , features )
2017-05-19 14:15:57 -04:00
providers . RegisterCustomRecordType ( "CF_REDIRECT" , "CLOUDFLAREAPI" , "" )
providers . RegisterCustomRecordType ( "CF_TEMP_REDIRECT" , "CLOUDFLAREAPI" , "" )
}
2018-01-09 12:53:16 -05:00
// CloudflareApi is the handle for API calls.
2016-08-22 18:31:50 -06:00
type CloudflareApi struct {
2017-05-19 14:15:57 -04:00
ApiKey string ` json:"apikey" `
ApiUser string ` json:"apiuser" `
2018-12-19 15:48:27 +01:00
AccountID string ` json:"accountid" `
AccountName string ` json:"accountname" `
2017-05-19 14:15:57 -04:00
domainIndex map [ string ] string
nameservers map [ string ] [ ] string
ipConversions [ ] transform . IpConversion
ignoredLabels [ ] string
manageRedirects bool
2016-08-22 18:31:50 -06:00
}
func labelMatches ( label string , matches [ ] string ) bool {
2018-10-08 13:10:44 -07:00
printer . Debugf ( "DEBUG: labelMatches(%#v, %#v)\n" , label , matches )
2016-08-22 18:31:50 -06:00
for _ , tst := range matches {
if label == tst {
return true
}
}
return false
}
2017-05-19 14:15:57 -04:00
2018-01-09 12:53:16 -05:00
// GetNameservers returns the nameservers for a domain.
2016-12-16 13:10:27 -07:00
func ( c * CloudflareApi ) GetNameservers ( domain string ) ( [ ] * models . Nameserver , error ) {
if c . domainIndex == nil {
if err := c . fetchDomainList ( ) ; err != nil {
return nil , err
}
}
ns , ok := c . nameservers [ domain ]
if ! ok {
2018-02-05 16:17:20 -05:00
return nil , errors . Errorf ( "Nameservers for %s not found in cloudflare account" , domain )
2016-12-16 13:10:27 -07:00
}
return models . StringsToNameservers ( ns ) , nil
}
2016-08-22 18:31:50 -06:00
2018-01-09 12:53:16 -05:00
// GetDomainCorrections returns a list of corrections to update a domain.
2016-08-22 18:31:50 -06:00
func ( c * CloudflareApi ) GetDomainCorrections ( dc * models . DomainConfig ) ( [ ] * models . Correction , error ) {
if c . domainIndex == nil {
if err := c . fetchDomainList ( ) ; err != nil {
return nil , err
}
}
id , ok := c . domainIndex [ dc . Name ]
if ! ok {
2018-02-05 16:17:20 -05:00
return nil , errors . Errorf ( "%s not listed in zones for cloudflare account" , dc . Name )
2016-08-22 18:31:50 -06:00
}
if err := c . preprocessConfig ( dc ) ; err != nil {
return nil , err
}
2017-01-11 12:38:07 -07:00
records , err := c . getRecordsForDomain ( id , dc . Name )
2016-08-22 18:31:50 -06:00
if err != nil {
return nil , err
}
for i := len ( records ) - 1 ; i >= 0 ; i -- {
rec := records [ i ]
// Delete ignore labels
2017-01-11 12:38:07 -07:00
if labelMatches ( dnsutil . TrimDomainName ( rec . Original . ( * cfRecord ) . Name , dc . Name ) , c . ignoredLabels ) {
2018-10-08 13:10:44 -07:00
printer . Debugf ( "ignored_label: %s\n" , rec . Original . ( * cfRecord ) . Name )
2016-08-22 18:31:50 -06:00
records = append ( records [ : i ] , records [ i + 1 : ] ... )
}
}
2017-05-19 14:15:57 -04:00
if c . manageRedirects {
prs , err := c . getPageRules ( id , dc . Name )
if err != nil {
return nil , err
}
records = append ( records , prs ... )
}
2016-08-22 18:31:50 -06:00
for _ , rec := range dc . Records {
2017-04-19 13:13:28 -06:00
if rec . Type == "ALIAS" {
rec . Type = "CNAME"
}
2019-05-20 21:27:37 +02:00
// As per CF-API documentation proxied records are always forced to have a TTL of 1.
// When not forcing this property change here, dnscontrol tries each time to update
// the TTL of a record which simply cannot be changed anyway.
if rec . Metadata [ metaProxy ] != "off" {
rec . TTL = 1
}
2018-02-15 12:02:50 -05:00
if labelMatches ( rec . GetLabel ( ) , c . ignoredLabels ) {
log . Fatalf ( "FATAL: dnsconfig contains label that matches ignored_labels: %#v is in %v)\n" , rec . GetLabel ( ) , c . ignoredLabels )
2016-08-22 18:31:50 -06:00
}
}
2017-01-11 12:38:07 -07:00
checkNSModifications ( dc )
2017-11-07 14:12:17 -08:00
// Normalize
2018-01-04 19:19:35 -05:00
models . PostProcessRecords ( records )
2017-11-07 14:12:17 -08:00
2017-01-11 12:38:07 -07:00
differ := diff . New ( dc , getProxyMetadata )
_ , create , del , mod := differ . IncrementalDiff ( records )
2016-08-22 18:31:50 -06:00
corrections := [ ] * models . Correction { }
for _ , d := range del {
2017-05-19 14:15:57 -04:00
ex := d . Existing
if ex . Type == "PAGE_RULE" {
corrections = append ( corrections , & models . Correction {
Msg : d . String ( ) ,
F : func ( ) error { return c . deletePageRule ( ex . Original . ( * pageRule ) . ID , id ) } ,
} )
} else {
corrections = append ( corrections , c . deleteRec ( ex . Original . ( * cfRecord ) , id ) )
}
2016-08-22 18:31:50 -06:00
}
for _ , d := range create {
2017-05-19 14:15:57 -04:00
des := d . Desired
if des . Type == "PAGE_RULE" {
corrections = append ( corrections , & models . Correction {
Msg : d . String ( ) ,
2018-02-15 12:02:50 -05:00
F : func ( ) error { return c . createPageRule ( id , des . GetTargetField ( ) ) } ,
2017-05-19 14:15:57 -04:00
} )
} else {
corrections = append ( corrections , c . createRec ( des , id ) ... )
}
2016-08-22 18:31:50 -06:00
}
for _ , d := range mod {
2017-05-19 14:15:57 -04:00
rec := d . Desired
ex := d . Existing
if rec . Type == "PAGE_RULE" {
corrections = append ( corrections , & models . Correction {
Msg : d . String ( ) ,
2018-02-15 12:02:50 -05:00
F : func ( ) error { return c . updatePageRule ( ex . Original . ( * pageRule ) . ID , id , rec . GetTargetField ( ) ) } ,
2017-05-19 14:15:57 -04:00
} )
} else {
e := ex . Original . ( * cfRecord )
proxy := e . Proxiable && rec . Metadata [ metaProxy ] != "off"
corrections = append ( corrections , & models . Correction {
Msg : d . String ( ) ,
F : func ( ) error { return c . modifyRecord ( id , e . ID , proxy , rec ) } ,
} )
}
2016-08-22 18:31:50 -06:00
}
return corrections , nil
}
2017-01-11 12:38:07 -07:00
func checkNSModifications ( dc * models . DomainConfig ) {
newList := make ( [ ] * models . RecordConfig , 0 , len ( dc . Records ) )
for _ , rec := range dc . Records {
2018-02-15 12:02:50 -05:00
if rec . Type == "NS" && rec . GetLabelFQDN ( ) == dc . Name {
if ! strings . HasSuffix ( rec . GetTargetField ( ) , ".ns.cloudflare.com." ) {
2018-10-08 13:10:44 -07:00
printer . Warnf ( "cloudflare does not support modifying NS records on base domain. %s will not be added.\n" , rec . GetTargetField ( ) )
2017-01-11 12:38:07 -07:00
}
continue
}
newList = append ( newList , rec )
}
dc . Records = newList
}
2016-08-22 18:31:50 -06:00
const (
metaProxy = "cloudflare_proxy"
metaProxyDefault = metaProxy + "_default"
metaOriginalIP = "original_ip" // TODO(tlim): Unclear what this means.
metaIPConversions = "ip_conversions" // TODO(tlim): Rename to obscure_rules.
)
func checkProxyVal ( v string ) ( string , error ) {
v = strings . ToLower ( v )
if v != "on" && v != "off" && v != "full" {
2018-02-05 16:17:20 -05:00
return "" , errors . Errorf ( "Bad metadata value for cloudflare_proxy: '%s'. Use on/off/full" , v )
2016-08-22 18:31:50 -06:00
}
return v , nil
}
func ( c * CloudflareApi ) preprocessConfig ( dc * models . DomainConfig ) error {
// Determine the default proxy setting.
var defProxy string
var err error
if defProxy = dc . Metadata [ metaProxyDefault ] ; defProxy == "" {
defProxy = "off"
} else {
defProxy , err = checkProxyVal ( defProxy )
if err != nil {
return err
}
}
2017-05-19 14:15:57 -04:00
currentPrPrio := 1
2016-08-22 18:31:50 -06:00
// Normalize the proxy setting for each record.
// A and CNAMEs: Validate. If null, set to default.
// else: Make sure it wasn't set. Set to default.
2017-05-19 14:15:57 -04:00
// iterate backwards so first defined page rules have highest priority
for i := len ( dc . Records ) - 1 ; i >= 0 ; i -- {
rec := dc . Records [ i ]
2017-04-19 13:13:28 -06:00
if rec . Metadata == nil {
rec . Metadata = map [ string ] string { }
}
2017-01-11 12:38:07 -07:00
if rec . TTL == 0 || rec . TTL == 300 {
rec . TTL = 1
}
2017-04-19 13:13:28 -06:00
if rec . TTL != 1 && rec . TTL < 120 {
rec . TTL = 120
}
2017-04-20 07:13:21 -06:00
if rec . Type != "A" && rec . Type != "CNAME" && rec . Type != "AAAA" && rec . Type != "ALIAS" {
2016-08-22 18:31:50 -06:00
if rec . Metadata [ metaProxy ] != "" {
2018-02-15 12:02:50 -05:00
return errors . Errorf ( "cloudflare_proxy set on %v record: %#v cloudflare_proxy=%#v" , rec . Type , rec . GetLabel ( ) , rec . Metadata [ metaProxy ] )
2016-08-22 18:31:50 -06:00
}
// Force it to off.
rec . Metadata [ metaProxy ] = "off"
} else {
if val := rec . Metadata [ metaProxy ] ; val == "" {
rec . Metadata [ metaProxy ] = defProxy
} else {
val , err := checkProxyVal ( val )
if err != nil {
return err
}
rec . Metadata [ metaProxy ] = val
}
}
2017-05-19 14:15:57 -04:00
// CF_REDIRECT record types. Encode target as $FROM,$TO,$PRIO,$CODE
if rec . Type == "CF_REDIRECT" || rec . Type == "CF_TEMP_REDIRECT" {
if ! c . manageRedirects {
2018-02-05 16:17:20 -05:00
return errors . Errorf ( "you must add 'manage_redirects: true' metadata to cloudflare provider to use CF_REDIRECT records" )
2017-05-19 14:15:57 -04:00
}
2018-02-15 12:02:50 -05:00
parts := strings . Split ( rec . GetTargetField ( ) , "," )
2017-05-19 14:15:57 -04:00
if len ( parts ) != 2 {
2018-02-05 16:17:20 -05:00
return errors . Errorf ( "Invalid data specified for cloudflare redirect record" )
2017-05-19 14:15:57 -04:00
}
code := 301
if rec . Type == "CF_TEMP_REDIRECT" {
code = 302
}
2018-02-15 12:02:50 -05:00
rec . SetTarget ( fmt . Sprintf ( "%s,%d,%d" , rec . GetTargetField ( ) , currentPrPrio , code ) )
2017-05-19 14:15:57 -04:00
currentPrPrio ++
rec . Type = "PAGE_RULE"
}
2016-08-22 18:31:50 -06:00
}
// look for ip conversions and transform records
for _ , rec := range dc . Records {
if rec . Type != "A" {
continue
}
2018-01-09 12:53:16 -05:00
// only transform "full"
2016-08-22 18:31:50 -06:00
if rec . Metadata [ metaProxy ] != "full" {
continue
}
2018-02-15 12:02:50 -05:00
ip := net . ParseIP ( rec . GetTargetField ( ) )
2016-08-22 18:31:50 -06:00
if ip == nil {
2018-02-15 12:02:50 -05:00
return errors . Errorf ( "%s is not a valid ip address" , rec . GetTargetField ( ) )
2016-08-22 18:31:50 -06:00
}
newIP , err := transform . TransformIP ( ip , c . ipConversions )
if err != nil {
return err
}
2018-02-15 12:02:50 -05:00
rec . Metadata [ metaOriginalIP ] = rec . GetTargetField ( )
rec . SetTarget ( newIP . String ( ) )
2016-08-22 18:31:50 -06:00
}
return nil
}
func newCloudflare ( m map [ string ] string , metadata json . RawMessage ) ( providers . DNSServiceProvider , error ) {
api := & CloudflareApi { }
api . ApiUser , api . ApiKey = m [ "apiuser" ] , m [ "apikey" ]
// check api keys from creds json file
if api . ApiKey == "" || api . ApiUser == "" {
2018-02-05 16:17:20 -05:00
return nil , errors . Errorf ( "cloudflare apikey and apiuser must be provided" )
2016-08-22 18:31:50 -06:00
}
2018-12-19 15:48:27 +01:00
// Check account data if set
api . AccountID , api . AccountName = m [ "accountid" ] , m [ "accountname" ]
if ( api . AccountID != "" && api . AccountName == "" ) || ( api . AccountID == "" && api . AccountName != "" ) {
return nil , errors . Errorf ( "either both cloudflare accountid and accountname must be provided or neither" )
}
2017-05-05 22:20:43 +01:00
err := api . fetchDomainList ( )
if err != nil {
return nil , err
}
2016-08-22 18:31:50 -06:00
if len ( metadata ) > 0 {
parsedMeta := & struct {
2017-05-19 14:15:57 -04:00
IPConversions string ` json:"ip_conversions" `
IgnoredLabels [ ] string ` json:"ignored_labels" `
ManageRedirects bool ` json:"manage_redirects" `
2016-08-22 18:31:50 -06:00
} { }
err := json . Unmarshal ( [ ] byte ( metadata ) , parsedMeta )
if err != nil {
return nil , err
}
2017-05-19 14:15:57 -04:00
api . manageRedirects = parsedMeta . ManageRedirects
2016-08-22 18:31:50 -06:00
// ignored_labels:
for _ , l := range parsedMeta . IgnoredLabels {
api . ignoredLabels = append ( api . ignoredLabels , l )
}
2018-01-15 21:39:29 +01:00
if len ( api . ignoredLabels ) > 0 {
2018-10-08 13:10:44 -07:00
printer . Warnf ( "Cloudflare 'ignored_labels' configuration is deprecated and might be removed. Please use the IGNORE domain directive to achieve the same effect.\n" )
2018-01-15 21:39:29 +01:00
}
2016-08-22 18:31:50 -06:00
// parse provider level metadata
2017-05-19 14:15:57 -04:00
if len ( parsedMeta . IPConversions ) > 0 {
api . ipConversions , err = transform . DecodeTransformTable ( parsedMeta . IPConversions )
if err != nil {
return nil , err
}
2016-08-22 18:31:50 -06:00
}
}
return api , nil
}
// Used on the "existing" records.
2017-08-10 16:02:06 -04:00
type cfRecData struct {
2019-05-21 04:32:39 +02:00
Name string ` json:"name" `
Target string ` json:"target" `
Service string ` json:"service" ` // SRV
Proto string ` json:"proto" ` // SRV
Priority uint16 ` json:"priority" ` // SRV
Weight uint16 ` json:"weight" ` // SRV
Port uint16 ` json:"port" ` // SRV
Tag string ` json:"tag" ` // CAA
Flags uint8 ` json:"flags" ` // CAA
Value string ` json:"value" ` // CAA
Usage uint8 ` json:"usage" ` // TLSA
Selector uint8 ` json:"selector" ` // TLSA
Matching_Type uint8 ` json:"matching_type" ` // TLSA
Certificate string ` json:"certificate" ` // TLSA
Algorithm uint8 ` json:"algorithm" ` // SSHFP
Hash_Type uint8 ` json:"type" ` // SSHFP
Fingerprint string ` json:"fingerprint" ` // SSHFP
2017-08-10 16:02:06 -04:00
}
2016-08-22 18:31:50 -06:00
type cfRecord struct {
2018-06-22 14:11:05 -04:00
ID string ` json:"id" `
Type string ` json:"type" `
Name string ` json:"name" `
Content string ` json:"content" `
Proxiable bool ` json:"proxiable" `
Proxied bool ` json:"proxied" `
TTL uint32 ` json:"ttl" `
Locked bool ` json:"locked" `
ZoneID string ` json:"zone_id" `
ZoneName string ` json:"zone_name" `
CreatedOn time . Time ` json:"created_on" `
ModifiedOn time . Time ` json:"modified_on" `
Data * cfRecData ` json:"data" `
Priority json . Number ` json:"priority" `
2016-08-22 18:31:50 -06:00
}
2018-02-15 12:02:50 -05:00
func ( c * cfRecord ) nativeToRecord ( domain string ) * models . RecordConfig {
2018-01-09 12:53:16 -05:00
// normalize cname,mx,ns records with dots to be consistent with our config format.
2017-08-10 16:02:06 -04:00
if c . Type == "CNAME" || c . Type == "MX" || c . Type == "NS" || c . Type == "SRV" {
2017-01-11 12:38:07 -07:00
c . Content = dnsutil . AddOrigin ( c . Content + "." , domain )
2016-08-22 18:31:50 -06:00
}
2018-02-15 12:02:50 -05:00
2017-08-10 16:02:06 -04:00
rc := & models . RecordConfig {
2017-12-20 10:25:23 -05:00
TTL : c . TTL ,
Original : c ,
2016-08-22 18:31:50 -06:00
}
2018-02-15 12:02:50 -05:00
rc . SetLabelFromFQDN ( c . Name , domain )
2019-05-18 17:08:18 +02:00
// workaround for https://github.com/StackExchange/dnscontrol/issues/446
if c . Type == "SPF" {
c . Type = "TXT"
}
2018-02-15 12:02:50 -05:00
switch rType := c . Type ; rType { // #rtype_variations
2017-12-20 10:25:23 -05:00
case "MX" :
2018-06-22 14:11:05 -04:00
var priority uint16
2018-07-24 22:51:10 +10:00
if c . Priority == "" {
priority = 0
2018-06-22 14:11:05 -04:00
} else {
2018-07-24 22:51:10 +10:00
if p , err := c . Priority . Int64 ( ) ; err != nil {
panic ( errors . Wrap ( err , "error decoding priority from cloudflare record" ) )
} else {
priority = uint16 ( p )
}
2018-06-22 14:11:05 -04:00
}
if err := rc . SetTargetMX ( priority , c . Content ) ; err != nil {
2018-02-15 12:02:50 -05:00
panic ( errors . Wrap ( err , "unparsable MX record received from cloudflare" ) )
}
2017-12-20 10:25:23 -05:00
case "SRV" :
2017-08-10 16:02:06 -04:00
data := * c . Data
2018-02-15 12:02:50 -05:00
if err := rc . SetTargetSRV ( data . Priority , data . Weight , data . Port ,
dnsutil . AddOrigin ( data . Target + "." , domain ) ) ; err != nil {
panic ( errors . Wrap ( err , "unparsable SRV record received from cloudflare" ) )
}
default : // "A", "AAAA", "ANAME", "CAA", "CNAME", "NS", "PTR", "TXT"
if err := rc . PopulateFromString ( rType , c . Content , domain ) ; err != nil {
panic ( errors . Wrap ( err , "unparsable record received from cloudflare" ) )
}
2017-08-10 16:02:06 -04:00
}
2017-12-20 10:25:23 -05:00
2017-08-10 16:02:06 -04:00
return rc
2016-08-22 18:31:50 -06:00
}
2017-01-11 12:38:07 -07:00
func getProxyMetadata ( r * models . RecordConfig ) map [ string ] string {
if r . Type != "A" && r . Type != "AAAA" && r . Type != "CNAME" {
return nil
2016-08-22 18:31:50 -06:00
}
2017-01-11 12:38:07 -07:00
proxied := false
if r . Original != nil {
proxied = r . Original . ( * cfRecord ) . Proxied
} else {
proxied = r . Metadata [ metaProxy ] != "off"
2016-08-22 18:31:50 -06:00
}
2017-01-11 12:38:07 -07:00
return map [ string ] string {
"proxy" : fmt . Sprint ( proxied ) ,
2016-08-22 18:31:50 -06:00
}
}
2017-05-05 22:20:43 +01:00
2018-01-09 12:53:16 -05:00
// EnsureDomainExists returns an error of domain does not exist.
2017-05-05 22:20:43 +01:00
func ( c * CloudflareApi ) EnsureDomainExists ( domain string ) error {
if _ , ok := c . domainIndex [ domain ] ; ok {
return nil
}
var id string
id , err := c . createZone ( domain )
fmt . Printf ( "Added zone for %s to Cloudflare account: %s\n" , domain , id )
return err
}