dnscontrol/models/cloudflare_convert.go
2025-01-10 10:43:16 -05:00

175 lines
6.3 KiB
Go

package models
import (
"fmt"
"net"
"net/url"
"strings"
)
// TranscodePRtoSR takes a PAGE_RULE record, stores transcoded versions of the fields, and makes the record a CLOUDFLAREAPI_SINGLE_REDDIRECT.
func TranscodePRtoSR(rec *RecordConfig) error {
rec.Type = "CF_SINGLE_REDIRECT" // This record is now a CF_SINGLE_REDIRECT
// Extract the fields we're reading from:
sr := rec.AsCFSINGLEREDIRECT()
code := sr.Code
prWhen := sr.PRWhen
prThen := sr.PRThen
srName := sr.PRDisplay
// Convert old-style patterns to new-style rules:
srWhen, srThen, err := makeRuleFromPattern(prWhen, prThen)
if err != nil {
return err
}
// Fix the RecordConfig
return makeSingleRedirectFromConvert(rec,
sr.PRPriority,
prWhen, prThen,
code,
srName, srWhen, srThen)
}
// makeRuleFromPattern compile old-style patterns and replacements into new-style rules and expressions.
func makeRuleFromPattern(pattern, replacement string) (string, string, error) {
var srWhen, srThen string
var err error
var phost, ppath string
origPattern := pattern
pattern, phost, ppath, err = normalizeURL(pattern)
_ = pattern
if err != nil {
return "", "", err
}
var rhost, rpath string
origReplacement := replacement
replacement, rhost, rpath, err = normalizeURL(replacement)
_ = rpath
if err != nil {
return "", "", err
}
// 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.
// "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(phost, `*`) && (ppath == `/` || ppath == "") {
// https://i.sstatic.net/ (No Wildcards)
srWhen = fmt.Sprintf(`http.host eq "%s" and http.request.uri.path eq "%s"`, phost, "/")
} else if !strings.Contains(phost, `*`) && (ppath == `/*`) {
// https://i.stack.imgur.com/*
srWhen = fmt.Sprintf(`http.host eq "%s"`, phost)
} else if !strings.Contains(phost, `*`) && !strings.Contains(ppath, "*") {
// https://insights.stackoverflow.com/trends
srWhen = fmt.Sprintf(`http.host eq "%s" and http.request.uri.path eq "%s"`, phost, ppath)
} else if phost[0] == '*' && strings.Count(phost, `*`) == 1 && !strings.Contains(ppath, "*") {
// *stackoverflow.careers/ (wildcard at beginning only)
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 phost[0] == '*' && strings.Count(phost, `*`) == 1 && ppath == "/*" {
// *stackoverflow.careers/* (wildcard at beginning and end)
srWhen = fmt.Sprintf(`http.host eq "%s" or ends_with(http.host, ".%s")`, phost[1:], phost[1:])
} else if strings.Contains(phost, `*`) && ppath == "/*" {
// meta.*yodeya.com/* (wildcard in host)
h := simpleGlobToRegex(phost)
srWhen = fmt.Sprintf(`http.host matches r###"%s"###`, h)
} else if !strings.Contains(phost, `*`) && strings.Count(ppath, `*`) == 1 && strings.HasSuffix(ppath, "*") {
// domain.tld/.well-known* (wildcard in path)
srWhen = fmt.Sprintf(`(starts_with(http.request.uri.path, "%s") and http.host eq "%s")`,
ppath[0:len(ppath)-1],
phost)
}
// replacement
if !strings.Contains(replacement, `$`) {
// https://stackexchange.com/ (no substitutions)
srThen = fmt.Sprintf(`concat("%s", "")`, replacement)
} 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 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)
srThen = fmt.Sprintf(`concat("https://%s", http.request.uri.path)`, rhost)
} else if strings.Count(phost, `*`) == 1 && strings.Count(ppath, `*`) == 1 &&
strings.Count(replacement, `$`) == 1 && strings.HasSuffix(rpath, `/$2`) {
// https://careers.stackoverflow.com/$2
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)
} 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 srWhen == "" {
return "", "", fmt.Errorf("conversion not implemented for pattern: %s", origPattern)
}
if srThen == "" {
return "", "", fmt.Errorf("conversion not implemented for replacement: %s", origReplacement)
}
return srWhen, srThen, nil
}
// normalizeURL turns foo.com into https://foo.com and replaces HTTP with HTTPS.
// It also returns an error if there is a port specified (like :8080)
func normalizeURL(s string) (string, string, string, error) {
orig := s
if strings.HasPrefix(s, `http://`) {
s = "https://" + s[7:]
} else if !strings.HasPrefix(s, `https://`) {
s = `https://` + s
}
// Make sure it parses.
u, err := url.Parse(s)
if err != nil {
return "", "", "", err
}
// Make sure it doesn't have a port (https://example.com:8080)
_, port, _ := net.SplitHostPort(u.Host)
if port != "" {
return "", "", "", fmt.Errorf("unimplemented port: %q", orig)
}
return s, u.Host, u.Path, nil
}
// simpleGlobToRegex translates very simple Glob patterns into regexp-compatible expressions.
// It only handles `.` and `*` currently. See singleredirect_test.go for supported patterns.
func simpleGlobToRegex(g string) string {
if g == "" {
return `.*`
}
if !strings.HasSuffix(g, "*") {
g = g + `$`
}
if !strings.HasPrefix(g, "*") {
g = `^` + g
}
g = strings.ReplaceAll(g, `.`, `\.`)
g = strings.ReplaceAll(g, `*`, `.*`)
return g
}