diff --git a/TODO.md b/TODO.md index b11fdc4..8b50676 100644 --- a/TODO.md +++ b/TODO.md @@ -1,10 +1,11 @@ # doggo - v1.0 Milestone ## Resolver -- [ ] Create a DNS Resolver struct +- [x] Create a DNS Resolver struct - [ ] Add methods to initialise the config, set defaults -- [ ] Add a resolve method -- [ ] Make it separate from Hub +- [x] Add a resolve method +- [x] Make it separate from Hub +- [ ] Parse output into separate fields ## CLI Features - [ ] `digfile` @@ -15,10 +16,10 @@ - [ ] Table output ## CLI Grunt -- [ ] Query args -- [ ] Neatly package them to load args in different functions -- [ ] Upper case is not mandatory for query type/classes - +- [x] Query args +- [x] Neatly package them to load args in different functions +- [x] Upper case is not mandatory for query type/classes +- [ ] Output ## Tests ## Documentation diff --git a/bin/doggo.bin b/bin/doggo.bin index 62b2033..b756699 100755 Binary files a/bin/doggo.bin and b/bin/doggo.bin differ diff --git a/cmd/cli.go b/cmd/cli.go index a4573b6..2694765 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -2,24 +2,24 @@ package main import ( "os" - "strings" - resolver "github.com/mr-karan/doggo/pkg/resolve" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) var ( // Version and date of the build. This is injected at build-time. - buildVersion = "unknown" - buildDate = "unknown" + buildVersion = "unknown" + buildDate = "unknown" + verboseEnabled = false ) // initLogger initializes logger func initLogger(verbose bool) *logrus.Logger { logger := logrus.New() logger.SetFormatter(&logrus.TextFormatter{ - FullTimestamp: true, + DisableTimestamp: true, + DisableLevelTruncation: true, }) // Set logger level if verbose { @@ -32,66 +32,61 @@ func initLogger(verbose bool) *logrus.Logger { } func main() { - // Intialize new CLI app - app := cli.NewApp() - app.Name = "doggo" - app.Usage = "Command-line DNS Client" - app.Version = buildVersion var ( - logger = initLogger(true) + logger = initLogger(verboseEnabled) + app = cli.NewApp() ) // Initialize hub. hub := NewHub(logger, buildVersion) - // Register command line args. + + // Configure CLI app. + app.Name = "doggo" + app.Usage = "Command-line DNS Client" + app.Version = buildVersion + + var qFlags QueryFlags + // Register command line flags. app.Flags = []cli.Flag{ &cli.StringSliceFlag{ Name: "query", Usage: "Domain name to query", - Destination: hub.Domains, + Destination: qFlags.QNames, + }, + &cli.StringSliceFlag{ + Name: "type", + Usage: "Type of DNS record to be queried (A, AAAA, MX etc)", + Destination: qFlags.QTypes, + }, + &cli.StringSliceFlag{ + Name: "nameserver", + Usage: "Address of the nameserver to send packets to", + Destination: qFlags.Nameservers, + }, + &cli.StringSliceFlag{ + Name: "class", + Usage: "Network class of the DNS record to be queried (IN, CH, HS etc)", + Destination: qFlags.QClasses, }, &cli.BoolFlag{ - Name: "verbose", - Usage: "Enable verbose logging", + Name: "verbose", + Usage: "Enable verbose logging", + Destination: &verboseEnabled, + DefaultText: "false", }, } - app.Action = func(c *cli.Context) error { - // parse arguments - var domains cli.StringSlice - for _, arg := range c.Args().Slice() { - if strings.HasPrefix(arg, "@") { - hub.Nameservers = append(hub.Nameservers, arg) - } else if isUpper(arg) { - if parseQueryType(arg) { - hub.QTypes = append(hub.QTypes, arg) - } else if parseQueryClass(arg) { - hub.QClass = append(hub.QClass, arg) - } - } else { - domains.Set(arg) - hub.Domains = &domains - } + app.Before = hub.loadQueryArgs + app.Action = func(c *cli.Context) error { + if len(hub.QueryFlags.QNames.Value()) == 0 { + cli.ShowAppHelpAndExit(c, 0) } - // load defaults - if len(hub.QTypes) == 0 { - hub.QTypes = append(hub.QTypes, "A") - } - if len(hub.Nameservers) == 0 { - ns, err := resolver.GetDefaultNameserver() - if err != nil { - panic(err) - } - hub.Nameservers = append(hub.Nameservers, ns) - } - // resolve query - hub.Resolve() + hub.Lookup(c) return nil } - // Run the app. - hub.Logger.Info("Starting doggo...") + hub.Logger.Debug("Starting doggo...") err := app.Run(os.Args) if err != nil { - logger.Errorf("Something terrbily went wrong: %s", err) + logger.Errorf("oops! we encountered an issue: %s", err) } } diff --git a/cmd/hub.go b/cmd/hub.go index 18f6280..beeea5b 100644 --- a/cmd/hub.go +++ b/cmd/hub.go @@ -1,33 +1,41 @@ package main import ( + "github.com/miekg/dns" + "github.com/mr-karan/doggo/pkg/resolve" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) // Hub represents the structure for all app wide functions and structs. type Hub struct { - Logger *logrus.Logger - Version string - Domains *cli.StringSlice - QTypes []string - QClass []string - Nameservers []string + Logger *logrus.Logger + Version string + QueryFlags QueryFlags + Questions []dns.Question + Resolver *resolve.Resolver +} + +// QueryFlags is used store the value of CLI flags. +type QueryFlags struct { + QNames *cli.StringSlice + QTypes *cli.StringSlice + QClasses *cli.StringSlice + Nameservers *cli.StringSlice } // NewHub initializes an instance of Hub which holds app wide configuration. func NewHub(logger *logrus.Logger, buildVersion string) *Hub { + // Initialise Resolver hub := &Hub{ Logger: logger, Version: buildVersion, + QueryFlags: QueryFlags{ + QNames: cli.NewStringSlice(), + QTypes: cli.NewStringSlice(), + QClasses: cli.NewStringSlice(), + Nameservers: cli.NewStringSlice(), + }, } return hub } - -// initApp acts like a middleware to load app managers with Hub before running any command. -// Use this middleware to perform any action before the command is run. -func (hub *Hub) initApp(fn cli.ActionFunc) cli.ActionFunc { - return func(c *cli.Context) error { - return fn(c) - } -} diff --git a/cmd/lookup.go b/cmd/lookup.go new file mode 100644 index 0000000..207d5ce --- /dev/null +++ b/cmd/lookup.go @@ -0,0 +1,37 @@ +package main + +import ( + "strings" + + "github.com/miekg/dns" + "github.com/urfave/cli/v2" +) + +func (hub *Hub) Lookup(c *cli.Context) error { + hub.prepareQuestions() + err := hub.Resolver.Lookup(hub.Questions) + if err != nil { + hub.Logger.Error(err) + } + return nil +} + +// prepareQuestions iterates on list of domain names +// and prepare a list of questions +// sent to the server with all possible combinations. +func (hub *Hub) prepareQuestions() { + var question dns.Question + for _, name := range hub.QueryFlags.QNames.Value() { + question.Name = dns.Fqdn(name) + // iterate on a list of query types. + for _, q := range hub.QueryFlags.QTypes.Value() { + question.Qtype = dns.StringToType[strings.ToUpper(q)] + // iterate on a list of query classes. + for _, c := range hub.QueryFlags.QClasses.Value() { + question.Qclass = dns.StringToClass[strings.ToUpper(c)] + // append a new question for each possible pair. + hub.Questions = append(hub.Questions, question) + } + } + } +} diff --git a/cmd/parse.go b/cmd/parse.go index 9172bc2..91f476c 100644 --- a/cmd/parse.go +++ b/cmd/parse.go @@ -1,13 +1,76 @@ package main -func (hub *Hub) loadFlags() { +import ( + "runtime" + "strings" + "github.com/miekg/dns" + "github.com/mr-karan/doggo/pkg/resolve" + "github.com/urfave/cli/v2" +) + +func (hub *Hub) loadQueryArgs(c *cli.Context) error { + err := hub.parseFreeArgs(c) + if err != nil { + cli.Exit("Error parsing arguments", -1) + } + err = hub.loadResolver(c) + if err != nil { + cli.Exit("Error parsing nameservers", -1) + } + hub.loadFallbacks(c) + return err } -func (hub *Hub) loadFreeForm() { - +// parseFreeArgs tries to parse all the arguments +// given to the CLI. 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 map it to internal query +// options. In case an argument isn't able to fit in any of the existing +// pattern it is considered to be a "query name". +func (hub *Hub) parseFreeArgs(c *cli.Context) error { + for _, arg := range c.Args().Slice() { + if strings.HasPrefix(arg, "@") { + hub.QueryFlags.Nameservers.Set(arg) + } else if _, ok := dns.StringToType[strings.ToUpper(arg)]; ok { + hub.QueryFlags.QTypes.Set(arg) + } else if _, ok := dns.StringToClass[strings.ToUpper(arg)]; ok { + hub.QueryFlags.QClasses.Set(arg) + } else { + // if nothing matches, consider it's a query name. + hub.QueryFlags.QNames.Set(arg) + } + } + return nil } -func (hub *Hub) loadFallbacks() { - +// loadFallbacks sets fallbacks for options +// that are not specified by the user. +func (hub *Hub) loadFallbacks(c *cli.Context) { + if len(hub.QueryFlags.QTypes.Value()) == 0 { + hub.QueryFlags.QTypes.Set("A") + } + if len(hub.QueryFlags.QClasses.Value()) == 0 { + hub.QueryFlags.QClasses.Set("IN") + } +} + +// loadResolver checks +func (hub *Hub) loadResolver(c *cli.Context) error { + if len(hub.QueryFlags.Nameservers.Value()) == 0 { + if runtime.GOOS == "windows" { + // TODO: Add a method for reading system default nameserver in windows. + } else { + rslvr, err := resolve.NewResolverFromResolvFile(resolve.DefaultResolvConfPath) + if err != nil { + return err + } + hub.Resolver = rslvr + } + } else { + rslvr := resolve.NewResolver(hub.QueryFlags.Nameservers.Value()) + hub.Resolver = rslvr + } + return nil } diff --git a/cmd/resolve.go b/cmd/resolve.go deleted file mode 100644 index a595c5b..0000000 --- a/cmd/resolve.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/miekg/dns" -) - -// Resolve resolves the domain name -func (hub *Hub) Resolve() { - var messages = make([]dns.Msg, 0, len(hub.Domains.Value())) - for _, d := range hub.Domains.Value() { - msg := dns.Msg{} - msg.Id = dns.Id() - msg.RecursionDesired = true - msg.Question = []dns.Question{(dns.Question{dns.Fqdn(d), dns.TypeA, dns.ClassINET})} - messages = append(messages, msg) - } - c := new(dns.Client) - for _, msg := range messages { - in, rtt, err := c.Exchange(&msg, "127.0.0.1:53") - if err != nil { - panic(err) - } - for _, ans := range in.Answer { - if t, ok := ans.(*dns.A); ok { - fmt.Println(t.String()) - } - } - fmt.Println("rtt is", rtt, msg.Question[0].Name) - } -} diff --git a/cmd/utils.go b/cmd/utils.go deleted file mode 100644 index e5685ca..0000000 --- a/cmd/utils.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import "unicode" - -var ( - QTYPES = []string{"A", "AAAA", "MX"} - QCLASS = []string{"CN", "AAAA", "MX"} -) - -func isUpper(s string) bool { - for _, r := range s { - if !unicode.IsUpper(r) && unicode.IsLetter(r) { - return false - } - } - return true -} - -func parseQueryType(s string) bool { - for _, b := range QTYPES { - if b == s { - return true - } - } - return false -} - -func parseQueryClass(s string) bool { - for _, b := range QCLASS { - if b == s { - return true - } - } - return false -} diff --git a/pkg/models/models.go b/pkg/models/models.go deleted file mode 100644 index 1a8e24b..0000000 --- a/pkg/models/models.go +++ /dev/null @@ -1,12 +0,0 @@ -package models - -// Question represents a given query to the client. -// A question can have multiple domains, multiple nameservers -// but it's the responsibility of the client to send each question -// to the nameserver and collect responses. -type Question struct { - Domain []string - Nameservers []string - QClass []uint16 - QType []uint16 -} diff --git a/pkg/resolve/nameserver.go b/pkg/resolve/nameserver.go deleted file mode 100644 index c6388e8..0000000 --- a/pkg/resolve/nameserver.go +++ /dev/null @@ -1,33 +0,0 @@ -package resolve - -import ( - "bufio" - "os" - "strings" -) - -// GetDefaultNameserver reads `/etc/resolv.conf` to determine the default -// nameserver configured. Returns an error if unable to parse or -// no nameserver specified. It returns as soon as it finds a line -// with `nameserver` prefix. -// An example format: -// `nameserver 127.0.0.1` - -func GetDefaultNameserver() (string, error) { - file, err := os.Open("/etc/resolv.conf") - if err != nil { - return "", err - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - if strings.HasPrefix(scanner.Text(), "nameserver") { - return strings.Fields(scanner.Text())[1], nil - } - } - if err := scanner.Err(); err != nil { - return "", err - } - return "", err -} diff --git a/pkg/resolve/resolver.go b/pkg/resolve/resolver.go index 4b365b6..242fc5e 100644 --- a/pkg/resolve/resolver.go +++ b/pkg/resolve/resolver.go @@ -54,30 +54,24 @@ func NewResolverFromResolvFile(resolvFilePath string) (*Resolver, error) { }, nil } -func (r *Resolver) Lookup(domains []string) []error { - // Prepare a list of DNS messages to be sent to the server. - // It's possible to send multiple question in one message - // but some nameservers are not able to - var messages = make([]dns.Msg, 0, len(domains)) - - for _, d := range domains { +// Lookup prepare a list of DNS messages to be sent to the server. +// It's possible to send multiple question in one message +// but some nameservers are not able to +func (r *Resolver) Lookup(questions []dns.Question) error { + var messages = make([]dns.Msg, 0, len(questions)) + for _, q := range questions { msg := dns.Msg{} msg.Id = dns.Id() msg.RecursionDesired = true // It's recommended to only send 1 question for 1 DNS message. - msg.Question = []dns.Question{(dns.Question{ - Name: dns.Fqdn(d), - Qtype: dns.TypeA, - Qclass: dns.ClassINET, - })} + msg.Question = []dns.Question{q} messages = append(messages, msg) } - var errors []error for _, msg := range messages { for _, srv := range r.servers { in, rtt, err := r.client.Exchange(&msg, srv) if err != nil { - errors = append(errors, err) + return err } for _, ans := range in.Answer { if t, ok := ans.(*dns.A); ok { @@ -87,5 +81,5 @@ func (r *Resolver) Lookup(domains []string) []error { fmt.Println("rtt is", rtt, msg.Question) } } - return errors + return nil }