mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-12-09 13:46:07 +08:00
# Issue Fixes https://github.com/StackExchange/dnscontrol/issues/3842 CC @das7pad # Resolution Convert domain.Name to IDN earlier in the pipeline. Hack the --domains processing to convert everything to IDN. * Domain names are now stored 3 ways: The original input from dnsconfig.js, canonical IDN format (`xn--...`), and Unicode format. All are downcased. Providers that haven't been updated will receive the IDN format instead of the original input format. This might break some providers but only for users with unicode in their D("domain.tld"). PLEASE TEST YOUR PROVIDER. * BIND filename formatting options have been added to access the new formats. # Breaking changes * BIND zonefiles may change. The default used the name input in the D() statement. It now defaults to the IDN name + "!tag" if there is a tag. * Providers that are not IDN-aware may break (hopefully only if they weren't processing IDN already) --------- Co-authored-by: Jakob Ackermann <das7pad@outlook.com>
426 lines
14 KiB
Go
426 lines
14 KiB
Go
package commands
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/models"
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/credsfile"
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/prettyzone"
|
|
"github.com/StackExchange/dnscontrol/v4/providers"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
var _ = cmd(catUtils, func() *cli.Command {
|
|
var args GetZoneArgs
|
|
return &cli.Command{
|
|
Name: "get-zones",
|
|
Aliases: []string{"get-zone"},
|
|
Usage: "gets a zone from a provider (stand-alone)",
|
|
Action: func(ctx *cli.Context) error {
|
|
if ctx.NArg() < 3 {
|
|
return cli.Exit("Arguments should be: credskey providername zone(s) (Ex: r53 ROUTE53 example.com)", 1)
|
|
}
|
|
args.CredName = ctx.Args().Get(0)
|
|
arg1 := ctx.Args().Get(1)
|
|
args.ProviderName = arg1
|
|
// In v4.0, skip the first args.ZoneNames if it equals "-".
|
|
args.ZoneNames = ctx.Args().Slice()[2:]
|
|
|
|
if arg1 != "" && arg1 != "-" {
|
|
// NB(tlim): In v4.0 this "if" can be removed.
|
|
fmt.Fprintf(os.Stderr, "WARNING: To retain compatibility in future versions, please change %q to %q. See %q\n",
|
|
arg1, "-",
|
|
"https://docs.dnscontrol.org/commands/get-zones",
|
|
)
|
|
}
|
|
|
|
return exit(GetZone(args))
|
|
},
|
|
Flags: args.flags(),
|
|
UsageText: "dnscontrol get-zones [command options] credkey provider zone [...]",
|
|
Description: `Download a zone from a provider. This is a stand-alone utility.
|
|
|
|
ARGUMENTS:
|
|
credkey: The name used in creds.json (first parameter to NewDnsProvider() in dnsconfig.js)
|
|
provider: The name of the provider (second parameter to NewDnsProvider() in dnsconfig.js)
|
|
zone: One or more zones (domains) to download; or "all".
|
|
|
|
FORMATS:
|
|
--format=js dnsconfig.js format (not perfect, just a decent first draft)
|
|
--format=djs js with disco commas (leading commas)
|
|
--format=zone BIND zonefile format
|
|
--format=tsv TAB separated value (useful for AWK)
|
|
--format=nameonly Just print the zone names
|
|
|
|
The columns in --format=tsv are:
|
|
FQDN (the label with the domain)
|
|
ShortName (just the label, "@" if it is the naked domain)
|
|
TTL
|
|
Record Type (A, AAAA, CNAME, etc.)
|
|
Target and arguments (quoted like in a zonefile)
|
|
Either empty or a comma-separated list of properties like "cloudflare_proxy=true"
|
|
|
|
The --ttl flag only applies to zone/js/djs formats.
|
|
|
|
EXAMPLES:
|
|
dnscontrol get-zones myr53 ROUTE53 example.com
|
|
dnscontrol get-zones gmain GANDI_V5 example.com other.com
|
|
dnscontrol get-zones cfmain CLOUDFLAREAPI all
|
|
dnscontrol get-zones --format=tsv bind BIND example.com
|
|
dnscontrol get-zones --format=djs --out=draft.js gcloud GCLOUD example.com`,
|
|
}
|
|
}())
|
|
|
|
// check-creds foo bar
|
|
// is the same as
|
|
// get-zones --format=nameonly foo bar all
|
|
var _ = cmd(catUtils, func() *cli.Command {
|
|
var args GetZoneArgs
|
|
return &cli.Command{
|
|
Name: "check-creds",
|
|
Usage: "Do a small operation to verify credentials (stand-alone)",
|
|
Action: func(ctx *cli.Context) error {
|
|
var arg0, arg1 string
|
|
// This takes one or two command-line args.
|
|
// Starting in v3.16: Using it with 2 args will generate a warning.
|
|
// Starting in v4.0: Using it with 2 args might be an error.
|
|
if ctx.NArg() == 1 {
|
|
arg0 = ctx.Args().Get(0)
|
|
arg1 = ""
|
|
} else if ctx.NArg() == 2 {
|
|
arg0 = ctx.Args().Get(0)
|
|
arg1 = ctx.Args().Get(1)
|
|
} else {
|
|
return cli.Exit("Arguments should be: credskey [providername] (Ex: r53 ROUTE53)", 1)
|
|
}
|
|
args.CredName = arg0
|
|
args.ProviderName = arg1
|
|
args.ZoneNames = []string{"all"}
|
|
args.OutputFormat = "nameonly"
|
|
return exit(GetZone(args))
|
|
},
|
|
Flags: args.flags(),
|
|
UsageText: "dnscontrol check-creds [command options] credkey provider",
|
|
Description: `Do a trivia operation to verify credentials. This is a stand-alone utility.
|
|
|
|
If successful, a list of zones will be output. If not, hopefully you
|
|
see verbose error messages.
|
|
|
|
ARGUMENTS:
|
|
credkey: The name used in creds.json (first parameter to NewDnsProvider() in dnsconfig.js)
|
|
provider: The name of the provider (second parameter to NewDnsProvider() in dnsconfig.js)
|
|
|
|
EXAMPLES:
|
|
dnscontrol check-creds myr53 ROUTE53 # Pre v3.16, or pre-v4.0 for backwards-compatibility
|
|
dnscontrol check-creds myr53
|
|
dnscontrol check-creds --out=/dev/null myr53 && echo Success`,
|
|
}
|
|
}())
|
|
|
|
// GetZoneArgs args required for the create-domain subcommand.
|
|
type GetZoneArgs struct {
|
|
GetCredentialsArgs // Args related to creds.json
|
|
CredName string // key in creds.json
|
|
ProviderName string // provider type: BIND, GANDI_V5, etc or "-" (NB(tlim): In 4.0, this field goes away.)
|
|
ZoneNames []string // The zones to get
|
|
OutputFormat string // Output format
|
|
OutputFile string // Filename to send output ("" means stdout)
|
|
DefaultTTL int // default TTL for providers where it is unknown
|
|
}
|
|
|
|
func (args *GetZoneArgs) flags() []cli.Flag {
|
|
flags := args.GetCredentialsArgs.flags()
|
|
flags = append(flags, &cli.StringFlag{
|
|
Name: "format",
|
|
Destination: &args.OutputFormat,
|
|
Value: "zone",
|
|
Usage: `Output format: js djs zone tsv nameonly`,
|
|
})
|
|
flags = append(flags, &cli.StringFlag{
|
|
Name: "out",
|
|
Destination: &args.OutputFile,
|
|
Usage: `Instead of stdout, write to this file`,
|
|
})
|
|
flags = append(flags, &cli.IntFlag{
|
|
Name: "ttl",
|
|
Destination: &args.DefaultTTL,
|
|
Usage: `Default TTL (0 picks the most common TTL)`,
|
|
})
|
|
return flags
|
|
}
|
|
|
|
// GetZone contains all data/flags needed to run get-zones, independently of CLI.
|
|
func GetZone(args GetZoneArgs) error {
|
|
var providerConfigs map[string]map[string]string
|
|
var err error
|
|
|
|
// Read it in:
|
|
providerConfigs, err = credsfile.LoadProviderConfigs(args.CredsFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed GetZone LoadProviderConfigs(%q): %w", args.CredsFile, err)
|
|
}
|
|
provider, err := providers.CreateDNSProvider(args.ProviderName, providerConfigs[args.CredName], nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed GetZone CDP: %w", err)
|
|
}
|
|
|
|
// decide which zones we need to convert
|
|
zones := args.ZoneNames
|
|
if len(args.ZoneNames) == 1 && args.ZoneNames[0] == "all" {
|
|
lister, ok := provider.(providers.ZoneLister)
|
|
if !ok {
|
|
return fmt.Errorf("provider type %s:%s cannot list zones to use the 'all' feature", args.CredName, args.ProviderName)
|
|
}
|
|
zones, err = lister.ListZones()
|
|
if err != nil {
|
|
return fmt.Errorf("failed GetZone LZ: %w", err)
|
|
}
|
|
}
|
|
|
|
// first open output stream and print initial header (if applicable)
|
|
w := os.Stdout
|
|
if args.OutputFile != "" {
|
|
w, err = os.Create(args.OutputFile)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed GetZone Create(%q): %w", args.OutputFile, err)
|
|
}
|
|
defer w.Close()
|
|
|
|
if args.OutputFormat == "nameonly" {
|
|
for _, zone := range zones {
|
|
fmt.Fprintln(w, zone)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// fetch all of the records
|
|
zoneRecs := make([]models.Records, len(zones))
|
|
for i, zone := range zones {
|
|
recs, err := provider.GetZoneRecords(zone,
|
|
map[string]string{
|
|
models.DomainUniqueName: zone,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed GetZone gzr: %w", err)
|
|
}
|
|
zoneRecs[i] = recs
|
|
}
|
|
|
|
// Write the heading:
|
|
|
|
dspVariableName := "DSP_" + strings.ToUpper(args.CredName)
|
|
|
|
if args.OutputFormat == "js" || args.OutputFormat == "djs" {
|
|
fmt.Fprintf(w, "// generated by get-zones. This is 'a decent first draft' and requires editing.\n")
|
|
fmt.Fprintf(w, "\n")
|
|
if args.ProviderName == "-" {
|
|
fmt.Fprintf(w, `var %s = NewDnsProvider("%s");`+"\n",
|
|
dspVariableName, args.CredName)
|
|
} else {
|
|
fmt.Fprintf(w, `var %s = NewDnsProvider("%s", "%s");`+"\n",
|
|
dspVariableName, args.CredName, args.ProviderName)
|
|
}
|
|
fmt.Fprintf(w, `var REG_CHANGEME = NewRegistrar("none");`+"\n\n")
|
|
}
|
|
|
|
// print each zone
|
|
for i, recs := range zoneRecs {
|
|
zoneName := zones[i]
|
|
|
|
z := prettyzone.PrettySort(recs, zoneName, 0, nil)
|
|
switch args.OutputFormat {
|
|
case "zone":
|
|
fmt.Fprintf(w, "$ORIGIN %s.\n", zoneName)
|
|
if err := prettyzone.WriteZoneFileRC(w, z.Records, zoneName, uint32(args.DefaultTTL), nil); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintln(w)
|
|
|
|
case "js", "djs":
|
|
sep := ",\n\t" // Commas at EOL
|
|
if args.OutputFormat == "djs" {
|
|
sep = "\n\t, " // Funky comma mode
|
|
}
|
|
|
|
fmt.Fprintf(w, `D("%s", REG_CHANGEME%s`, zoneName, sep)
|
|
var o []string
|
|
o = append(o, fmt.Sprintf("DnsProvider(%s)", dspVariableName))
|
|
defaultTTL := uint32(args.DefaultTTL)
|
|
if defaultTTL == 0 {
|
|
defaultTTL = prettyzone.MostCommonTTL(recs)
|
|
}
|
|
if defaultTTL != models.DefaultTTL && defaultTTL != 0 {
|
|
o = append(o, fmt.Sprintf("DefaultTTL(%d)", defaultTTL))
|
|
}
|
|
for _, rec := range recs {
|
|
if (rec.Type == "CNAME") && (rec.Name == "@") {
|
|
o = append(o, "// NOTE: CNAME at apex may require manual editing.")
|
|
}
|
|
o = append(o, formatDsl(rec, defaultTTL))
|
|
}
|
|
out := strings.Join(o, sep)
|
|
|
|
// Joining with a comma between each item works great but
|
|
// makes comments look terrible. Here we clean them up
|
|
// after the fact.
|
|
if args.OutputFormat == "djs" {
|
|
out = strings.ReplaceAll(out, "\n\t, //", "\n\t//, ") // Fix comments
|
|
out = strings.ReplaceAll(out,
|
|
"//, NOTE: CNAME at apex may require manual editing.",
|
|
"// NOTE: CNAME at apex may require manual editing.",
|
|
)
|
|
fmt.Fprint(w, out)
|
|
fmt.Fprint(w, "\n)\n\n")
|
|
} else {
|
|
out = out + ","
|
|
out = strings.ReplaceAll(out,
|
|
"// NOTE: CNAME at apex may require manual editing.,",
|
|
"// NOTE: CNAME at apex may require manual editing.",
|
|
)
|
|
fmt.Fprint(w, out)
|
|
fmt.Fprint(w, "\n);\n\n")
|
|
}
|
|
|
|
case "tsv":
|
|
for _, rec := range recs {
|
|
cfproxy := ""
|
|
if cp, ok := rec.Metadata["cloudflare_proxy"]; ok {
|
|
if cp == "true" {
|
|
cfproxy = "\tcloudflare_proxy=true"
|
|
}
|
|
}
|
|
|
|
ty := rec.Type
|
|
if rec.Type == "UNKNOWN" {
|
|
ty = rec.UnknownTypeName
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s\t%d\tIN\t%s\t%s%s\n",
|
|
rec.NameFQDN, rec.Name, rec.TTL, ty, rec.GetTargetCombinedFunc(nil), cfproxy)
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("format %q unknown", args.OutputFormat)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// jsonQuoted returns a properly escaped JSON string (without quotes).
|
|
func jsonQuoted(i string) string {
|
|
// https://stackoverflow.com/questions/51691901
|
|
b, err := json.Marshal(i)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func formatDsl(rec *models.RecordConfig, defaultTTL uint32) string {
|
|
target := rec.GetTargetCombined()
|
|
|
|
ttl := uint32(0)
|
|
ttlop := ""
|
|
if rec.TTL != defaultTTL && rec.TTL != 0 {
|
|
ttl = rec.TTL
|
|
ttlop = fmt.Sprintf(", TTL(%d)", ttl)
|
|
}
|
|
|
|
cfproxy := ""
|
|
if cp, ok := rec.Metadata["cloudflare_proxy"]; ok {
|
|
if cp == "true" {
|
|
cfproxy = ", CF_PROXY_ON"
|
|
}
|
|
}
|
|
|
|
switch rec.Type { // #rtype_variations
|
|
case "CAA":
|
|
return makeCaa(rec, ttlop)
|
|
case "DS":
|
|
target = fmt.Sprintf(`%d, %d, %d, "%s"`, rec.DsKeyTag, rec.DsAlgorithm, rec.DsDigestType, rec.DsDigest)
|
|
case "DNSKEY":
|
|
target = fmt.Sprintf(`%d, %d, %d, "%s"`, rec.DnskeyFlags, rec.DnskeyProtocol, rec.DnskeyAlgorithm, rec.DnskeyPublicKey)
|
|
case "MX":
|
|
target = fmt.Sprintf(`%d, "%s"`, rec.MxPreference, rec.GetTargetField())
|
|
case "NAPTR":
|
|
target = fmt.Sprintf(`%d, %d, %s, %s, %s, %s`,
|
|
rec.NaptrOrder, // 1
|
|
rec.NaptrPreference, // 10
|
|
jsonQuoted(rec.NaptrFlags), // U
|
|
jsonQuoted(rec.NaptrService), // E2U+sip
|
|
jsonQuoted(rec.NaptrRegexp), // regex
|
|
jsonQuoted(rec.GetTargetField()), // .
|
|
)
|
|
case "SMIMEA":
|
|
target = fmt.Sprintf(`%d, %d, %d, "%s"`, rec.SmimeaUsage, rec.SmimeaSelector, rec.SmimeaMatchingType, rec.GetTargetField())
|
|
case "SSHFP":
|
|
target = fmt.Sprintf(`%d, %d, "%s"`, rec.SshfpAlgorithm, rec.SshfpFingerprint, rec.GetTargetField())
|
|
case "SOA":
|
|
rec.Type = "//SOA"
|
|
target = fmt.Sprintf(`"%s", "%s", %d, %d, %d, %d`, rec.GetTargetField(), rec.SoaMbox, rec.SoaRefresh, rec.SoaRetry, rec.SoaExpire, rec.SoaMinttl)
|
|
case "SRV":
|
|
target = fmt.Sprintf(`%d, %d, %d, "%s"`, rec.SrvPriority, rec.SrvWeight, rec.SrvPort, rec.GetTargetField())
|
|
case "SVCB", "HTTPS":
|
|
target = fmt.Sprintf(`%d, "%s", "%s"`, rec.SvcPriority, rec.GetTargetField(), rec.SvcParams)
|
|
case "TLSA":
|
|
target = fmt.Sprintf(`%d, %d, %d, "%s"`, rec.TlsaUsage, rec.TlsaSelector, rec.TlsaMatchingType, rec.GetTargetField())
|
|
case "TXT":
|
|
target = jsonQuoted(rec.GetTargetTXTJoined())
|
|
// TODO(tlim): If this is an SPF record, generate a SPF_BUILDER().
|
|
case "LUA":
|
|
target = fmt.Sprintf("%q, %s", rec.LuaRType, jsonQuoted(rec.GetTargetTXTJoined()))
|
|
case "NS":
|
|
// NS records at the apex should be NAMESERVER() records.
|
|
// DnsControl uses the API to get this info. NAMESERVER() is just
|
|
// to override that when needed.
|
|
if rec.Name == "@" {
|
|
return fmt.Sprintf(`//NAMESERVER("%s")`, target)
|
|
}
|
|
target = `"` + target + `"`
|
|
case "R53_ALIAS":
|
|
return makeR53alias(rec, ttl)
|
|
case "UNKNOWN":
|
|
return makeUknown(rec, ttl)
|
|
default:
|
|
target = `"` + target + `"`
|
|
}
|
|
|
|
return fmt.Sprintf(`%s("%s", %s%s%s)`, rec.Type, rec.Name, target, cfproxy, ttlop)
|
|
}
|
|
|
|
func makeCaa(rec *models.RecordConfig, ttlop string) string {
|
|
var target string
|
|
if rec.CaaFlag == 128 {
|
|
target = fmt.Sprintf(`"%s", "%s", CAA_CRITICAL`, rec.CaaTag, rec.GetTargetField())
|
|
} else {
|
|
target = fmt.Sprintf(`"%s", "%s"`, rec.CaaTag, rec.GetTargetField())
|
|
}
|
|
return fmt.Sprintf(`%s("%s", %s%s)`, rec.Type, rec.Name, target, ttlop)
|
|
|
|
// TODO(tlim): Generate a CAA_BUILDER() instead?
|
|
}
|
|
|
|
func makeR53alias(rec *models.RecordConfig, ttl uint32) string {
|
|
items := []string{
|
|
`"` + rec.Name + `"`,
|
|
`"` + rec.R53Alias["type"] + `"`,
|
|
`"` + rec.GetTargetField() + `"`,
|
|
}
|
|
if z, ok := rec.R53Alias["zone_id"]; ok {
|
|
items = append(items, `R53_ZONE("`+z+`")`)
|
|
}
|
|
if e, ok := rec.R53Alias["evaluate_target_health"]; ok && e == "true" {
|
|
items = append(items, "R53_EVALUATE_TARGET_HEALTH(true)")
|
|
}
|
|
if ttl != 0 {
|
|
items = append(items, fmt.Sprintf("TTL(%d)", ttl))
|
|
}
|
|
return rec.Type + "(" + strings.Join(items, ", ") + ")"
|
|
}
|
|
|
|
func makeUknown(rc *models.RecordConfig, ttl uint32) string {
|
|
return fmt.Sprintf(`// %s("%s", TTL(%d))`, rc.UnknownTypeName, rc.GetTargetField(), ttl)
|
|
}
|