feat: API Handlers
This commit is contained in:
		
							parent
							
								
									b753631012
								
							
						
					
					
						commit
						f389c9c876
					
				
					 8 changed files with 249 additions and 71 deletions
				
			
		
							
								
								
									
										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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										60
									
								
								cmd/doggo/api/config.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								cmd/doggo/api/config.go
									
										
									
									
									
										Normal 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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										130
									
								
								cmd/doggo/api/handlers.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								cmd/doggo/api/handlers.go
									
										
									
									
									
										Normal 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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										9
									
								
								config-api-sample.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								config-api-sample.toml
									
										
									
									
									
										Normal 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
 | 
			
		||||
| 
						 | 
				
			
			@ -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
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								www/api/api.md
									
										
									
									
									
										Normal 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"]
 | 
			
		||||
}'
 | 
			
		||||
```
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue