CLOUDFLAREAPI: Add CF_SINGLE_REDIRECT to manage "dynamic single" redirects (#3035)

This commit is contained in:
Tom Limoncelli 2024-07-08 12:38:38 -04:00 committed by GitHub
parent 937c0dc46c
commit 088306883d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 652 additions and 107 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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).

View file

@ -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" %}

View file

@ -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 %}

View file

@ -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",

View file

@ -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
View 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"`
}

View file

@ -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.
SubDomain string `json:"subdomain,omitempty"`
NameFQDN string `json:"-"` // Must end with ".$origin". See above.
SubDomain string `json:"subdomain,omitempty"`
target string // If a name, must end with "."
TTL uint32 `json:"ttl,omitempty"`
Metadata map[string]string `json:"meta,omitempty"`
@ -155,14 +155,14 @@ 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"`
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." {

View file

@ -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

View file

@ -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');

View file

@ -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:

View 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 } )
);

View 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
}
]
}
]
}
]
}

View file

@ -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())

View 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
}

View 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
}

View 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
View 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
}

View file

@ -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.
} else if rec.Type == "CF_WORKER_ROUTE" {
// CF_WORKER_ROUTE record types. Encode target as $PATTERN,$SCRIPT
if rec.Type == "CF_WORKER_ROUTE" {
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
}

View file

@ -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,
}},
},
}

View file

@ -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
}

View file

@ -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.

View file

@ -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

View 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
}