diff --git a/.drone.jsonnet b/.drone.jsonnet index 6ac4e64..95a30fb 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -8,19 +8,9 @@ local testing(version, arch) = { arch: arch }, steps: [ - { - name: "compile", - image: "golang:" + version, - commands: [ - "make awl" - ], - }, { name: "lint", image: "rancher/drone-golangci-lint:latest", - depends_on: [ - "compile", - ], }, { name: "test", @@ -74,7 +64,7 @@ local release() = { name: "test", image: "golang", commands: [ - "make test" + "make test-ci" ] }, { diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ebe51d3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml index 9eca14e..badf518 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -32,6 +32,7 @@ linters: - predeclared - revive - staticcheck + - tagliatelle - whitespace - wrapcheck - wsl @@ -70,6 +71,16 @@ linters-settings: - name: unexported-return - name: var-declaration - name: var-naming + linters-settings: + tagliatelle: + case: + use-field-name: false + rules: + # Any struct tag type can be used. + # Support string case: `camel`, `pascal`, `kebab`, `snake`, `goCamel`, `goPascal`, `goKebab`, `goSnake`, `upper`, `lower` + json: goCamel + yaml: goCamel + xml: goCamel issues: exclude-use-default: false diff --git a/cli/cli.go b/cli/cli.go index 8a593cb..f002afe 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -5,7 +5,6 @@ package cli import ( "errors" "fmt" - "os" "runtime" "strconv" "strings" @@ -18,8 +17,8 @@ import ( // ParseCLI parses arguments given from the CLI and passes them into an `Options` // struct. -func ParseCLI(version string) (util.Options, error) { - flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) +func ParseCLI(args []string, version string) (util.Options, error) { + flag.CommandLine = flag.NewFlagSet(args[0], flag.ContinueOnError) flag.Usage = func() { fmt.Println(`awl - drill, writ small @@ -104,7 +103,7 @@ func ParseCLI(version string) (util.Options, error) { flag.CommandLine.SortFlags = false // Parse the flags - if err := flag.CommandLine.Parse(os.Args[1:]); err != nil { + if err := flag.CommandLine.Parse(args[1:]); err != nil { return util.Options{Logger: util.InitLogger(*verbosity)}, fmt.Errorf("flag: %w", err) } @@ -116,7 +115,6 @@ func ParseCLI(version string) (util.Options, error) { opts := util.Options{ Logger: util.InitLogger(*verbosity), - Port: *port, IPv4: *ipv4, IPv6: *ipv6, Short: *short, @@ -128,29 +126,29 @@ func ParseCLI(version string) (util.Options, error) { HTTPS: *https, QUIC: *quic, Truncate: *truncate, - ShowQuery: false, - AA: *aaflag, - AD: *adflag, - TC: *tcflag, - Z: *zflag, - CD: *cdflag, - QR: *qrflag, - RD: *rdflag, - RA: *raflag, Reverse: *reverse, - HumanTTL: false, - ShowTTL: true, JSON: *json, XML: *xml, YAML: *yaml, + HeaderFlags: util.HeaderFlags{ + AA: *aaflag, + AD: *adflag, + TC: *tcflag, + Z: *zflag, + CD: *cdflag, + QR: *qrflag, + RD: *rdflag, + RA: *raflag, + }, Request: util.Request{ Type: dns.StringToType[strings.ToUpper(*qType)], Class: dns.StringToClass[strings.ToUpper(*class)], Name: *query, Timeout: time.Duration(*timeout * float32(time.Second)), Retries: *retry, + Port: *port, }, - Display: util.Displays{ + Display: util.Display{ Comments: !*noC, Question: !*noQ, Opt: !*noOpt, @@ -158,6 +156,9 @@ func ParseCLI(version string) (util.Options, error) { Authority: !*noAuth, Additional: !*noAdd, Statistics: !*noStats, + HumanTTL: false, + ShowQuery: false, + TTL: true, }, EDNS: util.EDNS{ EnableEDNS: !*edns, @@ -199,15 +200,15 @@ func ParseCLI(version string) (util.Options, error) { opts.Logger.Info("Dig/Drill flags parsed") opts.Logger.Debug(fmt.Sprintf("%+v", opts)) - if opts.Port == 0 { + if opts.Request.Port == 0 { if opts.TLS || opts.QUIC { - opts.Port = 853 + opts.Request.Port = 853 } else { - opts.Port = 53 + opts.Request.Port = 53 } } - opts.Logger.Info("Port set to", opts.Port) + opts.Logger.Info("Port set to", opts.Request.Port) // Set timeout to 0.5 seconds if set below 0.5 if opts.Request.Timeout < (time.Second / 2) { @@ -218,6 +219,9 @@ func ParseCLI(version string) (util.Options, error) { opts.Request.Retries = 0 } + opts.Logger.Info("Options fully populated") + opts.Logger.Debug(fmt.Sprintf("%+v", opts)) + return opts, nil } diff --git a/cli/cli_test.go b/cli/cli_test.go index 0930fbe..e830c2f 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -3,7 +3,6 @@ package cli_test import ( - "os" "testing" "time" @@ -12,156 +11,110 @@ import ( ) func TestEmpty(t *testing.T) { - args := os.Args - os.Args = []string{"awl", "-4"} + args := []string{"awl", "-4"} - opts, err := cli.ParseCLI("TEST") + opts, err := cli.ParseCLI(args, "TEST") assert.NilError(t, err) - assert.Equal(t, opts.Port, 53) + assert.Equal(t, opts.Request.Port, 53) assert.Assert(t, opts.IPv4) - - os.Args = args } func TestTLSPort(t *testing.T) { - args := os.Args - os.Args = []string{"awl", "-T"} + args := []string{"awl", "-T"} - opts, err := cli.ParseCLI("TEST") + opts, err := cli.ParseCLI(args, "TEST") assert.NilError(t, err) - assert.Equal(t, opts.Port, 853) - - os.Args = args + assert.Equal(t, opts.Request.Port, 853) } func TestSubnet(t *testing.T) { - args := os.Args - os.Args = []string{"awl", "--subnet", "127.0.0.1/32"} + args := []string{"awl", "--subnet", "127.0.0.1/32"} - opts, err := cli.ParseCLI("TEST") + opts, err := cli.ParseCLI(args, "TEST") assert.NilError(t, err) assert.Equal(t, opts.EDNS.Subnet.Family, uint16(1)) - os.Args = args + args = []string{"awl", "--subnet", "0"} - os.Args = []string{"awl", "--subnet", "0"} - - opts, err = cli.ParseCLI("TEST") + opts, err = cli.ParseCLI(args, "TEST") assert.NilError(t, err) assert.Equal(t, opts.EDNS.Subnet.Family, uint16(1)) - os.Args = args + args = []string{"awl", "--subnet", "::/0"} - os.Args = []string{"awl", "--subnet", "::/0"} - - opts, err = cli.ParseCLI("TEST") + opts, err = cli.ParseCLI(args, "TEST") assert.NilError(t, err) assert.Equal(t, opts.EDNS.Subnet.Family, uint16(2)) - os.Args = args + args = []string{"awl", "--subnet", "/"} - os.Args = []string{"awl", "--subnet", "/"} - - opts, err = cli.ParseCLI("TEST") + opts, err = cli.ParseCLI(args, "TEST") assert.ErrorContains(t, err, "EDNS subnet") - - os.Args = args } -func TestMBZ(t *testing.T) { //nolint: paralleltest // Race conditions - args := os.Args - os.Args = []string{"awl", "--zflag", "G"} +func TestMBZ(t *testing.T) { + args := []string{"awl", "--zflag", "G"} - _, err := cli.ParseCLI("TEST") + _, err := cli.ParseCLI(args, "TEST") assert.ErrorContains(t, err, "EDNS MBZ") - - os.Args = args } -func TestInvalidFlag(t *testing.T) { //nolint: paralleltest // Race conditions - args := os.Args - stdout := os.Stdout - stderr := os.Stderr +func TestInvalidFlag(t *testing.T) { + args := []string{"awl", "--treebug"} - os.Stdout = os.NewFile(0, os.DevNull) - os.Stderr = os.NewFile(0, os.DevNull) - - os.Args = []string{"awl", "--treebug"} - - _, err := cli.ParseCLI("TEST") + _, err := cli.ParseCLI(args, "TEST") assert.ErrorContains(t, err, "unknown flag") - - os.Args = args - os.Stdout = stdout - os.Stderr = stderr } -func TestInvalidDig(t *testing.T) { //nolint: paralleltest // Race conditions - args := os.Args - os.Args = []string{"awl", "+a"} +func TestInvalidDig(t *testing.T) { + args := []string{"awl", "+a"} - _, err := cli.ParseCLI("TEST") + _, err := cli.ParseCLI(args, "TEST") assert.ErrorContains(t, err, "digflags: invalid argument") - - os.Args = args } -func TestVersion(t *testing.T) { //nolint: paralleltest // Race conditions - args := os.Args - stdout := os.Stdout - stderr := os.Stderr +func TestVersion(t *testing.T) { + args := []string{"awl", "--version"} - os.Args = []string{"awl", "--version"} - - _, err := cli.ParseCLI("test") + _, err := cli.ParseCLI(args, "test") assert.ErrorType(t, err, cli.ErrNotError) - - os.Args = args - os.Stdout = stdout - os.Stderr = stderr } -func TestTimeout(t *testing.T) { //nolint: paralleltest // Race conditions +func TestTimeout(t *testing.T) { args := [][]string{ {"awl", "+timeout=0"}, {"awl", "--timeout", "0"}, } for _, test := range args { - args := os.Args - os.Args = test + test := test - opt, err := cli.ParseCLI("TEST") + opt, err := cli.ParseCLI(test, "TEST") assert.NilError(t, err) assert.Equal(t, opt.Request.Timeout, time.Second/2) - - os.Args = args } } -func TestRetries(t *testing.T) { //nolint: paralleltest // Race conditions +func TestRetries(t *testing.T) { args := [][]string{ {"awl", "+retry=-2"}, {"awl", "+tries=-2"}, {"awl", "--retries", "-2"}, } for _, test := range args { - args := os.Args - os.Args = test + test := test - opt, err := cli.ParseCLI("TEST") + opt, err := cli.ParseCLI(test, "TEST") assert.NilError(t, err) assert.Equal(t, opt.Request.Retries, 0) - - os.Args = args } } @@ -175,10 +128,8 @@ func FuzzFlags(f *testing.F) { f.Fuzz(func(t *testing.T, orig string) { // Get rid of outputs - args := os.Args - os.Args = []string{"awl", orig} + args := []string{"awl", orig} //nolint:errcheck,gosec // Only make sure the program does not crash - cli.ParseCLI("TEST") - os.Args = args + cli.ParseCLI(args, "TEST") }) } diff --git a/cli/dig.go b/cli/dig.go index 6d75b6c..00ee8a6 100644 --- a/cli/dig.go +++ b/cli/dig.go @@ -45,13 +45,13 @@ func ParseDig(arg string, opts *util.Options) error { // End DNS query flags case "qr": - opts.ShowQuery = isNo + opts.Display.ShowQuery = isNo case "ttlunits": - opts.HumanTTL = isNo + opts.Display.HumanTTL = isNo case "ttl", "ttlid": - opts.ShowTTL = isNo + opts.Display.TTL = isNo case "class": - opts.ShowClass = isNo + opts.Display.ShowClass = isNo // EDNS queries case "dnssec": diff --git a/go.mod b/go.mod index fa831c7..9b6a2f8 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/miekg/dns v1.1.50 github.com/stefansundin/go-zflag v1.1.1 golang.org/x/net v0.0.0-20220909164309-bea034e7d591 + golang.org/x/sys v0.0.0-20220913175220-63ea55921009 gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.3.0 ) @@ -18,22 +19,18 @@ require ( github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect github.com/ameshkov/dnsstamps v1.0.3 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/golang/mock v1.6.0 // indirect github.com/google/go-cmp v0.5.8 // indirect github.com/kr/pretty v0.3.0 // indirect - github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect - golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 // indirect -) - -require ( - github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect + github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/onsi/ginkgo v1.16.5 // indirect golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect + golang.org/x/exp v0.0.0-20220914170420-dc92f8653013 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect - golang.org/x/sys v0.0.0-20220913175220-63ea55921009 golang.org/x/text v0.3.7 // indirect golang.org/x/tools v0.1.12 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect diff --git a/go.sum b/go.sum index 4863d95..5defd9f 100644 --- a/go.sum +++ b/go.sum @@ -79,10 +79,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw= -golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20220914170420-dc92f8653013 h1:ZjglnWxEUdPyXl4o/j4T89SRCI+4X6NW6185PNLEOF4= +golang.org/x/exp v0.0.0-20220914170420-dc92f8653013/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= @@ -94,8 +92,6 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220907135653-1e95f45603a7 h1:1WGATo9HAhkWMbfyuVU0tEFP88OIkUvwaHFveQPvzCQ= -golang.org/x/net v0.0.0-20220907135653-1e95f45603a7/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI= golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -119,12 +115,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220907062415-87db552b00fd h1:AZeIEzg+8RCELJYq8w+ODLVxFgLMMigSwO/ffKPEd9U= -golang.org/x/sys v0.0.0-20220907062415-87db552b00fd/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2 h1:wM1k/lXfpc5HdkJJyW9GELpd8ERGdnh8sMGL6Gzq3Ho= -golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220913175220-63ea55921009 h1:PuvuRMeLWqsf/ZdT1UUZz0syhioyv1mzuFZsXs4fvhw= golang.org/x/sys v0.0.0-20220913175220-63ea55921009/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/logawl/logger.go b/logawl/logger.go index e6aebfd..b0993ea 100644 --- a/logawl/logger.go +++ b/logawl/logger.go @@ -139,17 +139,37 @@ func (l *Logger) Debug(v ...any) { l.Println(DebugLevel, v...) } +// Debugf calls print after formatting the string with Debug level. +func (l *Logger) Debugf(format string, v ...any) { + l.Println(ErrLevel, fmt.Sprintf(format, v...)) +} + // Info calls print directly with Info level. func (l *Logger) Info(v ...any) { l.Println(InfoLevel, v...) } +// Infof calls print after formatting the string with Info level. +func (l *Logger) Infof(format string, v ...any) { + l.Println(ErrLevel, fmt.Sprintf(format, v...)) +} + // Warn calls print directly with Warn level. func (l *Logger) Warn(v ...any) { l.Println(WarnLevel, v...) } +// Warnf calls print after formatting the string with Warn level. +func (l *Logger) Warnf(format string, v ...any) { + l.Println(WarnLevel, fmt.Sprintf(format, v...)) +} + // Error calls print directly with Error level. func (l *Logger) Error(v ...any) { l.Println(ErrLevel, v...) } + +// Errorf calls print after formatting the string with Error level. +func (l *Logger) Errorf(format string, v ...any) { + l.Println(ErrLevel, fmt.Sprintf(format, v...)) +} diff --git a/logawl/logging_test.go b/logawl/logging_test.go index 1b63b01..24f001c 100644 --- a/logawl/logging_test.go +++ b/logawl/logging_test.go @@ -52,6 +52,7 @@ func TestLogger(t *testing.T) { case 0: fn := func() { logger.Error("Test", "E") + logger.Errorf("%s", "Test") } var buffer bytes.Buffer @@ -62,6 +63,7 @@ func TestLogger(t *testing.T) { case 1: fn := func() { logger.Warn("Test") + logger.Warnf("%s", "Test") } var buffer bytes.Buffer @@ -72,6 +74,7 @@ func TestLogger(t *testing.T) { case 2: fn := func() { logger.Info("Test") + logger.Infof("%s", "Test") } var buffer bytes.Buffer @@ -83,6 +86,8 @@ func TestLogger(t *testing.T) { fn := func() { logger.Debug("Test") logger.Debug("Test 2") + logger.Debugf("%s", "Test") + logger.Debugf("%s %d", "Test", 2) } var buffer bytes.Buffer diff --git a/main.go b/main.go index 641deac..7771998 100644 --- a/main.go +++ b/main.go @@ -17,7 +17,7 @@ import ( var version = "DEV" func main() { - if opts, code, err := run(); err != nil { + if opts, code, err := run(os.Args); err != nil { // TODO: Make not ew if errors.Is(err, cli.ErrNotError) || strings.Contains(err.Error(), "help requested") { os.Exit(0) @@ -28,8 +28,8 @@ func main() { } } -func run() (opts util.Options, code int, err error) { - opts, err = cli.ParseCLI(version) +func run(args []string) (opts util.Options, code int, err error) { + opts, err = cli.ParseCLI(args, version) if err != nil { return opts, 1, fmt.Errorf("parse: %w", err) } @@ -53,7 +53,7 @@ func run() (opts util.Options, code int, err error) { var str string if opts.JSON || opts.XML || opts.YAML { - str, err = query.PrintSpecial(resp.DNS, opts) + str, err = query.PrintSpecial(resp, opts) if err != nil { return opts, 10, fmt.Errorf("format print: %w", err) } diff --git a/main_test.go b/main_test.go index eb42484..57186c9 100644 --- a/main_test.go +++ b/main_test.go @@ -14,33 +14,23 @@ func TestMain(t *testing.T) { //nolint: paralleltest // Race conditions os.Stdout = os.NewFile(0, os.DevNull) os.Stderr = os.NewFile(0, os.DevNull) - old := os.Args + args := []string{"awl", "+yaml", "@1.1.1.1"} - os.Args = []string{"awl", "+yaml", "@1.1.1.1"} - - _, code, err := run() + _, code, err := run(args) assert.NilError(t, err) assert.Equal(t, code, 0) - os.Args = []string{"awl", "+short", "@1.1.1.1"} + args = []string{"awl", "+short", "@1.1.1.1"} - _, code, err = run() + _, code, err = run(args) assert.NilError(t, err) assert.Equal(t, code, 0) - - os.Args = old } func TestHelp(t *testing.T) { - old := os.Args - os.Stdout = os.NewFile(0, os.DevNull) - os.Stderr = os.NewFile(0, os.DevNull) + args := []string{"awl", "-h"} - os.Args = []string{"awl", "-h"} - - _, code, err := run() + _, code, err := run(args) assert.ErrorIs(t, err, zflag.ErrHelp) assert.Equal(t, code, 1) - - os.Args = old } diff --git a/pkg/awl-dns-git b/pkg/awl-dns-git index c8686b1..d876d6d 160000 --- a/pkg/awl-dns-git +++ b/pkg/awl-dns-git @@ -1 +1 @@ -Subproject commit c8686b1e149b6c42db019c77caf36475aafadd03 +Subproject commit d876d6de34a78298ed041f575662015fb7eccdb5 diff --git a/query/QUIC_test.go b/query/QUIC_test.go index 6158254..6d83199 100644 --- a/query/QUIC_test.go +++ b/query/QUIC_test.go @@ -22,8 +22,7 @@ func TestQuic(t *testing.T) { opts := util.Options{ QUIC: true, Logger: util.InitLogger(0), - Port: 853, - Request: util.Request{Server: "dns.adguard.com"}, + Request: util.Request{Server: "dns.adguard.com", Port: 853}, } testCase := util.Request{Server: "dns.//./,,adguard.com", Type: dns.TypeA, Name: "git.froth.zone"} testCase2 := util.Request{Server: "dns.adguard.com", Type: dns.TypeA, Name: "git.froth.zone"} @@ -54,7 +53,7 @@ func TestQuic(t *testing.T) { resolver, err := query.LoadResolver(opts) assert.NilError(t, err) - testCase2.Server = net.JoinHostPort(testCase2.Server, strconv.Itoa(opts.Port)) + testCase2.Server = net.JoinHostPort(testCase2.Server, strconv.Itoa(opts.Request.Port)) // if the domain is not canonical, make it canonical if !strings.HasSuffix(testCase2.Name, ".") { @@ -78,8 +77,7 @@ func TestInvalidQuic(t *testing.T) { opts := util.Options{ QUIC: true, Logger: util.InitLogger(0), - Port: 853, - Request: util.Request{Server: "example.com", Type: dns.TypeA, Name: "git.froth.zone", Timeout: 10 * time.Millisecond}, + Request: util.Request{Server: "example.com", Port: 853, Type: dns.TypeA, Name: "git.froth.zone", Timeout: 10 * time.Millisecond}, } resolver, err := query.LoadResolver(opts) assert.NilError(t, err) diff --git a/query/general_test.go b/query/general_test.go index 312cf60..fe06c12 100644 --- a/query/general_test.go +++ b/query/general_test.go @@ -17,9 +17,9 @@ func TestResolve(t *testing.T) { opts := util.Options{ Logger: util.InitLogger(0), - Port: 53, Request: util.Request{ Server: "8.8.4.1", + Port: 53, Type: dns.TypeA, Name: "example.com.", Timeout: time.Second / 2, @@ -42,9 +42,9 @@ func TestTruncate(t *testing.T) { opts := util.Options{ Logger: util.InitLogger(0), IPv4: true, - Port: 5301, Request: util.Request{ Server: "madns.binarystar.systems", + Port: 5301, Type: dns.TypeTXT, Name: "limit.txt.example.", }, @@ -70,9 +70,10 @@ func TestResolveAgain(t *testing.T) { util.Options{ Logger: util.InitLogger(0), TCP: true, - Port: 53, + Request: util.Request{ Server: "8.8.4.4", + Port: 53, Type: dns.TypeA, Name: "example.com.", }, @@ -81,9 +82,9 @@ func TestResolveAgain(t *testing.T) { { util.Options{ Logger: util.InitLogger(0), - Port: 53, Request: util.Request{ Server: "8.8.4.4", + Port: 53, Type: dns.TypeAAAA, Name: "example.com.", }, @@ -93,9 +94,9 @@ func TestResolveAgain(t *testing.T) { util.Options{ Logger: util.InitLogger(0), TLS: true, - Port: 853, Request: util.Request{ Server: "dns.google", + Port: 853, Type: dns.TypeAAAA, Name: "example.com.", }, diff --git a/query/print.go b/query/print.go index 5eaa6bf..d3a79c3 100644 --- a/query/print.go +++ b/query/print.go @@ -18,10 +18,210 @@ import ( "gopkg.in/yaml.v3" ) +// ToString turns the response into something that looks a lot like dig +// +// Much of this is taken from https://github.com/miekg/dns/blob/master/msg.go#L900 +func ToString(res util.Response, opts util.Options) (string, error) { + if res.DNS == nil { + return " MsgHdr", errNoMessage + } + + var ( + s string + opt *dns.OPT + ) + + if !opts.Short { + if opts.Display.Comments { + s += res.DNS.MsgHdr.String() + " " + s += "QUERY: " + strconv.Itoa(len(res.DNS.Question)) + ", " + s += "ANSWER: " + strconv.Itoa(len(res.DNS.Answer)) + ", " + s += "AUTHORITY: " + strconv.Itoa(len(res.DNS.Ns)) + ", " + s += "ADDITIONAL: " + strconv.Itoa(len(res.DNS.Extra)) + "\n" + opt = res.DNS.IsEdns0() + + if opt != nil && opts.Display.Opt { + // OPT PSEUDOSECTION + s += opt.String() + "\n" + } + } + + if opts.Display.Question { + if len(res.DNS.Question) > 0 { + if opts.Display.Comments { + s += "\n;; QUESTION SECTION:\n" + } + + for _, r := range res.DNS.Question { + str, err := stringParse(r.String(), false, opts) + if err != nil { + return "", fmt.Errorf("%w", err) + } + + s += str + "\n" + } + } + } + + if opts.Display.Answer { + if len(res.DNS.Answer) > 0 { + if opts.Display.Comments { + s += "\n;; ANSWER SECTION:\n" + } + + for _, r := range res.DNS.Answer { + if r != nil { + str, err := stringParse(r.String(), true, opts) + if err != nil { + return "", fmt.Errorf("%w", err) + } + + s += str + "\n" + } + } + } + } + + if opts.Display.Authority { + if len(res.DNS.Ns) > 0 { + if opts.Display.Comments { + s += "\n;; AUTHORITY SECTION:\n" + } + + for _, r := range res.DNS.Ns { + if r != nil { + str, err := stringParse(r.String(), true, opts) + if err != nil { + return "", fmt.Errorf("%w", err) + } + + s += str + "\n" + } + } + } + } + + if opts.Display.Additional { + if len(res.DNS.Extra) > 0 && (opt == nil || len(res.DNS.Extra) > 1) { + if opts.Display.Comments { + s += "\n;; ADDITIONAL SECTION:\n" + } + + for _, r := range res.DNS.Extra { + if r != nil && r.Header().Rrtype != dns.TypeOPT { + str, err := stringParse(r.String(), true, opts) + if err != nil { + return "", fmt.Errorf("%w", err) + } + + s += str + "\n" + } + } + } + } + + if opts.Display.Statistics { + s += "\n;; Query time: " + res.RTT.String() + s += "\n;; SERVER: " + opts.Request.Server + serverExtra(opts) + s += "\n;; WHEN: " + time.Now().Format(time.RFC1123Z) + s += "\n;; MSG SIZE rcvd: " + strconv.Itoa(res.DNS.Len()) + "\n" + } + } else { + // Print just the responses, nothing else + for i, resp := range res.DNS.Answer { + temp := strings.Split(resp.String(), "\t") + s += temp[len(temp)-1] + + if opts.Identify { + s += " from server " + opts.Request.Server + " in " + res.RTT.String() + } + + // Don't print newline on last line + if i != len(res.DNS.Answer)-1 { + s += "\n" + } + } + } + + return s, nil +} + +func serverExtra(opts util.Options) string { + // Add extra information to server string + var extra string + + switch { + case opts.TCP: + extra = ":" + strconv.Itoa(opts.Request.Port) + " (TCP)" + case opts.TLS: + extra = ":" + strconv.Itoa(opts.Request.Port) + " (TLS)" + case opts.HTTPS, opts.DNSCrypt: + extra = "" + case opts.QUIC: + extra = ":" + strconv.Itoa(opts.Request.Port) + " (QUIC)" + default: + extra = ":" + strconv.Itoa(opts.Request.Port) + " (UDP)" + } + + return extra +} + +// stringParse edits the raw responses to user requests. +func stringParse(str string, isAns bool, opts util.Options) (string, error) { + split := strings.Split(str, "\t") + + // Make edits if so requested + + // TODO: make less ew? + // This exists because the question section should be left alone EXCEPT for punycode. + + if isAns { + if !opts.Display.TTL { + // Remove from existence + split = append(split[:1], split[2:]...) + } + + if !opts.Display.ShowClass { + // Position depends on if the TTL is there or not. + if opts.Display.TTL { + split = append(split[:2], split[3:]...) + } else { + split = append(split[:1], split[2:]...) + } + } + + if opts.Display.TTL && opts.Display.HumanTTL { + ttl, _ := strconv.Atoi(split[1]) + split[1] = (time.Duration(ttl) * time.Second).String() + } + } + + if opts.Display.UcodeTranslate { + var ( + err error + semi string + ) + + if strings.HasPrefix(split[0], ";") { + split[0] = strings.TrimPrefix(split[0], ";") + semi = ";" + } + + split[0], err = idna.ToUnicode(split[0]) + if err != nil { + return "", fmt.Errorf("punycode: %w", err) + } + + split[0] = semi + split[0] + } + + return strings.Join(split, "\t"), nil +} + // PrintSpecial is for printing as JSON, XML or YAML. // As of now JSON and XML use the stdlib version. -func PrintSpecial(msg *dns.Msg, opts util.Options) (string, error) { - formatted, err := MakePrintable(msg, opts) +func PrintSpecial(res util.Response, opts util.Options) (string, error) { + formatted, err := MakePrintable(res, opts) if err != nil { return "", err } @@ -52,11 +252,34 @@ func PrintSpecial(msg *dns.Msg, opts util.Options) (string, error) { // MakePrintable takes a DNS message and makes it nicer to be printed as JSON,YAML, // and XML. Little is changed beyond naming. -func MakePrintable(msg *dns.Msg, opts util.Options) (*Message, error) { - var err error - +func MakePrintable(res util.Response, opts util.Options) (*Message, error) { + var ( + err error + msg = res.DNS + ) + // The things I do for compatibility ret := Message{ - Header: msg.MsgHdr, + Header: Header{ + ID: msg.Id, + Response: msg.Response, + Opcode: dns.OpcodeToString[msg.Opcode], + Authoritative: msg.Authoritative, + Truncated: msg.Truncated, + RecursionDesired: msg.RecursionDesired, + RecursionAvailable: msg.RecursionAvailable, + Zero: msg.Zero, + AuthenticatedData: msg.AuthenticatedData, + CheckingDisabled: msg.CheckingDisabled, + Status: dns.RcodeToString[msg.Rcode], + }, + } + + opt := msg.IsEdns0() + if opt != nil && opts.Display.Opt { + ret.Opt, err = parseOpt(*opt) + if err != nil { + return nil, fmt.Errorf("edns print: %w", err) + } } for _, question := range msg.Question { @@ -85,8 +308,8 @@ func MakePrintable(msg *dns.Msg, opts util.Options) (*Message, error) { name string ) - if opts.ShowTTL { - if opts.HumanTTL { + if opts.Display.TTL { + if opts.Display.HumanTTL { ttl = (time.Duration(answer.Header().Ttl) * time.Second).String() } else { ttl = strconv.Itoa(int(answer.Header().Ttl)) @@ -122,8 +345,8 @@ func MakePrintable(msg *dns.Msg, opts util.Options) (*Message, error) { name string ) - if opts.ShowTTL { - if opts.HumanTTL { + if opts.Display.TTL { + if opts.Display.HumanTTL { ttl = (time.Duration(ns.Header().Ttl) * time.Second).String() } else { ttl = strconv.Itoa(int(ns.Header().Ttl)) @@ -139,7 +362,7 @@ func MakePrintable(msg *dns.Msg, opts util.Options) (*Message, error) { name = ns.Header().Name } - ret.Ns = append(ret.Ns, Answer{ + ret.Authority = append(ret.Authority, Answer{ RRHeader: RRHeader{ Name: name, Type: dns.TypeToString[ns.Header().Rrtype], @@ -162,8 +385,8 @@ func MakePrintable(msg *dns.Msg, opts util.Options) (*Message, error) { name string ) - if opts.ShowTTL { - if opts.HumanTTL { + if opts.Display.TTL { + if opts.Display.HumanTTL { ttl = (time.Duration(additional.Header().Ttl) * time.Second).String() } else { ttl = strconv.Itoa(int(additional.Header().Ttl)) @@ -192,12 +415,15 @@ func MakePrintable(msg *dns.Msg, opts util.Options) (*Message, error) { } } - opt := msg.IsEdns0() - if opt != nil && opts.Display.Opt { - ret.Opt, err = parseOpt(*opt) - if err != nil { - return nil, fmt.Errorf("edns print: %w", err) + if opts.Display.Statistics { + ret.Statistics = Statistics{ + RTT: res.RTT.String(), + Server: opts.Request.Server + serverExtra(opts), + When: time.Now().Format(time.RFC1123Z), + MsgSize: res.DNS.Len(), } + } else { + ret.Statistics = Statistics{} } return &ret, nil diff --git a/query/print_test.go b/query/print_test.go index 8dce5d1..3f88e3c 100644 --- a/query/print_test.go +++ b/query/print_test.go @@ -16,15 +16,16 @@ func TestRealPrint(t *testing.T) { opts := []util.Options{ { - Logger: util.InitLogger(0), - Port: 53, - TCP: true, - ShowQuery: true, - RD: true, - ShowTTL: true, - HumanTTL: true, - JSON: true, - Display: util.Displays{ + Logger: util.InitLogger(0), + + TCP: true, + + HeaderFlags: util.HeaderFlags{ + RD: true, + }, + + JSON: true, + Display: util.Display{ Comments: true, Question: true, Answer: true, @@ -32,9 +33,13 @@ func TestRealPrint(t *testing.T) { Additional: true, Statistics: true, UcodeTranslate: true, + TTL: true, + HumanTTL: true, + ShowQuery: true, }, Request: util.Request{ Server: "a.gtld-servers.net", + Port: 53, Type: dns.StringToType["NS"], Class: 1, Name: "google.com.", @@ -44,17 +49,18 @@ func TestRealPrint(t *testing.T) { }, }, { - Logger: util.InitLogger(0), - Port: 53, - TCP: true, - ShowQuery: true, - RD: true, + Logger: util.InitLogger(0), + + TCP: true, + HeaderFlags: util.HeaderFlags{ + RD: true, + }, Verbosity: 0, - ShowTTL: true, - Short: true, - Identify: true, - YAML: false, - Display: util.Displays{ + + Short: true, + Identify: true, + YAML: false, + Display: util.Display{ Comments: true, Question: true, Answer: true, @@ -62,9 +68,12 @@ func TestRealPrint(t *testing.T) { Additional: true, Statistics: true, UcodeTranslate: true, + TTL: true, + ShowQuery: true, }, Request: util.Request{ Server: "ns1.google.com", + Port: 53, Type: dns.StringToType["NS"], Class: 1, Name: "google.com.", @@ -76,16 +85,14 @@ func TestRealPrint(t *testing.T) { }, }, { - Logger: util.InitLogger(0), - Port: 53, - HTTPS: true, - ShowQuery: true, - RD: true, - ShowTTL: true, - HumanTTL: true, - Identify: true, - XML: true, - Display: util.Displays{ + Logger: util.InitLogger(0), + HTTPS: true, + HeaderFlags: util.HeaderFlags{ + RD: true, + }, + Identify: true, + XML: true, + Display: util.Display{ Comments: true, Question: true, Answer: true, @@ -93,9 +100,13 @@ func TestRealPrint(t *testing.T) { Additional: true, Statistics: true, UcodeTranslate: false, + TTL: true, + HumanTTL: true, + ShowQuery: true, }, Request: util.Request{ Server: "https://dns.froth.zone/dns-query", + Port: 443, Type: dns.StringToType["NS"], Class: 1, Name: "freecumextremist.com.", @@ -108,14 +119,13 @@ func TestRealPrint(t *testing.T) { }, }, { - Logger: util.InitLogger(0), - Port: 853, - TLS: true, - ShowQuery: true, - RD: true, + Logger: util.InitLogger(0), + TLS: true, + HeaderFlags: util.HeaderFlags{ + RD: true, + }, Verbosity: 0, - ShowTTL: false, - Display: util.Displays{ + Display: util.Display{ Comments: true, Question: true, Answer: true, @@ -123,25 +133,29 @@ func TestRealPrint(t *testing.T) { Additional: true, Statistics: true, UcodeTranslate: true, + TTL: false, + ShowQuery: true, }, Request: util.Request{ Server: "dns.google", + Port: 853, Type: dns.StringToType["NS"], Class: 1, Name: "freecumextremist.com.", }, }, { - Logger: util.InitLogger(0), - Port: 53, - TCP: true, - ShowQuery: true, - AA: true, - RD: true, + Logger: util.InitLogger(0), + TCP: true, + + HeaderFlags: util.HeaderFlags{ + AA: true, + RD: true, + }, Verbosity: 0, - ShowTTL: true, - YAML: true, - Display: util.Displays{ + + YAML: true, + Display: util.Display{ Comments: true, Question: true, Answer: true, @@ -149,9 +163,12 @@ func TestRealPrint(t *testing.T) { Additional: true, Statistics: true, UcodeTranslate: false, + TTL: true, + ShowQuery: true, }, Request: util.Request{ Server: "rin.froth.zone", + Port: 53, Type: dns.StringToType["A"], Class: 1, Name: "froth.zone.", @@ -176,7 +193,7 @@ func TestRealPrint(t *testing.T) { if test.JSON || test.XML || test.YAML { str := "" - str, err = query.PrintSpecial(resp.DNS, test) + str, err = query.PrintSpecial(resp, test) assert.NilError(t, err) assert.Assert(t, str != "") } @@ -190,7 +207,7 @@ func TestRealPrint(t *testing.T) { func TestBadFormat(t *testing.T) { t.Parallel() - _, err := query.PrintSpecial(new(dns.Msg), util.Options{}) + _, err := query.PrintSpecial(util.Response{DNS: new(dns.Msg)}, util.Options{}) assert.ErrorContains(t, err, "never happen") } diff --git a/query/query.go b/query/query.go index 9b1cda9..6bccb34 100644 --- a/query/query.go +++ b/query/query.go @@ -102,7 +102,7 @@ func CreateQuery(opts util.Options) (util.Response, error) { opts.Logger.Debug(req) if !opts.Short { - if opts.ShowQuery { + if opts.Display.ShowQuery { opts.Logger.Info("Printing constructed query") var ( @@ -111,7 +111,7 @@ func CreateQuery(opts util.Options) (util.Response, error) { ) if opts.JSON || opts.XML || opts.YAML { - str, err = PrintSpecial(req, opts) + str, err = PrintSpecial(util.Response{DNS: req}, opts) if err != nil { return util.Response{}, err } @@ -133,7 +133,7 @@ func CreateQuery(opts util.Options) (util.Response, error) { fmt.Println(str) - opts.ShowQuery = false + opts.Display.ShowQuery = false } } diff --git a/query/query_test.go b/query/query_test.go index 6b4d504..5a5183f 100644 --- a/query/query_test.go +++ b/query/query_test.go @@ -16,18 +16,20 @@ func TestCreateQ(t *testing.T) { in := []util.Options{ { - Logger: util.InitLogger(0), - Port: 53, - Z: true, - ShowQuery: true, - YAML: true, + Logger: util.InitLogger(0), + HeaderFlags: util.HeaderFlags{ + Z: true, + }, + + YAML: true, Request: util.Request{ Server: "8.8.4.4", + Port: 53, Type: dns.TypeA, Name: "example.com.", }, - Display: util.Displays{ + Display: util.Display{ Comments: true, Question: true, Opt: true, @@ -35,6 +37,7 @@ func TestCreateQ(t *testing.T) { Authority: true, Additional: true, Statistics: true, + ShowQuery: true, }, EDNS: util.EDNS{ ZFlag: 1, @@ -50,18 +53,19 @@ func TestCreateQ(t *testing.T) { }, }, { - Logger: util.InitLogger(0), - Port: 53, - Z: true, - ShowQuery: true, - XML: true, + Logger: util.InitLogger(0), + HeaderFlags: util.HeaderFlags{ + Z: true, + }, + XML: true, Request: util.Request{ Server: "8.8.4.4", + Port: 53, Type: dns.TypeA, Name: "example.com.", }, - Display: util.Displays{ + Display: util.Display{ Comments: true, Question: true, Opt: true, @@ -70,22 +74,21 @@ func TestCreateQ(t *testing.T) { Additional: true, Statistics: true, UcodeTranslate: true, + ShowQuery: true, }, }, { Logger: util.InitLogger(0), - Port: 853, - // Z: true, - ShowQuery: true, - JSON: true, - QUIC: true, + JSON: true, + QUIC: true, Request: util.Request{ Server: "dns.adguard.com", + Port: 853, Type: dns.TypeA, Name: "example.com.", }, - Display: util.Displays{ + Display: util.Display{ Comments: true, Question: true, Opt: true, @@ -93,6 +96,7 @@ func TestCreateQ(t *testing.T) { Authority: true, Additional: true, Statistics: true, + ShowQuery: true, }, EDNS: util.EDNS{ EnableEDNS: true, @@ -113,7 +117,7 @@ func TestCreateQ(t *testing.T) { assert.NilError(t, err) assert.Assert(t, res != util.Response{}) - str, err := query.PrintSpecial(res.DNS, opt) + str, err := query.PrintSpecial(res, opt) assert.NilError(t, err) assert.Assert(t, str != "") diff --git a/query/resolver.go b/query/resolver.go index 2ede1db..049522e 100644 --- a/query/resolver.go +++ b/query/resolver.go @@ -31,7 +31,7 @@ func LoadResolver(opts util.Options) (Resolver, error) { }, nil case opts.QUIC: opts.Logger.Info("loading DNS-over-QUIC resolver") - opts.Request.Server = net.JoinHostPort(opts.Request.Server, strconv.Itoa(opts.Port)) + opts.Request.Server = net.JoinHostPort(opts.Request.Server, strconv.Itoa(opts.Request.Port)) return &QUICResolver{ opts: opts, @@ -48,7 +48,7 @@ func LoadResolver(opts util.Options) (Resolver, error) { }, nil default: opts.Logger.Info("loading standard/DNS-over-TLS resolver") - opts.Request.Server = net.JoinHostPort(opts.Request.Server, strconv.Itoa(opts.Port)) + opts.Request.Server = net.JoinHostPort(opts.Request.Server, strconv.Itoa(opts.Request.Port)) return &StandardResolver{ opts: opts, diff --git a/query/struct.go b/query/struct.go index 2617b4f..3ad2159 100644 --- a/query/struct.go +++ b/query/struct.go @@ -4,48 +4,63 @@ package query import ( "errors" - "fmt" - "strconv" - "strings" - "time" - - "git.froth.zone/sam/awl/util" - "github.com/miekg/dns" - "golang.org/x/net/idna" ) // Message is for overall DNS responses. // -//nolint:govet // Better output is worth 32 bytes. +//nolint:govet // Better looking output is worth a few bytes. type Message struct { - Header dns.MsgHdr `json:"header,omitempty" xml:"header,omitempty" yaml:",omitempty"` - Opt []Opts `json:"opt,omitempty" xml:"opt,omitempty" yaml:"opt,omitempty"` - Question []Question `json:"question,omitempty" xml:"question,omitempty" yaml:",omitempty"` - Answer []Answer `json:"answer,omitempty" xml:"answer,omitempty" yaml:",omitempty"` - Ns []Answer `json:"ns,omitempty" xml:"ns,omitempty" yaml:",omitempty"` - Additional []Answer `json:"additional,omitempty" xml:"additional,omitempty" yaml:",omitempty"` + // Header section + Header `json:"header,omitempty" xml:"header,omitempty" yaml:"header,omitempty"` + // Opt Pseudosection + Opt []Opts `json:"opt,omitempty" xml:"opt,omitempty" yaml:"opt,omitempty"` + // Question Section + Question []Question `json:"question,omitempty" xml:"question,omitempty" yaml:"question,omitempty"` + // Answer Section + Answer []Answer `json:"answer,omitempty" xml:"answer,omitempty" yaml:"answer,omitempty"` + // Authority Section + Authority []Answer `json:"authority,omitempty" xml:"authority,omitempty" yaml:"authority,omitempty"` + // Additional Section + Additional []Answer `json:"additional,omitempty" xml:"additional,omitempty" yaml:"additional,omitempty"` + // Statistics :) + Statistics `json:"statistics,omitempty" xml:"statistics,omitempty" yaml:"statistics,omitempty"` +} + +// Header is the header. +type Header struct { + Opcode string `json:"opcode," xml:"opcode," yaml:"opcode" example:"QUERY"` + Status string `json:"status," xml:"status," yaml:"status" example:"NOERR"` + ID uint16 `json:"id," xml:"id," yaml:"id" example:"12"` + Response bool `json:"response," xml:"response," yaml:"response" example:"true"` + Authoritative bool `json:"authoritative," xml:"authoritative," yaml:"authoritative" example:"false"` + Truncated bool `json:"truncated," xml:"truncated," yaml:"truncated" example:"false"` + RecursionDesired bool `json:"recursionDesired," xml:"recursionDesired," yaml:"recursionDesired" example:"true"` + RecursionAvailable bool `json:"recursionAvailable," xml:"recursionAvailable," yaml:"recursionAvailable" example:"true"` + Zero bool `json:"zero," xml:"zero," yaml:"zero" example:"false"` + AuthenticatedData bool `json:"authenticatedData," xml:"authenticatedData," yaml:"authenticatedData" example:"false"` + CheckingDisabled bool `json:"checkingDisabled," xml:"checkingDisabled," yaml:"checkingDisabled" example:"false"` } // Question is a DNS Query. type Question struct { - Name string `json:"name,omitempty" xml:"name,omitempty" yaml:",omitempty"` - Class string `json:"class,omitempty" xml:"class,omitempty" yaml:",omitempty"` - Type string `json:"type,omitempty" xml:"type,omitempty" yaml:",omitempty"` + Name string `json:"name,omitempty" xml:"name,omitempty" yaml:"name,omitempty" example:"localhost"` + Class string `json:"class,omitempty" xml:"class,omitempty" yaml:"class,omitempty" example:"A"` + Type string `json:"type,omitempty" xml:"type,omitempty" yaml:"type,omitempty" example:"IN"` } // RRHeader is for DNS Resource Headers. type RRHeader struct { - Name string `json:"name,omitempty" xml:"name,omitempty" yaml:",omitempty"` - TTL string `json:"ttl,omitempty" xml:"ttl,omitempty" yaml:",omitempty"` - Class string `json:"class,omitempty" xml:"class,omitempty" yaml:",omitempty"` - Type string `json:"type,omitempty" xml:"type,omitempty" yaml:",omitempty"` + Name string `json:"name,omitempty" xml:"name,omitempty" yaml:"name,omitempty" example:"127.0.0.1"` + TTL string `json:"ttl,omitempty" xml:"ttl,omitempty" yaml:"ttl,omitempty" example:"0ms"` + Class string `json:"class,omitempty" xml:"class,omitempty" yaml:"class,omitempty" example:"A"` + Type string `json:"type,omitempty" xml:"type,omitempty" yaml:"type,omitempty" example:"IN"` Rdlength uint16 `json:"-" xml:"-" yaml:"-"` } // Opts is for the OPT pseudosection, nearly exclusively for EDNS. type Opts struct { - Name string `json:"name,omitempty" xml:"name,omitempty" yaml:",omitempty"` - Value string `json:"value" xml:"value" yaml:""` + Name string `json:"name,omitempty" xml:"name,omitempty" yaml:"name,omitempty"` + Value string `json:"value" xml:"value" yaml:"value"` } // Answer is for a DNS Response. @@ -54,199 +69,12 @@ type Answer struct { RRHeader `json:"header,omitempty" xml:"header,omitempty" yaml:"header,omitempty"` } -// ToString turns the response into something that looks a lot like dig -// -// Much of this is taken from https://github.com/miekg/dns/blob/master/msg.go#L900 -func ToString(res util.Response, opts util.Options) (string, error) { - if res.DNS == nil { - return " MsgHdr", errNoMessage - } - - var ( - s string - opt *dns.OPT - ) - - if !opts.Short { - if opts.Display.Comments { - s += res.DNS.MsgHdr.String() + " " - s += "QUERY: " + strconv.Itoa(len(res.DNS.Question)) + ", " - s += "ANSWER: " + strconv.Itoa(len(res.DNS.Answer)) + ", " - s += "AUTHORITY: " + strconv.Itoa(len(res.DNS.Ns)) + ", " - s += "ADDITIONAL: " + strconv.Itoa(len(res.DNS.Extra)) + "\n" - opt = res.DNS.IsEdns0() - - if opt != nil && opts.Display.Opt { - // OPT PSEUDOSECTION - s += opt.String() + "\n" - } - } - - if opts.Display.Question { - if len(res.DNS.Question) > 0 { - if opts.Display.Comments { - s += "\n;; QUESTION SECTION:\n" - } - - for _, r := range res.DNS.Question { - str, err := stringParse(r.String(), false, opts) - if err != nil { - return "", fmt.Errorf("%w", err) - } - - s += str + "\n" - } - } - } - - if opts.Display.Answer { - if len(res.DNS.Answer) > 0 { - if opts.Display.Comments { - s += "\n;; ANSWER SECTION:\n" - } - - for _, r := range res.DNS.Answer { - if r != nil { - str, err := stringParse(r.String(), true, opts) - if err != nil { - return "", fmt.Errorf("%w", err) - } - - s += str + "\n" - } - } - } - } - - if opts.Display.Authority { - if len(res.DNS.Ns) > 0 { - if opts.Display.Comments { - s += "\n;; AUTHORITY SECTION:\n" - } - - for _, r := range res.DNS.Ns { - if r != nil { - str, err := stringParse(r.String(), true, opts) - if err != nil { - return "", fmt.Errorf("%w", err) - } - - s += str + "\n" - } - } - } - } - - if opts.Display.Additional { - if len(res.DNS.Extra) > 0 && (opt == nil || len(res.DNS.Extra) > 1) { - if opts.Display.Comments { - s += "\n;; ADDITIONAL SECTION:\n" - } - - for _, r := range res.DNS.Extra { - if r != nil && r.Header().Rrtype != dns.TypeOPT { - str, err := stringParse(r.String(), true, opts) - if err != nil { - return "", fmt.Errorf("%w", err) - } - - s += str + "\n" - } - } - } - } - - if opts.Display.Statistics { - s += "\n;; Query time: " + res.RTT.String() - // Add extra information to server string - var extra string - - switch { - case opts.TCP: - extra = ":" + strconv.Itoa(opts.Port) + " (TCP)" - case opts.TLS: - extra = ":" + strconv.Itoa(opts.Port) + " (TLS)" - case opts.HTTPS, opts.DNSCrypt: - extra = "" - case opts.QUIC: - extra = ":" + strconv.Itoa(opts.Port) + " (QUIC)" - default: - extra = ":" + strconv.Itoa(opts.Port) + " (UDP)" - } - - s += "\n;; SERVER: " + opts.Request.Server + extra - s += "\n;; WHEN: " + time.Now().Format(time.RFC1123Z) - s += "\n;; MSG SIZE rcvd: " + strconv.Itoa(res.DNS.Len()) + "\n" - } - } else { - // Print just the responses, nothing else - for i, resp := range res.DNS.Answer { - temp := strings.Split(resp.String(), "\t") - s += temp[len(temp)-1] - - if opts.Identify { - s += " from server " + opts.Request.Server + " in " + res.RTT.String() - } - - // Don't print newline on last line - if i != len(res.DNS.Answer)-1 { - s += "\n" - } - } - } - - return s, nil -} - -func stringParse(str string, isAns bool, opts util.Options) (string, error) { - split := strings.Split(str, "\t") - - // Make edits if so requested - - // TODO: make less ew? - // This exists because the question section should be left alone EXCEPT for punycode. - - if isAns { - if !opts.ShowTTL { - // Remove from existence - split = append(split[:1], split[2:]...) - } - - if !opts.ShowClass { - // Position depends on if the TTL is there or not. - if opts.ShowTTL { - split = append(split[:2], split[3:]...) - } else { - split = append(split[:1], split[2:]...) - } - } - - if opts.ShowTTL && opts.HumanTTL { - ttl, _ := strconv.Atoi(split[1]) - split[1] = (time.Duration(ttl) * time.Second).String() - } - } - - if opts.Display.UcodeTranslate { - var ( - err error - semi string - ) - - if strings.HasPrefix(split[0], ";") { - split[0] = strings.TrimPrefix(split[0], ";") - semi = ";" - } - - split[0], err = idna.ToUnicode(split[0]) - if err != nil { - return "", fmt.Errorf("punycode: %w", err) - } - - split[0] = semi + split[0] - } - - return strings.Join(split, "\t"), nil +// Statistics is the little bit at the bottom :). +type Statistics struct { + RTT string `json:"queryTime,omitempty" xml:"queryTime,omitempty" yaml:"queryTime,omitempty"` + Server string `json:"server,omitempty" xml:"server,omitempty" yaml:"server,omitempty"` + When string `json:"when,omitempty" xml:"when,omitempty" yaml:"when,omitempty"` + MsgSize int `json:"msgSize,omitempty" xml:"msgSize,omitempty" yaml:"msgSize,omitempty"` } var errNoMessage = errors.New("no message") diff --git a/template.mk b/template.mk index fe820f5..0223929 100644 --- a/template.mk +++ b/template.mk @@ -1,7 +1,7 @@ # SPDX-License-Identifier: BSD-3-Clause # Template for the BSD/GNU makefiles -HASH ?= `git describe --always --dirty --broken | sed 's/\([^-]*-g\)/r\1/;s/-/./g' || echo "UNKNOWN"` +HASH ?= `git describe --always --dirty --broken | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' || echo "UNKNOWN"` SOURCES ?= $(shell find . -name "*.go" -type f ! -name '*_test*') TEST_SOURCES ?= $(shell find . -name "*_test.go" -type f) @@ -90,4 +90,4 @@ clean: .PHONY: help help: @echo "Usage: " - @sed -n 's/^##//p' $(MAKEFILE_LIST) | column -t -s ':' | sed -e 's/^/ /' \ No newline at end of file + @sed -n 's/^##//p' $(MAKEFILE_LIST) | column -t -s ':' | sed -e 's/^/ /' diff --git a/util/options.go b/util/options.go index 91e5f14..c406801 100644 --- a/util/options.go +++ b/util/options.go @@ -12,67 +12,134 @@ import ( // Options is the grand structure for all query options. type Options struct { - Logger *logawl.Logger - TLSHost string + // The logger + Logger *logawl.Logger `json:"-"` + // Host to verify TLS cert with + TLSHost string `json:"tlsHost" example:""` + // EDNS Options EDNS - Request Request - Port int - Verbosity int - Display Displays - TC bool - ShowTTL bool - ShowClass bool - ShowQuery bool - AA bool - AD bool - CD bool - QR bool - RD bool - RA bool - IPv4 bool - Z bool - Reverse bool - HumanTTL bool - Truncate bool - Short bool - Identify bool - JSON bool - XML bool - YAML bool - QUIC bool - HTTPS bool - TLSNoVerify bool - TLS bool - DNSCrypt bool - TCP bool - IPv6 bool + // DNS request :) + Request Request + + // Verbosity levels, see [logawl.AllLevels] + Verbosity int `json:"-" example:"0"` + // Display options + Display Display + // Ignore Truncation + Truncate bool `json:"ignoreTruncate" example:"false"` + // Print only the answer + Short bool `json:"short" example:"false"` + // When Short is true, display where the query came from + Identify bool `json:"identify" example:"false"` + // Perform a reverse DNS query when true + Reverse bool `json:"reverse" example:"false"` + + HeaderFlags + + // Display resposne as JSON + JSON bool `json:"-" xml:"-" yaml:"-"` + // Display response as XML + XML bool `json:"-" xml:"-" yaml:"-"` + // Display response as YAML + YAML bool `json:"-" xml:"-" yaml:"-"` + + // Use TCP instead of UDP to make the query + TCP bool `json:"tcp" example:"false"` + // Use DNS-over-TLS to make the query + TLS bool `json:"dnsOverTLS" example:"false"` + // When using TLS, ignore certificates + TLSNoVerify bool `json:"tlsNoVerify" example:"false"` + // Use DNS-over-HTTPS to make the query + HTTPS bool `json:"dnsOverHTTPS" example:"false"` + // Use DNS-over-QUIC to make the query + //nolint:tagliatelle // QUIC is an acronym + QUIC bool `json:"dnsOverQUIC" example:"false"` + // Use DNSCrypt to make the query + DNSCrypt bool `json:"dnscrypt" example:"false"` + + // Force IPv4 only + IPv4 bool `json:"forceIPv4" example:"false"` + // Force IPv6 only + IPv6 bool `json:"forceIPv6" example:"false"` } -// Displays contains toggles for what to (and not to) display. -type Displays struct { - Comments bool - Question bool // QUESTION SECTION - Opt bool // OPT PSEUDOSECTION - Answer bool // ANSWER SECTION - Authority bool // AUTHORITY SECTION - Additional bool // ADDITIONAL SECTION - Statistics bool // Query time, message size, etc. - UcodeTranslate bool // Translate Punycode back to Unicode +// HeaderFlags are the flags that are in DNS headers. +type HeaderFlags struct { + // Authoritative Answer DNS query flag + AA bool `json:"authoritative" example:"false"` + // Authenticated Data DNS query flag + AD bool `json:"authenticatedData" example:"false"` + // Checking Disabled DNS query flag + CD bool `json:"checkingDisabled" example:"false"` + // QueRy DNS query flag + QR bool `json:"query" example:"false"` + // Recursion Desired DNS query flag + RD bool `json:"recursionDesired" example:"true"` + // Recursion Available DNS query flag + RA bool `json:"recursionAvailable" example:"false"` + // TrunCated DNS query flag + TC bool `json:"truncated" example:"false"` + // Zero DNS query flag + Z bool `json:"zero" example:"false"` +} + +// Display contains toggles for what to (and not to) display. +type Display struct { + /* Section displaying */ + + // Comments? + Comments bool `json:"comments" example:"true"` + // QUESTION SECTION + Question bool `json:"question" example:"true"` + // OPT PSEUDOSECTION + Opt bool `json:"opt" example:"true"` + // ANSWER SECTION + Answer bool `json:"answer" example:"true"` + // AUTHORITY SECTION + Authority bool `json:"authority" example:"true"` + // ADDITIONAL SECTION + Additional bool `json:"additional" example:"true"` + // Query time, message size, etc. + Statistics bool `json:"statistics" example:"true"` + // Display TTL in response + TTL bool `json:"ttl" example:"true"` + + /* Answer formatting */ + + // Display Class in response + ShowClass bool `json:"showClass" example:"true"` + // Display query before it is sent + ShowQuery bool `json:"showQuery" example:"false"` + // Display TTL as human-readable + HumanTTL bool `json:"humanTTL" example:"false"` + // Translate Punycode back to Unicode + UcodeTranslate bool `json:"unicode" example:"true"` } // EDNS contains toggles for various EDNS options. type EDNS struct { - Subnet dns.EDNS0_SUBNET - ZFlag uint16 - BufSize uint16 - EnableEDNS bool - Cookie bool - DNSSEC bool - Expire bool - KeepOpen bool - Nsid bool - Padding bool - Version uint8 + // Subnet to originate query from. + Subnet dns.EDNS0_SUBNET `json:"subnet"` + // Must Be Zero flag + ZFlag uint16 `json:"zflag" example:"0"` + // UDP buffer size + BufSize uint16 `json:"bufSize" example:"1232"` + // Enable/Disable EDNS entirely + EnableEDNS bool `json:"edns" example:"false"` + // Sending EDNS cookie + Cookie bool `json:"cookie" example:"true"` + // Enabling DNSSEC + DNSSEC bool `json:"dnssec" example:"false"` + // Sending EDNS Expire + Expire bool `json:"expire" example:"false"` + // Sending EDNS TCP keepopen + KeepOpen bool `json:"keepOpen" example:"false"` + // Sending EDNS NSID + Nsid bool `json:"nsid" example:"false"` + // Send EDNS Padding + Padding bool `json:"padding" example:"false"` + // Set EDNS version (default: 0) + Version uint8 `json:"version" example:"0"` } // ParseSubnet takes a subnet argument and makes it into one that the DNS library diff --git a/util/query.go b/util/query.go index 20851b6..8e1dd72 100644 --- a/util/query.go +++ b/util/query.go @@ -10,16 +10,26 @@ import ( // Response is the DNS response. type Response struct { - DNS *dns.Msg // The full DNS response - RTT time.Duration `json:"rtt"` // The time it took to make the DNS query + // The full DNS response + DNS *dns.Msg `json:"response"` + // The time it took to make the DNS query + RTT time.Duration `json:"rtt" example:"2000000000"` } // Request is a structure for a DNS query. type Request struct { - Server string `json:"server"` - Name string `json:"name"` - Timeout time.Duration - Retries int - Type uint16 `json:"request"` - Class uint16 `json:"class"` + // Server to query + Server string `json:"server" example:"1.0.0.1"` + // Domain to query + Name string `json:"name" example:"example.com"` + // Duration to wait until marking request as failed + Timeout time.Duration `json:"timeout" example:"2000000000"` + // Port to make DNS request on + Port int `json:"port" example:"53"` + // Number of failures to make before giving up + Retries int `json:"retries" example:"2"` + // Request type, eg. A, AAAA, NAPTR + Type uint16 `json:"type" example:"1"` + // Request class, eg. IN + Class uint16 `json:"class" example:"1"` }