Merge branch 'main' into tlim_linting

This commit is contained in:
Thomas Limoncelli 2025-12-03 20:34:25 -05:00
commit e12515b6d2
No known key found for this signature in database
15 changed files with 315 additions and 173 deletions

View file

@ -1773,6 +1773,13 @@ declare function IGNORE(labelSpec: string, typeSpec?: string, targetSpec?: strin
*
* ## Caveats
*
* ### One per domain
*
* Only one `IGNORE_EXTERNAL_DNS()` should be used per domain. If you call it multiple
* times, the last prefix wins. If you have multiple external-dns instances with
* different prefixes managing the same zone, use `IGNORE()` patterns for additional
* prefixes.
*
* ### TXT Registry Format
*
* This feature relies on external-dns's [TXT registry](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/registry/txt.md),

View file

@ -22,13 +22,18 @@ Example:
{
"bind": {
"TYPE": "BIND",
"directory": "myzones",
"filenameformat": "%U.zone"
"directory": "myzones"
}
}
```
{% endcode %}
As of v4.2.0 `dnscontrol push` will create subdirectories along the path to
the filename. This includes both the portion of the path created by the
`directory` setting and the `filenameformat` setting. For security reasons, the
automatic creation of subdirectories is disabled if `dnscontrol` is running as
root. (Running DNSControl as root is not recommended in general.)
## Meta configuration
This provider accepts some optional metadata in the `NewDnsProvider()` call.
@ -85,43 +90,59 @@ DNSControl does not handle special serial number math such as "looping through z
# filenameformat
The `filenameformat` parameter specifies the file name to be used when
writing the zone file. The default (`%U.zone`) is acceptable in most cases: the
writing the zone file. The default (`%c.zone`) is acceptable in most cases: the
file name is the name as specified in the `D()` function plus ".zone".
The filenameformat is a string with a few printf-like `%` verbs:
* The domain name without tag (the `example.com` part of `example.com!tag`):
* `%D` as specified in `D()` (no IDN conversion, but downcased)
* `%I` converted to IDN/Punycode (`xn--...`) and downcased.
* `%N` converted to Unicode (downcased first)
* `%T` the split horizon tag, or "" (the `tag` part of `example.com!tag`)
* `%?x` this returns `x` if the split horizon tag is non-null, otherwise nothing. `x` can be any printable but is usually `!`.
* `%U` short for "%I%?!%T". This is the universal, canonical, name for the domain used for comparisons within DNSControl. This is best for filenames which is why it is used in the default.
* `%%` `%`
* ordinary characters (not `%`) are copied unchanged to the output stream
* FYI: format strings must not end with an incomplete `%` or `%?`
| Verb | Description | `EXAMple.com` | `EXAMple.com!MyTag` | `рф.com!myTag` |
| ------- | ------------------------------------------------- | ------------- | ------------------- | -------------------- |
| `%T` | the tag | "" (null) | `myTag` | `myTag` |
| `%c` | canonical name, globally unique and comparable | `example.com` | `example.com!myTag` | `xn--p1ai.com!myTag` |
| `%a` | ASCII domain (Punycode, downcased) | `example.com` | `example.com` | `xn--p1ai.com` |
| `%u` | Unicode domain (non-Unicode parts downcased) | `example.com` | `example.com` | `рф.com` |
| `%r` | Raw (unmodified) Domain from `D()` (risky!) | `EXAMple.com` | `EXAMple.com` | `рф.com` |
| `%f` | like `%c` but better for filenames (`%a%?_%T`) | `example.com` | `example.com_myTag` | `xn--p1ai.com_myTag` |
| `%F` | like `%f` but reversed order (`%T%?_%a`) | `example.com` | `myTag_example.com` | `myTag_xn--p1ai.com` |
| `%?x` | returns `x` if tag exists, otherwise "" | "" (null) | `x` | `x` |
| `%%` | a literal percent sign | `%` | `%` | `%` |
| `a-Z./` | other printable characters are copied exactly | `a-Z./` | `a-Z./` | `a-Z./` |
| `%U` | (deprecated, use `%c`) Same as `%D%?!%T` (risky!) | `example.com` | `example.com!myTag` | `рф.com!myTag` |
| `%D` | (deprecated, use `%r`) mangles Unicode (risky!) | `example.com` | `example.com` | `рф.com` |
Typical values:
* `%?x` is typically used to generate an optional `!` or `_` if there is a tag.
* `%r` is considered "risky" because it can produce a domain name that is not
canonical. For example, if you use `D("FOO.com")` and later change it to `D("foo.com")`, your file names will change.
* Format strings must not end with an incomplete `%` or `%?`
* Generating a filename without a tag is risky. For example, if the same
`dnsconfig.js` has `D("example.com!inside", DSP_BIND)` and
`D("example.com!outside", DSP_BIND)`, both will use the same filename.
DNSControl will write both zone files to the same file, flapping between the
two. No error or warning will be output.
* `%U.zone` (The default)
* `example.com.zone` or `example.com!tag.zone`
* `%T%?_%I.zone` (optional tag and `_` + domain + `.zone`)
* `tag_example.com.zone` or `example.com.zone`
* `db_%T%?_%D`
* `db_inside_example.com` or `db_example.com`
Useful examples:
{% hint style="warning" %}
**Warning** DNSControl will not warn you if two zones generate the same
filename. Instead, each will write to the same place. The content would end up
flapping back and forth between the two. The best way to prevent this is to
always include the tag (`%T`) or use `%U` which includes the tag.
{% endhint %}
| Verb | Description | `EXAMple.com` | `EXAMple.com!MyTag` | `рф.com!myTag` |
| ------------ | ----------------------------------- | ------------------ | ------------------------ | ------------------------- |
| `%c.zone` | Default format (v4.28 and later) | `example.com.zone` | `example.com!myTag.zone` | `xn--p1ai.com!myTag.zone` |
| `%U.zone` | Default format (pre-v4.28) (risky!) | `example.com.zone` | `example.com!myTag.zone` | `рф.com!myTag.zone` |
| `db_%f` | Recommended in a popular DNS book | `db_example.com` | `db_example.com_myTag` | `db_xn--p1ai.com_myTag` |
| `db_%a%?_%T` | same as above but using `%?_` | `db_example.com` | `db_example.com_myTag` | `db_xn--p1ai.com_myTag` |
(new in v4.2.0) `dnscontrol push` will create subdirectories along the path to
the filename. This includes both the portion of the path created by the
`directory` setting and the `filenameformat` setting. The automatic creation of
subdirectories is disabled if `dnscontrol` is running as root for security
reasons.
Compatibility notes:
* `%D` should not be used. It downcases the string in a way that is probably
incompatible with Unicode characters. It is retained for compatibility with
pre-v4.28 releases. If your domain has capital Unicode characters, backwards
compatibility is not guaranteed. Use `%r` instead.
* `%U` relies on `%D` which is deprecated. Use `%c` instead.
* As of v4.28 the default format string changed from `%U.zone` to `%c.zone`. This
should only matter if your `D()` statements included non-ASCII (Unicode)
runes that were capitalized.
* If you are using pre-v4.28 releases the above table is slightly misleading
because uppercase ASCII letters do not always work. If you are using
pre-v4.28 releases, assume the above table lists `example.com` instead
of `EXAMpl.com`.
# FYI: get-zones
@ -132,7 +153,8 @@ any files named `*.zone` and assumes they are zone files.
dnscontrol get-zones --format=nameonly - BIND all
```
If `filenameformat` is defined, `dnscontrol` makes a guess at which
filenames are zones but doesn't try to hard to get it right, which is
mathematically impossible in some cases. Feel free to file an issue if
your format string doesn't work. I love a challenge!
If `filenameformat` is defined, `dnscontrol` makes a guess at which filenames
are zones by reversing the logic of the format string. It doesn't try very hard
to get this right, as getting it right in all situations is mathematically
impossible. Feel free to file an issue if find a situation where it doesn't
work. I love a challenge!

View file

@ -2033,11 +2033,6 @@ func makeTests() []*TestGroup {
),
),
// This MUST be the last test.
testgroup("final",
tc("final", txt("final", `TestDNSProviders was successful!`)),
),
testgroup("SMIMEA",
requires(providers.CanUseSMIMEA),
tc("SMIMEA record", smimea("_443._tcp", 3, 1, 1, sha256hash)),
@ -2060,6 +2055,12 @@ func makeTests() []*TestGroup {
// every quarter. There may be library updates, API changes,
// etc.
// This SHOULD be the last test. We do this so that we always
// leave zones with a single TXT record exclaming our success.
// Nothing depends on this record existing or should depend on it.
testgroup("final",
tc("final", txt("final", `TestDNSProviders was successful!`)),
),
}
return tests

View file

@ -13,7 +13,7 @@ const (
DomainTag = "dnscontrol_tag" // A copy of DomainConfig.Tag
DomainUniqueName = "dnscontrol_uniquename" // A copy of DomainConfig.UniqueName
DomainNameRaw = "dnscontrol_nameraw" // A copy of DomainConfig.NameRaw
DomainNameIDN = "dnscontrol_nameidn" // A copy of DomainConfig.NameIDN
DomainNameASCII = "dnscontrol_nameascii" // A copy of DomainConfig.NameASCII
DomainNameUnicode = "dnscontrol_nameunicode" // A copy of DomainConfig.NameUnicode
)
@ -74,7 +74,7 @@ func (dc *DomainConfig) PostProcess() {
// Turn the user-supplied name into the fixed forms.
ff := domaintags.MakeDomainFixForms(dc.Name)
dc.Tag, dc.NameRaw, dc.Name, dc.NameUnicode, dc.UniqueName = ff.Tag, ff.NameRaw, ff.NameIDN, ff.NameUnicode, ff.UniqueName
dc.Tag, dc.NameRaw, dc.Name, dc.NameUnicode, dc.UniqueName = ff.Tag, ff.NameRaw, ff.NameASCII, ff.NameUnicode, ff.UniqueName
// Store the FixForms is Metadata so we don't have to change the signature of every function that might need them.
// This is a bit ugly but avoids a huge refactor. Please avoid using these to make the future refactor easier.
@ -82,7 +82,7 @@ func (dc *DomainConfig) PostProcess() {
dc.Metadata[DomainTag] = dc.Tag
}
//dc.Metadata[DomainNameRaw] = dc.NameRaw
//dc.Metadata[DomainNameIDN] = dc.Name
//dc.Metadata[DomainNameASCII] = dc.Name
//dc.Metadata[DomainNameUnicode] = dc.NameUnicode
dc.Metadata[DomainUniqueName] = dc.UniqueName
}

11
package-lock.json generated
View file

@ -6,7 +6,7 @@
"": {
"dependencies": {
"@umbrelladocs/linkspector": "^0.3.13",
"prettier": "^3.6.2"
"prettier": "^3.7.4"
}
},
"node_modules/@babel/code-frame": {
@ -715,7 +715,8 @@
"version": "0.0.1367902",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz",
"integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==",
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
@ -2221,9 +2222,9 @@
"license": "ISC"
},
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"

View file

@ -1,6 +1,6 @@
{
"dependencies": {
"@umbrelladocs/linkspector": "^0.3.13",
"prettier": "^3.6.2"
"prettier": "^3.7.4"
}
}

View file

@ -9,7 +9,7 @@ import (
// DomainFixedForms stores the various fixed forms of a domain name and tag.
type DomainFixedForms struct {
NameRaw string // "originalinput.com" (name as input by the user, lowercased (no tag))
NameIDN string // "punycode.com"
NameASCII string // "punycode.com"
NameUnicode string // "unicode.com" (converted to downcase BEFORE unicode conversion)
UniqueName string // "punycode.com!tag"
@ -25,7 +25,7 @@ type DomainFixedForms struct {
// * .UniqueName: "example.com!tag" unique across the entire config.
func MakeDomainFixForms(n string) DomainFixedForms {
var err error
var tag, nameRaw, nameIDN, nameUnicode, uniqueName string
var tag, nameRaw, nameASCII, nameUnicode, uniqueName string
var hasBang bool
// Split tag from name.
@ -38,42 +38,45 @@ func MakeDomainFixForms(n string) DomainFixedForms {
hasBang = false
}
nameRaw = strings.ToLower(p[0])
nameRaw = p[0]
if strings.HasPrefix(n, nameRaw) {
// Avoid pointless duplication.
nameRaw = n[0:len(nameRaw)]
}
nameIDN, err = idna.ToASCII(nameRaw)
nameASCII, err = idna.ToASCII(nameRaw)
if err != nil {
nameIDN = nameRaw // Fallback to raw name on error.
nameASCII = nameRaw // Fallback to raw name on error.
} else {
nameASCII = strings.ToLower(nameASCII)
// Avoid pointless duplication.
if nameIDN == nameRaw {
nameIDN = nameRaw
if strings.HasPrefix(n, nameASCII) {
// Avoid pointless duplication.
nameASCII = n[0:len(nameASCII)]
}
}
nameUnicode, err = idna.ToUnicode(nameRaw)
nameUnicode, err = idna.ToUnicode(nameASCII) // We use nameASCII since it is already lowercased.
if err != nil {
nameUnicode = nameRaw // Fallback to raw name on error.
} else {
// Avoid pointless duplication.
if nameUnicode == nameRaw {
nameUnicode = nameRaw
if strings.HasPrefix(n, nameUnicode) {
// Avoid pointless duplication.
nameUnicode = n[0:len(nameUnicode)]
}
}
if hasBang {
uniqueName = nameIDN + "!" + tag
uniqueName = nameASCII + "!" + tag
} else {
uniqueName = nameIDN
uniqueName = nameASCII
}
return DomainFixedForms{
Tag: tag,
NameRaw: nameRaw,
NameIDN: nameIDN,
NameASCII: nameASCII,
NameUnicode: nameUnicode,
UniqueName: uniqueName,
HasBang: hasBang,

View file

@ -10,7 +10,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input string
wantTag string
wantNameRaw string
wantNameIDN string
wantNameASCII string
wantNameUnicode string
wantUniqueName string
wantHasBang bool
@ -20,7 +20,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "example.com",
wantTag: "",
wantNameRaw: "example.com",
wantNameIDN: "example.com",
wantNameASCII: "example.com",
wantNameUnicode: "example.com",
wantUniqueName: "example.com",
wantHasBang: false,
@ -30,7 +30,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "example.com!mytag",
wantTag: "mytag",
wantNameRaw: "example.com",
wantNameIDN: "example.com",
wantNameASCII: "example.com",
wantNameUnicode: "example.com",
wantUniqueName: "example.com!mytag",
wantHasBang: true,
@ -40,7 +40,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "example.com!",
wantTag: "",
wantNameRaw: "example.com",
wantNameIDN: "example.com",
wantNameASCII: "example.com",
wantNameUnicode: "example.com",
wantUniqueName: "example.com!",
wantHasBang: true,
@ -50,7 +50,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "उदाहरण.com",
wantTag: "",
wantNameRaw: "उदाहरण.com",
wantNameIDN: "xn--p1b6ci4b4b3a.com",
wantNameASCII: "xn--p1b6ci4b4b3a.com",
wantNameUnicode: "उदाहरण.com",
wantUniqueName: "xn--p1b6ci4b4b3a.com",
wantHasBang: false,
@ -60,7 +60,7 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "उदाहरण.com!mytag",
wantTag: "mytag",
wantNameRaw: "उदाहरण.com",
wantNameIDN: "xn--p1b6ci4b4b3a.com",
wantNameASCII: "xn--p1b6ci4b4b3a.com",
wantNameUnicode: "उदाहरण.com",
wantUniqueName: "xn--p1b6ci4b4b3a.com!mytag",
wantHasBang: true,
@ -70,17 +70,29 @@ func Test_MakeDomainFixForms(t *testing.T) {
input: "xn--p1b6ci4b4b3a.com",
wantTag: "",
wantNameRaw: "xn--p1b6ci4b4b3a.com",
wantNameIDN: "xn--p1b6ci4b4b3a.com",
wantNameASCII: "xn--p1b6ci4b4b3a.com",
wantNameUnicode: "उदाहरण.com",
wantUniqueName: "xn--p1b6ci4b4b3a.com",
wantHasBang: false,
},
{
// Unicode chars should be left alone (as far as case folding goes)
// Here are some Armenian characters https://tools.lgm.cl/lettercase.html
name: "mixed case unicode",
input: "fooԷէԸըԹ.com!myTag",
wantTag: "myTag",
wantNameRaw: "fooԷէԸըԹ.com",
wantNameASCII: "xn--foo-b7dfg43aja.com",
wantNameUnicode: "fooԷէԸըԹ.com",
wantUniqueName: "xn--foo-b7dfg43aja.com!myTag",
wantHasBang: true,
},
{
name: "punycode domain with tag",
input: "xn--p1b6ci4b4b3a.com!mytag",
wantTag: "mytag",
wantNameRaw: "xn--p1b6ci4b4b3a.com",
wantNameIDN: "xn--p1b6ci4b4b3a.com",
wantNameASCII: "xn--p1b6ci4b4b3a.com",
wantNameUnicode: "उदाहरण.com",
wantUniqueName: "xn--p1b6ci4b4b3a.com!mytag",
wantHasBang: true,
@ -89,8 +101,8 @@ func Test_MakeDomainFixForms(t *testing.T) {
name: "mixed case domain",
input: "Example.COM",
wantTag: "",
wantNameRaw: "example.com",
wantNameIDN: "example.com",
wantNameRaw: "Example.COM",
wantNameASCII: "example.com",
wantNameUnicode: "example.com",
wantUniqueName: "example.com",
wantHasBang: false,
@ -99,12 +111,34 @@ func Test_MakeDomainFixForms(t *testing.T) {
name: "mixed case domain with tag",
input: "Example.COM!MyTag",
wantTag: "MyTag",
wantNameRaw: "example.com",
wantNameIDN: "example.com",
wantNameRaw: "Example.COM",
wantNameASCII: "example.com",
wantNameUnicode: "example.com",
wantUniqueName: "example.com!MyTag",
wantHasBang: true,
},
// This is used in the documentation for the BIND provider, thus we test
// it to make sure we got it right.
{
name: "BIND example 1",
input: "рф.com!myTag",
wantTag: "myTag",
wantNameRaw: "рф.com",
wantNameASCII: "xn--p1ai.com",
wantNameUnicode: "рф.com",
wantUniqueName: "xn--p1ai.com!myTag",
wantHasBang: true,
},
{
name: "BIND example 2",
input: "рф.com",
wantTag: "",
wantNameRaw: "рф.com",
wantNameASCII: "xn--p1ai.com",
wantNameUnicode: "рф.com",
wantUniqueName: "xn--p1ai.com",
wantHasBang: false,
},
}
for _, tt := range tests {
@ -116,8 +150,8 @@ func Test_MakeDomainFixForms(t *testing.T) {
if got.NameRaw != tt.wantNameRaw {
t.Errorf("MakeDomainFixForms() gotNameRaw = %v, want %v", got.NameRaw, tt.wantNameRaw)
}
if got.NameIDN != tt.wantNameIDN {
t.Errorf("MakeDomainFixForms() gotNameIDN = %v, want %v", got.NameIDN, tt.wantNameIDN)
if got.NameASCII != tt.wantNameASCII {
t.Errorf("MakeDomainFixForms() gotNameASCII = %v, want %v", got.NameASCII, tt.wantNameASCII)
}
if got.NameUnicode != tt.wantNameUnicode {
t.Errorf("MakeDomainFixForms() gotNameUnicode = %v, want %v", got.NameUnicode, tt.wantNameUnicode)

View file

@ -24,8 +24,8 @@ func CompilePermitList(s string) PermitList {
continue
}
ff := MakeDomainFixForms(l)
if ff.HasBang && ff.NameIDN == "" { // Treat empty name as wildcard.
ff.NameIDN = "*"
if ff.HasBang && ff.NameASCII == "" { // Treat empty name as wildcard.
ff.NameASCII = "*"
}
sl.items = append(sl.items, ff)
}
@ -65,24 +65,24 @@ func (pl *PermitList) Permitted(domToCheck string) bool {
// Now that we know the tag matches, we can focus on the name.
// `*!tag` or `*` matches everything.
if filterItem.NameIDN == "*" {
if filterItem.NameASCII == "*" {
return true
}
// If the name starts with "*." then match the suffix.
if strings.HasPrefix(filterItem.NameIDN, "*.") {
if strings.HasPrefix(filterItem.NameASCII, "*.") {
// example.com matches *.example.com
if domToCheckFF.NameIDN == filterItem.NameIDN[2:] || domToCheckFF.NameUnicode == filterItem.NameUnicode[2:] {
if domToCheckFF.NameASCII == filterItem.NameASCII[2:] || domToCheckFF.NameUnicode == filterItem.NameUnicode[2:] {
return true
}
// foo.example.com matches *.example.com
if strings.HasSuffix(domToCheckFF.NameIDN, filterItem.NameIDN[1:]) || strings.HasSuffix(domToCheckFF.NameUnicode, filterItem.NameUnicode[1:]) {
if strings.HasSuffix(domToCheckFF.NameASCII, filterItem.NameASCII[1:]) || strings.HasSuffix(domToCheckFF.NameUnicode, filterItem.NameUnicode[1:]) {
return true
}
}
// No wildcards? Exact match.
if filterItem.NameIDN == domToCheckFF.NameIDN || filterItem.NameUnicode == domToCheckFF.NameUnicode {
if filterItem.NameASCII == domToCheckFF.NameASCII || filterItem.NameUnicode == domToCheckFF.NameUnicode {
return true
}
}

View file

@ -1,52 +1,52 @@
{
"registrars": [],
"dns_providers": [],
"domains": [
{
"name": "extdns-default.com",
"registrar": "none",
"dnsProviders": {},
"ignore_external_dns": true,
"meta": {
"dnscontrol_uniquename": "extdns-default.com"
},
"name": "extdns-default.com",
"records": [],
"ignore_external_dns": true
"registrar": "none"
},
{
"name": "extdns-custom.com",
"registrar": "none",
"dnsProviders": {},
"external_dns_prefix": "extdns-",
"ignore_external_dns": true,
"meta": {
"dnscontrol_uniquename": "extdns-custom.com"
},
"name": "extdns-custom.com",
"records": [],
"ignore_external_dns": true,
"external_dns_prefix": "extdns-"
"registrar": "none"
},
{
"name": "extdns-combined.com",
"registrar": "none",
"dnsProviders": {},
"ignore_external_dns": true,
"meta": {
"dnscontrol_uniquename": "extdns-combined.com"
},
"name": "extdns-combined.com",
"records": [
{
"type": "CNAME",
"name": "api",
"ttl": 300,
"filepos": "[line:12:5]",
"target": "www.extdns-combined.com."
"name": "api",
"target": "www.extdns-combined.com.",
"ttl": 300,
"type": "CNAME"
},
{
"type": "A",
"name": "www",
"ttl": 300,
"filepos": "[line:11:5]",
"target": "1.2.3.4"
"name": "www",
"target": "1.2.3.4",
"ttl": 300,
"type": "A"
}
],
"ignore_external_dns": true
"registrar": "none"
}
]
],
"registrars": []
}

View file

@ -69,7 +69,7 @@ func initBind(config map[string]string, providermeta json.RawMessage) (providers
api.directory = "zones"
}
if api.filenameformat == "" {
api.filenameformat = "%U.zone"
api.filenameformat = "%c.zone"
}
if len(providermeta) != 0 {
err := json.Unmarshal(providermeta, api)
@ -171,7 +171,7 @@ func (c *bindProvider) GetZoneRecords(domain string, meta map[string]string) (mo
ff := domaintags.DomainFixedForms{
Tag: meta[models.DomainTag],
NameRaw: meta[models.DomainNameRaw],
NameIDN: domain,
NameASCII: domain,
NameUnicode: meta[models.DomainNameUnicode],
UniqueName: meta[models.DomainUniqueName],
}
@ -282,7 +282,7 @@ func (c *bindProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundR
domaintags.DomainFixedForms{
Tag: dc.Tag,
NameRaw: dc.NameRaw,
NameIDN: dc.Name,
NameASCII: dc.Name,
NameUnicode: dc.NameUnicode,
UniqueName: dc.UniqueName,
},

View file

@ -13,11 +13,6 @@ import (
// makeFileName uses format to generate a zone's filename. See the
func makeFileName(format string, ff domaintags.DomainFixedForms) string {
//fmt.Printf("DEBUG: makeFileName(%q, %+v)\n", format, ff)
nameRaw := ff.NameRaw
nameIDN := ff.NameIDN
nameUnicode := ff.NameUnicode
uniquename := ff.UniqueName
tag := ff.Tag
if format == "" {
panic("BUG: makeFileName called with null format")
}
@ -40,16 +35,31 @@ func makeFileName(format string, ff domaintags.DomainFixedForms) string {
pos++
tok = tokens[pos]
switch tok {
case "D":
b.WriteString(nameRaw)
case "T":
b.WriteString(tag)
case "U":
b.WriteString(uniquename)
case "I":
b.WriteString(nameIDN)
case "N":
b.WriteString(nameUnicode)
// v4.28 names
case "r": // NameRaw "originalinput.com" (i for input)
b.WriteString(ff.NameRaw)
case "a": // NameASCII "punycode.com" (a for ascii)
b.WriteString(ff.NameASCII)
case "u": // NameUnicode "unicode.com" (u for unicode)
b.WriteString(ff.NameUnicode)
case "c": // UniqueName "punycode.com!tag" or "punycode.com" if no tag (c for canonical)
b.WriteString(ff.UniqueName)
case "f": //
b.WriteString(ff.NameASCII)
if ff.Tag != "" {
b.WriteString("_")
b.WriteString(ff.Tag)
}
case "F": //
if ff.Tag != "" {
b.WriteString(ff.Tag)
b.WriteString("_")
}
b.WriteString(ff.NameASCII)
case "T": // Tag The tag portion of `example.com!tag`
b.WriteString(ff.Tag)
case "%":
b.WriteString("%")
case "?":
@ -59,9 +69,20 @@ func makeFileName(format string, ff domaintags.DomainFixedForms) string {
}
pos++
tok = tokens[pos]
if tag != "" {
if ff.Tag != "" {
b.WriteString(tok)
}
// Legacy names kept for compatibility
case "U": // the domain name as specified in `D()`
b.WriteString(strings.ToLower(ff.NameRaw))
if ff.Tag != "" {
b.WriteString("!")
b.WriteString(ff.Tag)
}
case "D": // domain (without tag) as specified in D() (no IDN conversion, but downcased)
b.WriteString(strings.ToLower(ff.NameRaw))
default:
fmt.Fprintf(&b, "%%(unknown %%verb %%%s)", tok)
}
@ -156,14 +177,14 @@ func makeExtractor(format string) (string, error) {
pos++
tok = tokens[pos]
switch tok {
case "D":
case "D", "a", "u", "r":
b.WriteString(`(.*)`)
case "T":
if pass == 0 {
// On the second pass, nothing is generated.
b.WriteString(`.*`)
}
case "U":
case "U", "c":
if pass == 0 {
b.WriteString(`(.*)!.+`)
} else {

View file

@ -8,29 +8,28 @@ import (
)
func Test_makeFileName(t *testing.T) {
ff := domaintags.DomainFixedForms{
NameRaw: "raw",
NameIDN: "idn",
NameUnicode: "unicode",
UniqueName: "unique!taga",
Tag: "tagb",
}
tagless := domaintags.DomainFixedForms{
NameRaw: "raw",
NameIDN: "idn",
NameUnicode: "unicode",
UniqueName: "unique",
Tag: "",
}
fmtDefault := "%U.zone"
fmtBasic := "%U - %T - %D"
fmtBk1 := "db_%U" // Something I've seen in books on DNS
fmtBk2 := "db_%T_%D" // Something I've seen in books on DNS
fmtFancy := "%T%?_%D.zone" // Include the tag_ only if there is a tag
fmtErrorPct := "literal%"
fmtErrorOpt := "literal%?"
fmtErrorUnk := "literal%o" // Unknown % verb
ff := domaintags.DomainFixedForms{
NameRaw: "domy",
NameASCII: "idn",
NameUnicode: "uni",
UniqueName: "unique!taga",
Tag: "tagy",
}
tagless := domaintags.DomainFixedForms{
NameRaw: "domy",
NameASCII: "idn",
NameUnicode: "uni",
UniqueName: "unique",
Tag: "",
}
type args struct {
format string
ff domaintags.DomainFixedForms
@ -40,25 +39,15 @@ func Test_makeFileName(t *testing.T) {
args args
want string
}{
// Test corner cases and common cases.
{"literal", args{"literal", ff}, "literal"},
{"middle", args{"mid%Dle", ff}, "midrawle"},
{"D", args{"%D", ff}, "raw"},
{"I", args{"%I", ff}, "idn"},
{"N", args{"%N", ff}, "unicode"},
{"T", args{"%T", ff}, "tagb"},
{"x1", args{"XX%?xYY", ff}, "XXxYY"},
{"x2", args{"AA%?xBB", tagless}, "AABB"},
{"U", args{"%U", ff}, "unique!taga"},
{"percent", args{"%%", ff}, "%"},
//
{"default", args{fmtDefault, ff}, "unique!taga.zone"},
{"basic", args{fmtBasic, ff}, "unique!taga - tagb - raw"},
{"front", args{"%Daaa", ff}, "rawaaa"},
{"tail", args{"bbb%D", ff}, "bbbraw"},
{"bk1", args{fmtBk1, ff}, "db_unique!taga"},
{"bk2", args{fmtBk2, ff}, "db_tagb_raw"},
{"fanWI", args{fmtFancy, ff}, "tagb_raw.zone"},
{"fanWO", args{fmtFancy, tagless}, "raw.zone"},
{"basic", args{fmtBasic, ff}, "domy!tagy - tagy - domy"},
{"solo", args{"%D", ff}, "domy"},
{"front", args{"%Daaa", ff}, "domyaaa"},
{"tail", args{"bbb%D", ff}, "bbbdomy"},
{"def", args{fmtDefault, ff}, "domy!tagy.zone"},
{"fanWI", args{fmtFancy, ff}, "tagy_domy.zone"},
{"fanWO", args{fmtFancy, tagless}, "domy.zone"},
{"errP", args{fmtErrorPct, ff}, "literal%(format may not end in %)"},
{"errQ", args{fmtErrorOpt, ff}, "literal%(format may not end in %?)"},
{"errU", args{fmtErrorUnk, ff}, "literal%(unknown %verb %o)"},
@ -73,6 +62,66 @@ func Test_makeFileName(t *testing.T) {
}
}
func Test_makeFileName_2(t *testing.T) {
ff1 := domaintags.MakeDomainFixForms(`EXAMple.com`)
ff2 := domaintags.MakeDomainFixForms(`EXAMple.com!myTag`)
ff3 := domaintags.MakeDomainFixForms(`рф.com!myTag`)
tests := []struct {
name string
format string
want1 string
want2 string
want3 string
descr string // Not used in test, just for documentation generation
}{
// NOTE: "Domain" in these descriptions means the domain name without any split horizon tag. Technically the "Zone".
{`T`, `%T`, ``, `myTag`, `myTag`, `the tag`},
{`c`, `%c`, `example.com`, `example.com!myTag`, `xn--p1ai.com!myTag`, `canonical name, globally unique and comparable`},
{`a`, `%a`, `example.com`, `example.com`, `xn--p1ai.com`, `ASCII domain (Punycode, downcased)`},
{`u`, `%u`, `example.com`, `example.com`, `рф.com`, `Unicode domain (non-Unicode parts downcased)`},
{`r`, `%r`, `EXAMple.com`, `EXAMple.com`, `рф.com`, "Raw (unmodified) Domain from `D()` (risky!)"},
{`f`, `%f`, `example.com`, `example.com_myTag`, `xn--p1ai.com_myTag`, "like `%c` but better for filenames (`%a%?_%T`)"},
{`F`, `%F`, `example.com`, `myTag_example.com`, `myTag_xn--p1ai.com`, "like `%f` but reversed order (`%T%?_%a`)"},
{`%?x`, `%?x`, ``, `x`, `x`, "returns `x` if tag exists, otherwise \"\""},
{`%`, `%%`, `%`, `%`, `%`, `a literal percent sign`},
// Pre-v4.28 names kept for compatibility (note: pre v4.28 did not permit mixed case domain names, we downcased them here for the tests)
{`U`, `%U`, `example.com`, `example.com!myTag`, `рф.com!myTag`, "(deprecated, use `%c`) Same as `%D%?!%T` (risky!)"},
{`D`, `%D`, `example.com`, `example.com`, `рф.com`, "(deprecated, use `%r`) mangles Unicode (risky!)"},
{`%T%?_%D.zone`, `%T%?_%D.zone`, `example.com.zone`, `myTag_example.com.zone`, `myTag_рф.com.zone`, `mentioned in the docs`},
{`db_%T%?_%D`, `db_%T%?_%D`, `db_example.com`, `db_myTag_example.com`, `db_myTag_рф.com`, `mentioned in the docs`},
{`db_%D`, `db_%D`, `db_example.com`, `db_example.com`, `db_рф.com`, `mentioned in the docs`},
// Examples used in the documentation for the BIND provider
{`%c.zone`, `%c.zone`, "example.com.zone", "example.com!myTag.zone", "xn--p1ai.com!myTag.zone", "Default format (v4.28 and later)"},
{`%U.zone`, `%U.zone`, `example.com.zone`, `example.com!myTag.zone`, `рф.com!myTag.zone`, "Default format (pre-v4.28) (risky!)"},
{`db_%f`, `db_%f`, `db_example.com`, `db_example.com_myTag`, `db_xn--p1ai.com_myTag`, "Recommended in a popular DNS book"},
{`db_a%?_%T`, `db_%a%?_%T`, `db_example.com`, `db_example.com_myTag`, `db_xn--p1ai.com_myTag`, "same as above but using `%?_`"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got1 := makeFileName(tt.format, ff1)
if got1 != tt.want1 {
t.Errorf("makeFileName(%q) = ff1 %q, want %q", tt.format, got1, tt.want1)
}
got2 := makeFileName(tt.format, ff2)
if got2 != tt.want2 {
t.Errorf("makeFileName(%q) = ff2 %q, want %q", tt.format, got2, tt.want2)
}
got3 := makeFileName(tt.format, ff3)
if got3 != tt.want3 {
t.Errorf("makeFileName(%q) = ff3 %q, want %q", tt.format, got3, tt.want3)
}
//Uncomment to regenerate lines used in documentation/provider/bind.md 's table:
// fmt.Print(strings.ReplaceAll(fmt.Sprintf("| `%s` | %s | `%s` | `%s` | `%s` |\n", tt.format, tt.descr, got1, got2, got3), "``", "`\"\"` (null)"))
//Uncomment to regenerate the above test cases:
// fmt.Printf("{`%s`, `%s`, `%s`, `%s`, `%s`, %q},\n", tt.name, tt.format, got1, got2, got3, tt.descr)
})
}
}
func Test_makeExtractor(t *testing.T) {
type args struct {
format string
@ -83,6 +132,10 @@ func Test_makeExtractor(t *testing.T) {
want string
wantErr bool
}{
{"c", args{"%c.zone"}, `(.*)!.+\.zone|(.*)\.zone`, false},
{"a", args{"%a.zone"}, `(.*)\.zone`, false},
{"u", args{"%u.zone"}, `(.*)\.zone`, false},
{"r", args{"%r.zone"}, `(.*)\.zone`, false},
// TODO: Add test cases.
{"u", args{"%U.zone"}, `(.*)!.+\.zone|(.*)\.zone`, false},
{"d", args{"%D.zone"}, `(.*)\.zone`, false},