more API stuffs
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing

Signed-off-by: Sam Therapy <sam@samtherapy.net>
This commit is contained in:
Sam Therapy 2022-09-15 12:47:00 +02:00
parent 4d37988607
commit 1304afce16
Signed by: sam
GPG key ID: 4D8B07C18F31ACBD
17 changed files with 386 additions and 416 deletions

View file

@ -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"
]
},
{

View file

@ -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

View file

@ -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)
}

View file

@ -12,156 +12,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.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.Request.Port, 853)
os.Args = args
}
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 +129,9 @@ 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")
cli.ParseCLI(args, "TEST")
os.Args = args
})
}

8
go.mod
View file

@ -19,7 +19,7 @@ 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.4.9 // 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
@ -27,9 +27,9 @@ require (
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.4 // indirect
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d // indirect
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // 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/text v0.3.7 // indirect
golang.org/x/tools v0.1.12 // indirect

16
go.sum
View file

@ -15,8 +15,9 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs=
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
@ -56,8 +57,8 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
@ -76,10 +77,10 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
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-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=
@ -113,6 +114,7 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-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=

View file

@ -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)
}

View file

@ -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
}

@ -1 +1 @@
Subproject commit c8686b1e149b6c42db019c77caf36475aafadd03
Subproject commit d876d6de34a78298ed041f575662015fb7eccdb5

View file

@ -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 "<nil> 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,9 +252,11 @@ 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: Header{
@ -72,6 +274,14 @@ 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)
}
}
for _, question := range msg.Question {
var name string
if opts.Display.UcodeTranslate {
@ -205,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

View file

@ -193,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 != "")
}
@ -207,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")
}

View file

@ -5,13 +5,10 @@ package query
import (
"fmt"
"strconv"
"strings"
"time"
"git.froth.zone/sam/awl/util"
"github.com/dchest/uniuri"
"github.com/miekg/dns"
"golang.org/x/net/idna"
)
const (
@ -19,202 +16,6 @@ const (
udp = "udp"
)
// 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 "<nil> 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.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)"
}
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
}
// 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
}
// CreateQuery creates a DNS query from the options given.
// It sets query flags and EDNS flags from the respective options.
func CreateQuery(opts util.Options) (util.Response, error) {
@ -310,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
}

View file

@ -117,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 != "")

View file

