Changes: - forbid to overwrite secrets of other users - add some logs of success - add validation on code (length 50) - use same limiter for POST requests
118 lines
3.6 KiB
Go
118 lines
3.6 KiB
Go
// Package httpserver assembles the gin engine (API routes + web UI) and runs it
|
|
// with graceful shutdown.
|
|
package httpserver
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/kanootoko/codevault/internal/api"
|
|
"github.com/kanootoko/codevault/internal/db"
|
|
"github.com/kanootoko/codevault/internal/observability/logging"
|
|
"github.com/kanootoko/codevault/internal/observability/metrics"
|
|
"github.com/kanootoko/codevault/internal/ratelimit"
|
|
"github.com/kanootoko/codevault/internal/utils/gshutdown"
|
|
"github.com/kanootoko/codevault/internal/webui"
|
|
)
|
|
|
|
const shutdownTimeout = 10 * time.Second
|
|
|
|
const cleanupInterval = time.Minute
|
|
|
|
func New(
|
|
secrets *db.SecretsRepository,
|
|
users *db.UsersRepository,
|
|
limiter *ratelimit.Limiter,
|
|
m metrics.Metrics,
|
|
trustedProxies []string,
|
|
logger *zap.Logger,
|
|
) (*gin.Engine, error) {
|
|
router := gin.New()
|
|
// Strict-server handlers only see context.Context (via *gin.Context, which
|
|
// implements it), not *gin.Context directly. Without this flag, gin.Context.Value
|
|
// never falls back to c.Request.Context(), so values stashed there by middleware
|
|
// (e.g. LoginFromContext) would be invisible to handlers.
|
|
router.ContextWithFallback = true
|
|
router.Use(gin.Recovery())
|
|
if err := router.SetTrustedProxies(trustedProxies); err != nil {
|
|
return nil, fmt.Errorf("error on setting trusted proxies: %w", err)
|
|
}
|
|
|
|
router.GET("/", func(c *gin.Context) { serveStatic(c, webui.GetPagePath) })
|
|
router.GET("/set", func(c *gin.Context) { serveStatic(c, webui.SetPagePath) })
|
|
|
|
if err := api.RegisterSwaggerHandlers(router); err != nil {
|
|
return nil, fmt.Errorf("error on registering swagger handlers: %w", err)
|
|
}
|
|
|
|
handler := api.NewHandler(secrets, m.Value)
|
|
strictHandler := api.NewStrictHandler(handler, nil)
|
|
wrapper := api.ServerInterfaceWrapper{Handler: strictHandler}
|
|
|
|
router.POST("/api/v1/getValue",
|
|
api.InstrumentMiddleware("getValue", m.HTTP, logger),
|
|
api.RateLimitMiddleware(limiter, m.Value),
|
|
wrapper.GetValue,
|
|
)
|
|
router.POST("/api/v1/setValue",
|
|
api.InstrumentMiddleware("setValue", m.HTTP, logger),
|
|
api.BasicAuthMiddleware(users, limiter, m.Value),
|
|
wrapper.SetValue,
|
|
)
|
|
|
|
return router, nil
|
|
}
|
|
|
|
func serveStatic(c *gin.Context, path string) {
|
|
data, err := webui.Static.ReadFile(path)
|
|
if err != nil {
|
|
c.AbortWithStatus(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
c.Data(http.StatusOK, "text/html; charset=utf-8", data)
|
|
}
|
|
|
|
// Run starts the HTTP server and the rate limiter's cleanup goroutine, both
|
|
// tracked as gshutdown subtasks, and blocks until ctx is done.
|
|
func Run(ctx context.Context, address string, router *gin.Engine, limiter *ratelimit.Limiter) error {
|
|
logger := logging.LoggerMustFromContext(ctx)
|
|
|
|
server := &http.Server{
|
|
Addr: address,
|
|
Handler: router,
|
|
}
|
|
|
|
_ = gshutdown.SubtaskStarted(ctx, "http-server")
|
|
go func() {
|
|
defer func() { _ = gshutdown.SubtaskFinished(ctx, "http-server") }()
|
|
<-ctx.Done()
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
|
defer cancel()
|
|
if err := server.Shutdown(shutdownCtx); err != nil {
|
|
logger.Error("error on http server shutdown", zap.Error(err))
|
|
}
|
|
}()
|
|
|
|
cleanupDone := make(chan struct{})
|
|
_ = gshutdown.SubtaskStarted(ctx, "ratelimit-cleanup")
|
|
go func() {
|
|
defer func() { _ = gshutdown.SubtaskFinished(ctx, "ratelimit-cleanup") }()
|
|
limiter.Run(cleanupDone, cleanupInterval)
|
|
}()
|
|
go func() {
|
|
<-ctx.Done()
|
|
close(cleanupDone)
|
|
}()
|
|
|
|
logger.Info("starting http server", zap.String("address", address))
|
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
return fmt.Errorf("http server error: %w", err)
|
|
}
|
|
return nil
|
|
}
|