From 1992dd31cc7d88fd202a6ee499589632a3e0bda2 Mon Sep 17 00:00:00 2001 From: Erik Winter Date: Sat, 16 Dec 2023 12:11:16 +0100 Subject: [PATCH] project start --- go.mod | 3 ++ handler/handler.go | 28 ++++++++++++++ handler/server.go | 91 ++++++++++++++++++++++++++++++++++++++++++++++ service.go | 38 +++++++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 go.mod create mode 100644 handler/handler.go create mode 100644 handler/server.go create mode 100644 service.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9689b18 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module "ewintr.nl/emdb" + +go 1.21 diff --git a/handler/handler.go b/handler/handler.go new file mode 100644 index 0000000..96715dd --- /dev/null +++ b/handler/handler.go @@ -0,0 +1,28 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" +) + +func Index(w http.ResponseWriter) { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{"message":"emdb index"}`) +} + +func Error(w http.ResponseWriter, status int, message string, err error) { + w.WriteHeader(status) + + var resBody []byte + res := struct { + Message string `json:"message"` + Error string `json:"error"` + }{ + Message: message, + Error: err.Error(), + } + resBody, _ = json.Marshal(res) + + fmt.Fprint(w, string(resBody)) +} diff --git a/handler/server.go b/handler/server.go new file mode 100644 index 0000000..5a58e7a --- /dev/null +++ b/handler/server.go @@ -0,0 +1,91 @@ +package handler + +import ( + "fmt" + "log/slog" + "net/http" + "net/http/httptest" + "path" + "strings" +) + +type APIIndex map[string]http.Handler + +type Server struct { + apiKey string + apis map[string]http.Handler + logger *slog.Logger +} + +func NewServer(apiKey string, apis map[string]http.Handler, logger *slog.Logger) *Server { + return &Server{ + apiKey: apiKey, + apis: apis, + logger: logger, + } +} + +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + rec := httptest.NewRecorder() // records the response to be able to mix writing headers and content + + // cors + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") + w.Header().Add("Access-Control-Allow-Headers", "Content-Type, Authorization") + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + returnResponse(w, rec, r, s.logger) + return + } + + logger := s.logger.With("path", r.URL.Path) + w.Header().Add("Content-Type", "application/json") + + // authenticate + if key := r.Header.Get("Authorization"); key != s.apiKey { + Error(rec, http.StatusUnauthorized, "unauthorized", fmt.Errorf("invalid api key")) + logger.Info("unauthorized", "key", key) + returnResponse(w, rec, r, logger) + return + } + + // route to internal + head, tail := ShiftPath(r.URL.Path) + if len(head) == 0 { + Index(rec) + returnResponse(w, rec, r, logger) + return + } + api, ok := s.apis[head] + if !ok { + Error(rec, http.StatusNotFound, "Not found", fmt.Errorf("%s is not a valid path", r.URL.Path)) + returnResponse(w, rec, r, logger) + return + } + + r.URL.Path = tail + api.ServeHTTP(rec, r) + returnResponse(w, rec, r, logger) +} + +func returnResponse(w http.ResponseWriter, rec *httptest.ResponseRecorder, r *http.Request, logger *slog.Logger) { + for k, v := range rec.Header() { + w.Header()[k] = v + } + w.WriteHeader(rec.Code) + w.Write(rec.Body.Bytes()) + logger.Info("request served", "method", r.Method, "status", rec.Code) +} + +// ShiftPath splits off the first component of p, which will be cleaned of +// relative components before processing. head will never contain a slash and +// tail will always be a rooted path without trailing slash. +// See https://blog.merovius.de/posts/2017-06-18-how-not-to-use-an-http-router/ +func ShiftPath(p string) (string, string) { + p = path.Clean("/" + p) + i := strings.Index(p[1:], "/") + 1 + if i <= 0 { + return p[1:], "/" + } + return p[1:i], p[i:] +} diff --git a/service.go b/service.go new file mode 100644 index 0000000..236a145 --- /dev/null +++ b/service.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "log/slog" + "net/http" + "os" + "os/signal" + "strconv" + "syscall" + + "ewintr.nl/emdb/handler" +) + +func main() { + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + port, err := strconv.Atoi(getParam("API_PORT", "8080")) + if err != nil { + fmt.Printf("invalid port: %s", err.Error()) + os.Exit(1) + } + apiKey := getParam("API_KEY", "hoi") + + apis := handler.APIIndex{} + + go http.ListenAndServe(fmt.Sprintf(":%d", port), handler.NewServer(apiKey, apis, logger)) + + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + <-c +} + +func getParam(param, def string) string { + if val, ok := os.LookupEnv(param); ok { + return val + } + return def +}