mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-11-14 12:15:06 +08:00
400 lines
13 KiB
Go
400 lines
13 KiB
Go
package commands
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/StackExchange/dnscontrol/v3/models"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/credsfile"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/prettyzone"
|
|
"github.com/StackExchange/dnscontrol/v3/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 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://stackexchange.github.io/dnscontrol/get-zones.html",
|
|
)
|
|
}
|
|
|
|
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 glcoud 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 zone's 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 cannot list zones to use the 'all' feature", 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)
|
|
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" {
|
|
|
|
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")
|
|
}
|
|
|
|
// 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)
|
|
prettyzone.WriteZoneFileRC(w, z.Records, zoneName, uint32(args.DefaultTTL), nil)
|
|
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(zoneName, 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.",
|
|
)
|
|
} else {
|
|
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")
|
|
|
|
case "tsv":
|
|
for _, rec := range recs {
|
|
|
|
cfproxy := ""
|
|
if cp, ok := rec.Metadata["cloudflare_proxy"]; ok {
|
|
if cp == "true" {
|
|
cfproxy = "\tcloudflare_proxy=true"
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(w, "%s\t%s\t%d\tIN\t%s\t%s%s\n",
|
|
rec.NameFQDN, rec.Name, rec.TTL, rec.Type, rec.GetTargetCombined(), 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(zonename string, 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 "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 "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, %d", rec.GetTargetField(), rec.SoaMbox, rec.SoaSerial, 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 "TLSA":
|
|
target = fmt.Sprintf("%d, %d, %d, '%s'", rec.TlsaUsage, rec.TlsaSelector, rec.TlsaMatchingType, rec.GetTargetField())
|
|
case "TXT":
|
|
if len(rec.TxtStrings) == 1 {
|
|
target = `'` + rec.TxtStrings[0] + `'`
|
|
} else {
|
|
target = `['` + strings.Join(rec.TxtStrings, `', '`) + `']`
|
|
}
|
|
// TODO(tlim): If this is an SPF record, generate a SPF_BUILDER().
|
|
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)
|
|
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 ttl != 0 {
|
|
items = append(items, fmt.Sprintf("TTL(%d)", ttl))
|
|
}
|
|
return rec.Type + "(" + strings.Join(items, ", ") + ")"
|
|
}
|