feat: API Handlers
parent
b753631012
commit
f389c9c876
2
Makefile
2
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:
|
||||
|
|
|
@ -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)
|
||||
// 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,
|
||||
}
|
||||
|
||||
// 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")
|
||||
logger.WithFields(logrus.Fields{
|
||||
"address": srv.Addr,
|
||||
}).Info("starting server")
|
||||
|
||||
out, err := json.Marshal(resp{Status: statusText, Data: data})
|
||||
if err != nil {
|
||||
sendErrorResponse("Internal Server Error", http.StatusInternalServerError, nil, w)
|
||||
return
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
logger.Fatalf("couldn't start server: %v", err)
|
||||
}
|
||||
|
||||
_, _ = 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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -17,14 +17,11 @@ var (
|
|||
// Version and date of the build. This is injected at build-time.
|
||||
buildVersion = "unknown"
|
||||
buildDate = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
logger = utils.InitLogger()
|
||||
k = koanf.New(".")
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize app.
|
||||
app := app.New(logger, buildVersion)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
}'
|
||||
```
|
Loading…
Reference in New Issue