Add comments to package handlers

This commit is contained in:
perennial
2024-09-24 15:51:23 +10:00
parent 32a52d4f29
commit 3c1bb0badb
8 changed files with 89 additions and 12 deletions
+1
View File
@@ -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()
+12
View File
@@ -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
+13
View File
@@ -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)
}
})
+19 -7
View File
@@ -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)
}
})
+15 -1
View File
@@ -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,
+2
View File
@@ -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
+25 -4
View File
@@ -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)
})
+2
View File
@@ -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())))