From 3c1bb0badb2a12dcbff9de6ab31bbdd576c8bf9b Mon Sep 17 00:00:00 2001 From: perennial Date: Tue, 24 Sep 2024 15:51:23 +1000 Subject: [PATCH] Add comments to package handlers --- server/handlers/csp.go | 1 + server/handlers/doc.go | 12 ++++++++++++ server/handlers/error_handler.go | 13 +++++++++++++ server/handlers/limiter.go | 26 +++++++++++++++++++------- server/handlers/logger.go | 16 +++++++++++++++- server/handlers/recover.go | 2 ++ server/handlers/router.go | 29 +++++++++++++++++++++++++---- server/handlers/set_context.go | 2 ++ 8 files changed, 89 insertions(+), 12 deletions(-) create mode 100644 server/handlers/doc.go diff --git a/server/handlers/csp.go b/server/handlers/csp.go index 0ced4f5..5f1d55a 100644 --- a/server/handlers/csp.go +++ b/server/handlers/csp.go @@ -8,6 +8,7 @@ import ( "codeberg.org/vnpower/pixivfe/v2/server/session" ) +// SetPrivacyHeaders is a middleware that adds security headers to HTTP responses. func SetPrivacyHeaders(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { header := w.Header() diff --git a/server/handlers/doc.go b/server/handlers/doc.go new file mode 100644 index 0000000..5611a3b --- /dev/null +++ b/server/handlers/doc.go @@ -0,0 +1,12 @@ +// Package handlers provides HTTP request handling functionality for PixivFE. +// It defines various middleware functions, request handlers, and routing logic to manage +// incoming HTTP requests and produce appropriate responses. +// +// The package includes middleware for security headers (SetPrivacyHeaders), error catching +// and handling (CatchError, HandleError), rate limiting (IPRateLimiter, RateLimitRequest), logging (LogRequest), +// panic recovery (RecoverFromPanic), and user context injection (ProvideUserContext). These +// middlewares can be applied to routes to add cross-cutting functionality across multiple endpoints. +// +// Route definitions are centralized in the DefineRoutes function, which sets up all paths +// and their corresponding handlers using the gorilla/mux router. +package handlers diff --git a/server/handlers/error_handler.go b/server/handlers/error_handler.go index f37e487..447bdcf 100644 --- a/server/handlers/error_handler.go +++ b/server/handlers/error_handler.go @@ -11,21 +11,30 @@ import ( "codeberg.org/vnpower/pixivfe/v2/server/routes" ) +// CatchError is a middleware that wraps an HTTP handler to catch and manage errors. +// It allows for graceful error handling and response manipulation. func CatchError(handler func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + // Backup the original response headers header_backup := http.Header{} for k, v := range w.Header() { header_backup[k] = slices.Clone(v) } + + // Create a response recorder to capture the handler's output recorder := httptest.ResponseRecorder{ HeaderMap: w.Header(), Body: new(bytes.Buffer), Code: 200, } + + // Execute the handler and catch any returned error err := handler(&recorder, r) if err != nil { + // If an error occurred, restore the original headers clear(header_backup) maps.Copy(w.Header(), header_backup) + // Store the error in the request context for later handling request_context.Get(r).CaughtError = err } else { w.WriteHeader(recorder.Code) @@ -34,13 +43,17 @@ func CatchError(handler func(w http.ResponseWriter, r *http.Request) error) http } } +// HandleError is a middleware that checks for errors caught by CatchError and renders an error page if necessary. func HandleError(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Execute the wrapped handler h.ServeHTTP(w, r) + // Check if an error was caught during the request processing err := request_context.Get(r).CaughtError if err != nil { + // If an error was caught, render the error page routes.ErrorPage(w, r, err, http.StatusInternalServerError) } }) diff --git a/server/handlers/limiter.go b/server/handlers/limiter.go index c2a89ca..54e696a 100644 --- a/server/handlers/limiter.go +++ b/server/handlers/limiter.go @@ -14,6 +14,8 @@ import ( "codeberg.org/vnpower/pixivfe/v2/server/routes" ) +// CanRequestSkipLimiter determines if a request should bypass the rate limiter. +// It exempts static assets and proxied image requests from rate limiting. func CanRequestSkipLimiter(r *http.Request) bool { path := r.URL.Path return strings.HasPrefix(path, "/img/") || @@ -22,15 +24,16 @@ func CanRequestSkipLimiter(r *http.Request) bool { strings.HasPrefix(path, "/proxy/s.pximg.net/") } -// Todo: Should we put middlewares in a separate file? -// IPRateLimiter represents an IP rate limiter. +// IPRateLimiter manages rate limiting on a per-IP basis. +// +// TODO: Should we put middlewares in a separate file? type IPRateLimiter struct { - ips map[string]*rate.Limiter - mu *sync.RWMutex - limiter *rate.Limiter + ips map[string]*rate.Limiter // Maps IP addresses to their respective rate limiters + mu *sync.RWMutex // Ensures thread-safe access to the map + limiter *rate.Limiter // Global rate limiter used as a template for per-IP limiters } -// NewIPRateLimiter creates a new instance of IPRateLimiter with the given rate limit. +// NewIPRateLimiter creates a new instance of IPRateLimiter with the specified rate limit and burst. func NewIPRateLimiter(r rate.Limit, burst int) *IPRateLimiter { return &IPRateLimiter{ ips: make(map[string]*rate.Limiter), @@ -39,7 +42,8 @@ func NewIPRateLimiter(r rate.Limit, burst int) *IPRateLimiter { } } -// Allow checks if the request from the given IP is allowed. +// Allow checks if a request from the given IP is allowed based on the rate limit. +// If the IP doesn't have a limiter, a new one is created. func (lim *IPRateLimiter) Allow(ip string) bool { lim.mu.RLock() rl, exists := lim.ips[ip] @@ -49,6 +53,7 @@ func (lim *IPRateLimiter) Allow(ip string) bool { lim.mu.Lock() rl, exists = lim.ips[ip] if !exists { + // Create a new limiter for this IP using the global limiter's settings rl = rate.NewLimiter(lim.limiter.Limit(), lim.limiter.Burst()) lim.ips[ip] = rl } @@ -58,8 +63,11 @@ func (lim *IPRateLimiter) Allow(ip string) bool { return rl.Allow() } +// Global rate limiter instance var limiter *IPRateLimiter +// InitializeRateLimiter sets up the global rate limiter based on the application's configuration. +// If the request limit is less than 1, it sets an infinite rate limit. func InitializeRateLimiter() { r := float64(config.GlobalConfig.RequestLimit) / 30.0 if config.GlobalConfig.RequestLimit < 1 { @@ -68,6 +76,8 @@ func InitializeRateLimiter() { limiter = NewIPRateLimiter(rate.Limit(r), 3) } +// RateLimitRequest is a middleware that applies rate limiting to incoming HTTP requests. +// It exempts certain requests (as defined by CanRequestSkipLimiter) from rate limiting. func RateLimitRequest(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ip, _, _ := net.SplitHostPort(r.RemoteAddr) @@ -78,8 +88,10 @@ func RateLimitRequest(h http.Handler) http.Handler { } if !limiter.Allow(ip) { + // If the request exceeds the rate limit, return an HTTP 429 Too Many Requests error routes.ErrorPage(w, r, errors.New("Too many requests"), http.StatusTooManyRequests) } else { + // If the request is within the rate limit, proceed to the next handler h.ServeHTTP(w, r) } }) diff --git a/server/handlers/logger.go b/server/handlers/logger.go index 20bb7d4..2a3bb48 100644 --- a/server/handlers/logger.go +++ b/server/handlers/logger.go @@ -10,17 +10,22 @@ import ( "codeberg.org/vnpower/pixivfe/v2/server/request_context" ) +// ResponseWriterInterceptStatus wraps http.ResponseWriter to intercept the status code. type ResponseWriterInterceptStatus struct { statusCode int http.ResponseWriter } +// WriteHeader intercepts the status code before writing it to the response. func (w *ResponseWriterInterceptStatus) WriteHeader(code int) { w.statusCode = code w.ResponseWriter.WriteHeader(code) } +// CanRequestSkipLogger determines if a request should bypass the logging middleware. +// This is useful for reducing log clutter from static assets and development-specific routes. func CanRequestSkipLogger(r *http.Request) bool { + // Uncomment the following line to log all requests // return false path := r.URL.Path return strings.HasPrefix(path, "/img/") || @@ -31,23 +36,32 @@ func CanRequestSkipLogger(r *http.Request) bool { (strings.HasPrefix(path, "/proxy/s.pximg.net/") || strings.HasPrefix(path, "/proxy/i.pximg.net/"))) } +// LogRequest is a middleware that logs incoming HTTP requests and their corresponding responses. +// It wraps the next handler in the chain and provides detailed logging of request/response metrics. func LogRequest(h http.Handler) http.Handler { return http.HandlerFunc(func(w_ http.ResponseWriter, r *http.Request) { if CanRequestSkipLogger(r) { + // If the request should skip logging, pass it directly to the next handler h.ServeHTTP(w_, r) } else { + // Wrap the ResponseWriter to intercept the status code w := &ResponseWriterInterceptStatus{ statusCode: 0, ResponseWriter: w_, } - // set user context + // TODO: Set user context here if needed + + // Record the start time of the request start_time := time.Now() + // Call the next handler in the chain h.ServeHTTP(w, r) + // Record the end time of the request end_time := time.Now() + // Log the request details using the audit package audit.LogServerRoundTrip(audit.ServedRequestSpan{ StartTime: start_time, EndTime: end_time, diff --git a/server/handlers/recover.go b/server/handlers/recover.go index b3d6333..9732c9b 100644 --- a/server/handlers/recover.go +++ b/server/handlers/recover.go @@ -5,6 +5,8 @@ import ( "net/http" ) +// RecoverFromPanic wraps an http.Handler and recovers from any panics that occur during its execution. +// If a panic occurs, it sends an HTTP 500 Internal Server Error response with the panic message. func RecoverFromPanic(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var err error diff --git a/server/handlers/router.go b/server/handlers/router.go index abee092..915e25c 100644 --- a/server/handlers/router.go +++ b/server/handlers/router.go @@ -9,20 +9,27 @@ import ( "github.com/gorilla/mux" ) +// handleStripPrefix is a utility function that combines path prefix matching with +// stripping the prefix from the request URL before passing it to the handler. func handleStripPrefix(router *mux.Router, pathPrefix string, handler http.Handler) *mux.Route { return router.PathPrefix(pathPrefix).Handler(http.StripPrefix(pathPrefix, handler)) } +// serveFile returns an http.HandlerFunc that serves a specific file. +// This is useful for serving static files like robots.txt. func serveFile(filename string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, filename) } } +// DefineRoutes sets up all the routes for the application. +// It returns a configured mux.Router with all paths and their corresponding handlers. func DefineRoutes() *mux.Router { router := mux.NewRouter() - // redirect handler: strip trailing / to make router behave + // Redirect handler: strip trailing / to make router behave consistently + // This ensures that URLs with and without trailing slashes are treated the same router.MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool { return r.URL.Path != "/" && strings.HasSuffix(r.URL.Path, "/") }).HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -31,52 +38,62 @@ func DefineRoutes() *mux.Router { http.Redirect(w, r, url.String(), http.StatusPermanentRedirect) }) + // Serve static files router.HandleFunc("/robots.txt", serveFile("./assets/robots.txt")) handleStripPrefix(router, "/img/", http.FileServer(http.Dir("./assets/img"))) handleStripPrefix(router, "/css/", http.FileServer(http.Dir("./assets/css"))) handleStripPrefix(router, "/js/", http.FileServer(http.Dir("./assets/js"))) - // Proxy routes. cache headers set by upstream servers. + // Proxy routes for handling image requests + // These routes maintain cache headers set by upstream servers handleStripPrefix(router, "/proxy/i.pximg.net/", CatchError(routes.IPximgProxy)).Methods("GET") handleStripPrefix(router, "/proxy/s.pximg.net/", CatchError(routes.SPximgProxy)).Methods("GET") handleStripPrefix(router, "/proxy/ugoira.com/", CatchError(routes.UgoiraProxy)).Methods("GET") + // Main application routes router.HandleFunc("/", CatchError(routes.IndexPage)).Methods("GET") router.HandleFunc("/about", CatchError(routes.AboutPage)).Methods("GET") router.HandleFunc("/newest", CatchError(routes.NewestPage)).Methods("GET") router.HandleFunc("/discovery", CatchError(routes.DiscoveryPage)).Methods("GET") router.HandleFunc("/discovery/novel", CatchError(routes.NovelDiscoveryPage)).Methods("GET") + // Ranking related routes router.HandleFunc("/ranking", CatchError(routes.RankingPage)).Methods("GET") router.HandleFunc("/rankingCalendar", CatchError(routes.RankingCalendarPage)).Methods("GET") router.HandleFunc("/rankingCalendar", CatchError(routes.RankingCalendarPicker)).Methods("POST") + // User related routes, including Atom feeds router.HandleFunc("/users/{id}.atom.xml", CatchError(routes.UserAtomFeed)).Methods("GET") router.HandleFunc("/users/{id}/{category}.atom.xml", CatchError(routes.UserAtomFeed)).Methods("GET") router.HandleFunc("/users/{id}", CatchError(routes.UserPage)).Methods("GET") router.HandleFunc("/users/{id}/{category}", CatchError(routes.UserPage)).Methods("GET") + // Artwork related routes router.HandleFunc("/artworks/{id}", CatchError(routes.ArtworkPage)).Methods("GET") router.HandleFunc("/artworks-multi/{ids}", CatchError(routes.ArtworkMultiPage)).Methods("GET") - // Legacy illust URL + // Legacy illust URL redirect router.HandleFunc("/member_illust.php", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/artworks/"+routes.GetQueryParam(r, "illust_id"), http.StatusPermanentRedirect) }).Methods("GET") + // Novel related routes router.HandleFunc("/novel/show.php", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/novel/"+routes.GetQueryParam(r, "id"), http.StatusPermanentRedirect) }).Methods("GET") router.HandleFunc("/novel/{id}", CatchError(routes.NovelPage)).Methods("GET") router.HandleFunc("/novel/series/{id}", CatchError(routes.NovelSeriesPage)).Methods("GET") + // Pixivision related routes router.HandleFunc("/pixivision", CatchError(routes.PixivisionHomePage)).Methods("GET") router.HandleFunc("/pixivision/a/{id}", CatchError(routes.PixivisionArticlePage)).Methods("GET") router.HandleFunc("/pixivision/c/{id}", CatchError(routes.PixivisionCategoryPage)).Methods("GET") router.HandleFunc("/pixivision/t/{id}", CatchError(routes.PixivisionTagPage)).Methods("GET") + // Settings related routes router.HandleFunc("/settings", CatchError(routes.SettingsPage)).Methods("GET") router.HandleFunc("/settings/{type}", CatchError(routes.SettingsPost)).Methods("POST") + // User action routes (login, bookmarks, likes, etc.) router.HandleFunc("/self", CatchError(routes.LoginUserPage)).Methods("GET") router.HandleFunc("/self/followingWorks", CatchError(routes.FollowingWorksPage)).Methods("GET") router.HandleFunc("/self/bookmarks", CatchError(routes.LoginBookmarkPage)).Methods("GET") @@ -84,18 +101,22 @@ func DefineRoutes() *mux.Router { router.HandleFunc("/self/deleteBookmark/{id}", CatchError(routes.DeleteBookmarkRoute)).Methods("GET") router.HandleFunc("/self/like/{id}", CatchError(routes.LikeRoute)).Methods("GET") + // oEmbed endpoint for embedding Pixiv content router.HandleFunc("/oembed", CatchError(routes.Oembed)).Methods("GET") + // Tag related routes router.HandleFunc("/tags/{name}", CatchError(routes.TagPage)).Methods("GET") router.HandleFunc("/tags/{name}", CatchError(routes.TagPage)).Methods("POST") router.HandleFunc("/tags", CatchError(routes.TagPage)).Methods("GET") router.HandleFunc("/tags", CatchError(routes.AdvancedTagPost)).Methods("POST") + // Diagnostic routes for monitoring and debugging router.HandleFunc("/diagnostics", CatchError(routes.Diagnostics)).Methods("GET") router.HandleFunc("/diagnostics/spans.json", CatchError(routes.DiagnosticsData)).Methods("GET") router.HandleFunc("/diagnostics/reset", routes.ResetDiagnosticsData) - // fallback route (if nothing else matches) + // Fallback route (if nothing else matches) + // This ensures that a proper HTTP 404 error is returned for undefined routes router.NewRoute().HandlerFunc(func(w http.ResponseWriter, r *http.Request) { routes.ErrorPage(w, r, errors.New("Route not found"), http.StatusNotFound) }) diff --git a/server/handlers/set_context.go b/server/handlers/set_context.go index be91c66..d384b25 100644 --- a/server/handlers/set_context.go +++ b/server/handlers/set_context.go @@ -6,6 +6,8 @@ import ( "codeberg.org/vnpower/pixivfe/v2/server/request_context" ) +// ProvideUserContext is a middleware that wraps an http.Handler +// to inject a user context into each incoming request. func ProvideUserContext(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h.ServeHTTP(w, r.WithContext(request_context.ProvideWith(r.Context())))