From ae2f695608bf61aa92bfee52b499ea808a31763b Mon Sep 17 00:00:00 2001 From: Nate Sales Date: Thu, 7 Sep 2023 02:15:01 -0400 Subject: [PATCH] refactor: reformat output system --- cli/flags.go | 69 ++++++++++ main.go | 109 ++++++---------- output.go | 292 ------------------------------------------- output/output.go | 18 +++ output/pretty.go | 165 ++++++++++++++++++++++++ output/raw.go | 65 ++++++++++ output/structured.go | 40 ++++++ util.go | 12 -- util/util.go | 57 +++++++++ xfr.go | 8 +- 10 files changed, 456 insertions(+), 379 deletions(-) create mode 100644 cli/flags.go delete mode 100644 output.go create mode 100644 output/output.go create mode 100644 output/pretty.go create mode 100644 output/raw.go create mode 100644 output/structured.go delete mode 100644 util.go create mode 100644 util/util.go diff --git a/cli/flags.go b/cli/flags.go new file mode 100644 index 0000000..39c4c8a --- /dev/null +++ b/cli/flags.go @@ -0,0 +1,69 @@ +package cli + +import "time" + +type Flags struct { + Name string `short:"q" long:"qname" description:"Query name"` + Server string `short:"s" long:"server" description:"DNS server"` + Types []string `short:"t" long:"type" description:"RR type (e.g. A, AAAA, MX, etc.) or type integer"` + Reverse bool `short:"x" long:"reverse" description:"Reverse lookup"` + DNSSEC bool `short:"d" long:"dnssec" description:"Set the DO (DNSSEC OK) bit in the OPT record"` + NSID bool `short:"n" long:"nsid" description:"Set EDNS0 NSID opt"` + ClientSubnet string `long:"subnet" description:"Set EDNS0 client subnet"` + Chaos bool `short:"c" long:"chaos" description:"Use CHAOS query class"` + Class uint16 `short:"C" description:"Set query class (default: IN 0x01)" default:"1"` + ODoHProxy string `short:"p" long:"odoh-proxy" description:"ODoH proxy"` + Timeout time.Duration `long:"timeout" description:"Query timeout" default:"10s"` + Pad bool `long:"pad" description:"Set EDNS0 padding"` + HTTP3 bool `long:"http3" description:"Use HTTP/3 for DoH"` + NoIDCheck bool `long:"no-id-check" description:"Disable checking of DNS response ID"` + NoReuseConn bool `long:"no-reuse-conn" description:"Use a new connection for each query"` + + RecAXFR bool `long:"recaxfr" description:"Perform recursive AXFR"` + + // Output + Format string `short:"f" long:"format" description:"Output format (pretty, json, yaml, raw)" default:"pretty"` + PrettyTTLs bool `long:"pretty-ttls" description:"Format TTLs in human readable format (default: true)"` + Color bool `long:"color" description:"Enable color output"` + ShowQuestion bool `long:"question" description:"Show question section"` + ShowAnswer bool `long:"answer" description:"Show answer section (default: true)"` + ShowAuthority bool `long:"authority" description:"Show authority section"` + ShowAdditional bool `long:"additional" description:"Show additional section"` + ShowStats bool `short:"S" long:"stats" description:"Show time statistics"` + ShowAll bool `long:"all" description:"Show all sections and statistics"` + Whois bool `short:"w" description:"Resolve ASN/ASName for A and AAAA records"` + ValueOnly bool `short:"r" long:"value" description:"Show record values only"` + + // Header flags + AuthoritativeAnswer bool `long:"aa" description:"Set AA (Authoritative Answer) flag in query"` + AuthenticData bool `long:"ad" description:"Set AD (Authentic Data) flag in query"` + CheckingDisabled bool `long:"cd" description:"Set CD (Checking Disabled) flag in query"` + RecursionDesired bool `long:"rd" description:"Set RD (Recursion Desired) flag in query (default: true)"` + RecursionAvailable bool `long:"ra" description:"Set RA (Recursion Available) flag in query"` + Zero bool `long:"z" description:"Set Z (Zero) flag in query"` + Truncated bool `long:"t" description:"Set TC (Truncated) flag in query"` + + // TCP parameters + TLSNoVerify bool `short:"i" long:"tls-no-verify" description:"Disable TLS certificate verification"` + TLSServerName string `long:"tls-server-name" description:"TLS server name for host verification"` + TLSMinVersion string `long:"tls-min-version" description:"Minimum TLS version to use" default:"1.0"` + TLSMaxVersion string `long:"tls-max-version" description:"Maximum TLS version to use" default:"1.3"` + TLSNextProtos []string `long:"tls-next-protos" description:"TLS next protocols for ALPN"` + TLSCipherSuites []string `long:"tls-cipher-suites" description:"TLS cipher suites"` + + // HTTP + HTTPUserAgent string `long:"http-user-agent" description:"HTTP user agent" default:""` + HTTPMethod string `long:"http-method" description:"HTTP method" default:"GET"` + + // QUIC + QUICALPNTokens []string `long:"quic-alpn-tokens" description:"QUIC ALPN tokens" default:"doq" default:"doq-i11"` + QUICNoPMTUD bool `long:"quic-no-pmtud" description:"Disable QUIC PMTU discovery"` + QUICNoLengthPrefix bool `long:"quic-no-length-prefix" description:"Don't add RFC 9250 compliant length prefix"` + + DefaultRRTypes []string `long:"default-rr-types" description:"Default record types" default:"A" default:"AAAA" default:"NS" default:"MX" default:"TXT" default:"CNAME"` + + UDPBuffer uint16 `long:"udp-buffer" description:"Set EDNS0 UDP size in query" default:"1232"` + Verbose bool `short:"v" long:"verbose" description:"Show verbose log messages"` + Trace bool `long:"trace" description:"Show trace log messages"` + ShowVersion bool `short:"V" long:"version" description:"Show version and exit"` +} diff --git a/main.go b/main.go index 81b2c91..d532e27 100644 --- a/main.go +++ b/main.go @@ -11,82 +11,21 @@ import ( "strings" "time" + "github.com/natesales/q/output" + + "github.com/natesales/q/util" + "github.com/jedisct1/go-dnsstamps" "github.com/jessevdk/go-flags" "github.com/miekg/dns" log "github.com/sirupsen/logrus" + + "github.com/natesales/q/cli" ) const defaultServerVar = "Q_DEFAULT_SERVER" -// CLI flags -type optsTemplate struct { - Name string `short:"q" long:"qname" description:"Query name"` - Server string `short:"s" long:"server" description:"DNS server"` - Types []string `short:"t" long:"type" description:"RR type (e.g. A, AAAA, MX, etc.) or type integer"` - Reverse bool `short:"x" long:"reverse" description:"Reverse lookup"` - DNSSEC bool `short:"d" long:"dnssec" description:"Set the DO (DNSSEC OK) bit in the OPT record"` - NSID bool `short:"n" long:"nsid" description:"Set EDNS0 NSID opt"` - ClientSubnet string `long:"subnet" description:"Set EDNS0 client subnet"` - Chaos bool `short:"c" long:"chaos" description:"Use CHAOS query class"` - Class uint16 `short:"C" description:"Set query class (default: IN 0x01)" default:"1"` - ODoHProxy string `short:"p" long:"odoh-proxy" description:"ODoH proxy"` - Timeout time.Duration `long:"timeout" description:"Query timeout" default:"10s"` - Pad bool `long:"pad" description:"Set EDNS0 padding"` - HTTP3 bool `long:"http3" description:"Use HTTP/3 for DoH"` - NoIDCheck bool `long:"no-id-check" description:"Disable checking of DNS response ID"` - NoReuseConn bool `long:"no-reuse-conn" description:"Use a new connection for each query"` - - RecAXFR bool `long:"recaxfr" description:"Perform recursive AXFR"` - - // Output - Format string `short:"f" long:"format" description:"Output format (pretty, json, yaml, raw)" default:"pretty"` - PrettyTTLs bool `long:"pretty-ttls" description:"Format TTLs in human readable format (default: true)"` - Color bool `long:"color" description:"Enable color output"` - ShowQuestion bool `long:"question" description:"Show question section"` - ShowAnswer bool `long:"answer" description:"Show answer section (default: true)"` - ShowAuthority bool `long:"authority" description:"Show authority section"` - ShowAdditional bool `long:"additional" description:"Show additional section"` - ShowStats bool `short:"S" long:"stats" description:"Show time statistics"` - ShowAll bool `long:"all" description:"Show all sections and statistics"` - Whois bool `short:"w" description:"Resolve ASN/ASName for A and AAAA records"` - ValueOnly bool `short:"r" long:"value" description:"Show record values only"` - - // Header flags - AuthoritativeAnswer bool `long:"aa" description:"Set AA (Authoritative Answer) flag in query"` - AuthenticData bool `long:"ad" description:"Set AD (Authentic Data) flag in query"` - CheckingDisabled bool `long:"cd" description:"Set CD (Checking Disabled) flag in query"` - RecursionDesired bool `long:"rd" description:"Set RD (Recursion Desired) flag in query (default: true)"` - RecursionAvailable bool `long:"ra" description:"Set RA (Recursion Available) flag in query"` - Zero bool `long:"z" description:"Set Z (Zero) flag in query"` - Truncated bool `long:"t" description:"Set TC (Truncated) flag in query"` - - // TCP parameters - TLSNoVerify bool `short:"i" long:"tls-no-verify" description:"Disable TLS certificate verification"` - TLSServerName string `long:"tls-server-name" description:"TLS server name for host verification"` - TLSMinVersion string `long:"tls-min-version" description:"Minimum TLS version to use" default:"1.0"` - TLSMaxVersion string `long:"tls-max-version" description:"Maximum TLS version to use" default:"1.3"` - TLSNextProtos []string `long:"tls-next-protos" description:"TLS next protocols for ALPN"` - TLSCipherSuites []string `long:"tls-cipher-suites" description:"TLS cipher suites"` - - // HTTP - HTTPUserAgent string `long:"http-user-agent" description:"HTTP user agent" default:""` - HTTPMethod string `long:"http-method" description:"HTTP method" default:"GET"` - - // QUIC - QUICALPNTokens []string `long:"quic-alpn-tokens" description:"QUIC ALPN tokens" default:"doq" default:"doq-i11"` - QUICNoPMTUD bool `long:"quic-no-pmtud" description:"Disable QUIC PMTU discovery"` - QUICNoLengthPrefix bool `long:"quic-no-length-prefix" description:"Don't add RFC 9250 compliant length prefix"` - - DefaultRRTypes []string `long:"default-rr-types" description:"Default record types" default:"A" default:"AAAA" default:"NS" default:"MX" default:"TXT" default:"CNAME"` - - UDPBuffer uint16 `long:"udp-buffer" description:"Set EDNS0 UDP size in query" default:"1232"` - Verbose bool `short:"v" long:"verbose" description:"Show verbose log messages"` - Trace bool `long:"trace" description:"Show trace log messages"` - ShowVersion bool `short:"V" long:"version" description:"Show version and exit"` -} - -var opts = optsTemplate{} +var opts = cli.Flags{} // Build process flags var ( @@ -141,7 +80,7 @@ func parseTLSCipherSuites(cipherSuites []string) []uint16 { // clearOpts sets the default values for the CLI options func clearOpts() { - opts = optsTemplate{} + opts = cli.Flags{} opts.RecursionDesired = true opts.ShowAnswer = true opts.PrettyTTLs = true @@ -156,6 +95,7 @@ func clearOpts() { log.Debug("NO_COLOR set") opts.Color = false } + util.UseColor = opts.Color } // tlsVersion returns a TLS version number by given protocol string @@ -345,6 +285,7 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation os.Exit(1) } parsePlusFlags(args) + util.UseColor = opts.Color if opts.Verbose { log.SetLevel(log.DebugLevel) @@ -354,7 +295,7 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation } if opts.ShowVersion { - mustWritef(out, "https://github.com/natesales/q version %s (%s %s)\n", version, commit, date) + util.MustWritef(out, "https://github.com/natesales/q version %s (%s %s)\n", version, commit, date) return nil } @@ -402,7 +343,7 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation // Set qname if not set by flag if opts.Name == "" && - !containsAny(arg, []string{"@", "/", "\\", "+"}) && // Not a server, path, or flag + !util.ContainsAny(arg, []string{"@", "/", "\\", "+"}) && // Not a server, path, or flag !typeFound && // Not a RR type !strings.HasSuffix(arg, ".exe") && // Not an executable !strings.HasPrefix(arg, "-") { // Not a flag @@ -541,7 +482,31 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation } queryTime := time.Since(startTime) - return display(replies, server, queryTime, out) + if opts.NSID && opts.Format == "pretty" { + output.PrettyPrintNSID(replies, out) + } + + printer := output.Printer{ + Server: server, + Out: out, + Opts: &opts, + QueryTime: queryTime, + NumReplies: len(replies), + } + for i, reply := range replies { + switch opts.Format { + case "pretty": + printer.PrintPretty(i, reply) + case "raw": + printer.PrintRaw(i, reply) + case "json", "yml", "yaml": + printer.PrintStructured(i, reply) + default: + return fmt.Errorf("invalid output format") + } + } + + return nil } func main() { diff --git a/output.go b/output.go deleted file mode 100644 index e03fa80..0000000 --- a/output.go +++ /dev/null @@ -1,292 +0,0 @@ -package main - -import ( - "encoding/hex" - "encoding/json" - "fmt" - "io" - "strconv" - "strings" - "time" - - "github.com/miekg/dns" - whois "github.com/natesales/bgptools-go" - log "github.com/sirupsen/logrus" - "gopkg.in/yaml.v3" -) - -var existingRRs = map[string]bool{} - -// ANSI colors -var colors = map[string]string{ - "black": "\033[1;30m%s\033[0m", - "red": "\033[1;31m%s\033[0m", - "green": "\033[1;32m%s\033[0m", - "yellow": "\033[1;33m%s\033[0m", - "purple": "\033[1;34m%s\033[0m", - "magenta": "\033[1;35m%s\033[0m", - "teal": "\033[1;36m%s\033[0m", - "white": "\033[1;37m%s\033[0m", -} - -// color returns a color formatted string -func color(color string, args ...interface{}) string { - if _, ok := colors[color]; !ok { - panic("invalid color: " + color) - } - - if opts.Color { - return fmt.Sprintf(colors[color], fmt.Sprint(args...)) - } else { - return fmt.Sprint(args...) - } -} - -func mustWriteln(out io.Writer, s string) { - if _, err := out.Write([]byte(s + "\n")); err != nil { - log.Fatal(err) - } -} - -func mustWritef(out io.Writer, format string, a ...interface{}) { - if _, err := out.Write([]byte(fmt.Sprintf(format, a...))); err != nil { - log.Fatal(err) - } -} - -// printPrettyRR prints a pretty RR -func printPrettyRR(a dns.RR, doWhois bool, out io.Writer) { - val := strings.TrimSpace(strings.Join(strings.Split(a.String(), dns.TypeToString[a.Header().Rrtype])[1:], "")) - rrSignature := fmt.Sprintf("%s %d %s %s", a.Header().Name, a.Header().Ttl, dns.TypeToString[a.Header().Rrtype], val) - if ok := existingRRs[rrSignature]; ok { - return - } else { - existingRRs[rrSignature] = true - } - - ttl := fmt.Sprintf("%d", a.Header().Ttl) - if opts.PrettyTTLs { - ttl = (time.Duration(a.Header().Ttl) * time.Second).String() - } - - if doWhois && (a.Header().Rrtype == dns.TypeA || a.Header().Rrtype == dns.TypeAAAA) { - resp, err := whois.Query(val) - if err != nil { - log.Warnf("bgp.tools query: %s", err) - } else { - val += color("teal", fmt.Sprintf(" (AS%d %s)", resp.AS, resp.ASName)) - } - } - - if opts.ValueOnly { - mustWriteln(out, val) - } else { - mustWritef(out, "%s %s %s %s\n", - color("purple", a.Header().Name), - color("green", ttl), - color("magenta", dns.TypeToString[a.Header().Rrtype]), - val, - ) - } -} - -func prettyPrintNSID(opt []*dns.Msg, out io.Writer) { - for _, r := range opt { - for _, o := range r.Extra { - if o.Header().Rrtype == dns.TypeOPT { - opt := o.(*dns.OPT) - for _, e := range opt.Option { - if e.Option() == dns.EDNS0NSID { - nsidStr, err := hex.DecodeString(e.String()) - if err != nil { - log.Warnf("error decoding NSID: %s", err) - return - } - mustWritef(out, "%s %s\n", - color("white", "NSID:"), - color("purple", string(nsidStr)), - ) - return - } - } - } - } - } -} - -func display(replies []*dns.Msg, server string, queryTime time.Duration, out io.Writer) error { - if opts.NSID && opts.Format == "pretty" { - prettyPrintNSID(replies, out) - } - - for i, reply := range replies { - // Print answers - switch opts.Format { - case "pretty": - if opts.ShowQuestion { - mustWriteln(out, color("white", "Question:")) - for _, a := range reply.Question { - mustWritef(out, "%s %s\n", - color("purple", a.Name), - color("magenta", dns.TypeToString[a.Qtype]), - ) - } - } - if opts.ShowAnswer && len(reply.Answer) > 0 { - if opts.ShowQuestion || opts.ShowAuthority || opts.ShowAdditional { - mustWriteln(out, color("white", "Answer:")) - } - for _, a := range reply.Answer { - printPrettyRR(a, opts.Whois, out) - } - } - if opts.ShowAuthority && len(reply.Ns) > 0 { - mustWriteln(out, color("white", "Authority:")) - for _, a := range reply.Ns { - printPrettyRR(a, opts.Whois, out) - } - } - if opts.ShowAdditional && len(reply.Extra) > 0 { - mustWriteln(out, color("white", "Additional:")) - for _, a := range reply.Extra { - printPrettyRR(a, opts.Whois, out) - } - } - - // Print separator if there is more than one query - if (opts.ShowQuestion || opts.ShowAuthority || opts.ShowAdditional) && (len(replies) > 0 && i != len(replies)-1) { - mustWritef(out, "\nā”€ā”€\n\n") - } - - if opts.ShowStats { - mustWriteln(out, color("white", "Stats:")) - mustWritef(out, "Received %s from %s in %s (%s)\n", - color("purple", fmt.Sprintf("%d B", reply.Len())), - color("green", server), - color("teal", queryTime.Round(100*time.Microsecond)), - color("magenta", time.Now().Format("15:04:05 01-02-2006 MST")), - ) - - flags := "" - if reply.MsgHdr.Response { - flags = "qr" - } - if reply.MsgHdr.Authoritative { - flags = "aa" - } - if reply.MsgHdr.Truncated { - flags = "tc" - } - if reply.MsgHdr.RecursionDesired { - flags = "rd" - } - if reply.MsgHdr.RecursionAvailable { - flags = "ra" - } - if reply.MsgHdr.Zero { - flags = "z" - } - if reply.MsgHdr.AuthenticatedData { - flags = "ad" - } - if reply.MsgHdr.CheckingDisabled { - flags = "cd" - } - - mustWritef(out, "Opcode: %s Status: %s ID %s: Flags: %s (%s Query %s Ans %s Auth %s Add)\n", - color("magenta", dns.OpcodeToString[reply.MsgHdr.Opcode]), - color("teal", dns.RcodeToString[reply.MsgHdr.Rcode]), - color("green", fmt.Sprintf("%d", reply.MsgHdr.Id)), - color("purple", flags), - color("purple", fmt.Sprintf("%d", len(reply.Question))), - color("green", fmt.Sprintf("%d", len(reply.Answer))), - color("teal", fmt.Sprintf("%d", len(reply.Ns))), - color("magenta", fmt.Sprintf("%d", len(reply.Extra))), - ) - } - case "raw": - s := reply.MsgHdr.String() + " " - s += "QUERY: " + strconv.Itoa(len(reply.Question)) + ", " - s += "ANSWER: " + strconv.Itoa(len(reply.Answer)) + ", " - s += "AUTHORITY: " + strconv.Itoa(len(reply.Ns)) + ", " - s += "ADDITIONAL: " + strconv.Itoa(len(reply.Extra)) + "\n" - opt := reply.IsEdns0() - if opt != nil { - // OPT PSEUDOSECTION - s += opt.String() + "\n" - } - if opts.ShowQuestion && len(reply.Question) > 0 { - s += "\n;; QUESTION SECTION:\n" - for _, r := range reply.Question { - s += r.String() + "\n" - } - } - if opts.ShowAnswer && len(reply.Answer) > 0 { - s += "\n;; ANSWER SECTION:\n" - for _, r := range reply.Answer { - if r != nil { - s += r.String() + "\n" - } - } - } - if opts.ShowAuthority && len(reply.Ns) > 0 { - s += "\n;; AUTHORITY SECTION:\n" - for _, r := range reply.Ns { - if r != nil { - s += r.String() + "\n" - } - } - } - if opts.ShowAdditional && len(reply.Extra) > 0 && (opt == nil || len(reply.Extra) > 1) { - s += "\n;; ADDITIONAL SECTION:\n" - for _, r := range reply.Extra { - if r != nil && r.Header().Rrtype != dns.TypeOPT { - s += r.String() + "\n" - } - } - } - mustWriteln(out, s) - - if opts.ShowStats { - mustWritef(out, ";; Received %d B\n", reply.Len()) - mustWritef(out, ";; Time %s\n", time.Now().Format("15:04:05 01-02-2006 MST")) - mustWritef(out, ";; From %s in %s\n", server, queryTime.Round(100*time.Microsecond)) - } - - // Print separator if there is more than one query - if len(replies) > 0 && i != len(replies)-1 { - mustWritef(out, "\n--\n\n") - } - case "json", "yml", "yaml": - body := struct { - Server string - QueryTime int64 - Answers []dns.RR - ID uint16 - Truncated bool - }{ - Server: opts.Server, - QueryTime: int64(queryTime / time.Millisecond), - Answers: reply.Answer, - ID: reply.Id, - Truncated: reply.Truncated, - } - var b []byte - var err error - if opts.Format == "json" { - b, err = json.Marshal(body) - } else { // yaml - b, err = yaml.Marshal(body) - } - if err != nil { - return err - } - - mustWriteln(out, string(b)) - default: - return fmt.Errorf("invalid output format") - } - } - - return nil -} diff --git a/output/output.go b/output/output.go new file mode 100644 index 0000000..20eff86 --- /dev/null +++ b/output/output.go @@ -0,0 +1,18 @@ +package output + +import ( + "io" + "time" + + "github.com/natesales/q/cli" +) + +type Printer struct { + Out io.Writer + Opts *cli.Flags + QueryTime time.Duration + Server string + NumReplies int + + existingRRs map[string]bool +} diff --git a/output/pretty.go b/output/pretty.go new file mode 100644 index 0000000..d1043d0 --- /dev/null +++ b/output/pretty.go @@ -0,0 +1,165 @@ +package output + +import ( + "encoding/hex" + "fmt" + "io" + "strings" + "time" + + "github.com/miekg/dns" + whois "github.com/natesales/bgptools-go" + log "github.com/sirupsen/logrus" + + "github.com/natesales/q/util" +) + +func PrettyPrintNSID(opt []*dns.Msg, out io.Writer) { + for _, r := range opt { + for _, o := range r.Extra { + if o.Header().Rrtype == dns.TypeOPT { + opt := o.(*dns.OPT) + for _, e := range opt.Option { + if e.Option() == dns.EDNS0NSID { + nsidStr, err := hex.DecodeString(e.String()) + if err != nil { + log.Warnf("error decoding NSID: %s", err) + return + } + util.MustWritef(out, "%s %s\n", + util.Color("white", "NSID:"), + util.Color("purple", string(nsidStr)), + ) + return + } + } + } + } + } +} + +// printPrettyRR prints a pretty RR +func (p Printer) printPrettyRR(a dns.RR, doWhois bool) { + // Initialize existingRRs map if it doesn't exist + if p.existingRRs == nil { + p.existingRRs = make(map[string]bool) + } + + val := strings.TrimSpace(strings.Join(strings.Split(a.String(), dns.TypeToString[a.Header().Rrtype])[1:], "")) + rrSignature := fmt.Sprintf("%s %d %s %s", a.Header().Name, a.Header().Ttl, dns.TypeToString[a.Header().Rrtype], val) + if ok := p.existingRRs[rrSignature]; ok { + return + } else { + p.existingRRs[rrSignature] = true + } + + ttl := fmt.Sprintf("%d", a.Header().Ttl) + if p.Opts.PrettyTTLs { + ttl = (time.Duration(a.Header().Ttl) * time.Second).String() + } + + if doWhois && (a.Header().Rrtype == dns.TypeA || a.Header().Rrtype == dns.TypeAAAA) { + resp, err := whois.Query(val) + if err != nil { + log.Warnf("bgp.tools query: %s", err) + } else { + val += util.Color("teal", fmt.Sprintf(" (AS%d %s)", resp.AS, resp.ASName)) + } + } + + if p.Opts.ValueOnly { + util.MustWriteln(p.Out, val) + } else { + util.MustWritef(p.Out, "%s %s %s %s\n", + util.Color("purple", a.Header().Name), + util.Color("green", ttl), + util.Color("magenta", dns.TypeToString[a.Header().Rrtype]), + val, + ) + } +} + +func (p Printer) PrintPretty(i int, reply *dns.Msg) { + if p.Opts.ShowQuestion { + util.MustWriteln(p.Out, util.Color("white", "Question:")) + for _, a := range reply.Question { + util.MustWritef(p.Out, "%s %s\n", + util.Color("purple", a.Name), + util.Color("magenta", dns.TypeToString[a.Qtype]), + ) + } + } + if p.Opts.ShowAnswer && len(reply.Answer) > 0 { + if p.Opts.ShowQuestion || p.Opts.ShowAuthority || p.Opts.ShowAdditional { + util.MustWriteln(p.Out, util.Color("white", "Answer:")) + } + for _, a := range reply.Answer { + p.printPrettyRR(a, p.Opts.Whois) + } + } + if p.Opts.ShowAuthority && len(reply.Ns) > 0 { + util.MustWriteln(p.Out, util.Color("white", "Authority:")) + for _, a := range reply.Ns { + p.printPrettyRR(a, p.Opts.Whois) + } + } + if p.Opts.ShowAdditional && len(reply.Extra) > 0 { + util.MustWriteln(p.Out, util.Color("white", "Additional:")) + for _, a := range reply.Extra { + p.printPrettyRR(a, p.Opts.Whois) + } + } + + // Print separator if there is more than one query + if (p.Opts.ShowQuestion || p.Opts.ShowAuthority || p.Opts.ShowAdditional) && + (p.NumReplies > 0 && i != p.NumReplies-1) { + util.MustWritef(p.Out, "\nā”€ā”€\n\n") + } + + if p.Opts.ShowStats { + util.MustWriteln(p.Out, util.Color("white", "Stats:")) + util.MustWritef(p.Out, "Received %s from %s in %s (%s)\n", + util.Color("purple", fmt.Sprintf("%d B", reply.Len())), + util.Color("green", p.Server), + util.Color("teal", p.QueryTime.Round(100*time.Microsecond)), + util.Color("magenta", time.Now().Format("15:04:05 01-02-2006 MST")), + ) + + flags := "" + if reply.MsgHdr.Response { + flags = "qr" + } + if reply.MsgHdr.Authoritative { + flags = "aa" + } + if reply.MsgHdr.Truncated { + flags = "tc" + } + if reply.MsgHdr.RecursionDesired { + flags = "rd" + } + if reply.MsgHdr.RecursionAvailable { + flags = "ra" + } + if reply.MsgHdr.Zero { + flags = "z" + } + if reply.MsgHdr.AuthenticatedData { + flags = "ad" + } + if reply.MsgHdr.CheckingDisabled { + flags = "cd" + } + + util.MustWritef(p.Out, "Opcode: %s Status: %s ID %s: Flags: %s (%s Query %s Ans %s Auth %s Add)\n", + util.Color("magenta", dns.OpcodeToString[reply.MsgHdr.Opcode]), + util.Color("teal", dns.RcodeToString[reply.MsgHdr.Rcode]), + util.Color("green", fmt.Sprintf("%d", reply.MsgHdr.Id)), + util.Color("purple", flags), + util.Color("purple", fmt.Sprintf("%d", len(reply.Question))), + util.Color("green", fmt.Sprintf("%d", len(reply.Answer))), + util.Color("teal", fmt.Sprintf("%d", len(reply.Ns))), + util.Color("magenta", fmt.Sprintf("%d", len(reply.Extra))), + ) + } +} diff --git a/output/raw.go b/output/raw.go new file mode 100644 index 0000000..8c79a5d --- /dev/null +++ b/output/raw.go @@ -0,0 +1,65 @@ +package output + +import ( + "strconv" + "time" + + "github.com/miekg/dns" + + "github.com/natesales/q/util" +) + +func (p Printer) PrintRaw(i int, reply *dns.Msg) { + s := reply.MsgHdr.String() + " " + s += "QUERY: " + strconv.Itoa(len(reply.Question)) + ", " + s += "ANSWER: " + strconv.Itoa(len(reply.Answer)) + ", " + s += "AUTHORITY: " + strconv.Itoa(len(reply.Ns)) + ", " + s += "ADDITIONAL: " + strconv.Itoa(len(reply.Extra)) + "\n" + opt := reply.IsEdns0() + if opt != nil { + // OPT PSEUDOSECTION + s += opt.String() + "\n" + } + if p.Opts.ShowQuestion && len(reply.Question) > 0 { + s += "\n;; QUESTION SECTION:\n" + for _, r := range reply.Question { + s += r.String() + "\n" + } + } + if p.Opts.ShowAnswer && len(reply.Answer) > 0 { + s += "\n;; ANSWER SECTION:\n" + for _, r := range reply.Answer { + if r != nil { + s += r.String() + "\n" + } + } + } + if p.Opts.ShowAuthority && len(reply.Ns) > 0 { + s += "\n;; AUTHORITY SECTION:\n" + for _, r := range reply.Ns { + if r != nil { + s += r.String() + "\n" + } + } + } + if p.Opts.ShowAdditional && len(reply.Extra) > 0 && (opt == nil || len(reply.Extra) > 1) { + s += "\n;; ADDITIONAL SECTION:\n" + for _, r := range reply.Extra { + if r != nil && r.Header().Rrtype != dns.TypeOPT { + s += r.String() + "\n" + } + } + } + util.MustWriteln(p.Out, s) + + if p.Opts.ShowStats { + util.MustWritef(p.Out, ";; Received %d B\n", reply.Len()) + util.MustWritef(p.Out, ";; Time %s\n", time.Now().Format("15:04:05 01-02-2006 MST")) + util.MustWritef(p.Out, ";; From %s in %s\n", p.Server, p.QueryTime.Round(100*time.Microsecond)) + } + + // Print separator if there is more than one query + if p.NumReplies > 0 && i != p.NumReplies-1 { + util.MustWritef(p.Out, "\n--\n\n") + } +} diff --git a/output/structured.go b/output/structured.go new file mode 100644 index 0000000..b7a1bb8 --- /dev/null +++ b/output/structured.go @@ -0,0 +1,40 @@ +package output + +import ( + "encoding/json" + "time" + + "github.com/miekg/dns" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" + + "github.com/natesales/q/util" +) + +func (p Printer) PrintStructured(i int, reply *dns.Msg) { + body := struct { + Server string + QueryTime int64 + Answers []dns.RR + ID uint16 + Truncated bool + }{ + Server: p.Server, + QueryTime: int64(p.QueryTime.Round(time.Millisecond)), + Answers: reply.Answer, + ID: reply.Id, + Truncated: reply.Truncated, + } + var b []byte + var err error + if p.Opts.Format == "json" { + b, err = json.Marshal(body) + } else { // yaml + b, err = yaml.Marshal(body) + } + if err != nil { + log.Fatalf("error marshaling output: %s", err) + } + + util.MustWriteln(p.Out, string(b)) +} diff --git a/util.go b/util.go deleted file mode 100644 index 4cb3b3b..0000000 --- a/util.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import "strings" - -func containsAny(s string, subStrings []string) bool { - for _, sub := range subStrings { - if strings.Contains(s, sub) { - return true - } - } - return false -} diff --git a/util/util.go b/util/util.go new file mode 100644 index 0000000..acffd6d --- /dev/null +++ b/util/util.go @@ -0,0 +1,57 @@ +package util + +import ( + "fmt" + "io" + "strings" + + log "github.com/sirupsen/logrus" +) + +var UseColor = true + +// ANSI colors +var colors = map[string]string{ + "black": "\033[1;30m%s\033[0m", + "red": "\033[1;31m%s\033[0m", + "green": "\033[1;32m%s\033[0m", + "yellow": "\033[1;33m%s\033[0m", + "purple": "\033[1;34m%s\033[0m", + "magenta": "\033[1;35m%s\033[0m", + "teal": "\033[1;36m%s\033[0m", + "white": "\033[1;37m%s\033[0m", +} + +// Color returns a color formatted string +func Color(color string, args ...interface{}) string { + if _, ok := colors[color]; !ok { + panic("invalid color: " + color) + } + + if UseColor { + return fmt.Sprintf(colors[color], fmt.Sprint(args...)) + } else { + return fmt.Sprint(args...) + } +} + +func ContainsAny(s string, subStrings []string) bool { + for _, sub := range subStrings { + if strings.Contains(s, sub) { + return true + } + } + return false +} + +func MustWriteln(out io.Writer, s string) { + if _, err := out.Write([]byte(s + "\n")); err != nil { + log.Fatal(err) + } +} + +func MustWritef(out io.Writer, format string, a ...interface{}) { + if _, err := out.Write([]byte(fmt.Sprintf(format, a...))); err != nil { + log.Fatal(err) + } +} diff --git a/xfr.go b/xfr.go index 38ccc4e..a4eb634 100644 --- a/xfr.go +++ b/xfr.go @@ -8,6 +8,8 @@ import ( "strings" "time" + "github.com/natesales/q/util" + "github.com/miekg/dns" log "github.com/sirupsen/logrus" ) @@ -40,7 +42,7 @@ func axfr(label, server string) []dns.RR { // RecAXFR performs an AXFR on the given label and all of its children and writes the zone file to disk func RecAXFR(label, server string, out io.Writer) []dns.RR { - mustWritef(out, "Attempting recursive AXFR for %s\n", label) + util.MustWritef(out, "Attempting recursive AXFR for %s\n", label) // Reset state queried = make(map[string]bool) @@ -60,7 +62,7 @@ func RecAXFR(label, server string, out io.Writer) []dns.RR { } addToTree(label, dir, server, out) - mustWritef(out, "AXFR complete, %d records saved to %s\n", len(all), dir) + util.MustWritef(out, "AXFR complete, %d records saved to %s\n", len(all), dir) return all } @@ -70,7 +72,7 @@ func addToTree(label, dir, server string, out io.Writer) { if queried[label] { return } - mustWritef(out, "AXFR %s\n", label) + util.MustWritef(out, "AXFR %s\n", label) queried[label] = true rrs := axfr(label, server)