Replace fiber with net.http.ServeMux (#23)

This commit is contained in:
T. von Dein
2025-12-23 14:38:45 +01:00
parent 5a705b0af0
commit 3b37a67e02
10 changed files with 306 additions and 183 deletions

View File

@@ -1,5 +1,5 @@
/*
Copyright © 2024 Thomas von Dein
Copyright © 2024-2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -19,7 +19,10 @@ package rest
import (
//"github.com/alecthomas/repr"
"github.com/gofiber/fiber/v2"
"encoding/json"
"log"
"net/http"
"codeberg.org/scip/anydb/app"
"codeberg.org/scip/anydb/cfg"
)
@@ -40,101 +43,113 @@ type SingleResponse struct {
Entry *app.DbEntry
}
func RestList(c *fiber.Ctx, conf *cfg.Config) error {
func RestList(resp http.ResponseWriter, req *http.Request, conf *cfg.Config) {
attr := new(app.DbAttr)
if len(c.Body()) > 0 {
if err := c.BodyParser(attr); err != nil {
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{
"errors": err.Error(),
})
err := json.NewDecoder(req.Body).Decode(&attr)
if err != nil {
if err.Error() != `EOF` {
http.Error(resp, err.Error(), http.StatusBadRequest)
return
}
}
// get list
entries, err := conf.DB.List(attr, attr.Fulltext)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Unable to list keys: "+err.Error())
JsonStatus(resp, http.StatusForbidden, "Unable to list keys: "+err.Error())
return
}
return c.Status(fiber.StatusOK).JSON(
resp.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(resp).Encode(
ListResponse{
Code: http.StatusOK,
Success: true,
Code: fiber.StatusOK,
Entries: entries,
},
)
}
func RestGet(c *fiber.Ctx, conf *cfg.Config) error {
if c.Params("key") == "" {
return JsonStatus(c, fiber.StatusForbidden,
"key not provided")
}
// get list
entry, err := conf.DB.Get(&app.DbAttr{Key: c.Params("key")})
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Unable to get key: "+err.Error())
}
if entry.Key == "" {
return JsonStatus(c, fiber.StatusForbidden,
"Key does not exist")
}
return c.Status(fiber.StatusOK).JSON(
SingleResponse{
Success: true,
Code: fiber.StatusOK,
Entry: entry,
},
)
}
func RestDelete(c *fiber.Ctx, conf *cfg.Config) error {
if c.Params("key") == "" {
return JsonStatus(c, fiber.StatusForbidden,
"key not provided")
}
// get list
err := conf.DB.Del(&app.DbAttr{Key: c.Params("key")})
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Unable to delete key: "+err.Error())
}
return c.Status(fiber.StatusOK).JSON(
Result{
Success: true,
Code: fiber.StatusOK,
Message: "key deleted",
},
)
}
func RestSet(c *fiber.Ctx, conf *cfg.Config) error {
attr := new(app.DbAttr)
if err := c.BodyParser(attr); err != nil {
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{
"errors": err.Error(),
})
}
err := conf.DB.Set(attr)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Unable to set key: "+err.Error())
log.Fatal(err)
}
}
func RestGet(resp http.ResponseWriter, req *http.Request, key string, conf *cfg.Config) {
if key == "" {
JsonStatus(resp, http.StatusForbidden, "key not provided")
return
}
return c.Status(fiber.StatusOK).JSON(
Result{
// get list
entry, err := conf.DB.Get(&app.DbAttr{Key: key})
if err != nil {
JsonStatus(resp, http.StatusForbidden, "Unable to get key: "+err.Error())
return
}
if entry.Key == "" {
JsonStatus(resp, http.StatusForbidden, "Key does not exist")
return
}
resp.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(resp).Encode(
SingleResponse{
Code: http.StatusOK,
Success: true,
Code: fiber.StatusOK,
},
)
Entry: entry,
})
if err != nil {
log.Fatal(err)
}
}
func RestDelete(resp http.ResponseWriter, req *http.Request, key string, conf *cfg.Config) {
if key == "" {
JsonStatus(resp, http.StatusForbidden, "key not provided")
return
}
// get list
err := conf.DB.Del(&app.DbAttr{Key: key})
if err != nil {
JsonStatus(resp, http.StatusForbidden, "Unable to delete key: "+err.Error())
return
}
JsonStatus(resp, http.StatusOK, "key deleted")
}
func RestSet(resp http.ResponseWriter, req *http.Request, conf *cfg.Config) {
attr := new(app.DbAttr)
err := json.NewDecoder(req.Body).Decode(&attr)
if err != nil {
http.Error(resp, err.Error(), http.StatusBadRequest)
return
}
// attr.Data is a string, thus the decoder doesn't expect it to be
// base64 encoded. However, internally we need []byte, therefore
// we copy a cast to .Val. We also need to setup the .Preview
// value here.
attr.Val = []byte(attr.Data)
attr.SetPreview()
err = conf.DB.Set(attr)
if err != nil {
JsonStatus(resp, http.StatusForbidden, "Unable to set key: "+err.Error())
return
}
JsonStatus(resp, http.StatusOK, "key added/updated")
}
func Home(resp http.ResponseWriter) {
_, err := resp.Write([]byte("Use the REST API on " + apiprefix + "\r\n"))
if err != nil {
log.Fatal(err)
}
}

98
rest/log.go Normal file
View File

@@ -0,0 +1,98 @@
/*
This logging middleware is based on
https://github.com/elithrar/admission-control/blob/v0.6.3/request_logger.go
by Matt Silverlock licensed under the Apache-2.0 license.
I am using slog and added a couple of small modifications.
*/
package rest
import (
"log/slog"
"net/http"
"runtime/debug"
"time"
)
// responseWriter is a minimal wrapper for http.ResponseWriter that allows the
// written HTTP status code to be captured for logging.
type responseWriter struct {
http.ResponseWriter
status int
size int
wroteHeader bool
}
func wrapResponseWriter(w http.ResponseWriter) *responseWriter {
return &responseWriter{ResponseWriter: w}
}
func (rw *responseWriter) Status() int {
return rw.status
}
func (rw *responseWriter) Size() int {
return rw.size
}
func (rw *responseWriter) WriteHeader(code int) {
if rw.wroteHeader {
return
}
rw.status = code
rw.ResponseWriter.WriteHeader(code)
rw.wroteHeader = true
}
func (rw *responseWriter) Write(data []byte) (int, error) {
written, err := rw.ResponseWriter.Write(data)
rw.size += written
return written, err
}
// LoggingMiddleware logs the incoming HTTP request & its duration.
func LogHandler() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(resp http.ResponseWriter, req *http.Request) {
defer func() {
if err := recover(); err != nil {
resp.WriteHeader(http.StatusInternalServerError)
slog.Info(
"internal server error",
"err", err,
"trace", string(debug.Stack()),
)
}
}()
start := time.Now()
wrapped := wrapResponseWriter(resp)
next.ServeHTTP(wrapped, req)
header := wrapped.Header()["Content-Type"]
contenttype := ""
if header == nil {
contenttype = "text/plain"
} else {
contenttype = header[0]
}
slog.Info("request",
"ip", req.RemoteAddr,
"status", wrapped.status,
"method", req.Method,
"path", req.URL.EscapedPath(),
"size", wrapped.Size(),
"content-type", contenttype,
"duration", time.Since(start),
)
}
return http.HandlerFunc(fn)
}
}

