mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-09-20 06:46:19 +08:00
CLOUDFLAREAPI: Add CF_SINGLE_REDIRECT to manage "dynamic single" redirects (#3035)
This commit is contained in:
parent
937c0dc46c
commit
088306883d
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/StackExchange/dnscontrol/v4/pkg/js"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/normalize"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/rfc4183"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/rtypes"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
|
@ -128,6 +129,12 @@ func ExecuteDSL(args ExecuteDSLArgs) (*models.DNSConfig, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("executing %s: %w", args.JSFile, err)
|
||||
}
|
||||
|
||||
err = rtypes.PostProcess(dnsConfig.Domains)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dnsConfig, nil
|
||||
}
|
||||
|
||||
|
|
36
commands/types/dnscontrol.d.ts
vendored
36
commands/types/dnscontrol.d.ts
vendored
|
@ -477,7 +477,7 @@ declare function CAA_BUILDER(opts: { label?: string; iodef: string; iodef_critic
|
|||
*
|
||||
* If _any_ `CF_REDIRECT` or [`CF_TEMP_REDIRECT`](CF_TEMP_REDIRECT.md) functions are used then
|
||||
* `dnscontrol` will manage _all_ "Forwarding URL" type Page Rules for the domain.
|
||||
* Page Rule types other than "Forwarding URL” will be left alone.
|
||||
* Page Rule types other than "Forwarding URL" will be left alone.
|
||||
*
|
||||
* WARNING: Cloudflare does not currently fully document the Page Rules API and
|
||||
* this interface is not extensively tested. Take precautions such as making
|
||||
|
@ -502,6 +502,37 @@ declare function CAA_BUILDER(opts: { label?: string; iodef: string; iodef_critic
|
|||
*/
|
||||
declare function CF_REDIRECT(source: string, destination: string, ...modifiers: RecordModifier[]): DomainModifier;
|
||||
|
||||
/**
|
||||
* `CF_SINGLE_REDIRECT` is a Cloudflare-specific feature for creating HTTP 301
|
||||
* (permanent) or 302 (temporary) redirects.
|
||||
*
|
||||
* This feature manages dynamic "Single Redirects". (Single Redirects can be
|
||||
* static or dynamic but DNSControl only maintains dynamic redirects).
|
||||
*
|
||||
* Cloudflare documentation: https://developers.cloudflare.com/rules/url-forwarding/single-redirects/
|
||||
*
|
||||
* ```javascript
|
||||
* D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
|
||||
* CF_SINGLE_REDIRECT("name", 301, "when", "then"),
|
||||
* CF_SINGLE_REDIRECT('redirect www.example.com', 301, 'http.host eq "www.example.com"', 'concat("https://otherplace.com", http.request.uri.path)'),
|
||||
* CF_SINGLE_REDIRECT('redirect yyy.example.com', 301, 'http.host eq "yyy.example.com"', 'concat("https://survey.stackoverflow.co", "")'),
|
||||
* END);
|
||||
* ```
|
||||
*
|
||||
* The fields are:
|
||||
*
|
||||
* * name: The name (basically a comment, but it must be unique)
|
||||
* * code: Either 301 (permanent) or 302 (temporary) redirects. May be a number or string.
|
||||
* * when: What Cloudflare sometimes calls the "rule expression".
|
||||
* * then: The replacement expression.
|
||||
*
|
||||
* NOTE:
|
||||
* The features [`CF_REDIRECT`](CF_REDIRECT.md) and [`CF_TEMP_REDIRECT`](CF_TEMP_REDIRECT.md) generate `CF_SINGLE_REDIRECT` if enabled in [`CLOUDFLAREAPI`](../../provider/cloudflareapi.md).
|
||||
*
|
||||
* @see https://docs.dnscontrol.org/language-reference/domain-modifiers/service-provider-specific/cloudflare-dns/cf_single_redirect
|
||||
*/
|
||||
declare function CF_SINGLE_REDIRECT(name: string, code: number, when: string, then: string, ...modifiers: RecordModifier[]): DomainModifier;
|
||||
|
||||
/**
|
||||
* `CF_TEMP_REDIRECT` uses Cloudflare-specific features ("Forwarding URL" Page
|
||||
* Rules) to generate a HTTP 302 temporary redirect.
|
||||
|
@ -982,7 +1013,8 @@ declare function DS(name: string, keytag: number, algorithm: number, digesttype:
|
|||
* not `domain.tld`.
|
||||
*
|
||||
* Some operators only act on an apex domain (e.g.
|
||||
* [`CF_REDIRECT`](../domain-modifiers/CF_REDIRECT.md) and [`CF_TEMP_REDIRECT`](../domain-modifiers/CF_TEMP_REDIRECT.md)). Using them
|
||||
* [`CF_SINGLE_REDIRECT`](../domain-modifiers/CF_SINGLE_REDIRECT.md),
|
||||
* [`CF_REDIRECT`](../domain-modifiers/CF_REDIRECT.md), and [`CF_TEMP_REDIRECT`](../domain-modifiers/CF_TEMP_REDIRECT.md)). Using them
|
||||
* in a `D_EXTEND` subdomain may not be what you expect.
|
||||
*
|
||||
* ```javascript
|
||||
|
|
|
@ -84,6 +84,7 @@
|
|||
* Azure DNS
|
||||
* [AZURE_ALIAS](language-reference/domain-modifiers/AZURE_ALIAS.md)
|
||||
* Cloudflare DNS
|
||||
* [CF_SINGLE_REDIRECT](language-reference/domain-modifiers/CF_SINGLE_REDIRECT.md)
|
||||
* [CF_REDIRECT](language-reference/domain-modifiers/CF_REDIRECT.md)
|
||||
* [CF_TEMP_REDIRECT](language-reference/domain-modifiers/CF_TEMP_REDIRECT.md)
|
||||
* [CF_WORKER_ROUTE](language-reference/domain-modifiers/CF_WORKER_ROUTE.md)
|
||||
|
|
|
@ -16,7 +16,7 @@ generate a HTTP 301 permanent redirect.
|
|||
|
||||
If _any_ `CF_REDIRECT` or [`CF_TEMP_REDIRECT`](CF_TEMP_REDIRECT.md) functions are used then
|
||||
`dnscontrol` will manage _all_ "Forwarding URL" type Page Rules for the domain.
|
||||
Page Rule types other than "Forwarding URL” will be left alone.
|
||||
Page Rule types other than "Forwarding URL" will be left alone.
|
||||
|
||||
{% hint style="warning" %}
|
||||
**WARNING**: Cloudflare does not currently fully document the Page Rules API and
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
name: CF_SINGLE_REDIRECT
|
||||
parameters:
|
||||
- name
|
||||
- code
|
||||
- when
|
||||
- then
|
||||
- modifiers...
|
||||
provider: CLOUDFLAREAPI
|
||||
parameter_types:
|
||||
name: string
|
||||
code: number
|
||||
when: string
|
||||
then: string
|
||||
"modifiers...": RecordModifier[]
|
||||
---
|
||||
|
||||
`CF_SINGLE_REDIRECT` is a Cloudflare-specific feature for creating HTTP 301
|
||||
(permanent) or 302 (temporary) redirects.
|
||||
|
||||
This feature manages dynamic "Single Redirects". (Single Redirects can be
|
||||
static or dynamic but DNSControl only maintains dynamic redirects).
|
||||
|
||||
Cloudflare documentation: https://developers.cloudflare.com/rules/url-forwarding/single-redirects/
|
||||
|
||||
{% code title="dnsconfig.js" %}
|
||||
```javascript
|
||||
D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER),
|
||||
CF_SINGLE_REDIRECT("name", 301, "when", "then"),
|
||||
CF_SINGLE_REDIRECT('redirect www.example.com', 301, 'http.host eq "www.example.com"', 'concat("https://otherplace.com", http.request.uri.path)'),
|
||||
CF_SINGLE_REDIRECT('redirect yyy.example.com', 301, 'http.host eq "yyy.example.com"', 'concat("https://survey.stackoverflow.co", "")'),
|
||||
END);
|
||||
```
|
||||
{% endcode %}
|
||||
|
||||
The fields are:
|
||||
|
||||
* name: The name (basically a comment, but it must be unique)
|
||||
* code: Either 301 (permanent) or 302 (temporary) redirects. May be a number or string.
|
||||
* when: What Cloudflare sometimes calls the "rule expression".
|
||||
* then: The replacement expression.
|
||||
|
||||
NOTE:
|
||||
The features [`CF_REDIRECT`](CF_REDIRECT.md) and [`CF_TEMP_REDIRECT`](CF_TEMP_REDIRECT.md) generate `CF_SINGLE_REDIRECT` if enabled in [`CLOUDFLAREAPI`](../../provider/cloudflareapi.md).
|
|
@ -29,7 +29,8 @@ defined as separate domains via separate [`D()`](D.md) statements, then
|
|||
not `domain.tld`.
|
||||
|
||||
Some operators only act on an apex domain (e.g.
|
||||
[`CF_REDIRECT`](../domain-modifiers/CF_REDIRECT.md) and [`CF_TEMP_REDIRECT`](../domain-modifiers/CF_TEMP_REDIRECT.md)). Using them
|
||||
[`CF_SINGLE_REDIRECT`](../domain-modifiers/CF_SINGLE_REDIRECT.md),
|
||||
[`CF_REDIRECT`](../domain-modifiers/CF_REDIRECT.md), and [`CF_TEMP_REDIRECT`](../domain-modifiers/CF_TEMP_REDIRECT.md)). Using them
|
||||
in a `D_EXTEND` subdomain may not be what you expect.
|
||||
|
||||
{% code title="dnsconfig.js" %}
|
||||
|
|
|
@ -215,7 +215,7 @@ var DSP_CLOUDFLARE = NewDnsProvider("cloudflare", {
|
|||
});
|
||||
```
|
||||
|
||||
New redirects uses the [Single Redirects][https://developers.cloudflare.com/rules/url-forwarding/] product feature. In this mode,
|
||||
New redirects uses the [Single Redirects][https://developers.cloudflare.com/rules/url-forwarding/] product feature. In this mode,
|
||||
`CF_REDIRECT` and `CF_TEMP_REDIRECT` functions generates Single Redirects.
|
||||
|
||||
Enable it using:
|
||||
|
@ -240,7 +240,7 @@ and `manage_single_redirects` to true.
|
|||
|
||||
{% hint style="warning" %}
|
||||
The conversion process only handles a few, very simple, patterns.
|
||||
See `providers/cloudflare/singleredirect_test.go` for a list of patterns
|
||||
See `providers/cloudflare/rtypes/cfsingleredirect/convert_test.go` for a list of patterns
|
||||
supported. Please file bugs if you find problems. PRs welcome!
|
||||
{% endhint %}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/StackExchange/dnscontrol/v4/providers"
|
||||
_ "github.com/StackExchange/dnscontrol/v4/providers/_all"
|
||||
"github.com/StackExchange/dnscontrol/v4/providers/cloudflare"
|
||||
"github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/cfsingleredirect"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
)
|
||||
|
||||
|
@ -60,7 +61,7 @@ func getProvider(t *testing.T) (providers.DNSServiceProvider, string, map[string
|
|||
}
|
||||
|
||||
var metadata json.RawMessage
|
||||
// CLOUDFLAREAPI tests related to CF_REDIRECT/CF_TEMP_REDIRECT
|
||||
// CLOUDFLAREAPI tests related to CLOUDFLAREAPI_SINGLE_REDIRECT/CF_REDIRECT/CF_TEMP_REDIRECT
|
||||
// requires metadata to enable this feature.
|
||||
// In hindsight, I have no idea why this metadata flag is required to
|
||||
// use this feature. Maybe because we didn't have the capabilities
|
||||
|
@ -497,6 +498,19 @@ func cfProxyCNAME(name, target, status string) *models.RecordConfig {
|
|||
return r
|
||||
}
|
||||
|
||||
func cfSingleRedirectEnabled() bool {
|
||||
return ((*enableCFRedirectMode) != "")
|
||||
}
|
||||
|
||||
func cfSingleRedirect(name string, code any, when, then string) *models.RecordConfig {
|
||||
r := makeRec("@", name, "CLOUDFLAREAPI_SINGLE_REDIRECT")
|
||||
err := cfsingleredirect.FromRaw(r, []any{name, code, when, then})
|
||||
if err != nil {
|
||||
panic("Should not happen... cfSingleRedirect")
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func cfWorkerRoute(pattern, target string) *models.RecordConfig {
|
||||
t := fmt.Sprintf("%s,%s", pattern, target)
|
||||
r := makeRec("@", t, "CF_WORKER_ROUTE")
|
||||
|
@ -706,6 +720,9 @@ func tc(desc string, recs ...*models.RecordConfig) *TestCase {
|
|||
var records []*models.RecordConfig
|
||||
var unmanagedItems []*models.UnmanagedConfig
|
||||
for _, r := range recs {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
switch r.Type {
|
||||
case "IGNORE":
|
||||
unmanagedItems = append(unmanagedItems, &models.UnmanagedConfig{
|
||||
|
@ -806,7 +823,7 @@ func makeTests() []*TestGroup {
|
|||
// Only run this test if all these bool flags are true:
|
||||
// alltrue(*enableCFWorkers, *anotherFlag, myBoolValue)
|
||||
// NOTE: You can't mix not() and only()
|
||||
// reset(not("ROUTE53"), only("GCLOUD")), // ERROR!
|
||||
// not("ROUTE53"), only("GCLOUD"), // ERROR!
|
||||
// NOTE: All requires()/not()/only() must appear before any tc().
|
||||
|
||||
// tc()
|
||||
|
@ -1917,11 +1934,21 @@ func makeTests() []*TestGroup {
|
|||
|
||||
testgroup("CF_REDIRECT_CONVERT",
|
||||
only("CLOUDFLAREAPI"),
|
||||
alltrue(cfSingleRedirectEnabled()),
|
||||
tc("start301", cfRedir("cnn.**current-domain-no-trailing**/*", "https://www.cnn.com/$1")),
|
||||
tc("convert302", cfRedirTemp("cnn.**current-domain-no-trailing**/*", "https://www.cnn.com/$1")),
|
||||
tc("convert301", cfRedir("cnn.**current-domain-no-trailing**/*", "https://www.cnn.com/$1")),
|
||||
),
|
||||
|
||||
testgroup("CLOUDFLAREAPI_SINGLE_REDIRECT",
|
||||
only("CLOUDFLAREAPI"),
|
||||
alltrue(cfSingleRedirectEnabled()),
|
||||
tc("start301", cfSingleRedirect(`name1`, `301`, `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("changethen", cfSingleRedirect(`name1`, `302`, `http.host eq "msnbc.slackoverflow.com"`, `concat("https://www.msnbc.com", http.request.uri.path)`)),
|
||||
),
|
||||
|
||||
// CLOUDFLAREAPI: PROXY
|
||||
|
||||
testgroup("CF_PROXY A create",
|
||||
|
|
|
@ -45,6 +45,9 @@ type DomainConfig struct {
|
|||
RegistrarInstance *RegistrarInstance `json:"-"`
|
||||
DNSProviderInstances []*DNSProviderInstance `json:"-"`
|
||||
|
||||
// Raw user-input from dnsconfig.js that will be processed into RecordConfigs later:
|
||||
RawRecords []RawRecordConfig `json:"rawrecords,omitempty"`
|
||||
|
||||
// Pending work to do for each provider. Provider may be a registrar or DSP.
|
||||
pendingCorrectionsMutex sync.Mutex // Protect pendingCorrections*
|
||||
pendingCorrections map[string]([]*Correction) // Work to be done for each provider
|
||||
|
@ -127,7 +130,7 @@ func (dc *DomainConfig) Punycode() error {
|
|||
return err
|
||||
}
|
||||
rec.SetTarget(t)
|
||||
case "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE":
|
||||
case "CLOUDFLAREAPI_SINGLE_REDIRECT", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE":
|
||||
rec.SetTarget(rec.GetTargetField())
|
||||
case "A", "AAAA", "CAA", "DHCID", "DNSKEY", "DS", "HTTPS", "LOC", "NAPTR", "SOA", "SSHFP", "SVCB", "TXT", "TLSA", "AZURE_ALIAS":
|
||||
// Nothing to do.
|
||||
|
|
12
models/rawrecord.go
Normal file
12
models/rawrecord.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package models
|
||||
|
||||
// RawRecordConfig stores the user-input from dnsconfig.js for a DNS
|
||||
// Record. This is later processed (in Go) to become a RecordConfig.
|
||||
// NOTE: Only newer rtypes are processed this way. Eventually the
|
||||
// legacy types will be converted.
|
||||
type RawRecordConfig struct {
|
||||
Type string `json:"type"`
|
||||
Args []any `json:"args,omitempty"`
|
||||
Metas []map[string]any `json:"metas,omitempty"`
|
||||
TTL uint32 `json:"ttl,omitempty"`
|
||||
}
|
|
@ -91,8 +91,8 @@ import (
|
|||
type RecordConfig struct {
|
||||
Type string `json:"type"` // All caps rtype name.
|
||||
Name string `json:"name"` // The short name. See above.
|
||||
NameFQDN string `json:"-"` // Must end with ".$origin". See above.
|
||||
SubDomain string `json:"subdomain,omitempty"`
|
||||
NameFQDN string `json:"-"` // Must end with ".$origin". See above.
|
||||
target string // If a name, must end with "."
|
||||
TTL uint32 `json:"ttl,omitempty"`
|
||||
Metadata map[string]string `json:"meta,omitempty"`
|
||||
|
@ -154,15 +154,15 @@ type CloudflareSingleRedirectConfig struct {
|
|||
//
|
||||
Code int `json:"code,omitempty"` // 301 or 302
|
||||
// PR == PageRule
|
||||
PRDisplay string `json:"pr_display,omitempty"` // How is this displayed to the user
|
||||
PRMatcher string `json:"pr_matcher,omitempty"`
|
||||
PRReplacement string `json:"pr_replacement,omitempty"`
|
||||
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
|
||||
PRWhen string `json:"pr_when,omitempty"`
|
||||
PRThen string `json:"pr_then,omitempty"`
|
||||
PRPriority int `json:"pr_priority,omitempty"` // Really an identifier for the rule.
|
||||
//
|
||||
// SR == SingleRedirect
|
||||
SRDisplay string `json:"sr_display,omitempty"` // How is this displayed to the user
|
||||
SRMatcher string `json:"sr_matcher,omitempty"`
|
||||
SRReplacement string `json:"sr_replacement,omitempty"`
|
||||
SRWhen string `json:"sr_when,omitempty"`
|
||||
SRThen string `json:"sr_then,omitempty"`
|
||||
SRRRulesetID string `json:"sr_rulesetid,omitempty"`
|
||||
SRRRulesetRuleID string `json:"sr_rulesetruleid,omitempty"`
|
||||
}
|
||||
|
@ -196,6 +196,7 @@ func (rc *RecordConfig) UnmarshalJSON(b []byte) error {
|
|||
TTL uint32 `json:"ttl,omitempty"`
|
||||
Metadata map[string]string `json:"meta,omitempty"`
|
||||
Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing.
|
||||
Args []any `json:"args,omitempty"`
|
||||
|
||||
MxPreference uint16 `json:"mxpreference,omitempty"`
|
||||
SrvPriority uint16 `json:"srvpriority,omitempty"`
|
||||
|
@ -612,7 +613,7 @@ func Downcase(recs []*RecordConfig) {
|
|||
// Target is case insensitive. Downcase it.
|
||||
r.target = strings.ToLower(r.target)
|
||||
// BUGFIX(tlim): isn't ALIAS in the wrong case statement?
|
||||
case "A", "CAA", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "DHCID", "IMPORT_TRANSFORM", "LOC", "SSHFP", "TXT":
|
||||
case "A", "CAA", "CLOUDFLAREAPI_SINGLE_REDIRECT", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "DHCID", "IMPORT_TRANSFORM", "LOC", "SSHFP", "TXT":
|
||||
// Do nothing. (IP address or case sensitive target)
|
||||
case "SOA":
|
||||
if r.target != "DEFAULT_NOT_SET." {
|
||||
|
@ -636,7 +637,7 @@ func CanonicalizeTargets(recs []*RecordConfig, origin string) {
|
|||
case "ALIAS", "ANAME", "CNAME", "DNAME", "DS", "DNSKEY", "MX", "NS", "NAPTR", "PTR", "SRV":
|
||||
// Target is a hostname that might be a shortname. Turn it into a FQDN.
|
||||
r.target = dnsutil.AddOrigin(r.target, originFQDN)
|
||||
case "A", "AKAMAICDN", "CAA", "DHCID", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "HTTPS", "IMPORT_TRANSFORM", "LOC", "SSHFP", "SVCB", "TLSA", "TXT":
|
||||
case "A", "AKAMAICDN", "CAA", "DHCID", "CLOUDFLAREAPI_SINGLE_REDIRECT", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "HTTPS", "IMPORT_TRANSFORM", "LOC", "SSHFP", "SVCB", "TLSA", "TXT":
|
||||
// Do nothing.
|
||||
case "SOA":
|
||||
if r.target != "DEFAULT_NOT_SET." {
|
||||
|
|
|
@ -874,6 +874,7 @@ func (api *API) UpdateEntrypointRuleset(ctx context.Context, rc *ResourceContain
|
|||
}
|
||||
|
||||
uri := fmt.Sprintf("/%s/%s/rulesets/phases/%s/entrypoint", rc.Level, rc.Identifier, params.Phase)
|
||||
//fmt.Printf("DEBUG: update: uri=%v\n", uri)
|
||||
res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params)
|
||||
if err != nil {
|
||||
return Ruleset{}, err
|
||||
|
|
|
@ -104,6 +104,7 @@ function newDomain(name, registrar) {
|
|||
registrar: registrar,
|
||||
meta: {},
|
||||
records: [],
|
||||
rawrecords: [],
|
||||
recordsabsent: [],
|
||||
dnsProviders: {},
|
||||
defaultTTL: 0,
|
||||
|
@ -1097,6 +1098,7 @@ function recordBuilder(type, opts) {
|
|||
// Handle D_EXTEND() with subdomains.
|
||||
if (
|
||||
d.subdomain &&
|
||||
record.type != 'CF_SINGLE_REDIRECT' &&
|
||||
record.type != 'CF_REDIRECT' &&
|
||||
record.type != 'CF_TEMP_REDIRECT' &&
|
||||
record.type != 'CF_WORKER_ROUTE'
|
||||
|
@ -1915,3 +1917,73 @@ var DISABLE_REPEATED_DOMAIN_CHECK = { skip_fqdn_check: 'true' };
|
|||
// D("bar.com", ...
|
||||
// A("foo.bar.com", "10.1.1.1", DISABLE_REPEATED_DOMAIN_CHECK),
|
||||
// )
|
||||
|
||||
// ============================================================
|
||||
|
||||
// RTYPES
|
||||
|
||||
// Background:
|
||||
// Old-style commands: Commands built using recordBuild() are the original
|
||||
// style. They all validation and pre-processing here in helpers.js. This
|
||||
// seemed like a good idea at the time, but in hindsight it puts a burden on the
|
||||
// developer to know both Javascript and go.
|
||||
|
||||
// New-style commands: Command built using rawrecordBuilder() are the new style.
|
||||
// They simply pack up the arguments listed in dnsconfig.js and store them in
|
||||
// .rawrecords. This is passed to the Go code, which is responsible for all
|
||||
// validation, pre-processing, etc. The benefit is this minimizes the need for
|
||||
// Javascript knowledge, and allows us to use the testing platform build into
|
||||
// Go.
|
||||
|
||||
function rawrecordBuilder(type) {
|
||||
|
||||
return function () {
|
||||
|
||||
// Copy the raw args:
|
||||
var rawArgs = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
rawArgs.push(arguments[i]);
|
||||
}
|
||||
|
||||
return function (d) {
|
||||
|
||||
var record = {
|
||||
type: type,
|
||||
};
|
||||
|
||||
// Process the args: Functions are executed, objects are assumed to
|
||||
// be meta and stored, strings are assumed to be args and are
|
||||
// stored.
|
||||
// NB(tlim): Allowing for the intermixing of args and meta seems
|
||||
// bad. It might be better to simply preserve the first n items as
|
||||
// args, then assume the rest are metas. That would be more similar
|
||||
// to the old style functions. However at this time I can't think of
|
||||
// a reason this isn't sufficient.
|
||||
var processedArgs = [];
|
||||
var processedMetas = [];
|
||||
for (var i = 0; i < rawArgs.length; i++) {
|
||||
var r = rawArgs[i];
|
||||
if (_.isFunction(r)) {
|
||||
r(record);
|
||||
} else if (_.isObject(r)) {
|
||||
processedMetas.push(r);
|
||||
} else {
|
||||
processedArgs.push(r);
|
||||
}
|
||||
};
|
||||
// Store the processed args.
|
||||
record.args = processedArgs;
|
||||
record.metas = processedMetas;
|
||||
|
||||
// Add this raw record to the list of records.
|
||||
d.rawrecords.push(record);
|
||||
|
||||
return record;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// PLEASE KEEP THIS LIST ALPHABETICAL!
|
||||
|
||||
// CLOUDFLAREAPI:
|
||||
var CF_SINGLE_REDIRECT = rawrecordBuilder('CLOUDFLAREAPI_SINGLE_REDIRECT');
|
||||
|
|
|
@ -86,7 +86,7 @@ func TestParsedFiles(t *testing.T) {
|
|||
as := string(actualJSON)
|
||||
_, _ = es, as
|
||||
// When debugging, leave behind the actual result:
|
||||
//os.WriteFile(expectedFile+".ACTUAL", []byte(as), 0644)
|
||||
os.WriteFile(expectedFile+".ACTUAL", []byte(as), 0644) // Leave behind the actual result:
|
||||
testifyrequire.JSONEqf(t, es, as, "EXPECTING %q = \n```\n%s\n```", expectedFile, as)
|
||||
|
||||
// For each domain, if there is a zone file, test against it:
|
||||
|
|
8
pkg/js/parse_tests/050-cfSingleRedirect.js
Normal file
8
pkg/js/parse_tests/050-cfSingleRedirect.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
D("foo.com","none",
|
||||
A("name1", "1.2.3.4", { meta: "value" } ),
|
||||
CF_SINGLE_REDIRECT("name1", 301, "when1", "then1"),
|
||||
CF_SINGLE_REDIRECT("name2", 302, "when2", "then2"),
|
||||
CF_SINGLE_REDIRECT("name3", "301", "when3", "then3"),
|
||||
CF_SINGLE_REDIRECT("namettl", 302, "whenttl", "thenttl", TTL(999)),
|
||||
CF_SINGLE_REDIRECT("namemeta", 302, "whenmeta", "thenmeta", { metastr: "stringy"}, { metanum: 22 } )
|
||||
);
|
77
pkg/js/parse_tests/050-cfSingleRedirect.json
Normal file
77
pkg/js/parse_tests/050-cfSingleRedirect.json
Normal file
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"registrars": [],
|
||||
"dns_providers": [],
|
||||
"domains": [
|
||||
{
|
||||
"name": "foo.com",
|
||||
"registrar": "none",
|
||||
"dnsProviders": {},
|
||||
"records": [
|
||||
{
|
||||
"type": "A",
|
||||
"name": "name1",
|
||||
"meta": {
|
||||
"meta": "value"
|
||||
},
|
||||
"target": "1.2.3.4"
|
||||
}
|
||||
],
|
||||
"rawrecords": [
|
||||
{
|
||||
"type": "CLOUDFLAREAPI_SINGLE_REDIRECT",
|
||||
"args": [
|
||||
"name1",
|
||||
301,
|
||||
"when1",
|
||||
"then1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "CLOUDFLAREAPI_SINGLE_REDIRECT",
|
||||
"args": [
|
||||
"name2",
|
||||
302,
|
||||
"when2",
|
||||
"then2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "CLOUDFLAREAPI_SINGLE_REDIRECT",
|
||||
"args": [
|
||||
"name3",
|
||||
"301",
|
||||
"when3",
|
||||
"then3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "CLOUDFLAREAPI_SINGLE_REDIRECT",
|
||||
"args": [
|
||||
"namettl",
|
||||
302,
|
||||
"whenttl",
|
||||
"thenttl"
|
||||
],
|
||||
"ttl": 999
|
||||
},
|
||||
{
|
||||
"type": "CLOUDFLAREAPI_SINGLE_REDIRECT",
|
||||
"args": [
|
||||
"namemeta",
|
||||
302,
|
||||
"whenmeta",
|
||||
"thenmeta"
|
||||
],
|
||||
"metas": [
|
||||
{
|
||||
"metastr": "stringy"
|
||||
},
|
||||
{
|
||||
"metanum": 22
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/transform"
|
||||
"github.com/StackExchange/dnscontrol/v4/providers"
|
||||
"github.com/miekg/dns"
|
||||
|
@ -78,7 +79,7 @@ func validateRecordTypes(rec *models.RecordConfig, domain string, pTypes []strin
|
|||
"TXT": true,
|
||||
}
|
||||
_, ok := validTypes[rec.Type]
|
||||
if !ok {
|
||||
if !ok || rtypecontrol.IsValid(rec.Type) {
|
||||
cType := providers.GetCustomRecordType(rec.Type)
|
||||
if cType == nil {
|
||||
return fmt.Errorf("unsupported record type (%v) domain=%v name=%v", rec.Type, domain, rec.GetLabel())
|
||||
|
|
18
pkg/rtypecontrol/rtypecontrol.go
Normal file
18
pkg/rtypecontrol/rtypecontrol.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package rtypecontrol
|
||||
|
||||
var validTypes = map[string]struct{}{}
|
||||
|
||||
func Register(t string) {
|
||||
// Does this already exist?
|
||||
if _, ok := validTypes[t]; ok {
|
||||
panic("rtype %q already registered. Can't register it a second time!")
|
||||
}
|
||||
|
||||
validTypes[t] = struct{}{}
|
||||
|
||||
}
|
||||
|
||||
func IsValid(t string) bool {
|
||||
_, ok := validTypes[t]
|
||||
return ok
|
||||
}
|
43
pkg/rtypecontrol/validate.go
Normal file
43
pkg/rtypecontrol/validate.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package rtypecontrol
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// CheckArgTypes validates that the items in args are of appropriate types. argTypes is a string: "ssi" means the args should be string, string, int.
|
||||
// 's': Valid only if string.
|
||||
// 'i': Valid only if int, float64, or a string that Atoi() can convert to an int.
|
||||
func CheckArgTypes(args []any, argTypes string) error {
|
||||
|
||||
if len(args) != len(argTypes) {
|
||||
return fmt.Errorf("wrong number of arguments. Expected %v, got %v", len(argTypes), len(args))
|
||||
}
|
||||
|
||||
for i, at := range argTypes {
|
||||
arg := args[i]
|
||||
switch at {
|
||||
|
||||
case 'i':
|
||||
if s, ok := arg.(string); ok { // Is this a string-encoded int?
|
||||
ni, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("value %q is type %T, expected INT", arg, arg)
|
||||
}
|
||||
args[i] = ni
|
||||
} else if _, ok := arg.(float64); ok {
|
||||
args[i] = int(arg.(float64))
|
||||
} else if _, ok := arg.(int); !ok {
|
||||
return fmt.Errorf("value %q is type %T, expected INT", arg, arg)
|
||||
}
|
||||
|
||||
case 's':
|
||||
if _, ok := arg.(string); !ok {
|
||||
return fmt.Errorf("value %q is type %T, expected STRING", arg, arg)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
51
pkg/rtypecontrol/validate_test.go
Normal file
51
pkg/rtypecontrol/validate_test.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package rtypecontrol
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dataArgs []any
|
||||
dataRule string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "string",
|
||||
dataArgs: []any{"one"},
|
||||
dataRule: "s",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "int to string",
|
||||
dataArgs: []any{100},
|
||||
dataRule: "s",
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "int",
|
||||
dataArgs: []any{int(1)},
|
||||
dataRule: "i",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "string to int",
|
||||
dataArgs: []any{"111"},
|
||||
dataRule: "i",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "txt to int",
|
||||
dataArgs: []any{"one"},
|
||||
dataRule: "i",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := CheckArgTypes(tt.dataArgs, tt.dataRule); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateArgs() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
61
pkg/rtypes/postprocess.go
Normal file
61
pkg/rtypes/postprocess.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package rtypes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/cfsingleredirect"
|
||||
)
|
||||
|
||||
func PostProcess(domains []*models.DomainConfig) error {
|
||||
|
||||
var err error
|
||||
|
||||
for _, dc := range domains {
|
||||
fmt.Printf("DOMAIN: %d %s\n", len(dc.Records), dc.Name)
|
||||
|
||||
for _, rawRec := range dc.RawRecords {
|
||||
rec := &models.RecordConfig{
|
||||
Type: rawRec.Type,
|
||||
TTL: rawRec.TTL,
|
||||
Name: rawRec.Args[0].(string),
|
||||
Metadata: map[string]string{},
|
||||
}
|
||||
|
||||
// Copy the metadata (convert everything to string)
|
||||
for _, m := range rawRec.Metas {
|
||||
for mk, mv := range m {
|
||||
if v, ok := mv.(string); ok {
|
||||
rec.Metadata[mk] = v // Already a string. No new malloc.
|
||||
} else {
|
||||
rec.Metadata[mk] = fmt.Sprintf("%v", mv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call the proper initialize function.
|
||||
// TODO(tlim): Good candiate for an interface or a lookup table.
|
||||
switch rawRec.Type {
|
||||
|
||||
case "CLOUDFLAREAPI_SINGLE_REDIRECT":
|
||||
rec.Name = rawRec.Args[0].(string)
|
||||
err = cfsingleredirect.FromRaw(rec, rawRec.Args)
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("unknown rawrec type=%q", rawRec.Type)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s (%q, %q) record error: %w", rawRec.Type, rec.Name, dc.Name, err)
|
||||
}
|
||||
|
||||
// Free memeory:
|
||||
clear(rawRec.Args)
|
||||
rawRec.Args = nil
|
||||
|
||||
dc.Records = append(dc.Records, rec)
|
||||
}
|
||||
dc.RawRecords = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/transform"
|
||||
"github.com/StackExchange/dnscontrol/v4/providers"
|
||||
"github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/cfsingleredirect"
|
||||
"github.com/cloudflare/cloudflare-go"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
@ -68,6 +69,8 @@ func init() {
|
|||
RecordAuditor: AuditRecords,
|
||||
}
|
||||
providers.RegisterDomainServiceProviderType("CLOUDFLAREAPI", fns, features)
|
||||
providers.RegisterCustomRecordType("rtype", "CLOUDFLAREAPI", "")
|
||||
//providers.RegisterCustomRecordType("CF_SINGLE_REDIRECT", "CLOUDFLAREAPI", "")
|
||||
providers.RegisterCustomRecordType("CF_REDIRECT", "CLOUDFLAREAPI", "")
|
||||
providers.RegisterCustomRecordType("CF_TEMP_REDIRECT", "CLOUDFLAREAPI", "")
|
||||
providers.RegisterCustomRecordType("CF_WORKER_ROUTE", "CLOUDFLAREAPI", "")
|
||||
|
@ -341,6 +344,9 @@ func (c *cloudflareProvider) mkCreateCorrection(newrec *models.RecordConfig, dom
|
|||
F: func() error { return c.createWorkerRoute(domainID, newrec.GetTargetField()) },
|
||||
}}
|
||||
case "CLOUDFLAREAPI_SINGLE_REDIRECT":
|
||||
//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{{
|
||||
Msg: msg,
|
||||
F: func() error {
|
||||
|
@ -557,15 +563,13 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error {
|
|||
}
|
||||
}
|
||||
|
||||
// CF_REDIRECT record types. Encode target as $FROM,$TO,$PRIO,$CODE
|
||||
// CF_REDIRECT record types. Encode target as
|
||||
// $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 !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")
|
||||
}
|
||||
// parts := strings.Split(rec.GetTargetField(), ",")
|
||||
// if len(parts) != 2 {
|
||||
// return fmt.Errorf("invalid data specified for cloudflare redirect record")
|
||||
// }
|
||||
code := 301
|
||||
if rec.Type == "CF_TEMP_REDIRECT" {
|
||||
code = 302
|
||||
|
@ -574,7 +578,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error {
|
|||
if c.manageRedirects && !c.manageSingleRedirects {
|
||||
// Old-Style only. Convert this record to PAGE_RULE.
|
||||
//printer.Printf("DEBUG: prepro() target=%q\n", rec.GetTargetField())
|
||||
sr, err := newCfsrFromUserInput(rec.GetTargetField(), code, currentPrPrio)
|
||||
sr, err := cfsingleredirect.FromUserInput(rec.GetTargetField(), code, currentPrPrio)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -582,7 +586,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error {
|
|||
currentPrPrio++
|
||||
} else if !c.manageRedirects && c.manageSingleRedirects {
|
||||
// New-Style only. Convert this record to a CLOUDFLAREAPI_SINGLE_REDIRECT.
|
||||
sr, err := newCfsrFromUserInput(rec.GetTargetField(), code, currentPrPrio)
|
||||
sr, err := cfsingleredirect.FromUserInput(rec.GetTargetField(), code, currentPrPrio)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -602,7 +606,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error {
|
|||
}
|
||||
|
||||
// The copy becomes the CF SingleRedirect
|
||||
sr, err := newCfsrFromUserInput(target, code, currentPrPrio)
|
||||
sr, err := cfsingleredirect.FromUserInput(target, code, currentPrPrio)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -615,7 +619,7 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error {
|
|||
dc.Records = append(dc.Records, newRec)
|
||||
|
||||
// The original becomes the PAGE_RULE:
|
||||
sr, err = newCfsrFromUserInput(target, code, currentPrPrio)
|
||||
sr, err = cfsingleredirect.FromUserInput(target, code, currentPrPrio)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -624,10 +628,15 @@ func (c *cloudflareProvider) preprocessConfig(dc *models.DomainConfig) error {
|
|||
|
||||
}
|
||||
|
||||
}
|
||||
} else if rec.Type == "CLOUDFLAREAPI_SINGLE_REDIRECT" {
|
||||
// CLOUDFLAREAPI_SINGLE_REDIRECT record types.
|
||||
if !c.manageSingleRedirects {
|
||||
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.
|
||||
|
||||
// CF_WORKER_ROUTE record types. Encode target as $PATTERN,$SCRIPT
|
||||
if rec.Type == "CF_WORKER_ROUTE" {
|
||||
} else if rec.Type == "CF_WORKER_ROUTE" {
|
||||
// CF_WORKER_ROUTE record types. Encode target as $PATTERN,$SCRIPT
|
||||
parts := strings.Split(rec.GetTargetField(), ",")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid data specified for cloudflare worker record")
|
||||
|
@ -675,7 +684,7 @@ func fixSingleRedirect(rc *models.RecordConfig, sr *models.CloudflareSingleRedir
|
|||
rc.SetTarget(sr.SRDisplay)
|
||||
rc.CloudflareRedirect = sr
|
||||
|
||||
err := addNewStyleFields(sr)
|
||||
err := cfsingleredirect.AddNewStyleFields(sr)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"golang.org/x/net/idna"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/StackExchange/dnscontrol/v4/providers/cloudflare/rtypes/cfsingleredirect"
|
||||
"github.com/cloudflare/cloudflare-go"
|
||||
)
|
||||
|
||||
|
@ -303,10 +304,10 @@ func (c *cloudflareProvider) getSingleRedirects(id string, domain string) ([]*mo
|
|||
r.SetLabel("@", domain)
|
||||
|
||||
// Extract the valuables from the rule, use it to make the sr:
|
||||
srMatcher := pr.Expression
|
||||
srReplacement := pr.ActionParameters.FromValue.TargetURL.Expression
|
||||
srWhen := pr.Expression
|
||||
srThen := pr.ActionParameters.FromValue.TargetURL.Expression
|
||||
code := int(pr.ActionParameters.FromValue.StatusCode)
|
||||
sr := newCfsrFromAPIData(srMatcher, srReplacement, code)
|
||||
sr := cfsingleredirect.FromAPIData(srWhen, srThen, code)
|
||||
//sr.SRRRuleList = rulelist
|
||||
//printer.Printf("DEBUG: DESCRIPTION = %v\n", pr.Description)
|
||||
sr.SRDisplay = pr.Description
|
||||
|
@ -336,15 +337,15 @@ func (c *cloudflareProvider) createSingleRedirect(domainID string, cfr models.Cl
|
|||
newSingleRedirect := cloudflare.UpdateEntrypointRulesetParams{}
|
||||
|
||||
// Preserve query string if there isn't one in the replacement.
|
||||
preserveQueryString := !strings.Contains(cfr.SRReplacement, "?")
|
||||
preserveQueryString := !strings.Contains(cfr.SRThen, "?")
|
||||
|
||||
newSingleRedirectRulesActionParameters.FromValue = &cloudflare.RulesetRuleActionParametersFromValue{}
|
||||
// Redirect status code
|
||||
newSingleRedirectRulesActionParameters.FromValue.StatusCode = uint16(cfr.Code)
|
||||
// Incoming request expression
|
||||
newSingleRedirectRules[0].Expression = cfr.SRMatcher
|
||||
newSingleRedirectRules[0].Expression = cfr.SRWhen
|
||||
// Redirect expression
|
||||
newSingleRedirectRulesActionParameters.FromValue.TargetURL.Expression = cfr.SRReplacement
|
||||
newSingleRedirectRulesActionParameters.FromValue.TargetURL.Expression = cfr.SRThen
|
||||
// Redirect name
|
||||
newSingleRedirectRules[0].Description = cfr.SRDisplay
|
||||
// Rule action, should always be redirect in this case
|
||||
|
@ -449,7 +450,7 @@ func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.R
|
|||
code)
|
||||
r.SetTarget(raw)
|
||||
|
||||
cr, err := newCfsrFromUserInput(raw, code, pr.Priority)
|
||||
cr, err := cfsingleredirect.FromUserInput(raw, code, pr.Priority)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -485,18 +486,18 @@ func (c *cloudflareProvider) createPageRule(domainID string, cfr models.Cloudfla
|
|||
// printer.Printf("DEBUG: pr.PageRule code = %v\n", code)
|
||||
priority := cfr.PRPriority
|
||||
code := cfr.Code
|
||||
matcher := cfr.PRMatcher
|
||||
replacement := cfr.PRReplacement
|
||||
prWhen := cfr.PRWhen
|
||||
prThen := cfr.PRThen
|
||||
pr := cloudflare.PageRule{
|
||||
Status: "active",
|
||||
Priority: priority,
|
||||
Targets: []cloudflare.PageRuleTarget{
|
||||
{Target: "url", Constraint: pageRuleConstraint{Operator: "matches", Value: matcher}},
|
||||
{Target: "url", Constraint: pageRuleConstraint{Operator: "matches", Value: prWhen}},
|
||||
},
|
||||
Actions: []cloudflare.PageRuleAction{
|
||||
{ID: "forwarding_url", Value: &pageRuleFwdInfo{
|
||||
StatusCode: code,
|
||||
URL: replacement,
|
||||
URL: prThen,
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package cfsingleredirect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/rtypecontrol"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rtypecontrol.Register("CLOUDFLAREAPI_SINGLE_REDIRECT")
|
||||
}
|
||||
|
||||
func FromRaw(rc *models.RecordConfig, items []any) error {
|
||||
|
||||
var err error
|
||||
|
||||
// Validate types.
|
||||
if err := rtypecontrol.CheckArgTypes(items, "siss"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unpack the args:
|
||||
var name, when, then string
|
||||
var code int
|
||||
|
||||
name = items[0].(string)
|
||||
|
||||
ucode := items[1]
|
||||
switch v := ucode.(type) {
|
||||
case int:
|
||||
code = v
|
||||
case float64:
|
||||
code = int(v)
|
||||
case string:
|
||||
code, err = strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("code %q unexpected type %T", ucode, v)
|
||||
}
|
||||
|
||||
if code != 301 && code != 302 {
|
||||
return fmt.Errorf("code (%03d) is not 301 or 302", code)
|
||||
}
|
||||
|
||||
when, then = items[2].(string), items[3].(string)
|
||||
|
||||
rc.Name = name
|
||||
rc.CloudflareRedirect = FromAPIData(when, then, code)
|
||||
rc.SetTarget(rc.CloudflareRedirect.SRDisplay)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package cloudflare
|
||||
package cfsingleredirect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
)
|
||||
|
||||
func newCfsrFromUserInput(target string, code int, priority int) (*models.CloudflareSingleRedirectConfig, error) {
|
||||
func FromUserInput(target string, code int, priority int) (*models.CloudflareSingleRedirectConfig, error) {
|
||||
// target: matcher,replacement,priority,code
|
||||
// target: cable.slackoverflow.com/*,https://change.cnn.com/$1,1,302
|
||||
|
||||
|
@ -19,68 +19,54 @@ func newCfsrFromUserInput(target string, code int, priority int) (*models.Cloudf
|
|||
parts := strings.Split(target, ",")
|
||||
//printer.Printf("DEBUG: cfsrFromOldStyle: parts=%v\n", parts)
|
||||
r.PRDisplay = fmt.Sprintf("%s,%d,%03d", target, priority, code)
|
||||
r.PRMatcher = parts[0]
|
||||
r.PRReplacement = parts[1]
|
||||
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 {
|
||||
if err := AddNewStyleFields(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func newCfsrFromAPIData(sm, sr string, code int) *models.CloudflareSingleRedirectConfig {
|
||||
r := &models.CloudflareSingleRedirectConfig{
|
||||
PRMatcher: "UNKNOWABLE",
|
||||
PRReplacement: "UNKNOWABLE",
|
||||
//PRPriority: 0,
|
||||
Code: code,
|
||||
SRMatcher: sm,
|
||||
SRReplacement: sr,
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// addNewStyleFields takes a PAGE_RULE-style target and populates the CFSRC.
|
||||
func addNewStyleFields(sr *models.CloudflareSingleRedirectConfig) error {
|
||||
// AddNewStyleFields takes a PAGE_RULE-style target and populates the CFSRC.
|
||||
func AddNewStyleFields(sr *models.CloudflareSingleRedirectConfig) error {
|
||||
|
||||
// Extract the fields we're reading from:
|
||||
prMatcher := sr.PRMatcher
|
||||
prReplacement := sr.PRReplacement
|
||||
prWhen := sr.PRWhen
|
||||
prThen := sr.PRThen
|
||||
code := sr.Code
|
||||
|
||||
// Convert old-style patterns to new-style rules:
|
||||
srMatcher, srReplacement, err := makeRuleFromPattern(prMatcher, prReplacement, code != 301)
|
||||
srWhen, srThen, err := makeRuleFromPattern(prWhen, prThen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
display := fmt.Sprintf(`%s,%s,%d,%03d matcher=%q replacement=%q`,
|
||||
prMatcher, prReplacement,
|
||||
display := fmt.Sprintf(`%s,%s,%d,%03d matcher=%s replacement=%s`,
|
||||
prWhen, prThen,
|
||||
sr.PRPriority, code,
|
||||
srMatcher, srReplacement,
|
||||
srWhen, srThen,
|
||||
)
|
||||
|
||||
// Store the results in the fields we're writing to:
|
||||
sr.SRMatcher = srMatcher
|
||||
sr.SRReplacement = srReplacement
|
||||
sr.SRWhen = srWhen
|
||||
sr.SRThen = srThen
|
||||
sr.SRDisplay = display
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeRuleFromPattern compile old-style patterns and replacements into new-style rules and expressions.
|
||||
func makeRuleFromPattern(pattern, replacement string, temporary bool) (string, string, error) {
|
||||
func makeRuleFromPattern(pattern, replacement string) (string, string, error) {
|
||||
|
||||
_ = temporary // Prevents error due to this variable not (yet) being used
|
||||
|
||||
var matcher, expr string
|
||||
var srWhen, srThen string
|
||||
var err error
|
||||
|
||||
var host, path string
|
||||
var phost, ppath string
|
||||
origPattern := pattern
|
||||
pattern, host, path, err = normalizeURL(pattern)
|
||||
pattern, phost, ppath, err = normalizeURL(pattern)
|
||||
_ = pattern
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
|
@ -96,38 +82,41 @@ func makeRuleFromPattern(pattern, replacement string, temporary bool) (string, s
|
|||
// TODO(tlim): This could be a lot faster by not repeating itself so much.
|
||||
// However I want to get it working before it is optimized.
|
||||
|
||||
// pattern -> matcher
|
||||
// "pr" is Page Rule (old style)
|
||||
// "sr" is Static Rule (new style)
|
||||
// prWhen + prThen is the old-style matching pattern and replacement pattern.
|
||||
// srWhen + srThen is the new-style matching rule and replacement expression.
|
||||
|
||||
if !strings.Contains(host, `*`) && (path == `/` || path == "") {
|
||||
if !strings.Contains(phost, `*`) && (ppath == `/` || ppath == "") {
|
||||
// https://i.sstatic.net/ (No Wildcards)
|
||||
matcher = fmt.Sprintf(`http.host eq "%s" and http.request.uri.path eq "%s"`, host, "/")
|
||||
srWhen = fmt.Sprintf(`http.host eq "%s" and http.request.uri.path eq "%s"`, phost, "/")
|
||||
|
||||
} else if !strings.Contains(host, `*`) && (path == `/*`) {
|
||||
} else if !strings.Contains(phost, `*`) && (ppath == `/*`) {
|
||||
// https://i.stack.imgur.com/*
|
||||
matcher = fmt.Sprintf(`http.host eq "%s"`, host)
|
||||
srWhen = fmt.Sprintf(`http.host eq "%s"`, phost)
|
||||
|
||||
} else if !strings.Contains(host, `*`) && !strings.Contains(path, "*") {
|
||||
} else if !strings.Contains(phost, `*`) && !strings.Contains(ppath, "*") {
|
||||
// https://insights.stackoverflow.com/trends
|
||||
matcher = fmt.Sprintf(`http.host eq "%s" and http.request.uri.path eq "%s"`, host, path)
|
||||
srWhen = fmt.Sprintf(`http.host eq "%s" and http.request.uri.path eq "%s"`, phost, ppath)
|
||||
|
||||
} else if host[0] == '*' && strings.Count(host, `*`) == 1 && !strings.Contains(path, "*") {
|
||||
} else if phost[0] == '*' && strings.Count(phost, `*`) == 1 && !strings.Contains(ppath, "*") {
|
||||
// *stackoverflow.careers/ (wildcard at beginning only)
|
||||
matcher = fmt.Sprintf(`( http.host eq "%s" or ends_with(http.host, ".%s") ) and http.request.uri.path eq "%s"`, host[1:], host[1:], path)
|
||||
srWhen = fmt.Sprintf(`( http.host eq "%s" or ends_with(http.host, ".%s") ) and http.request.uri.path eq "%s"`, phost[1:], phost[1:], ppath)
|
||||
|
||||
} else if host[0] == '*' && strings.Count(host, `*`) == 1 && path == "/*" {
|
||||
} else if phost[0] == '*' && strings.Count(phost, `*`) == 1 && ppath == "/*" {
|
||||
// *stackoverflow.careers/* (wildcard at beginning and end)
|
||||
matcher = fmt.Sprintf(`http.host eq "%s" or ends_with(http.host, ".%s")`, host[1:], host[1:])
|
||||
srWhen = fmt.Sprintf(`http.host eq "%s" or ends_with(http.host, ".%s")`, phost[1:], phost[1:])
|
||||
|
||||
} else if strings.Contains(host, `*`) && path == "/*" {
|
||||
} else if strings.Contains(phost, `*`) && ppath == "/*" {
|
||||
// meta.*yodeya.com/* (wildcard in host)
|
||||
h := simpleGlobToRegex(host)
|
||||
matcher = fmt.Sprintf(`http.host matches r###"%s"###`, h)
|
||||
h := simpleGlobToRegex(phost)
|
||||
srWhen = fmt.Sprintf(`http.host matches r###"%s"###`, h)
|
||||
|
||||
} else if !strings.Contains(host, `*`) && strings.Count(path, `*`) == 1 && strings.HasSuffix(path, "*") {
|
||||
} else if !strings.Contains(phost, `*`) && strings.Count(ppath, `*`) == 1 && strings.HasSuffix(ppath, "*") {
|
||||
// domain.tld/.well-known* (wildcard in path)
|
||||
matcher = fmt.Sprintf(`(starts_with(http.request.uri.path, "%s") and http.host eq "%s")`,
|
||||
path[0:len(path)-1],
|
||||
host)
|
||||
srWhen = fmt.Sprintf(`(starts_with(http.request.uri.path, "%s") and http.host eq "%s")`,
|
||||
ppath[0:len(ppath)-1],
|
||||
phost)
|
||||
|
||||
}
|
||||
|
||||
|
@ -135,40 +124,51 @@ func makeRuleFromPattern(pattern, replacement string, temporary bool) (string, s
|
|||
|
||||
if !strings.Contains(replacement, `$`) {
|
||||
// https://stackexchange.com/ (no substitutions)
|
||||
expr = fmt.Sprintf(`concat("%s", "")`, replacement)
|
||||
srThen = fmt.Sprintf(`concat("%s", "")`, replacement)
|
||||
|
||||
} else if host[0] == '*' && strings.Count(host, `*`) == 1 && strings.Count(replacement, `$`) == 1 && len(rpath) > 3 && strings.HasSuffix(rpath, "/$2") {
|
||||
} else if phost[0] == '*' && strings.Count(phost, `*`) == 1 && strings.Count(replacement, `$`) == 1 && len(rpath) > 3 && strings.HasSuffix(rpath, "/$2") {
|
||||
// *stackoverflowenterprise.com/* -> https://www.stackoverflowbusiness.com/enterprise/$2
|
||||
expr = fmt.Sprintf(`concat("https://%s", "%s", http.request.uri.path)`,
|
||||
srThen = fmt.Sprintf(`concat("https://%s", "%s", http.request.uri.path)`,
|
||||
rhost,
|
||||
rpath[0:len(rpath)-3],
|
||||
)
|
||||
|
||||
} else if phost[0] == '*' && strings.Count(phost, `*`) == 1 && strings.Count(replacement, `$`) == 1 && len(rpath) > 3 && strings.HasSuffix(rpath, "/$2") {
|
||||
// *stackoverflowenterprise.com/* -> https://www.stackoverflowbusiness.com/enterprise/$2
|
||||
srThen = fmt.Sprintf(`concat("https://%s", "%s", http.request.uri.path)`,
|
||||
rhost,
|
||||
rpath[0:len(rpath)-3],
|
||||
)
|
||||
|
||||
} else if strings.Count(replacement, `$`) == 1 && rpath == `/$1` {
|
||||
// https://i.sstatic.net/$1 ($1 at end)
|
||||
expr = fmt.Sprintf(`concat("https://%s", http.request.uri.path)`, rhost)
|
||||
srThen = fmt.Sprintf(`concat("https://%s", http.request.uri.path)`, rhost)
|
||||
|
||||
} else if strings.Count(host, `*`) == 1 && strings.Count(path, `*`) == 1 &&
|
||||
} else if strings.Count(phost, `*`) == 1 && strings.Count(ppath, `*`) == 1 &&
|
||||
strings.Count(replacement, `$`) == 1 && strings.HasSuffix(rpath, `/$2`) {
|
||||
// https://careers.stackoverflow.com/$2
|
||||
expr = fmt.Sprintf(`concat("https://%s", http.request.uri.path)`, rhost)
|
||||
srThen = fmt.Sprintf(`concat("https://%s", http.request.uri.path)`, rhost)
|
||||
|
||||
} else if strings.Count(replacement, `$`) == 1 && strings.HasSuffix(replacement, `$1`) {
|
||||
// https://social.domain.tld/.well-known$1
|
||||
expr = fmt.Sprintf(`concat("https://%s", http.request.uri.path)`, rhost)
|
||||
srThen = fmt.Sprintf(`concat("https://%s", http.request.uri.path)`, rhost)
|
||||
|
||||
} else if strings.Count(replacement, `$`) == 1 && strings.HasSuffix(replacement, `$1`) {
|
||||
// https://social.domain.tld/.well-known$1
|
||||
srThen = fmt.Sprintf(`concat("https://%s", http.request.uri.path)`, rhost)
|
||||
|
||||
}
|
||||
|
||||
// Not implemented
|
||||
|
||||
if matcher == "" {
|
||||
if srWhen == "" {
|
||||
return "", "", fmt.Errorf("conversion not implemented for pattern: %s", origPattern)
|
||||
}
|
||||
if expr == "" {
|
||||
if srThen == "" {
|
||||
return "", "", fmt.Errorf("conversion not implemented for replacement: %s", origReplacement)
|
||||
}
|
||||
|
||||
return matcher, expr, nil
|
||||
return srWhen, srThen, nil
|
||||
}
|
||||
|
||||
// normalizeURL turns foo.com into https://foo.com and replaces HTTP with HTTPS.
|
|
@ -1,4 +1,4 @@
|
|||
package cloudflare
|
||||
package cfsingleredirect
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
@ -224,7 +224,7 @@ func Test_makeSingleDirectRule(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotMatch, gotExpr, err := makeRuleFromPattern(tt.pattern, tt.replace, true)
|
||||
gotMatch, gotExpr, err := makeRuleFromPattern(tt.pattern, tt.replace)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("makeSingleDirectRule() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
19
providers/cloudflare/rtypes/cfsingleredirect/native.go
Normal file
19
providers/cloudflare/rtypes/cfsingleredirect/native.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package cfsingleredirect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
)
|
||||
|
||||
func FromAPIData(sm, sr string, code int) *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
|
||||
}
|
Loading…
Reference in a new issue