feat: working finally

pull/2/head
Karan Sharma 2020-12-10 21:44:04 +05:30
parent df306e18a9
commit 252d11c764
11 changed files with 186 additions and 200 deletions

15
TODO.md
View File

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

Binary file not shown.

View File

@ -2,9 +2,7 @@ package main
import (
"os"
"strings"
resolver "github.com/mr-karan/doggo/pkg/resolve"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
@ -13,13 +11,15 @@ var (
// Version and date of the build. This is injected at build-time.
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",
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)
app.Before = hub.loadQueryArgs
app.Action = func(c *cli.Context) error {
if len(hub.QueryFlags.QNames.Value()) == 0 {
cli.ShowAppHelpAndExit(c, 0)
}
} else {
domains.Set(arg)
hub.Domains = &domains
}
}
// 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)
}
}

View File

@ -1,6 +1,8 @@
package main
import (
"github.com/miekg/dns"
"github.com/mr-karan/doggo/pkg/resolve"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
@ -9,25 +11,31 @@ import (
type Hub struct {
Logger *logrus.Logger
Version string
Domains *cli.StringSlice
QTypes []string
QClass []string
Nameservers []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)
}
}

37
cmd/lookup.go 100644
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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