View File

@@ -1,5 +1,5 @@
/*
Copyright © 2024 Thomas von Dein
Copyright © 2024-2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -17,10 +17,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package rest
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"encoding/json"
"log"
"net/http"
"codeberg.org/scip/anydb/cfg"
)
@@ -31,84 +31,62 @@ type Result struct {
Code int `json:"code"`
}
const apiprefix = `/anydb/v1/`
func Runserver(conf *cfg.Config, args []string) error {
// setup api server
router := SetupServer(conf)
mux := http.NewServeMux()
// public rest api routes
api := router.Group("/anydb/v1")
{
api.Get("/", func(c *fiber.Ctx) error {
return RestList(c, conf)
})
api.Post("/", func(c *fiber.Ctx) error {
// same thing as above but allows to supply parameters, see app.Dbattr{}
return RestList(c, conf)
})
api.Get("/:key", func(c *fiber.Ctx) error {
return RestGet(c, conf)
})
api.Delete("/:key", func(c *fiber.Ctx) error {
return RestDelete(c, conf)
})
api.Put("/", func(c *fiber.Ctx) error {
return RestSet(c, conf)
})
}
// public routes
{
router.Get("/", func(c *fiber.Ctx) error {
return c.Send([]byte("Use the REST API"))
})
}
return router.Listen(conf.Listen)
}
func SetupServer(conf *cfg.Config) *fiber.App {
// disable colors
fiber.DefaultColors = fiber.Colors{}
router := fiber.New(fiber.Config{
CaseSensitive: true,
StrictRouting: true,
Immutable: true,
ServerHeader: "anydb serve",
AppName: "anydb",
// just in case someone tries to access the non-api url
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
Home(w)
})
router.Use(logger.New(logger.Config{
Format: "${pid} ${ip}:${port} ${status} - ${method} ${path}\n",
DisableColors: true,
}))
mux.HandleFunc("GET "+apiprefix+"{$}", func(w http.ResponseWriter, r *http.Request) {
RestList(w, r, conf)
})
router.Use(cors.New(cors.Config{
AllowMethods: "GET,PUT,POST,DELETE",
ExposeHeaders: "Content-Type,Accept",
}))
mux.HandleFunc("POST "+apiprefix+"{$}", func(w http.ResponseWriter, r *http.Request) {
RestList(w, r, conf)
})
router.Use(compress.New(compress.Config{
Level: compress.LevelBestSpeed,
}))
mux.HandleFunc("GET "+apiprefix+"{key}", func(w http.ResponseWriter, r *http.Request) {
key := r.PathValue("key")
RestGet(w, r, key, conf)
})
return router
mux.HandleFunc("DELETE "+apiprefix+"{key}", func(w http.ResponseWriter, r *http.Request) {
key := r.PathValue("key")
RestDelete(w, r, key, conf)
})
mux.HandleFunc("PUT "+apiprefix, func(w http.ResponseWriter, r *http.Request) {
RestSet(w, r, conf)
})
logger := LogHandler()
return http.ListenAndServe(conf.Listen, logger(mux))
}
/*
Wrapper to respond with proper json status, message and code,
shall be prepared and called by the handlers directly.
*/
func JsonStatus(c *fiber.Ctx, code int, msg string) error {
success := code == fiber.StatusOK
func JsonStatus(resp http.ResponseWriter, code int, msg string) {
success := code == http.StatusOK
return c.Status(code).JSON(Result{
Code: code,
Message: msg,
Success: success,
})
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(code)
err := json.NewEncoder(resp).Encode(
Result{
Code: code,
Message: msg,
Success: success,
})
if err != nil {
log.Fatal(err)
}
}