feat: API Handlers

pull/15/head
Karan Sharma 2021-02-27 14:01:59 +05:30
parent b753631012
commit f389c9c876
8 changed files with 249 additions and 71 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

13
www/api/api.md 100644
View File

@ -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"]
}'
```