From f389c9c87665a7390d0fbdfa0a33b0a513cdd0d6 Mon Sep 17 00:00:00 2001 From: Karan Sharma Date: Sat, 27 Feb 2021 14:01:59 +0530 Subject: [PATCH] feat: API Handlers --- Makefile | 2 +- cmd/doggo/api/api.go | 93 +++++++++------------------ cmd/doggo/api/config.go | 60 ++++++++++++++++++ cmd/doggo/api/handlers.go | 130 ++++++++++++++++++++++++++++++++++++++ cmd/doggo/cli/cli.go | 7 +- config-api-sample.toml | 9 +++ pkg/models/models.go | 6 +- www/api/api.md | 13 ++++ 8 files changed, 249 insertions(+), 71 deletions(-) create mode 100644 cmd/doggo/api/config.go create mode 100644 cmd/doggo/api/handlers.go create mode 100644 config-api-sample.toml create mode 100644 www/api/api.md diff --git a/Makefile b/Makefile index 49184a9..0b039c9 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ run-cli: build-cli ## Build and Execute the CLI binary after the build step. .PHONY: run-api run-api: build-api ## Build and Execute the API binary after the build step. - ${API_BIN} + ${API_BIN} --config config-api-sample.toml .PHONY: clean clean: diff --git a/cmd/doggo/api/api.go b/cmd/doggo/api/api.go index 9f43c8e..e8f184f 100644 --- a/cmd/doggo/api/api.go +++ b/cmd/doggo/api/api.go @@ -1,82 +1,51 @@ package main import ( - "encoding/json" "net/http" + "time" + + "github.com/mr-karan/doggo/internal/app" + "github.com/mr-karan/doggo/pkg/utils" + "github.com/sirupsen/logrus" "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" "github.com/knadh/koanf" - "github.com/mr-karan/doggo/pkg/utils" ) var ( logger = utils.InitLogger() - k = koanf.New(".") + ko = koanf.New(".") + // Version and date of the build. This is injected at build-time. + buildVersion = "unknown" + buildDate = "unknown" ) -type resp struct { - Status string `json:"status"` - Message string `json:"message,omitempty"` - Data interface{} `json:"data,omitempty"` -} - func main() { + initConfig() + // Initialize app. + app := app.New(logger, buildVersion) + + // Register handles. r := chi.NewRouter() + r.Get("/", wrap(app, handleIndex)) + r.Get("/ping/", wrap(app, handleHealthCheck)) + r.Post("/lookup/", wrap(app, handleLookup)) - // Setup middlewares. - r.Use(middleware.RequestID) - r.Use(middleware.RealIP) - r.Use(middleware.Logger) - r.Use(middleware.Recoverer) - - r.Get("/", func(w http.ResponseWriter, r *http.Request) { - sendSuccessResponse("Welcome to Doggo DNS!", w) - return - }) - - r.Get("/ping/", func(w http.ResponseWriter, r *http.Request) { - sendSuccessResponse("PONG", w) - return - }) - - r.Post("/lookup/", func(w http.ResponseWriter, r *http.Request) { - return - }) - - http.ListenAndServe(":3000", r) -} - -// sendResponse sends an HTTP success response. -func sendResponse(data interface{}, statusText string, status int, w http.ResponseWriter) { - w.WriteHeader(status) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - - out, err := json.Marshal(resp{Status: statusText, Data: data}) - if err != nil { - sendErrorResponse("Internal Server Error", http.StatusInternalServerError, nil, w) - return + // HTTP Server. + srv := &http.Server{ + Addr: ko.String("server.address"), + Handler: r, + ReadTimeout: ko.Duration("server.read_timeout") * time.Millisecond, + WriteTimeout: ko.Duration("server.write_timeout") * time.Millisecond, + IdleTimeout: ko.Duration("server.keepalive_timeout") * time.Millisecond, } - _, _ = w.Write(out) -} - -// sendSuccessResponse sends an HTTP success (200 OK) response. -func sendSuccessResponse(data interface{}, w http.ResponseWriter) { - sendResponse(data, "success", http.StatusOK, w) -} - -// sendErrorResponse sends an HTTP error response. -func sendErrorResponse(message string, status int, data interface{}, w http.ResponseWriter) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(status) - - resp := resp{Status: "error", - Message: message, - Data: data} - - out, _ := json.Marshal(resp) - - _, _ = w.Write(out) + logger.WithFields(logrus.Fields{ + "address": srv.Addr, + }).Info("starting server") + + if err := srv.ListenAndServe(); err != nil { + logger.Fatalf("couldn't start server: %v", err) + } } diff --git a/cmd/doggo/api/config.go b/cmd/doggo/api/config.go new file mode 100644 index 0000000..c495293 --- /dev/null +++ b/cmd/doggo/api/config.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/knadh/koanf/parsers/toml" + "github.com/knadh/koanf/providers/env" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/providers/posflag" + "github.com/sirupsen/logrus" + flag "github.com/spf13/pflag" +) + +// Config is the config given by the user +type Config struct { + HTTPAddr string `koanf:"listen_addr"` +} + +func initConfig() { + f := flag.NewFlagSet("api", flag.ContinueOnError) + f.Usage = func() { + fmt.Println(f.FlagUsages()) + os.Exit(0) + } + + // Register --config flag. + f.StringSlice("config", []string{"config.toml"}, + "Path to one or more TOML config files to load in order") + + // Register --version flag. + f.Bool("version", false, "Show build version") + f.Parse(os.Args[1:]) + // Display version. + if ok, _ := f.GetBool("version"); ok { + fmt.Println(buildVersion, buildDate) + os.Exit(0) + } + + // Read the config files. + cFiles, _ := f.GetStringSlice("config") + for _, f := range cFiles { + logger.WithFields(logrus.Fields{ + "file": f, + }).Info("reading config") + if err := ko.Load(file.Provider(f), toml.Parser()); err != nil { + logger.Fatalf("error reading config: %v", err) + } + } + // Load environment variables and merge into the loaded config. + if err := ko.Load(env.Provider("DOGGO_API_", ".", func(s string) string { + return strings.Replace(strings.ToLower( + strings.TrimPrefix(s, "DOGGO_API_")), "__", ".", -1) + }), nil); err != nil { + logger.Fatalf("error loading env config: %v", err) + } + + ko.Load(posflag.Provider(f, ".", ko), nil) +} diff --git a/cmd/doggo/api/handlers.go b/cmd/doggo/api/handlers.go new file mode 100644 index 0000000..d7ba361 --- /dev/null +++ b/cmd/doggo/api/handlers.go @@ -0,0 +1,130 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/mr-karan/doggo/internal/app" + "github.com/mr-karan/doggo/pkg/models" + "github.com/mr-karan/doggo/pkg/resolvers" +) + +type httpResp struct { + Status string `json:"status"` + Message string `json:"message,omitempty"` + Data interface{} `json:"data,omitempty"` +} + +func handleIndex(w http.ResponseWriter, r *http.Request) { + sendResponse(w, http.StatusOK, "Welcome to Doggo API.") + return +} + +func handleHealthCheck(w http.ResponseWriter, r *http.Request) { + sendResponse(w, http.StatusOK, "PONG") + return +} + +func handleLookup(w http.ResponseWriter, r *http.Request) { + var ( + app = r.Context().Value("app").(app.App) + ) + + // Read body. + b, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + if err != nil { + app.Logger.WithError(err).Error("error reading request body") + sendErrorResponse(w, fmt.Sprintf("Invalid JSON payload"), http.StatusBadRequest, nil) + return + } + // Prepare query flags. + var qFlags models.QueryFlags + if err := json.Unmarshal(b, &qFlags); err != nil { + app.Logger.WithError(err).Error("error unmarshalling payload") + sendErrorResponse(w, fmt.Sprintf("Invalid JSON payload"), http.StatusBadRequest, nil) + return + } + app.QueryFlags = qFlags + // Load fallbacks. + app.LoadFallbacks() + + // Load Questions. + app.PrepareQuestions() + + // Load Nameservers. + err = app.LoadNameservers() + if err != nil { + app.Logger.WithError(err).Error("error loading nameservers") + sendErrorResponse(w, fmt.Sprintf("Error lookup up for records"), http.StatusInternalServerError, nil) + } + + // Load Resolvers. + rslvrs, err := resolvers.LoadResolvers(resolvers.Options{ + Nameservers: app.Nameservers, + UseIPv4: app.QueryFlags.UseIPv4, + UseIPv6: app.QueryFlags.UseIPv6, + SearchList: app.ResolverOpts.SearchList, + Ndots: app.ResolverOpts.Ndots, + Timeout: app.QueryFlags.Timeout * time.Second, + Logger: app.Logger, + }) + if err != nil { + app.Logger.WithError(err).Error("error loading resolver") + sendErrorResponse(w, fmt.Sprintf("Error lookup up for records"), http.StatusInternalServerError, nil) + + } + app.Resolvers = rslvrs + + var responses []resolvers.Response + for _, q := range app.Questions { + for _, rslv := range app.Resolvers { + resp, err := rslv.Lookup(q) + if err != nil { + app.Logger.WithError(err).Error("error looking up DNS records") + app.Logger.Exit(2) + } + responses = append(responses, resp) + } + } + sendResponse(w, http.StatusOK, responses) + return +} + +// wrap is a middleware that wraps HTTP handlers and injects the "app" context. +func wrap(app app.App, next http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), "app", app) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// sendResponse sends a JSON envelope to the HTTP response. +func sendResponse(w http.ResponseWriter, code int, data interface{}) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(code) + + out, err := json.Marshal(httpResp{Status: "success", Data: data}) + if err != nil { + sendErrorResponse(w, "Internal Server Error", http.StatusInternalServerError, nil) + return + } + + w.Write(out) +} + +// sendErrorResponse sends a JSON error envelope to the HTTP response. +func sendErrorResponse(w http.ResponseWriter, message string, code int, data interface{}) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(code) + + resp := httpResp{Status: "error", + Message: message, + Data: data} + out, _ := json.Marshal(resp) + w.Write(out) +} diff --git a/cmd/doggo/cli/cli.go b/cmd/doggo/cli/cli.go index 7b76f8e..958fbec 100644 --- a/cmd/doggo/cli/cli.go +++ b/cmd/doggo/cli/cli.go @@ -17,14 +17,11 @@ var ( // Version and date of the build. This is injected at build-time. buildVersion = "unknown" buildDate = "unknown" + logger = utils.InitLogger() + k = koanf.New(".") ) func main() { - var ( - logger = utils.InitLogger() - k = koanf.New(".") - ) - // Initialize app. app := app.New(logger, buildVersion) diff --git a/config-api-sample.toml b/config-api-sample.toml new file mode 100644 index 0000000..f69d335 --- /dev/null +++ b/config-api-sample.toml @@ -0,0 +1,9 @@ +[server] +address = ":8080" +name = "doggo-api" +# WARNING If these timeouts are less than 1s, +# the server connection breaks. +read_timeout=7000 +write_timeout=7000 +keepalive_timeout=5000 +max_body_size=10000 \ No newline at end of file diff --git a/pkg/models/models.go b/pkg/models/models.go index c09f6db..aa4dc1e 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -24,12 +24,12 @@ type QueryFlags struct { Nameservers []string `koanf:"nameservers" json:"nameservers"` UseIPv4 bool `koanf:"ipv4" json:"ipv4"` UseIPv6 bool `koanf:"ipv6" json:"ipv6"` - DisplayTimeTaken bool `koanf:"time" json:"-"` - ShowJSON bool `koanf:"json" json:"-"` - UseSearchList bool `koanf:"search" json:"-"` Ndots int `koanf:"ndots" json:"ndots"` Color bool `koanf:"color" json:"color"` Timeout time.Duration `koanf:"timeout" json:"timeout"` + DisplayTimeTaken bool `koanf:"time" json:"-"` + ShowJSON bool `koanf:"json" json:"-"` + UseSearchList bool `koanf:"search" json:"-"` } // Nameserver represents the type of Nameserver diff --git a/www/api/api.md b/www/api/api.md new file mode 100644 index 0000000..e7609cf --- /dev/null +++ b/www/api/api.md @@ -0,0 +1,13 @@ +# Usage + +``` +curl --request POST \ + --url http://localhost:8080/lookup/ \ + --header 'Content-Type: application/json' \ + --data '{ + "query": ["mrkaran.dev"], + "type": ["A"], + "class": ["IN"], + "nameservers": ["9.9.9.9"] +}' +```