fix: Make a separate cmd/doggo package for main files
Closes https://github.com/mr-karan/doggo/issues/1
This commit is contained in:
parent
724114e144
commit
4d618b892b
11 changed files with 14 additions and 9 deletions
123
cmd/doggo/cli.go
Normal file
123
cmd/doggo/cli.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/providers/posflag"
|
||||
"github.com/sirupsen/logrus"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var (
|
||||
// Version and date of the build. This is injected at build-time.
|
||||
buildVersion = "unknown"
|
||||
buildDate = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
logger = initLogger()
|
||||
k = koanf.New(".")
|
||||
)
|
||||
|
||||
// Initialize hub.
|
||||
hub := NewHub(logger, buildVersion)
|
||||
|
||||
// Configure Flags.
|
||||
f := flag.NewFlagSet("config", flag.ContinueOnError)
|
||||
hub.flag = f
|
||||
|
||||
// Custom Help Text.
|
||||
f.Usage = renderCustomHelp
|
||||
|
||||
// Query Options.
|
||||
f.StringSliceP("query", "q", []string{}, "Domain name to query")
|
||||
f.StringSliceP("type", "t", []string{}, "Type of DNS record to be queried (A, AAAA, MX etc)")
|
||||
f.StringSliceP("class", "c", []string{}, "Network class of the DNS record to be queried (IN, CH, HS etc)")
|
||||
f.StringSliceP("nameserver", "n", []string{}, "Address of the nameserver to send packets to")
|
||||
|
||||
// Resolver Options
|
||||
f.Int("timeout", 5, "Sets the timeout for a query to T seconds. The default timeout is 5 seconds.")
|
||||
f.Bool("search", true, "Use the search list provided in resolv.conf. It sets the `ndots` parameter as well unless overriden by `ndots` flag.")
|
||||
f.Int("ndots", 0, "Specify the ndots paramter. Default value is taken from resolv.conf and fallbacks to 1 if ndots statement is missing in resolv.conf")
|
||||
f.BoolP("ipv4", "4", false, "Use IPv4 only")
|
||||
f.BoolP("ipv6", "6", false, "Use IPv6 only")
|
||||
|
||||
// Output Options
|
||||
f.BoolP("json", "J", false, "Set the output format as JSON")
|
||||
f.Bool("time", false, "Display how long it took for the response to arrive")
|
||||
f.Bool("color", true, "Show colored output")
|
||||
f.Bool("debug", false, "Enable debug mode")
|
||||
|
||||
// Parse and Load Flags.
|
||||
err := f.Parse(os.Args[1:])
|
||||
if err != nil {
|
||||
hub.Logger.WithError(err).Error("error parsing flags")
|
||||
hub.Logger.Exit(2)
|
||||
}
|
||||
if err = k.Load(posflag.Provider(f, ".", k), nil); err != nil {
|
||||
hub.Logger.WithError(err).Error("error loading flags")
|
||||
f.Usage()
|
||||
hub.Logger.Exit(2)
|
||||
}
|
||||
|
||||
// Set log level.
|
||||
if k.Bool("debug") {
|
||||
// Set logger level
|
||||
hub.Logger.SetLevel(logrus.DebugLevel)
|
||||
} else {
|
||||
hub.Logger.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
|
||||
// Unmarshall flags to the hub.
|
||||
err = k.Unmarshal("", &hub.QueryFlags)
|
||||
if err != nil {
|
||||
hub.Logger.WithError(err).Error("error loading args")
|
||||
hub.Logger.Exit(2)
|
||||
}
|
||||
|
||||
// Load all `non-flag` arguments
|
||||
// which will be parsed separately.
|
||||
hub.UnparsedArgs = f.Args()
|
||||
|
||||
// Parse Query Args.
|
||||
err = hub.loadQueryArgs()
|
||||
if err != nil {
|
||||
hub.Logger.WithError(err).Error("error parsing flags/arguments")
|
||||
hub.Logger.Exit(2)
|
||||
}
|
||||
|
||||
// Load Nameservers.
|
||||
err = hub.loadNameservers()
|
||||
if err != nil {
|
||||
hub.Logger.WithError(err).Error("error loading nameservers")
|
||||
hub.Logger.Exit(2)
|
||||
}
|
||||
|
||||
// Load Resolvers.
|
||||
err = hub.loadResolvers()
|
||||
if err != nil {
|
||||
hub.Logger.WithError(err).Error("error loading resolver")
|
||||
hub.Logger.Exit(2)
|
||||
}
|
||||
|
||||
// Run the app.
|
||||
hub.Logger.Debug("Starting doggo 🐶")
|
||||
if len(hub.QueryFlags.QNames) == 0 {
|
||||
f.Usage()
|
||||
hub.Logger.Exit(0)
|
||||
}
|
||||
|
||||
// Resolve Queries.
|
||||
responses, err := hub.Lookup()
|
||||
if err != nil {
|
||||
hub.Logger.WithError(err).Error("error looking up DNS records")
|
||||
hub.Logger.Exit(2)
|
||||
}
|
||||
//Send the output.
|
||||
hub.Output(responses)
|
||||
|
||||
// Quitting.
|
||||
hub.Logger.Exit(0)
|
||||
}
|
101
cmd/doggo/help.go
Normal file
101
cmd/doggo/help.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// appHelpTextTemplate is the text/template to customise the Help output.
|
||||
// Uses text/template to render templates.
|
||||
var appHelpTextTemplate = `{{ "NAME" | color "" "heading" }}:
|
||||
{{ .Name | color "green" "bold" }} 🐶 {{.Description}}
|
||||
|
||||
{{ "USAGE" | color "" "heading" }}:
|
||||
{{ .Name | color "green" "bold" }} [--] {{ "[query options]" | color "yellow" "" }} {{ "[arguments...]" | color "cyan" "" }}
|
||||
|
||||
{{ "VERSION" | color "" "heading" }}:
|
||||
{{.Version | color "red" "" }} - {{.Date | color "red" ""}}
|
||||
|
||||
{{ "EXAMPLES" | color "" "heading" }}:
|
||||
{{ .Name | color "green" "bold" }} {{ "mrkaran.dev" | color "cyan" "" }} Query a domain using defaults.
|
||||
{{ .Name | color "green" "bold" }} {{ "mrkaran.dev CNAME" | color "cyan" "" }} Looks up for a CNAME record.
|
||||
{{ .Name | color "green" "bold" }} {{ "mrkaran.dev MX @9.9.9.9" | color "cyan" "" }} Uses a custom DNS resolver.
|
||||
{{ .Name | color "green" "bold" }} {{"-q mrkaran.dev -t MX -n 1.1.1.1" | color "yellow" ""}} Using named arguments.
|
||||
|
||||
{{ "Free Form Arguments" | color "" "heading" }}:
|
||||
Supply hostnames, query types, classes without any flag. For eg:
|
||||
{{ .Name | color "green" "bold" }} {{"mrkaran.dev A @1.1.1.1" | color "cyan" "" }}
|
||||
|
||||
{{ "Transport Options" | color "" "heading" }}:
|
||||
Based on the URL scheme the correct resolver is chosen.
|
||||
Fallbacks to UDP resolver if no scheme is present.
|
||||
|
||||
{{"@udp://" | color "yellow" ""}} eg: @1.1.1.1 initiates a {{"UDP" | color "cyan" ""}} resolver for 1.1.1.1:53.
|
||||
{{"@tcp://" | color "yellow" ""}} eg: @1.1.1.1 initiates a {{"TCP" | color "cyan" ""}} resolver for 1.1.1.1:53.
|
||||
{{"@https://" | color "yellow" ""}} eg: @https://cloudflare-dns.com/dns-query initiates a {{"DOH" | color "cyan" ""}} resolver for Cloudflare DoH server.
|
||||
{{"@tls://" | color "yellow" ""}} eg: @1.1.1.1 initiates a {{"DoT" | color "cyan" ""}} resolver for 1.1.1.1:853.
|
||||
|
||||
{{ "Query Options" | color "" "heading" }}:
|
||||
{{"-q, --query=HOSTNAME" | color "yellow" ""}} Hostname to query the DNS records for (eg {{"mrkaran.dev" | color "cyan" ""}}).
|
||||
{{"-t, --type=TYPE" | color "yellow" ""}} Type of the DNS Record ({{"A, MX, NS" | color "cyan" ""}} etc).
|
||||
{{"-n, --nameserver=ADDR" | color "yellow" ""}} Address of a specific nameserver to send queries to ({{"9.9.9.9, 8.8.8.8" | color "cyan" ""}} etc).
|
||||
{{"-c, --class=CLASS" | color "yellow" ""}} Network class of the DNS record ({{"IN, CH, HS" | color "cyan" ""}} etc).
|
||||
|
||||
{{ "Resolver Options" | color "" "heading" }}:
|
||||
{{"--ndots=INT" | color "yellow" ""}} Specify ndots parameter. Takes value from /etc/resolv.conf if using the system namesever or 1 otherwise.
|
||||
{{"--search" | color "yellow" ""}} Use the search list defined in resolv.conf. Defaults to true. Set --search=false to disable search list.
|
||||
{{"--timeout" | color "yellow" ""}} Specify timeout (in seconds) for the resolver to return a response.
|
||||
{{"-4 --ipv4" | color "yellow" ""}} Use IPv4 only.
|
||||
{{"-6 --ipv6" | color "yellow" ""}} Use IPv6 only.
|
||||
|
||||
{{ "Output Options" | color "" "heading" }}:
|
||||
{{"-J, --json " | color "yellow" ""}} Format the output as JSON.
|
||||
{{"--color " | color "yellow" ""}} Defaults to true. Set --color=false to disable colored output.
|
||||
{{"--debug " | color "yellow" ""}} Enable debug logging.
|
||||
{{"--time" | color "yellow" ""}} Shows how long the response took from the server.
|
||||
`
|
||||
|
||||
func renderCustomHelp() {
|
||||
helpTmplVars := map[string]string{
|
||||
"Name": "doggo",
|
||||
"Description": "DNS Client for Humans",
|
||||
"Version": buildVersion,
|
||||
"Date": buildDate,
|
||||
}
|
||||
tmpl, err := template.New("test").Funcs(template.FuncMap{
|
||||
"color": func(clr string, format string, str string) string {
|
||||
formatter := color.New()
|
||||
switch c := clr; c {
|
||||
case "yellow":
|
||||
formatter = formatter.Add(color.FgYellow)
|
||||
case "red":
|
||||
formatter = formatter.Add(color.FgRed)
|
||||
case "cyan":
|
||||
formatter = formatter.Add(color.FgCyan)
|
||||
case "green":
|
||||
formatter = formatter.Add(color.FgGreen)
|
||||
}
|
||||
switch f := format; f {
|
||||
case "bold":
|
||||
formatter = formatter.Add(color.Bold)
|
||||
case "underline":
|
||||
formatter = formatter.Add(color.Underline)
|
||||
case "heading":
|
||||
formatter = formatter.Add(color.Bold, color.Underline)
|
||||
}
|
||||
return formatter.SprintFunc()(str)
|
||||
},
|
||||
}).Parse(appHelpTextTemplate)
|
||||
if err != nil {
|
||||
// should ideally never happen.
|
||||
panic(err)
|
||||
}
|
||||
err = tmpl.Execute(os.Stdout, helpTmplVars)
|
||||
if err != nil {
|
||||
// should ideally never happen.
|
||||
panic(err)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
73
cmd/doggo/hub.go
Normal file
73
cmd/doggo/hub.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/mr-karan/doggo/pkg/resolvers"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Hub represents the structure for all app wide configuration.
|
||||
type Hub struct {
|
||||
Logger *logrus.Logger
|
||||
Version string
|
||||
QueryFlags QueryFlags
|
||||
UnparsedArgs []string
|
||||
Questions []dns.Question
|
||||
Resolver []resolvers.Resolver
|
||||
Nameservers []Nameserver
|
||||
flag *pflag.FlagSet
|
||||
}
|
||||
|
||||
// QueryFlags is used store the query params
|
||||
// supplied by the user.
|
||||
type QueryFlags struct {
|
||||
QNames []string `koanf:"query"`
|
||||
QTypes []string `koanf:"type"`
|
||||
QClasses []string `koanf:"class"`
|
||||
Nameservers []string `koanf:"nameserver"`
|
||||
UseIPv4 bool `koanf:"ipv4"`
|
||||
UseIPv6 bool `koanf:"ipv6"`
|
||||
DisplayTimeTaken bool `koanf:"time"`
|
||||
ShowJSON bool `koanf:"json"`
|
||||
UseSearchList bool `koanf:"search"`
|
||||
Ndots int `koanf:"ndots"`
|
||||
Color bool `koanf:"color"`
|
||||
Timeout time.Duration `koanf:"timeout"`
|
||||
isNdotsSet bool
|
||||
}
|
||||
|
||||
// Nameserver represents the type of Nameserver
|
||||
// along with the server address.
|
||||
type Nameserver struct {
|
||||
Address string
|
||||
Type string
|
||||
}
|
||||
|
||||
// NewHub initializes an instance of Hub which holds app wide configuration.
|
||||
func NewHub(logger *logrus.Logger, buildVersion string) *Hub {
|
||||
hub := &Hub{
|
||||
Logger: logger,
|
||||
Version: buildVersion,
|
||||
QueryFlags: QueryFlags{
|
||||
QNames: []string{},
|
||||
QTypes: []string{},
|
||||
QClasses: []string{},
|
||||
Nameservers: []string{},
|
||||
},
|
||||
Nameservers: []Nameserver{},
|
||||
}
|
||||
return hub
|
||||
}
|
||||
|
||||
// initLogger initializes logger
|
||||
func initLogger() *logrus.Logger {
|
||||
logger := logrus.New()
|
||||
logger.SetFormatter(&logrus.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
DisableLevelTruncation: true,
|
||||
})
|
||||
return logger
|
||||
}
|
90
cmd/doggo/lookup.go
Normal file
90
cmd/doggo/lookup.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/mr-karan/doggo/pkg/resolvers"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Lookup sends the DNS queries to the server.
|
||||
// It prepares a list of `dns.Questions` and sends
|
||||
// to all resolvers. It returns a list of []resolver.Response from
|
||||
// each resolver
|
||||
func (hub *Hub) Lookup() ([][]resolvers.Response, error) {
|
||||
questions, err := hub.prepareQuestions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hub.Questions = questions
|
||||
// for each type of resolver do a DNS lookup
|
||||
responses := make([][]resolvers.Response, 0, len(hub.Questions))
|
||||
for _, r := range hub.Resolver {
|
||||
resp, err := r.Lookup(hub.Questions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
responses = append(responses, resp)
|
||||
}
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
// prepareQuestions takes a list of hostnames and some
|
||||
// additional options and returns a list of all possible
|
||||
// `dns.Questions`.
|
||||
func (hub *Hub) prepareQuestions() ([]dns.Question, error) {
|
||||
var (
|
||||
questions []dns.Question
|
||||
)
|
||||
for _, name := range hub.QueryFlags.QNames {
|
||||
var (
|
||||
domains []string
|
||||
)
|
||||
// If `search` flag is specified then fetch the search list
|
||||
// from `resolv.conf` and set the
|
||||
if hub.QueryFlags.UseSearchList {
|
||||
list, err := fetchDomainList(name, hub.QueryFlags.Ndots)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
domains = list
|
||||
} else {
|
||||
domains = []string{dns.Fqdn(name)}
|
||||
}
|
||||
for _, d := range domains {
|
||||
hub.Logger.WithFields(logrus.Fields{
|
||||
"domain": d,
|
||||
"ndots": hub.QueryFlags.Ndots,
|
||||
}).Debug("Attmepting to resolve")
|
||||
question := dns.Question{
|
||||
Name: d,
|
||||
}
|
||||
// iterate on a list of query types.
|
||||
for _, q := range hub.QueryFlags.QTypes {
|
||||
question.Qtype = dns.StringToType[strings.ToUpper(q)]
|
||||
// iterate on a list of query classes.
|
||||
for _, c := range hub.QueryFlags.QClasses {
|
||||
question.Qclass = dns.StringToClass[strings.ToUpper(c)]
|
||||
// append a new question for each possible pair.
|
||||
questions = append(questions, question)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return questions, nil
|
||||
}
|
||||
|
||||
func fetchDomainList(d string, ndots int) ([]string, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// TODO: Add a method for reading system default nameserver in windows.
|
||||
return []string{d}, nil
|
||||
}
|
||||
cfg, err := dns.ClientConfigFromFile(DefaultResolvConfPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.Ndots = ndots
|
||||
return cfg.NameList(d), nil
|
||||
}
|
129
cmd/doggo/nameservers.go
Normal file
129
cmd/doggo/nameservers.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"runtime"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const (
|
||||
//DefaultResolvConfPath specifies path to default resolv config file on UNIX.
|
||||
DefaultResolvConfPath = "/etc/resolv.conf"
|
||||
// DefaultTLSPort specifies the default port for a DNS server connecting over TCP over TLS
|
||||
DefaultTLSPort = "853"
|
||||
// DefaultUDPPort specifies the default port for a DNS server connecting over UDP
|
||||
DefaultUDPPort = "53"
|
||||
// DefaultTCPPort specifies the default port for a DNS server connecting over TCP
|
||||
DefaultTCPPort = "53"
|
||||
UDPResolver = "udp"
|
||||
DOHResolver = "doh"
|
||||
TCPResolver = "tcp"
|
||||
DOTResolver = "dot"
|
||||
)
|
||||
|
||||
// loadNameservers reads all the user given
|
||||
// nameservers and loads to Hub.
|
||||
func (hub *Hub) loadNameservers() error {
|
||||
for _, srv := range hub.QueryFlags.Nameservers {
|
||||
ns, err := initNameserver(srv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing nameserver: %s", srv)
|
||||
}
|
||||
// check if properly initialised.
|
||||
if ns.Address != "" && ns.Type != "" {
|
||||
hub.Nameservers = append(hub.Nameservers, ns)
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to system nameserver
|
||||
// in case no nameserver is specified by user.
|
||||
if len(hub.Nameservers) == 0 {
|
||||
ns, ndots, err := getDefaultServers()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching system default nameserver")
|
||||
}
|
||||
if hub.QueryFlags.Ndots == 0 {
|
||||
hub.QueryFlags.Ndots = ndots
|
||||
}
|
||||
hub.Nameservers = append(hub.Nameservers, ns...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDefaultServers reads the `resolv.conf`
|
||||
// file and returns a list of nameservers.
|
||||
func getDefaultServers() ([]Nameserver, int, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// TODO: Add a method for reading system default nameserver in windows.
|
||||
return nil, 0, errors.New(`unable to read default nameservers in this machine`)
|
||||
}
|
||||
// if no nameserver is provided, take it from `resolv.conf`
|
||||
cfg, err := dns.ClientConfigFromFile(DefaultResolvConfPath)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
servers := make([]Nameserver, 0, len(cfg.Servers))
|
||||
for _, s := range cfg.Servers {
|
||||
var (
|
||||
ip = net.ParseIP(s)
|
||||
addr string
|
||||
)
|
||||
// handle IPv6
|
||||
if ip != nil && ip.To4() != nil {
|
||||
addr = fmt.Sprintf("%s:%s", s, cfg.Port)
|
||||
} else {
|
||||
addr = fmt.Sprintf("[%s]:%s", s, cfg.Port)
|
||||
}
|
||||
ns := Nameserver{
|
||||
Type: UDPResolver,
|
||||
Address: addr,
|
||||
}
|
||||
servers = append(servers, ns)
|
||||
}
|
||||
return servers, cfg.Ndots, nil
|
||||
}
|
||||
|
||||
func initNameserver(n string) (Nameserver, error) {
|
||||
// Instantiate a UDP resolver with default port as a fallback.
|
||||
ns := Nameserver{
|
||||
Type: UDPResolver,
|
||||
Address: net.JoinHostPort(n, DefaultUDPPort),
|
||||
}
|
||||
u, err := url.Parse(n)
|
||||
if err != nil {
|
||||
return ns, err
|
||||
}
|
||||
if u.Scheme == "https" {
|
||||
ns.Type = DOHResolver
|
||||
ns.Address = u.String()
|
||||
}
|
||||
if u.Scheme == "tls" {
|
||||
ns.Type = DOTResolver
|
||||
if u.Port() == "" {
|
||||
ns.Address = net.JoinHostPort(u.Hostname(), DefaultTLSPort)
|
||||
} else {
|
||||
ns.Address = net.JoinHostPort(u.Hostname(), u.Port())
|
||||
}
|
||||
}
|
||||
if u.Scheme == "tcp" {
|
||||
ns.Type = TCPResolver
|
||||
if u.Port() == "" {
|
||||
ns.Address = net.JoinHostPort(u.Hostname(), DefaultTCPPort)
|
||||
} else {
|
||||
ns.Address = net.JoinHostPort(u.Hostname(), u.Port())
|
||||
}
|
||||
}
|
||||
if u.Scheme == "udp" {
|
||||
ns.Type = UDPResolver
|
||||
if u.Port() == "" {
|
||||
ns.Address = net.JoinHostPort(u.Hostname(), DefaultUDPPort)
|
||||
} else {
|
||||
ns.Address = net.JoinHostPort(u.Hostname(), u.Port())
|
||||
}
|
||||
}
|
||||
return ns, nil
|
||||
}
|
223
cmd/doggo/output.go
Normal file
223
cmd/doggo/output.go
Normal file
|
@ -0,0 +1,223 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/mr-karan/doggo/pkg/resolvers"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
type Output struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Class string `json:"class"`
|
||||
TTL string `json:"ttl"`
|
||||
Address string `json:"address"`
|
||||
TimeTaken string `json:"rtt"`
|
||||
Nameserver string `json:"nameserver"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Class string `json:"class"`
|
||||
}
|
||||
type Response struct {
|
||||
Output []Output `json:"answers"`
|
||||
Queries []Query `json:"queries"`
|
||||
}
|
||||
|
||||
type JSONResponse struct {
|
||||
Response `json:"responses"`
|
||||
}
|
||||
|
||||
func (hub *Hub) outputJSON(out []Output) {
|
||||
// get the questions
|
||||
queries := make([]Query, 0)
|
||||
for _, ques := range hub.Questions {
|
||||
q := Query{
|
||||
Name: ques.Name,
|
||||
Type: dns.TypeToString[ques.Qtype],
|
||||
Class: dns.ClassToString[ques.Qclass],
|
||||
}
|
||||
queries = append(queries, q)
|
||||
}
|
||||
|
||||
resp := JSONResponse{
|
||||
Response{
|
||||
Output: out,
|
||||
Queries: queries,
|
||||
},
|
||||
}
|
||||
res, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
hub.Logger.WithError(err).Error("unable to output data in JSON")
|
||||
hub.Logger.Exit(-1)
|
||||
}
|
||||
fmt.Printf("%s", res)
|
||||
}
|
||||
|
||||
func (hub *Hub) outputTerminal(out []Output) {
|
||||
green := color.New(color.FgGreen, color.Bold).SprintFunc()
|
||||
blue := color.New(color.FgBlue, color.Bold).SprintFunc()
|
||||
yellow := color.New(color.FgYellow, color.Bold).SprintFunc()
|
||||
cyan := color.New(color.FgCyan, color.Bold).SprintFunc()
|
||||
red := color.New(color.FgRed, color.Bold).SprintFunc()
|
||||
|
||||
if !hub.QueryFlags.Color {
|
||||
color.NoColor = true // disables colorized output
|
||||
}
|
||||
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
header := []string{"Name", "Type", "Class", "TTL", "Address", "Nameserver"}
|
||||
if hub.QueryFlags.DisplayTimeTaken {
|
||||
header = append(header, "Time Taken")
|
||||
}
|
||||
table.SetHeader(header)
|
||||
table.SetAutoWrapText(false)
|
||||
table.SetAutoFormatHeaders(true)
|
||||
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetCenterSeparator("")
|
||||
table.SetColumnSeparator("")
|
||||
table.SetRowSeparator("")
|
||||
table.SetHeaderLine(false)
|
||||
table.SetBorder(false)
|
||||
table.SetTablePadding("\t") // pad with tabs
|
||||
table.SetNoWhiteSpace(true)
|
||||
|
||||
for _, o := range out {
|
||||
var typOut string
|
||||
switch typ := o.Type; typ {
|
||||
case "A":
|
||||
typOut = blue(o.Type)
|
||||
case "AAAA":
|
||||
typOut = blue(o.Type)
|
||||
case "MX":
|
||||
typOut = red(o.Type)
|
||||
case "NS":
|
||||
typOut = cyan(o.Type)
|
||||
case "CNAME":
|
||||
typOut = yellow(o.Type)
|
||||
case "TXT":
|
||||
typOut = yellow(o.Type)
|
||||
case "SOA":
|
||||
typOut = red(o.Type)
|
||||
default:
|
||||
typOut = blue(o.Type)
|
||||
}
|
||||
output := []string{green(o.Name), typOut, o.Class, o.TTL, o.Address, o.Nameserver}
|
||||
// Print how long it took
|
||||
if hub.QueryFlags.DisplayTimeTaken {
|
||||
output = append(output, o.TimeTaken)
|
||||
}
|
||||
table.Append(output)
|
||||
}
|
||||
table.Render()
|
||||
}
|
||||
|
||||
// Output takes a list of `dns.Answers` and based
|
||||
// on the output format specified displays the information.
|
||||
func (hub *Hub) Output(responses [][]resolvers.Response) {
|
||||
out := collectOutput(responses)
|
||||
if hub.QueryFlags.ShowJSON {
|
||||
hub.outputJSON(out)
|
||||
} else {
|
||||
hub.outputTerminal(out)
|
||||
}
|
||||
}
|
||||
|
||||
func collectOutput(responses [][]resolvers.Response) []Output {
|
||||
var out []Output
|
||||
// for each resolver
|
||||
for _, rslvr := range responses {
|
||||
// get the response
|
||||
for _, r := range rslvr {
|
||||
var addr string
|
||||
for _, ns := range r.Message.Ns {
|
||||
// check for SOA record
|
||||
soa, ok := ns.(*dns.SOA)
|
||||
if !ok {
|
||||
// skip this message
|
||||
continue
|
||||
}
|
||||
addr = soa.Ns + " " + soa.Mbox +
|
||||
" " + strconv.FormatInt(int64(soa.Serial), 10) +
|
||||
" " + strconv.FormatInt(int64(soa.Refresh), 10) +
|
||||
" " + strconv.FormatInt(int64(soa.Retry), 10) +
|
||||
" " + strconv.FormatInt(int64(soa.Expire), 10) +
|
||||
" " + strconv.FormatInt(int64(soa.Minttl), 10)
|
||||
h := ns.Header()
|
||||
name := h.Name
|
||||
qclass := dns.Class(h.Class).String()
|
||||
ttl := strconv.FormatInt(int64(h.Ttl), 10) + "s"
|
||||
qtype := dns.Type(h.Rrtype).String()
|
||||
rtt := fmt.Sprintf("%dms", r.RTT.Milliseconds())
|
||||
o := Output{
|
||||
Name: name,
|
||||
Type: qtype,
|
||||
TTL: ttl,
|
||||
Class: qclass,
|
||||
Address: addr,
|
||||
TimeTaken: rtt,
|
||||
Nameserver: r.Nameserver,
|
||||
}
|
||||
out = append(out, o)
|
||||
}
|
||||
for _, a := range r.Message.Answer {
|
||||
switch t := a.(type) {
|
||||
case *dns.A:
|
||||
addr = t.A.String()
|
||||
case *dns.AAAA:
|
||||
addr = t.AAAA.String()
|
||||
case *dns.CNAME:
|
||||
addr = t.Target
|
||||
case *dns.CAA:
|
||||
addr = t.Tag + " " + t.Value
|
||||
case *dns.HINFO:
|
||||
addr = t.Cpu + " " + t.Os
|
||||
case *dns.PTR:
|
||||
addr = t.Ptr
|
||||
case *dns.SRV:
|
||||
addr = strconv.Itoa(int(t.Priority)) + " " +
|
||||
strconv.Itoa(int(t.Weight)) + " " +
|
||||
t.Target + ":" + strconv.Itoa(int(t.Port))
|
||||
case *dns.TXT:
|
||||
addr = t.String()
|
||||
case *dns.NS:
|
||||
addr = t.Ns
|
||||
case *dns.MX:
|
||||
addr = strconv.Itoa(int(t.Preference)) + " " + t.Mx
|
||||
case *dns.SOA:
|
||||
addr = t.String()
|
||||
case *dns.NAPTR:
|
||||
addr = t.String()
|
||||
}
|
||||
|
||||
h := a.Header()
|
||||
name := h.Name
|
||||
qclass := dns.Class(h.Class).String()
|
||||
ttl := strconv.FormatInt(int64(h.Ttl), 10) + "s"
|
||||
qtype := dns.Type(h.Rrtype).String()
|
||||
rtt := fmt.Sprintf("%dms", r.RTT.Milliseconds())
|
||||
o := Output{
|
||||
Name: name,
|
||||
Type: qtype,
|
||||
TTL: ttl,
|
||||
Class: qclass,
|
||||
Address: addr,
|
||||
TimeTaken: rtt,
|
||||
Nameserver: r.Nameserver,
|
||||
}
|
||||
out = append(out, o)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
73
cmd/doggo/parse.go
Normal file
73
cmd/doggo/parse.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func (hub *Hub) loadQueryArgs() error {
|
||||
// Appends a list of unparsed args to
|
||||
// internal query flags.
|
||||
err := hub.loadUnparsedArgs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// check if ndots is set
|
||||
hub.QueryFlags.isNdotsSet = isFlagPassed("ndots", hub.flag)
|
||||
|
||||
// Load all fallbacks in internal query flags.
|
||||
hub.loadFallbacks()
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadUnparsedArgs tries to parse all the arguments
|
||||
// which are unparsed by `flag` library. These arguments don't have any specific
|
||||
// order so we have to deduce based on the pattern of argument.
|
||||
// For eg, a nameserver must always begin with `@`. In this
|
||||
// pattern we deduce the arguments and append it to the
|
||||
// list of internal query flags.
|
||||
// In case an argument isn't able to fit in any of the existing
|
||||
// pattern it is considered to be a "hostname".
|
||||
// Eg of unparsed argument: `dig mrkaran.dev @1.1.1.1 AAAA`
|
||||
// where `@1.1.1.1` and `AAAA` are "unparsed" args.
|
||||
func (hub *Hub) loadUnparsedArgs() error {
|
||||
for _, arg := range hub.UnparsedArgs {
|
||||
if strings.HasPrefix(arg, "@") {
|
||||
hub.QueryFlags.Nameservers = append(hub.QueryFlags.Nameservers, strings.Trim(arg, "@"))
|
||||
} else if _, ok := dns.StringToType[strings.ToUpper(arg)]; ok {
|
||||
hub.QueryFlags.QTypes = append(hub.QueryFlags.QTypes, arg)
|
||||
} else if _, ok := dns.StringToClass[strings.ToUpper(arg)]; ok {
|
||||
hub.QueryFlags.QClasses = append(hub.QueryFlags.QClasses, arg)
|
||||
} else {
|
||||
// if nothing matches, consider it's a query name.
|
||||
hub.QueryFlags.QNames = append(hub.QueryFlags.QNames, arg)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadFallbacks sets fallbacks for options
|
||||
// that are not specified by the user but necessary
|
||||
// for the resolver.
|
||||
func (hub *Hub) loadFallbacks() {
|
||||
if len(hub.QueryFlags.QTypes) == 0 {
|
||||
hub.QueryFlags.QTypes = append(hub.QueryFlags.QTypes, "A")
|
||||
}
|
||||
if len(hub.QueryFlags.QClasses) == 0 {
|
||||
hub.QueryFlags.QClasses = append(hub.QueryFlags.QClasses, "IN")
|
||||
}
|
||||
}
|
||||
|
||||
// isFlagPassed checks if the flag is supplied by
|
||||
//user or not.
|
||||
func isFlagPassed(name string, f *flag.FlagSet) bool {
|
||||
found := false
|
||||
f.Visit(func(f *flag.Flag) {
|
||||
if f.Name == name {
|
||||
found = true
|
||||
}
|
||||
})
|
||||
return found
|
||||
}
|
68
cmd/doggo/resolver.go
Normal file
68
cmd/doggo/resolver.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mr-karan/doggo/pkg/resolvers"
|
||||
)
|
||||
|
||||
// loadResolvers loads differently configured
|
||||
// resolvers based on a list of nameserver.
|
||||
func (hub *Hub) loadResolvers() error {
|
||||
// for each nameserver, initialise the correct resolver
|
||||
for _, ns := range hub.Nameservers {
|
||||
if ns.Type == DOHResolver {
|
||||
hub.Logger.Debug("initiating DOH resolver")
|
||||
rslvr, err := resolvers.NewDOHResolver(ns.Address, resolvers.DOHResolverOpts{
|
||||
Timeout: hub.QueryFlags.Timeout * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hub.Resolver = append(hub.Resolver, rslvr)
|
||||
}
|
||||
if ns.Type == DOTResolver {
|
||||
hub.Logger.Debug("initiating DOT resolver")
|
||||
rslvr, err := resolvers.NewClassicResolver(ns.Address, resolvers.ClassicResolverOpts{
|
||||
IPv4Only: hub.QueryFlags.UseIPv4,
|
||||
IPv6Only: hub.QueryFlags.UseIPv6,
|
||||
Timeout: hub.QueryFlags.Timeout * time.Second,
|
||||
UseTLS: true,
|
||||
UseTCP: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hub.Resolver = append(hub.Resolver, rslvr)
|
||||
}
|
||||
if ns.Type == TCPResolver {
|
||||
hub.Logger.Debug("initiating TCP resolver")
|
||||
rslvr, err := resolvers.NewClassicResolver(ns.Address, resolvers.ClassicResolverOpts{
|
||||
IPv4Only: hub.QueryFlags.UseIPv4,
|
||||
IPv6Only: hub.QueryFlags.UseIPv6,
|
||||
Timeout: hub.QueryFlags.Timeout * time.Second,
|
||||
UseTLS: false,
|
||||
UseTCP: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hub.Resolver = append(hub.Resolver, rslvr)
|
||||
}
|
||||
if ns.Type == UDPResolver {
|
||||
hub.Logger.Debug("initiating UDP resolver")
|
||||
rslvr, err := resolvers.NewClassicResolver(ns.Address, resolvers.ClassicResolverOpts{
|
||||
IPv4Only: hub.QueryFlags.UseIPv4,
|
||||
IPv6Only: hub.QueryFlags.UseIPv6,
|
||||
Timeout: hub.QueryFlags.Timeout * time.Second,
|
||||
UseTLS: false,
|
||||
UseTCP: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hub.Resolver = append(hub.Resolver, rslvr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue