mirror of
https://codeberg.org/VnPower/PixivFE
synced 2024-12-06 19:16:23 +01:00
Add comments to package handlers
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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())))
|
||||
|
||||
Reference in New Issue
Block a user