@ -11,7 +11,7 @@ import (
//nolint:govet // Better looking output is worth a few bytes.
type Message struct {
// Header section
Header Header `json:"header,omitempty" xml:"header,omitempty" yaml:"header,omitempty"`
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
@ -22,42 +22,44 @@ type Message struct {
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"`
Status string `json:"status," xml:"status," yaml:"status"`
ID uint16 `json:"id," xml:"id," yaml:"id"`
Response bool `json:"response," xml:"response," yaml:"response"`
Authoritative bool `json:"authoritative," xml:"authoritative," yaml:"authoritative"`
Truncated bool `json:"truncated," xml:"truncated," yaml:"truncated"`
RecursionDesired bool `json:"recursionDesired," xml:"recursionDesired," yaml:"recursionDesired"`
RecursionAvailable bool `json:"recursionAvailable," xml:"recursionAvailable," yaml:"recursionAvailable"`
Zero bool `json:"zero," xml:"zero," yaml:"zero"`
AuthenticatedData bool `json:"authenticatedData," xml:"authenticatedData," yaml:"authenticatedData"`
CheckingDisabled bool `json:"checkingDisabled," xml:"checkingDisabled," yaml:"checkingDisabled"`
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" 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:"name,omitempty"`
Class string `json:"class,omitempty" xml:"class,omitempty" yaml:"class,omitempty"`
Type string `json:"type,omitempty" xml:"type,omitempty" yaml:"type,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:"name,omitempty"`
TTL string `json:"ttl,omitempty" xml:"ttl,omitempty" yaml:"ttl,omitempty"`
Class string `json:"class,omitempty" xml:"class,omitempty" yaml:"class,omitempty"`
Type string `json:"type,omitempty" xml:"type,omitempty" yaml:"type,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"`
Name string `json:"name,omitempty" xml:"name,omitempty" yaml:"name,omitempty"`
Value string `json:"value" xml:"value" yaml:"value"`
}
@ -67,4 +69,12 @@ type Answer struct {
RRHeader `json:"header,omitempty" xml:"header,omitempty" yaml:"header,omitempty"`
}
// 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")

View file

@ -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/^/ /'
@sed -n 's/^##//p' $(MAKEFILE_LIST) | column -t -s ':' | sed -e 's/^/ /'

View file

@ -13,26 +13,26 @@ import (
// Options is the grand structure for all query options.
type Options struct {
// The logger
Logger *logawl.Logger
Logger *logawl.Logger `json:"-"`
// Host to verify TLS cert with
TLSHost string `json:"tlsHost"`
TLSHost string `json:"tlsHost" example:""`
// EDNS Options
EDNS
// DNS request :)
Request Request
// Verbosity levels, see [logawl.AllLevels]
Verbosity int `json:"-"`
Verbosity int `json:"-" example:"0"`
// Display options
Display Display
// Ignore Truncation
Truncate bool `json:"ignoreTruncate"`
Truncate bool `json:"ignoreTruncate" example:"false"`
// Print only the answer
Short bool `json:"short"`
Short bool `json:"short" example:"false"`
// When Short is true, display where the query came from
Identify bool `json:"identify"`
Identify bool `json:"identify" example:"false"`
// Perform a reverse DNS query when true
Reverse bool `json:"reverse"`
Reverse bool `json:"reverse" example:"false"`
HeaderFlags
@ -44,42 +44,43 @@ type Options struct {
YAML bool `json:"-" xml:"-" yaml:"-"`
// Use TCP instead of UDP to make the query
TCP bool `json:"tcp"`
TCP bool `json:"tcp" example:"false"`
// Use DNS-over-TLS to make the query
TLS bool `json:"dns_over_tls"`
TLS bool `json:"dnsOverTLS" example:"false"`
// When using TLS, ignore certificates
TLSNoVerify bool `json:"tlsNoVerify"`
TLSNoVerify bool `json:"tlsNoVerify" example:"false"`
// Use DNS-over-HTTPS to make the query
HTTPS bool `json:"dns_over_https"`
HTTPS bool `json:"dnsOverHTTPS" example:"false"`
// Use DNS-over-QUIC to make the query
QUIC bool `json:"dns_over_quic"`
//nolint:tagliatelle // QUIC is an acronym
QUIC bool `json:"dnsOverQUIC" example:"false"`
// Use DNSCrypt to make the query
DNSCrypt bool `json:"dnscrpyt"`
DNSCrypt bool `json:"dnscrypt" example:"false"`
// Force IPv4 only
IPv4 bool `json:"force_IPv4"`
IPv4 bool `json:"forceIPv4" example:"false"`
// Force IPv6 only
IPv6 bool `json:"force_IPv6"`
IPv6 bool `json:"forceIPv6" example:"false"`
}
// HeaderFlags are the flags that are in DNS headers.
type HeaderFlags struct {
// Authoritative Answer DNS query flag
AA bool `json:"authoritative,"`
AA bool `json:"authoritative" example:"false"`
// Authenticated Data DNS query flag
AD bool `json:"authenticatedData,"`
AD bool `json:"authenticatedData" example:"false"`
// Checking Disabled DNS query flag
CD bool `json:"checkingDisabled,"`
CD bool `json:"checkingDisabled" example:"false"`
// QueRy DNS query flag
QR bool `json:"query"`
QR bool `json:"query" example:"false"`
// Recursion Desired DNS query flag
RD bool `json:"recursionDesired"`
RD bool `json:"recursionDesired" example:"true"`
// Recursion Available DNS query flag
RA bool `json:"recursionAvailable"`
RA bool `json:"recursionAvailable" example:"false"`
// TrunCated DNS query flag
TC bool `json:"truncated"`
TC bool `json:"truncated" example:"false"`
// Zero DNS query flag
Z bool `json:"zero"`
Z bool `json:"zero" example:"false"`
}
// Display contains toggles for what to (and not to) display.
@ -87,32 +88,32 @@ type Display struct {
/* Section displaying */
// Comments?
Comments bool `json:"comments"`
Comments bool `json:"comments" example:"true"`
// QUESTION SECTION
Question bool `json:"question"`
Question bool `json:"question" example:"true"`
// OPT PSEUDOSECTION
Opt bool `json:"opt"`
Opt bool `json:"opt" example:"true"`
// ANSWER SECTION
Answer bool `json:"answer"`
Answer bool `json:"answer" example:"true"`
// AUTHORITY SECTION
Authority bool `json:"authority"`
Authority bool `json:"authority" example:"true"`
// ADDITIONAL SECTION
Additional bool `json:"additional"`
Additional bool `json:"additional" example:"true"`
// Query time, message size, etc.
Statistics bool `json:"statistics"`
Statistics bool `json:"statistics" example:"true"`
// Display TTL in response
TTL bool `json:"ttl"`
TTL bool `json:"ttl" example:"true"`
/* Answer formatting */
// Display Class in response
ShowClass bool `json:"showClass"`
ShowClass bool `json:"showClass" example:"true"`
// Display query before it is sent
ShowQuery bool `json:"showQuery"`
ShowQuery bool `json:"showQuery" example:"false"`
// Display TTL as human-readable
HumanTTL bool `json:"HumanTTL"`
HumanTTL bool `json:"humanTTL" example:"false"`
// Translate Punycode back to Unicode
UcodeTranslate bool `json:"unicode"`
UcodeTranslate bool `json:"unicode" example:"true"`
}
// EDNS contains toggles for various EDNS options.
@ -120,25 +121,25 @@ type EDNS struct {
// Subnet to originate query from.
Subnet dns.EDNS0_SUBNET `json:"subnet"`
// Must Be Zero flag
ZFlag uint16 `json:"zflag"`
ZFlag uint16 `json:"zflag" example:"0"`
// UDP buffer size
BufSize uint16 `json:"bufSize"`
BufSize uint16 `json:"bufSize" example:"1232"`
// Enable/Disable EDNS entirely
EnableEDNS bool `json:"edns"`
EnableEDNS bool `json:"edns" example:"false"`
// Sending EDNS cookie
Cookie bool `json:"cookie"`
Cookie bool `json:"cookie" example:"true"`
// Enabling DNSSEC
DNSSEC bool `json:"dnssec"`
DNSSEC bool `json:"dnssec" example:"false"`
// Sending EDNS Expire
Expire bool `json:"expire"`
Expire bool `json:"expire" example:"false"`
// Sending EDNS TCP keepopen
KeepOpen bool `json:"keepOpen"`
KeepOpen bool `json:"keepOpen" example:"false"`
// Sending EDNS NSID
Nsid bool `json:"nsid"`
Nsid bool `json:"nsid" example:"false"`
// Send EDNS Padding
Padding bool `json:"padding"`
Padding bool `json:"padding" example:"false"`
// Set EDNS version (default: 0)
Version uint8 `json:"version"`
Version uint8 `json:"version" example:"0"`
}
// ParseSubnet takes a subnet argument and makes it into one that the DNS library

View file

@ -13,23 +13,23 @@ type Response struct {
// The full DNS response
DNS *dns.Msg `json:"response"`
// The time it took to make the DNS query
RTT time.Duration `json:"rtt"`
RTT time.Duration `json:"rtt" example:"2000000000"`
}
// Request is a structure for a DNS query.
type Request struct {
// Server to query, eg. 8.8.8.8
Server string `json:"server"`
// Domain to query, eg. example.com
Name string `json:"name"`
// 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"`
Timeout time.Duration `json:"timeout" example:"2000000000"`
// Port to make DNS request on
Port int `json:"port"`
Port int `json:"port" example:"53"`
// Number of failures to make before giving up
Retries int `json:"retries"`
Retries int `json:"retries" example:"2"`
// Request type, eg. A, AAAA, NAPTR
Type uint16 `json:"type"`
Type uint16 `json:"type" example:"1"`
// Request class, eg. IN
Class uint16 `json:"class"`
Class uint16 `json:"class" example:"1"`
}