package server import ( "net/http" "time" sentryhttp "github.com/getsentry/sentry-go/http" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/prometheus/client_golang/prometheus/promhttp" "sneak.berlin/go/webhooker/static" ) // maxFormBodySize is the maximum allowed request body size (in // bytes) for form POST endpoints. 1 MB is generous for any form // submission while preventing abuse from oversized payloads. const maxFormBodySize int64 = 1 * 1024 * 1024 // 1 MB // requestTimeout is the maximum time allowed for a single HTTP // request. const requestTimeout = 60 * time.Second // SetupRoutes configures all HTTP routes and middleware on the // server's router. func (s *Server) SetupRoutes() { s.router = chi.NewRouter() s.setupGlobalMiddleware() s.setupRoutes() } func (s *Server) setupGlobalMiddleware() { s.router.Use(middleware.Recoverer) s.router.Use(middleware.RequestID) s.router.Use(s.mw.SecurityHeaders()) s.router.Use(s.mw.Logging()) // Metrics middleware (only if credentials are configured) if s.params.Config.MetricsUsername != "" { s.router.Use(s.mw.Metrics()) } s.router.Use(s.mw.CORS()) s.router.Use(middleware.Timeout(requestTimeout)) // Sentry error reporting (if SENTRY_DSN is set). Repanic is // true so panics still bubble up to the Recoverer middleware. if s.sentryEnabled { sentryHandler := sentryhttp.New(sentryhttp.Options{ Repanic: true, }) s.router.Use(sentryHandler.Handle) } } func (s *Server) setupRoutes() { s.router.Get("/", s.h.HandleIndex()) s.router.Mount( "/s", http.StripPrefix("/s", http.FileServer(http.FS(static.Static))), ) s.router.Route("/api/v1", func(_ chi.Router) { // API routes will be added here. }) s.router.Get( "/.well-known/healthcheck", s.h.HandleHealthCheck(), ) // set up authenticated /metrics route: if s.params.Config.MetricsUsername != "" { s.router.Group(func(r chi.Router) { r.Use(s.mw.MetricsAuth()) r.Get( "/metrics", http.HandlerFunc( promhttp.Handler().ServeHTTP, ), ) }) } s.setupPageRoutes() s.setupUserRoutes() s.setupSourceRoutes() s.setupWebhookRoutes() } func (s *Server) setupPageRoutes() { s.router.Route("/pages", func(r chi.Router) { r.Use(s.mw.CSRF()) r.Use(s.mw.MaxBodySize(maxFormBodySize)) r.Group(func(r chi.Router) { r.Use(s.mw.LoginRateLimit()) r.Get("/login", s.h.HandleLoginPage()) r.Post("/login", s.h.HandleLoginSubmit()) }) r.Post("/logout", s.h.HandleLogout()) }) } func (s *Server) setupUserRoutes() { s.router.Route("/user/{username}", func(r chi.Router) { r.Use(s.mw.CSRF()) r.Get("/", s.h.HandleProfile()) }) } func (s *Server) setupSourceRoutes() { s.router.Route("/sources", func(r chi.Router) { r.Use(s.mw.CSRF()) r.Use(s.mw.RequireAuth()) r.Use(s.mw.MaxBodySize(maxFormBodySize)) r.Get("/", s.h.HandleSourceList()) r.Get("/new", s.h.HandleSourceCreate()) r.Post("/new", s.h.HandleSourceCreateSubmit()) }) s.router.Route("/source/{sourceID}", func(r chi.Router) { r.Use(s.mw.CSRF()) r.Use(s.mw.RequireAuth()) r.Use(s.mw.MaxBodySize(maxFormBodySize)) r.Get("/", s.h.HandleSourceDetail()) r.Get("/edit", s.h.HandleSourceEdit()) r.Post("/edit", s.h.HandleSourceEditSubmit()) r.Post("/delete", s.h.HandleSourceDelete()) r.Get("/logs", s.h.HandleSourceLogs()) r.Post( "/entrypoints", s.h.HandleEntrypointCreate(), ) r.Post( "/entrypoints/{entrypointID}/delete", s.h.HandleEntrypointDelete(), ) r.Post( "/entrypoints/{entrypointID}/toggle", s.h.HandleEntrypointToggle(), ) r.Post("/targets", s.h.HandleTargetCreate()) r.Post( "/targets/{targetID}/delete", s.h.HandleTargetDelete(), ) r.Post( "/targets/{targetID}/toggle", s.h.HandleTargetToggle(), ) }) } func (s *Server) setupWebhookRoutes() { s.router.HandleFunc( "/webhook/{uuid}", s.h.HandleWebhook(), ) }