CLOUDFLAREAPI: CF_SINGLE_REDIRECT improvements: fix bugs, log translated redirects (#3051)

This commit is contained in:
Tom Limoncelli 2024-07-18 12:10:46 -04:00 committed by GitHub
parent 1d348de91c
commit 0869052419
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 313 additions and 221 deletions

View file

@ -472,6 +472,11 @@ declare function CAA(name: string, tag: "issue" | "issuewild" | "iodef", value:
declare function CAA_BUILDER(opts: { label?: string; iodef: string; iodef_critical?: boolean; issue: string[]; issue_critical?: boolean; issuewild: string[]; issuewild_critical?: boolean; ttl?: Duration }): DomainModifier; declare function CAA_BUILDER(opts: { label?: string; iodef: string; iodef_critical?: boolean; issue: string[]; issue_critical?: boolean; issuewild: string[]; issuewild_critical?: boolean; ttl?: Duration }): DomainModifier;
/** /**
* WARNING: Cloudflare is removing this feature and replacing it with a new
* feature called "Dynamic Single Redirect". DNSControl will automatically
* generate "Dynamic Single Redirects" for a limited number of use cases. See
* [`CLOUDFLAREAPI`](../provider/cloudflareapi.md) for details.
*
* `CF_REDIRECT` uses Cloudflare-specific features ("Forwarding URL" Page Rules) to * `CF_REDIRECT` uses Cloudflare-specific features ("Forwarding URL" Page Rules) to
* generate a HTTP 301 permanent redirect. * generate a HTTP 301 permanent redirect.
* *
@ -533,6 +538,11 @@ declare function CF_REDIRECT(source: string, destination: string, ...modifiers:
declare function CF_SINGLE_REDIRECT(name: string, code: number, when: string, then: string, ...modifiers: RecordModifier[]): DomainModifier; declare function CF_SINGLE_REDIRECT(name: string, code: number, when: string, then: string, ...modifiers: RecordModifier[]): DomainModifier;
/** /**
* WARNING: Cloudflare is removing this feature and replacing it with a new
* feature called "Dynamic Single Redirect". DNSControl will automatically
* generate "Dynamic Single Redirects" for a limited number of use cases. See
* [`CLOUDFLAREAPI`](../provider/cloudflareapi.md) for details.
*
* `CF_TEMP_REDIRECT` uses Cloudflare-specific features ("Forwarding URL" Page * `CF_TEMP_REDIRECT` uses Cloudflare-specific features ("Forwarding URL" Page
* Rules) to generate a HTTP 302 temporary redirect. * Rules) to generate a HTTP 302 temporary redirect.
* *
@ -1810,7 +1820,7 @@ declare function LOC_BUILDER_STR(opts: { label?: string; str: string; alt?: numb
* *
* ```javascript * ```javascript
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER), * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* M365_BUILDER({ * M365_BUILDER("example.com", {
* initialDomain: "example.onmicrosoft.com", * initialDomain: "example.onmicrosoft.com",
* }), * }),
* END); * END);
@ -1822,7 +1832,7 @@ declare function LOC_BUILDER_STR(opts: { label?: string; str: string; alt?: numb
* *
* ```javascript * ```javascript
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER), * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
* M365_BUILDER({ * M365_BUILDER("example.com", {
* label: "test", * label: "test",
* mx: false, * mx: false,
* autodiscover: false, * autodiscover: false,

View file

@ -11,6 +11,13 @@ parameter_types:
"modifiers...": RecordModifier[] "modifiers...": RecordModifier[]
--- ---
{% hint style="warning" %}
WARNING: Cloudflare is removing this feature and replacing it with a new
feature called "Dynamic Single Redirect". DNSControl will automatically
generate "Dynamic Single Redirects" for a limited number of use cases. See
[`CLOUDFLAREAPI`](../provider/cloudflareapi.md) for details.
{% endhint %}
`CF_REDIRECT` uses Cloudflare-specific features ("Forwarding URL" Page Rules) to `CF_REDIRECT` uses Cloudflare-specific features ("Forwarding URL" Page Rules) to
generate a HTTP 301 permanent redirect. generate a HTTP 301 permanent redirect.

View file

@ -11,6 +11,13 @@ parameter_types:
"modifiers...": RecordModifier[] "modifiers...": RecordModifier[]
--- ---
{% hint style="warning" %}
**WARNING**: Cloudflare is removing this feature and replacing it with a new
feature called "Dynamic Single Redirect". DNSControl will automatically
generate "Dynamic Single Redirects" for a limited number of use cases. See
[`CLOUDFLAREAPI`](../provider/cloudflareapi.md) for details.
{% endhint %}
`CF_TEMP_REDIRECT` uses Cloudflare-specific features ("Forwarding URL" Page `CF_TEMP_REDIRECT` uses Cloudflare-specific features ("Forwarding URL" Page
Rules) to generate a HTTP 302 temporary redirect. Rules) to generate a HTTP 302 temporary redirect.

View file

@ -211,7 +211,8 @@ Enable it using:
```javascript ```javascript
var DSP_CLOUDFLARE = NewDnsProvider("cloudflare", { var DSP_CLOUDFLARE = NewDnsProvider("cloudflare", {
"manage_redirects": true "manage_redirects": true,
"transcode_log": "transcode.log",
}); });
``` ```
@ -231,8 +232,7 @@ New-style redirects ("Single Redirect Rules") are a new feature of DNSControl
as of v4.12.0 and may have bugs. Please test carefully. as of v4.12.0 and may have bugs. Please test carefully.
{% endhint %} {% endhint %}
### Conversion mode:
Conversion mode:
DNSControl can convert from old-style redirects (Page Rules) to new-style DNSControl can convert from old-style redirects (Page Rules) to new-style
redirect (Single Redirects). To enable this mode, set both `manage_redirects` redirect (Single Redirects). To enable this mode, set both `manage_redirects`
@ -268,7 +268,7 @@ via the CloudFlare control panel or wait for Cloudflare to remove support for th
{% hint style="warning" %} {% hint style="warning" %}
Cloudflare's announcement says that they will convert old-style redirects (Page Rules) to new-style Cloudflare's announcement says that they will convert old-style redirects (Page Rules) to new-style
redirect (Single Redirects) but they do not give a date for when this will happen. DNSControl redirect (Single Redirects) but they do not give an exact date for when this will happen. DNSControl
will probably see these new redirects as foreign and delete them. will probably see these new redirects as foreign and delete them.
Therefore it is probably safer to do the conversion ahead of them. Therefore it is probably safer to do the conversion ahead of them.
@ -279,6 +279,54 @@ than DNSControl's. However there's no way for DNSControl to manage them since t
If you have suggestions on how to handle this better please file a bug. If you have suggestions on how to handle this better please file a bug.
{% endhint %} {% endhint %}
### Converting to CF_SINGLE_REDIRECT permanently
DNSControl will help convert `CF_REDIRECT`/`CF_TEMP_REDIRECT` statements into
`CF_SINGLE_REDIRECT` statements. You might choose to do this if you do not want
to rely on the automatic translation, or if you want to edit the results of the
translation.
DNSControl will generate a file of the translated statements if you specify
a filename using the `transcode_log` meta option.
```javascript
var DSP_CLOUDFLARE = NewDnsProvider("cloudflare", {
"manage_single_redirects": true,
"transcode_log": "transcode.log",
});
```
After running `dnscontrol preview` the contents will look something like this:
{% code title="transcode.log" %}
```text
D("example.com", ...
CF_SINGLE_REDIRECT("1,302,https://example.com/*,https://replacement.example.com/$1",
302,
'http.host eq "example.com"',
'concat("https://replacement.example.com", http.request.uri.path)'
),
CF_SINGLE_REDIRECT("2,302,https://img.example.com/*,https://replacement.example.com/$1",
302,
'http.host eq "img.example.com"',
'concat("https://replacement.example.com", http.request.uri.path)'
),
CF_SINGLE_REDIRECT("3,302,https://i.example.com/*,https://replacement.example.com/$1",
302,
'http.host eq "i.example.com"',
'concat("https://replacement.example.com", http.request.uri.path)'
),
D("otherdomain.com", ...
CF_SINGLE_REDIRECT("1,301,https://one.otherdomain.com/,https://www.google.com/",
301,
'http.host eq "one.otherdomain.com" and http.request.uri.path eq "/"',
'concat("https://www.google.com/", "")'
),
```
{% endcode %}
Copying the statements to the proper place in `dnsconfig.js` is manual.
## Redirects ## Redirects
The Cloudflare provider can manage "Forwarding URL" Page Rules (redirects) for your domains. Simply use the `CF_REDIRECT` and `CF_TEMP_REDIRECT` functions to make redirects: The Cloudflare provider can manage "Forwarding URL" Page Rules (redirects) for your domains. Simply use the `CF_REDIRECT` and `CF_TEMP_REDIRECT` functions to make redirects:

View file

@ -503,7 +503,7 @@ func cfSingleRedirectEnabled() bool {
} }
func cfSingleRedirect(name string, code any, when, then string) *models.RecordConfig { func cfSingleRedirect(name string, code any, when, then string) *models.RecordConfig {
r := makeRec("@", name, "CLOUDFLAREAPI_SINGLE_REDIRECT") r := makeRec("@", name, cfsingleredirect.SINGLEREDIRECT)
err := cfsingleredirect.FromRaw(r, []any{name, code, when, then}) err := cfsingleredirect.FromRaw(r, []any{name, code, when, then})
if err != nil { if err != nil {
panic("Should not happen... cfSingleRedirect") panic("Should not happen... cfSingleRedirect")
@ -1947,6 +1947,7 @@ func makeTests() []*TestGroup {
tc("changecode", cfSingleRedirect(`name1`, `302`, `http.host eq "cnn.slackoverflow.com"`, `concat("https://www.cnn.com", http.request.uri.path)`)), tc("changecode", cfSingleRedirect(`name1`, `302`, `http.host eq "cnn.slackoverflow.com"`, `concat("https://www.cnn.com", http.request.uri.path)`)),
tc("changewhen", cfSingleRedirect(`name1`, `302`, `http.host eq "msnbc.slackoverflow.com"`, `concat("https://www.cnn.com", http.request.uri.path)`)), tc("changewhen", cfSingleRedirect(`name1`, `302`, `http.host eq "msnbc.slackoverflow.com"`, `concat("https://www.cnn.com", http.request.uri.path)`)),
tc("changethen", cfSingleRedirect(`name1`, `302`, `http.host eq "msnbc.slackoverflow.com"`, `concat("https://www.msnbc.com", http.request.uri.path)`)), tc("changethen", cfSingleRedirect(`name1`, `302`, `http.host eq "msnbc.slackoverflow.com"`, `concat("https://www.msnbc.com", http.request.uri.path)`)),
tc("changename", cfSingleRedirect(`name1bis`, `302`, `http.host eq "msnbc.slackoverflow.com"`, `concat("https://www.msnbc.com", http.request.uri.path)`)),
), ),
// CLOUDFLAREAPI: PROXY // CLOUDFLAREAPI: PROXY

View file

@ -154,17 +154,18 @@ type CloudflareSingleRedirectConfig struct {
// //
Code uint16 `json:"code,omitempty"` // 301 or 302 Code uint16 `json:"code,omitempty"` // 301 or 302
// PR == PageRule // PR == PageRule
PRDisplay string `json:"pr_display,omitempty"` // How is this displayed to the user
PRWhen string `json:"pr_when,omitempty"` PRWhen string `json:"pr_when,omitempty"`
PRThen string `json:"pr_then,omitempty"` PRThen string `json:"pr_then,omitempty"`
PRPriority int `json:"pr_priority,omitempty"` // Really an identifier for the rule. PRPriority int `json:"pr_priority,omitempty"` // Really an identifier for the rule.
PRDisplay string `json:"pr_display,omitempty"` // How is this displayed to the user (SetTarget) for CF_REDIRECT/CF_TEMP_REDIRECT
// //
// SR == SingleRedirect // SR == SingleRedirect
SRDisplay string `json:"sr_display,omitempty"` // How is this displayed to the user SRName string `json:"sr_name,omitempty"` // How is this displayed to the user
SRWhen string `json:"sr_when,omitempty"` SRWhen string `json:"sr_when,omitempty"`
SRThen string `json:"sr_then,omitempty"` SRThen string `json:"sr_then,omitempty"`
SRRRulesetID string `json:"sr_rulesetid,omitempty"` SRRRulesetID string `json:"sr_rulesetid,omitempty"`
SRRRulesetRuleID string `json:"sr_rulesetruleid,omitempty"` SRRRulesetRuleID string `json:"sr_rulesetruleid,omitempty"`
SRDisplay string `json:"sr_display,omitempty"` // How is this displayed to the user (SetTarget) for CF_SINGLE_REDIRECT
} }
// MarshalJSON marshals RecordConfig. // MarshalJSON marshals RecordConfig.

View file

@ -38,6 +38,7 @@ func PostProcess(domains []*models.DomainConfig) error {
case "CLOUDFLAREAPI_SINGLE_REDIRECT": case "CLOUDFLAREAPI_SINGLE_REDIRECT":
err = cfsingleredirect.FromRaw(rec, rawRec.Args) err = cfsingleredirect.FromRaw(rec, rawRec.Args)
rec.SetLabel("@", dc.Name)
default: default:
err = fmt.Errorf("unknown rawrec type=%q", rawRec.Type) err = fmt.Errorf("unknown rawrec type=%q", rawRec.Type)

View file

@ -87,23 +87,17 @@ type cloudflareProvider struct {
cfClient *cloudflare.API cfClient *cloudflare.API
// //
manageSingleRedirects bool // New "Single Redirects"-style redirects. manageSingleRedirects bool // New "Single Redirects"-style redirects.
//
// Used by
tcLogFilename string // Transcode Log file name
tcLogFh *os.File // Transcode Log file handle
tcZone string // Transcode Current zone
sync.Mutex // Protects all access to the following fields: sync.Mutex // Protects all access to the following fields:
domainIndex map[string]string // Cache of zone name to zone ID. domainIndex map[string]string // Cache of zone name to zone ID.
nameservers map[string][]string // Cache of zone name to list of nameservers. nameservers map[string][]string // Cache of zone name to list of nameservers.
} }
// TODO(dlemenkov): remove this function after deleting all commented code referecing it
//func labelMatches(label string, matches []string) bool {
// printer.Debugf("DEBUG: labelMatches(%#v, %#v)\n", label, matches)
// for _, tst := range matches {
// if label == tst {
// return true
// }
// }
// return false
//}
// GetNameservers returns the nameservers for a domain. // GetNameservers returns the nameservers for a domain.
func (c *cloudflareProvider) GetNameservers(domain string) ([]*models.Nameserver, error) { func (c *cloudflareProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
@ -162,17 +156,6 @@ func (c *cloudflareProvider) GetZoneRecords(domain string, meta map[string]strin
} }
} }
// // FIXME(tlim) Why is this needed???
// // I don't know. Let's comment it out and see if anything breaks.
// for i := len(records) - 1; i >= 0; i-- {
// rec := records[i]
// // Delete ignore labels
// if labelMatches(dnsutil.TrimDomainName(rec.Original.(cloudflare.DNSRecord).Name, dc.Name), c.ignoredLabels) {
// printer.Debugf("ignored_label: %s\n", rec.Original.(cloudflare.DNSRecord).Name)
// records = append(records[:i], records[i+1:]...)
// }
// }
if c.manageRedirects { // if old if c.manageRedirects { // if old
prs, err := c.getPageRules(domainID, domain) prs, err := c.getPageRules(domainID, domain)
if err != nil { if err != nil {
@ -183,14 +166,11 @@ func (c *cloudflareProvider) GetZoneRecords(domain string, meta map[string]strin
if c.manageSingleRedirects { // if new xor old if c.manageSingleRedirects { // if new xor old
// Download the list of Single Redirects. // Download the list of Single Redirects.
// For each one, generate a CLOUDFLAREAPI_SINGLE_REDIRECT record // For each one, generate a SINGLEREDIRECT record
// Append these records to `records`
prs, err := c.getSingleRedirects(domainID, domain) prs, err := c.getSingleRedirects(domainID, domain)
if err != nil { if err != nil {
return nil, err return nil, err
} }
//printer.Printf("DEBUG: Single Redirects")
//fmt.Fprintf(os.Stdout, "DEBUG: Single Redirects")
records = append(records, prs...) records = append(records, prs...)
} }
@ -284,8 +264,6 @@ func (c *cloudflareProvider) GetZoneRecordsCorrections(dc *models.DomainConfig,
case diff2.DELETE: case diff2.DELETE:
deleteRec := inst.Old[0] deleteRec := inst.Old[0]
deleteRecType := deleteRec.Type deleteRecType := deleteRec.Type
//deleteRecOrig := deleteRec.Original
//corrs = c.mkDeleteCorrection(deleteRecType, deleteRecOrig, domainID, msg)
corrs = c.mkDeleteCorrection(deleteRecType, deleteRec, domainID, msg) corrs = c.mkDeleteCorrection(deleteRecType, deleteRec, domainID, msg)
// DS records must always have a corresponding NS record. // DS records must always have a corresponding NS record.
// Therefore, we remove DS records before any NS records. // Therefore, we remove DS records before any NS records.
@ -344,10 +322,7 @@ func (c *cloudflareProvider) mkCreateCorrection(newrec *models.RecordConfig, dom
Msg: msg, Msg: msg,
F: func() error { return c.createWorkerRoute(domainID, newrec.GetTargetField()) }, F: func() error { return c.createWorkerRoute(domainID, newrec.GetTargetField()) },
}} }}
case "CLOUDFLAREAPI_SINGLE_REDIRECT": case cfsingleredirect.SINGLEREDIRECT:
//fmt.Printf("DEBUG: mkCreateSingleRedir: newrec=%+v\n", *newrec)
//fmt.Printf("DEBUG: mkCreateSingleRedir: crn=%+v\n", (*newrec).CloudflareRedirect)
//fmt.Printf("DEBUG: mkCreateSingleRedir: cr=%+v\n", (*newrec).CloudflareRedirect)
return []*models.Correction{{ return []*models.Correction{{
Msg: msg, Msg: msg,
F: func() error { F: func() error {
@ -367,8 +342,7 @@ func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordCon
idTxt = oldrec.Original.(cloudflare.PageRule).ID idTxt = oldrec.Original.(cloudflare.PageRule).ID
case "WORKER_ROUTE": case "WORKER_ROUTE":
idTxt = oldrec.Original.(cloudflare.WorkerRoute).ID idTxt = oldrec.Original.(cloudflare.WorkerRoute).ID
case "CLOUDFLAREAPI_SINGLE_REDIRECT": case cfsingleredirect.SINGLEREDIRECT:
//idTxt = oldrec.Original.(cloudflare.RulesetRule).ID
idTxt = oldrec.CloudflareRedirect.SRRRulesetID idTxt = oldrec.CloudflareRedirect.SRRRulesetID
default: default:
idTxt = oldrec.Original.(cloudflare.DNSRecord).ID idTxt = oldrec.Original.(cloudflare.DNSRecord).ID
@ -383,7 +357,7 @@ func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordCon
return c.updatePageRule(idTxt, domainID, *newrec.CloudflareRedirect) return c.updatePageRule(idTxt, domainID, *newrec.CloudflareRedirect)
}, },
}} }}
case "CLOUDFLAREAPI_SINGLE_REDIRECT": case cfsingleredirect.SINGLEREDIRECT:
return []*models.Correction{{ return []*models.Correction{{
Msg: msg, Msg: msg,
F: func() error { F: func() error {
@ -416,7 +390,7 @@ func (c *cloudflareProvider) mkDeleteCorrection(recType string, origRec *models.
idTxt = origRec.Original.(cloudflare.PageRule).ID idTxt = origRec.Original.(cloudflare.PageRule).ID
case "WORKER_ROUTE": case "WORKER_ROUTE":
idTxt = origRec.Original.(cloudflare.WorkerRoute).ID idTxt = origRec.Original.(cloudflare.WorkerRoute).ID
case "CLOUDFLAREAPI_SINGLE_REDIRECT": case cfsingleredirect.SINGLEREDIRECT:
idTxt = origRec.Original.(cloudflare.RulesetRule).ID idTxt = origRec.Original.(cloudflare.RulesetRule).ID
default: default:
idTxt = origRec.Original.(cloudflare.DNSRecord).ID idTxt = origRec.Original.(cloudflare.DNSRecord).ID
@ -431,12 +405,7 @@ func (c *cloudflareProvider) mkDeleteCorrection(recType string, origRec *models.
return c.deletePageRule(origRec.Original.(cloudflare.PageRule).ID, domainID) return c.deletePageRule(origRec.Original.(cloudflare.PageRule).ID, domainID)
case "WORKER_ROUTE": case "WORKER_ROUTE":
return c.deleteWorkerRoute(origRec.Original.(cloudflare.WorkerRoute).ID, domainID) return c.deleteWorkerRoute(origRec.Original.(cloudflare.WorkerRoute).ID, domainID)
case "CLOUDFLAREAPI_SINGLE_REDIRECT": case cfsingleredirect.SINGLEREDIRECT:
//o := origRec.Original.(cloudflare.Ruleset)
//printer.Printf("DEBUG: DELETE %+v\n", o)
// printer.Printf("DEBUG: DELETE ID = %+v\n", o.ID)
// printer.Printf("DEBUG: DELETE ACTION %+v\n", o.ActionParameters)
// printer.Printf("DEBUG: DELETE FROMVALUE %+v\n", o.ActionParameters.FromValue)
return c.deleteSingleRedirects(domainID, *origRec.CloudflareRedirect) return c.deleteSingleRedirects(domainID, *origRec.CloudflareRedirect)
default: default:
return c.deleteDNSRecord(origRec.Original.(cloudflare.DNSRecord), domainID) return c.deleteDNSRecord(origRec.Original.(cloudflare.DNSRecord), domainID)
@ -530,7 +499,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error {
// A and CNAMEs: Validate. If null, set to default. // A and CNAMEs: Validate. If null, set to default.
// else: Make sure it wasn't set. Set to default. // else: Make sure it wasn't set. Set to default.
// iterate backwards so first defined page rules have highest priority // iterate backwards so first defined page rules have highest priority
currentPrPrio := 1 prPriority := 0
for i := len(dc.Records) - 1; i >= 0; i-- { for i := len(dc.Records) - 1; i >= 0; i-- {
rec := dc.Records[i] rec := dc.Records[i]
if rec.Metadata == nil { if rec.Metadata == nil {
@ -564,9 +533,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error {
} }
} }
// CF_REDIRECT record types. Encode target as // CF_REDIRECT record types:
// $FROM,$TO,$PRIO,$CODE or build Cfsr struct for new-style
// (Single Redirect) versions.
if rec.Type == "CF_REDIRECT" || rec.Type == "CF_TEMP_REDIRECT" { if rec.Type == "CF_REDIRECT" || rec.Type == "CF_TEMP_REDIRECT" {
if !c.manageRedirects && !c.manageSingleRedirects { if !c.manageRedirects && !c.manageSingleRedirects {
return fmt.Errorf("you must add 'manage_single_redirects: true' metadata to cloudflare provider to use CF_REDIRECT/CF_TEMP_REDIRECT records") return fmt.Errorf("you must add 'manage_single_redirects: true' metadata to cloudflare provider to use CF_REDIRECT/CF_TEMP_REDIRECT records")
@ -576,65 +543,49 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error {
code = 302 code = 302
} }
if c.manageRedirects && !c.manageSingleRedirects { part := strings.SplitN(rec.GetTargetField(), ",", 2)
// Old-Style only. Convert this record to PAGE_RULE. prWhen, prThen := part[0], part[1]
//printer.Printf("DEBUG: prepro() target=%q\n", rec.GetTargetField()) prPriority++
sr, err := cfsingleredirect.FromUserInput(rec.GetTargetField(), code, currentPrPrio)
if err != nil {
return err
}
fixPageRule(rec, sr)
currentPrPrio++
} else if !c.manageRedirects && c.manageSingleRedirects {
// New-Style only. Convert this record to a CLOUDFLAREAPI_SINGLE_REDIRECT.
sr, err := cfsingleredirect.FromUserInput(rec.GetTargetField(), code, currentPrPrio)
if err != nil {
return err
}
err = fixSingleRedirect(rec, sr)
if err != nil {
return err
}
} else {
// Both! Convert this record to PAGE_RULE and append an additional CLOUDFLAREAPI_SINGLE_REDIRECT.
target := rec.GetTargetField() // Convert this record to a PAGE_RULE.
cfsingleredirect.MakePageRule(rec, prPriority, code, prWhen, prThen)
rec.SetLabel("@", dc.Name)
if c.manageRedirects && !c.manageSingleRedirects {
// Old-Style only. No additional work needed.
} else if !c.manageRedirects && c.manageSingleRedirects {
// New-Style only. Convert PAGE_RULE to SINGLEREDIRECT.
cfsingleredirect.TranscodePRtoSR(rec)
if err := c.LogTranscode(dc.Name, rec.CloudflareRedirect); err != nil {
return err
}
} else {
// Both old-style and new-style enabled!
// Retain the PAGE_RULE and append an additional SINGLEREDIRECT.
// make a copy: // make a copy:
newRec, err := rec.Copy() newRec, err := rec.Copy()
if err != nil { if err != nil {
return err return err
} }
// The copy becomes the CF SingleRedirect // The copy becomes the CF SingleRedirect
sr, err := cfsingleredirect.FromUserInput(target, code, currentPrPrio) cfsingleredirect.TranscodePRtoSR(rec)
if err != nil { if err := c.LogTranscode(dc.Name, rec.CloudflareRedirect); err != nil {
return err return err
} }
err = fixSingleRedirect(newRec, sr)
if err != nil {
return err
}
// Append the copy to the end of the list. // Append the copy to the end of the list.
dc.Records = append(dc.Records, newRec) dc.Records = append(dc.Records, newRec)
// The original becomes the PAGE_RULE: // The original PAGE_RULE remains untouched.
sr, err = cfsingleredirect.FromUserInput(target, code, currentPrPrio)
if err != nil {
return err
}
fixPageRule(rec, sr)
currentPrPrio++
} }
} else if rec.Type == "CLOUDFLAREAPI_SINGLE_REDIRECT" { } else if rec.Type == cfsingleredirect.SINGLEREDIRECT {
// CLOUDFLAREAPI_SINGLE_REDIRECT record types. // SINGLEREDIRECT record types. Verify they are enabled.
if !c.manageSingleRedirects { if !c.manageSingleRedirects {
return fmt.Errorf("you must add 'manage_single_redirects: true' metadata to cloudflare provider to use CF_SINGLE__REDIRECT records") return fmt.Errorf("you must add 'manage_single_redirects: true' metadata to cloudflare provider to use CF_SINGLE__REDIRECT records")
} }
// Nothing needs to be done.
} else if rec.Type == "CF_WORKER_ROUTE" { } else if rec.Type == "CF_WORKER_ROUTE" {
// CF_WORKER_ROUTE record types. Encode target as $PATTERN,$SCRIPT // CF_WORKER_ROUTE record types. Encode target as $PATTERN,$SCRIPT
@ -672,20 +623,35 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error {
return nil return nil
} }
func fixPageRule(rc *models.RecordConfig, sr *models.CloudflareSingleRedirectConfig) { func (c *cloudflareProvider) LogTranscode(zone string, redirect *models.CloudflareSingleRedirectConfig) error {
rc.Type = "PAGE_RULE" // No filename? Don't log anything.
rc.TTL = 1 filename := c.tcLogFilename
rc.SetTarget(sr.PRDisplay) if filename == "" {
rc.CloudflareRedirect = sr return nil
} }
func fixSingleRedirect(rc *models.RecordConfig, sr *models.CloudflareSingleRedirectConfig) error { // File not opened already? Open it.
rc.Type = "CLOUDFLAREAPI_SINGLE_REDIRECT" if c.tcLogFh == nil {
rc.TTL = 1 f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
rc.SetTarget(sr.SRDisplay) if err != nil {
rc.CloudflareRedirect = sr return err
}
c.tcLogFh = f
}
fh := c.tcLogFh
err := cfsingleredirect.AddNewStyleFields(sr) // Output "D(zone)" if needed.
var text string
if c.tcZone != zone {
text = fmt.Sprintf("D(%q, ...\n", zone)
}
c.tcZone = zone
// Generate the new command and output.
text = text + fmt.Sprintf(" CF_SINGLE_REDIRECT(%q,\n %03d,\n '%s',\n '%s'\n ),\n",
redirect.SRName, redirect.Code,
redirect.SRWhen, redirect.SRThen)
_, err := fh.WriteString(text)
return err return err
} }
@ -732,7 +698,8 @@ func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNS
ManageRedirects bool `json:"manage_redirects"` // Old-style PAGE_RULE-based redirects ManageRedirects bool `json:"manage_redirects"` // Old-style PAGE_RULE-based redirects
ManageWorkers bool `json:"manage_workers"` ManageWorkers bool `json:"manage_workers"`
// //
ManageSingleRedirects bool `json:"manage_single_redirects"` // New-style Dynamic "Single Redirects" ManageSingleRedirects bool `json:"manage_single_redirects"` // New-style Dynamic "Single Redirects"
TranscodeLogFilename string `json:"transcode_log"` // Log the PAGE_RULE conversions.
}{} }{}
err := json.Unmarshal([]byte(metadata), parsedMeta) err := json.Unmarshal([]byte(metadata), parsedMeta)
if err != nil { if err != nil {
@ -740,6 +707,7 @@ func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNS
} }
api.manageSingleRedirects = parsedMeta.ManageSingleRedirects api.manageSingleRedirects = parsedMeta.ManageSingleRedirects
api.manageRedirects = parsedMeta.ManageRedirects api.manageRedirects = parsedMeta.ManageRedirects
api.tcLogFilename = parsedMeta.TranscodeLogFilename
api.manageWorkers = parsedMeta.ManageWorkers api.manageWorkers = parsedMeta.ManageWorkers
// ignored_labels: // ignored_labels:
api.ignoredLabels = append(api.ignoredLabels, parsedMeta.IgnoredLabels...) api.ignoredLabels = append(api.ignoredLabels, parsedMeta.IgnoredLabels...)

View file

@ -286,38 +286,28 @@ func (c *cloudflareProvider) getSingleRedirects(id string, domain string) ([]*mo
} }
return nil, fmt.Errorf("failed fetching redirect rule list cloudflare: %s (%T)", err, err) return nil, fmt.Errorf("failed fetching redirect rule list cloudflare: %s (%T)", err, err)
} }
//var rulelist []cloudflare.RulesetRule
//rulelist = rules.Rules
//rulelist := rules.Rules
//printer.Printf("DEBUG: rules %+v\n", rules)
recs := []*models.RecordConfig{} recs := []*models.RecordConfig{}
for _, pr := range rules.Rules { for _, pr := range rules.Rules {
//printer.Printf("DEBUG: %+v\n", pr)
var thisPr = pr var thisPr = pr
r := &models.RecordConfig{ r := &models.RecordConfig{
Type: "CLOUDFLAREAPI_SINGLE_REDIRECT",
Original: thisPr, Original: thisPr,
TTL: 1,
} }
r.SetLabel("@", domain)
// Extract the valuables from the rule, use it to make the sr: // Extract the valuables from the rule, use it to make the sr:
srName := pr.Description
srWhen := pr.Expression srWhen := pr.Expression
srThen := pr.ActionParameters.FromValue.TargetURL.Expression srThen := pr.ActionParameters.FromValue.TargetURL.Expression
code := uint16(pr.ActionParameters.FromValue.StatusCode) code := uint16(pr.ActionParameters.FromValue.StatusCode)
sr := cfsingleredirect.FromAPIData(srWhen, srThen, code)
//sr.SRRRuleList = rulelist
//printer.Printf("DEBUG: DESCRIPTION = %v\n", pr.Description)
sr.SRDisplay = pr.Description
// printer.Printf("DEBUG: PR = %+v\n", pr)
// printer.Printf("DEBUG: rules = %+v\n", rules)
sr.SRRRulesetID = rules.ID
sr.SRRRulesetRuleID = pr.ID //correct
r.CloudflareRedirect = sr cfsingleredirect.MakeSingleRedirectFromAPI(r, code, srName, srWhen, srThen)
r.SetTarget(pr.Description) r.SetLabel("@", domain)
// Store the IDs
sr := r.CloudflareRedirect
sr.SRRRulesetID = rules.ID
sr.SRRRulesetRuleID = pr.ID
recs = append(recs, r) recs = append(recs, r)
} }
@ -327,9 +317,6 @@ func (c *cloudflareProvider) getSingleRedirects(id string, domain string) ([]*mo
func (c *cloudflareProvider) createSingleRedirect(domainID string, cfr models.CloudflareSingleRedirectConfig) error { func (c *cloudflareProvider) createSingleRedirect(domainID string, cfr models.CloudflareSingleRedirectConfig) error {
//printer.Printf("DEBUG: createSingleRedir: d=%v crf=%+v\n", domainID, cfr)
// Asumption for target:
newSingleRedirectRulesActionParameters := cloudflare.RulesetRuleActionParameters{} newSingleRedirectRulesActionParameters := cloudflare.RulesetRuleActionParameters{}
newSingleRedirectRule := cloudflare.RulesetRule{} newSingleRedirectRule := cloudflare.RulesetRule{}
newSingleRedirectRules := []cloudflare.RulesetRule{} newSingleRedirectRules := []cloudflare.RulesetRule{}
@ -347,7 +334,8 @@ func (c *cloudflareProvider) createSingleRedirect(domainID string, cfr models.Cl
// Redirect expression // Redirect expression
newSingleRedirectRulesActionParameters.FromValue.TargetURL.Expression = cfr.SRThen newSingleRedirectRulesActionParameters.FromValue.TargetURL.Expression = cfr.SRThen
// Redirect name // Redirect name
newSingleRedirectRules[0].Description = cfr.SRDisplay newSingleRedirectRules[0].Description = cfr.SRName
// Rule action, should always be redirect in this case // Rule action, should always be redirect in this case
newSingleRedirectRules[0].Action = "redirect" newSingleRedirectRules[0].Action = "redirect"
// Phase should always be http_request_dynamic_redirect // Phase should always be http_request_dynamic_redirect
@ -401,7 +389,7 @@ func (c *cloudflareProvider) deleteSingleRedirects(domainID string, cfr models.C
//printer.Printf("DEBUG: CALLING API DeleteRulesetRule: SRRRulesetID=%v, cfr.SRRRulesetRuleID=%v\n", cfr.SRRRulesetID, cfr.SRRRulesetRuleID) //printer.Printf("DEBUG: CALLING API DeleteRulesetRule: SRRRulesetID=%v, cfr.SRRRulesetRuleID=%v\n", cfr.SRRRulesetID, cfr.SRRRulesetRuleID)
err := c.cfClient.DeleteRulesetRule(context.Background(), cloudflare.ZoneIdentifier(domainID), cfr.SRRRulesetID, cfr.SRRRulesetRuleID) err := c.cfClient.DeleteRulesetRule(context.Background(), cloudflare.ZoneIdentifier(domainID), cfr.SRRRulesetID, cfr.SRRRulesetRuleID)
// TODO(tlim): This is terrible. It returns an error even when it is successful. // NB(tlim): Yuck. This returns an error even when it is successful. Dig into the JSON for the real status.
if strings.Contains(err.Error(), `"success": true,`) { if strings.Contains(err.Error(), `"success": true,`) {
return nil return nil
} }
@ -410,13 +398,9 @@ func (c *cloudflareProvider) deleteSingleRedirects(domainID string, cfr models.C
} }
func (c *cloudflareProvider) updateSingleRedirect(domainID string, oldrec, newrec *models.RecordConfig) error { func (c *cloudflareProvider) updateSingleRedirect(domainID string, oldrec, newrec *models.RecordConfig) error {
// rulesetID := cfr.SRRRulesetID
// rulesetRuleID := cfr.SRRRulesetRuleID
//printer.Printf("DEBUG: UPDATE-DEL domID=%v sr=%+v\n", domainID, cfr)
if err := c.deleteSingleRedirects(domainID, *oldrec.CloudflareRedirect); err != nil { if err := c.deleteSingleRedirects(domainID, *oldrec.CloudflareRedirect); err != nil {
return err return err
} }
//printer.Printf("DEBUG: UPDATE-CREATE domID=%v sr=%+v\n", domainID, newrec.CloudflareRedirect)
return c.createSingleRedirect(domainID, *newrec.CloudflareRedirect) return c.createSingleRedirect(domainID, *newrec.CloudflareRedirect)
} }
@ -437,24 +421,17 @@ func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.R
value := pr.Actions[0].Value.(map[string]interface{}) value := pr.Actions[0].Value.(map[string]interface{})
var thisPr = pr var thisPr = pr
r := &models.RecordConfig{ r := &models.RecordConfig{
Type: "PAGE_RULE",
Original: thisPr, Original: thisPr,
TTL: 1,
} }
r.SetLabel("@", domain)
code := intZero(value["status_code"])
raw := fmt.Sprintf("%s,%s,%d,%d", // $FROM,$TO,$PRIO,$CODE
pr.Targets[0].Constraint.Value,
value["url"],
pr.Priority,
code)
r.SetTarget(raw)
cr, err := cfsingleredirect.FromUserInput(raw, code, pr.Priority) code := intZero(value["status_code"])
if err != nil {
return nil, err when := pr.Targets[0].Constraint.Value
} then := value["url"].(string)
r.CloudflareRedirect = cr currentPrPrio := pr.Priority
cfsingleredirect.MakePageRule(r, currentPrPrio, code, when, then)
r.SetLabel("@", domain)
recs = append(recs, r) recs = append(recs, r)
} }
@ -492,7 +469,6 @@ func (c *cloudflareProvider) createPageRule(domainID string, cfr models.Cloudfla
}}, }},
}, },
} }
//printer.Printf("DEBUG: createPageRule pr=%+v\n", pr)
_, err := c.cfClient.CreatePageRule(context.Background(), domainID, pr) _, err := c.cfClient.CreatePageRule(context.Background(), domainID, pr)
return err return err
} }

View file

@ -7,10 +7,14 @@ import (
"github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol" "github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol"
) )
// SINGLEREDIRECT is the string name for this rType.
const SINGLEREDIRECT = "CLOUDFLAREAPI_SINGLE_REDIRECT"
func init() { func init() {
rtypecontrol.Register("CLOUDFLAREAPI_SINGLE_REDIRECT") rtypecontrol.Register(SINGLEREDIRECT)
} }
// FromRaw convert RecordConfig using data from a RawRecordConfig's parameters.
func FromRaw(rc *models.RecordConfig, items []any) error { func FromRaw(rc *models.RecordConfig, items []any) error {
// Validate types. // Validate types.
@ -23,17 +27,14 @@ func FromRaw(rc *models.RecordConfig, items []any) error {
var code uint16 var code uint16
name = items[0].(string) name = items[0].(string)
code = items[1].(uint16) code = items[1].(uint16)
if code != 301 && code != 302 { if code != 301 && code != 302 {
return fmt.Errorf("code (%03d) is not 301 or 302", code) return fmt.Errorf("code (%03d) is not 301 or 302", code)
} }
when = items[2].(string)
then = items[3].(string)
when, then = items[2].(string), items[3].(string) makeSingleRedirectFromRawRec(rc, code, name, when, then)
rc.Name = name
rc.CloudflareRedirect = FromAPIData(when, then, code)
rc.SetTarget(rc.CloudflareRedirect.SRDisplay)
return nil return nil
} }

View file

@ -9,51 +9,29 @@ import (
"github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/models"
) )
func FromUserInput(target string, code uint16, priority int) (*models.CloudflareSingleRedirectConfig, error) { // TranscodePRtoSR takes a PAGE_RULE record, stores transcoded versions of the fields, and makes the record a CLOUDFLAREAPI_SINGLE_REDDIRECT.
// target: matcher,replacement,priority,code func TranscodePRtoSR(rec *models.RecordConfig) error {
// target: cable.slackoverflow.com/*,https://change.cnn.com/$1,1,302 rec.Type = SINGLEREDIRECT // This record is now a CLOUDFLAREAPI_SINGLE_REDIRECT
r := &models.CloudflareSingleRedirectConfig{}
// Break apart the 4-part string and store into the individual fields:
parts := strings.Split(target, ",")
//printer.Printf("DEBUG: cfsrFromOldStyle: parts=%v\n", parts)
r.PRDisplay = fmt.Sprintf("%s,%d,%03d", target, priority, code)
r.PRWhen = parts[0]
r.PRThen = parts[1]
r.PRPriority = priority
r.Code = code
// Convert old-style to new-style:
if err := AddNewStyleFields(r); err != nil {
return nil, err
}
return r, nil
}
// AddNewStyleFields takes a PAGE_RULE-style target and populates the CFSRC.
func AddNewStyleFields(sr *models.CloudflareSingleRedirectConfig) error {
// Extract the fields we're reading from: // Extract the fields we're reading from:
sr := rec.CloudflareRedirect
code := sr.Code
prWhen := sr.PRWhen prWhen := sr.PRWhen
prThen := sr.PRThen prThen := sr.PRThen
code := sr.Code srName := sr.PRDisplay
// Convert old-style patterns to new-style rules: // Convert old-style patterns to new-style rules:
srWhen, srThen, err := makeRuleFromPattern(prWhen, prThen) srWhen, srThen, err := makeRuleFromPattern(prWhen, prThen)
if err != nil { if err != nil {
return err return err
} }
display := fmt.Sprintf(`%s,%s,%d,%03d matcher=%s replacement=%s`,
prWhen, prThen,
sr.PRPriority, code,
srWhen, srThen,
)
// Store the results in the fields we're writing to: // Fix the RecordConfig
sr.SRWhen = srWhen makeSingleRedirectFromConvert(rec,
sr.SRThen = srThen sr.PRPriority,
sr.SRDisplay = display prWhen, prThen,
code,
srName, srWhen, srThen)
return nil return nil
} }

View file

@ -94,15 +94,6 @@ func Test_makeSingleDirectRule(t *testing.T) {
wantExpr: `concat("https://survey.stackoverflow.co/2021", "")`, wantExpr: `concat("https://survey.stackoverflow.co/2021", "")`,
wantErr: false, wantErr: false,
}, },
// {
// name: "27",
// pattern: "*www.stackoverflow.help/*",
// replace: "https://stackoverflow.help/$1",
/// FIXME(tlim): Should "$1" should be a "$2"? See dnsconfig.js:4344
// wantMatch: `FIXME`,
// wantExpr: `FIXME`,
// wantErr: false,
// },
{ {
name: "28", name: "28",
pattern: "*stackoverflow.help/support/solutions/articles/36000241656-write-an-article", pattern: "*stackoverflow.help/support/solutions/articles/36000241656-write-an-article",

View file

@ -0,0 +1,122 @@
package cfsingleredirect
import (
"fmt"
"github.com/StackExchange/dnscontrol/v4/models"
)
// MakePageRule updates a RecordConfig to be a PAGE_RULE using PAGE_RULE data.
func MakePageRule(rc *models.RecordConfig, priority int, code uint16, when, then string) {
display := mkPageRuleBlob(priority, code, when, then)
rc.Type = "PAGE_RULE"
rc.TTL = 1
rc.CloudflareRedirect = &models.CloudflareSingleRedirectConfig{
Code: code,
//
PRWhen: when,
PRThen: then,
PRPriority: priority,
PRDisplay: display,
}
rc.SetTarget(display)
}
// mkPageRuleBlob creates the 1,301,when,then string used in displays.
func mkPageRuleBlob(priority int, code uint16, when, then string) string {
return fmt.Sprintf("%d,%03d,%s,%s", priority, code, when, then)
}
// makeSingleRedirectFromRawRec updates a RecordConfig to be a
// SINGLEREDIRECT using the data from a RawRecord.
func makeSingleRedirectFromRawRec(rc *models.RecordConfig, code uint16, name, when, then string) {
target := targetFromRaw(name, code, when, then)
rc.Type = SINGLEREDIRECT
rc.TTL = 1
rc.CloudflareRedirect = &models.CloudflareSingleRedirectConfig{
Code: code,
//
PRWhen: "UNKNOWABLE",
PRThen: "UNKNOWABLE",
PRPriority: 0,
PRDisplay: "UNKNOWABLE",
//
SRName: name,
SRWhen: when,
SRThen: then,
SRDisplay: target,
}
rc.SetTarget(rc.CloudflareRedirect.SRDisplay)
}
// targetFromRaw create the display text used for a normal Redirect.
func targetFromRaw(name string, code uint16, when, then string) string {
return fmt.Sprintf("%s code=(%03d) when=(%s) then=(%s)",
name,
code,
when,
then,
)
}
// MakeSingleRedirectFromAPI updatese a RecordConfig to be a SINGLEREDIRECT using data downloaded via the API.
func MakeSingleRedirectFromAPI(rc *models.RecordConfig, code uint16, name, when, then string) {
// The target is the same as the name. It is the responsibility of the record creator to name it something diffable.
target := targetFromAPIData(name, code, when, then)
rc.Type = SINGLEREDIRECT
rc.TTL = 1
rc.CloudflareRedirect = &models.CloudflareSingleRedirectConfig{
Code: code,
//
PRWhen: "UNKNOWABLE",
PRThen: "UNKNOWABLE",
PRPriority: 0,
PRDisplay: "UNKNOWABLE",
//
SRName: name,
SRWhen: when,
SRThen: then,
SRDisplay: target,
}
rc.SetTarget(rc.CloudflareRedirect.SRDisplay)
}
// targetFromAPIData creates the display text used for a Redirect as received from Cloudflare's API.
func targetFromAPIData(name string, code uint16, when, then string) string {
return fmt.Sprintf("%s code=(%03d) when=(%s) then=(%s)",
name,
code,
when,
then,
)
}
// makeSingleRedirectFromConvert updates a RecordConfig to be a SINGLEREDIRECT using data from a PAGE_RULE conversion.
func makeSingleRedirectFromConvert(rc *models.RecordConfig,
priority int,
prWhen, prThen string,
code uint16,
srName, srWhen, srThen string) {
srDisplay := targetFromConverted(priority, code, prWhen, prThen, srWhen, srThen)
rc.Type = SINGLEREDIRECT
rc.TTL = 1
sr := rc.CloudflareRedirect
sr.Code = code
sr.SRName = srName
sr.SRWhen = srWhen
sr.SRThen = srThen
sr.SRDisplay = srDisplay
rc.SetTarget(rc.CloudflareRedirect.SRDisplay)
}
// targetFromConverted makes the display text used when a redirect was the result of converting a PAGE_RULE.
func targetFromConverted(prPriority int, code uint16, prWhen, prThen, srWhen, srThen string) string {
return fmt.Sprintf("%d,%03d,%s,%s code=(%03d) when=(%s) then=(%s)", prPriority, code, prWhen, prThen, code, srWhen, srThen)
}

View file

@ -1,19 +0,0 @@
package cfsingleredirect
import (
"fmt"
"github.com/StackExchange/dnscontrol/v4/models"
)
func FromAPIData(sm, sr string, code uint16) *models.CloudflareSingleRedirectConfig {
r := &models.CloudflareSingleRedirectConfig{
PRWhen: "UNKNOWABLE",
PRThen: "UNKNOWABLE",
Code: code,
SRDisplay: fmt.Sprintf("code=%03d when=(%v) then=(%v)", code, sm, sr),
SRWhen: sm,
SRThen: sr,
}
return r
}