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
|
.PHONY: run-api
|
||||||
run-api: build-api ## Build and Execute the API binary after the build step.
|
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
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
|
|
|
@ -1,82 +1,51 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"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"
|
||||||
"github.com/go-chi/chi/middleware"
|
|
||||||
"github.com/knadh/koanf"
|
"github.com/knadh/koanf"
|
||||||
"github.com/mr-karan/doggo/pkg/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logger = utils.InitLogger()
|
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() {
|
func main() {
|
||||||
|
initConfig()
|
||||||
|
|
||||||
|
// Initialize app.
|
||||||
|
app := app.New(logger, buildVersion)
|
||||||
|
|
||||||
|
// Register handles.
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
r.Get("/", wrap(app, handleIndex))
|
||||||
|
r.Get("/ping/", wrap(app, handleHealthCheck))
|
||||||
|
r.Post("/lookup/", wrap(app, handleLookup))
|
||||||
|
|
||||||
// Setup middlewares.
|
// HTTP Server.
|
||||||
r.Use(middleware.RequestID)
|
srv := &http.Server{
|
||||||
r.Use(middleware.RealIP)
|
Addr: ko.String("server.address"),
|
||||||
r.Use(middleware.Logger)
|
Handler: r,
|
||||||
r.Use(middleware.Recoverer)
|
ReadTimeout: ko.Duration("server.read_timeout") * time.Millisecond,
|
||||||
|
WriteTimeout: ko.Duration("server.write_timeout") * time.Millisecond,
|
||||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
IdleTimeout: ko.Duration("server.keepalive_timeout") * time.Millisecond,
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _ = w.Write(out)
|
logger.WithFields(logrus.Fields{
|
||||||
}
|
"address": srv.Addr,
|
||||||
|
}).Info("starting server")
|
||||||
// sendSuccessResponse sends an HTTP success (200 OK) response.
|
|
||||||
func sendSuccessResponse(data interface{}, w http.ResponseWriter) {
|
if err := srv.ListenAndServe(); err != nil {
|
||||||
sendResponse(data, "success", http.StatusOK, w)
|
logger.Fatalf("couldn't start server: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
// Version and date of the build. This is injected at build-time.
|
||||||
buildVersion = "unknown"
|
buildVersion = "unknown"
|
||||||
buildDate = "unknown"
|
buildDate = "unknown"
|
||||||
|
logger = utils.InitLogger()
|
||||||
|
k = koanf.New(".")
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
|
||||||
logger = utils.InitLogger()
|
|
||||||
k = koanf.New(".")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initialize app.
|
// Initialize app.
|
||||||
app := app.New(logger, buildVersion)
|
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"`
|
Nameservers []string `koanf:"nameservers" json:"nameservers"`
|
||||||
UseIPv4 bool `koanf:"ipv4" json:"ipv4"`
|
UseIPv4 bool `koanf:"ipv4" json:"ipv4"`
|
||||||
UseIPv6 bool `koanf:"ipv6" json:"ipv6"`
|
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"`
|
Ndots int `koanf:"ndots" json:"ndots"`
|
||||||
Color bool `koanf:"color" json:"color"`
|
Color bool `koanf:"color" json:"color"`
|
||||||
Timeout time.Duration `koanf:"timeout" json:"timeout"`
|
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
|
// 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