From 0d7f22afac2a797aa73f54f527e25317465eedcb Mon Sep 17 00:00:00 2001 From: perennial Date: Fri, 25 Oct 2024 15:47:37 +1100 Subject: [PATCH] move router to separate package --- i18n/locale/en/code.json | 4 +- main.go | 3 +- server/middleware/router.go | 167 ----------------------------------- server/router/router.go | 168 ++++++++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+), 170 deletions(-) delete mode 100644 server/middleware/router.go create mode 100644 server/router/router.go diff --git a/i18n/locale/en/code.json b/i18n/locale/en/code.json index 63430db..4707156 100644 --- a/i18n/locale/en/code.json +++ b/i18n/locale/en/code.json @@ -31,8 +31,8 @@ "core/requests.go:lLy9SHFUtQQ": "failed to copy response body: %w", "core/requests.go:o8eDPgojH4w": "Incompatible response body", "core/user.go:T3RwaHcnsYQ": "Invalid work category: %#v. Only \"%s\", \"%s\", \"%s\", \"%s\", \"%s\" and \"%s\" are available", - "server/middleware/router.go:HORkE0Obn1U": "Failed to redirect to %s", - "server/middleware/router.go:Z9UG-DYmQCk": "Route not found", + "server/router/router.go:HORkE0Obn1U": "Failed to redirect to %s", + "server/router/router.go:Z9UG-DYmQCk": "Route not found", "server/routes/actions.go:2RxigvKdaDk": "Method not allowed", "server/routes/actions.go:PG4HiUvZvVA": "No ID provided.", "server/routes/actions.go:ZamBnL56VXw": "No user ID provided.", diff --git a/main.go b/main.go index 6052787..a8ad619 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ import ( "codeberg.org/vnpower/pixivfe/v2/i18n" "codeberg.org/vnpower/pixivfe/v2/server/middleware" "codeberg.org/vnpower/pixivfe/v2/server/proxy_checker" + "codeberg.org/vnpower/pixivfe/v2/server/router" "codeberg.org/vnpower/pixivfe/v2/server/template" ) @@ -78,7 +79,7 @@ func main() { auditor.SugaredLogger.Info("Starting server...") - router := middleware.DefineRoutes(auditor) + router := router.DefineRoutes(auditor) // the first middleware is the most outer / first executed one router.Use(middleware.ProvideUserContext) // needed for everything else router.Use(middleware.SetLocaleFromCookie) // needed for i18n.*() diff --git a/server/middleware/router.go b/server/middleware/router.go deleted file mode 100644 index 8cc3708..0000000 --- a/server/middleware/router.go +++ /dev/null @@ -1,167 +0,0 @@ -package middleware - -import ( - "net/http" - "net/url" - "strings" - - "github.com/gorilla/mux" - - "codeberg.org/vnpower/pixivfe/v2/audit" - "codeberg.org/vnpower/pixivfe/v2/i18n" - "codeberg.org/vnpower/pixivfe/v2/server/routes" -) - -// AuditorHandler is a type for routes that require an auditor -type AuditorHandler func(auditor *audit.Auditor, w http.ResponseWriter, r *http.Request) error - -// WrapWithAuditor is a helper function to wrap AuditorHandler into CatchError -func WrapWithAuditor(auditor *audit.Auditor, handler AuditorHandler) func(w http.ResponseWriter, r *http.Request) error { - return func(w http.ResponseWriter, r *http.Request) error { - return handler(auditor, w, r) - } -} - -// 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(auditor *audit.Auditor) *mux.Router { - router := mux.NewRouter() - - // Tutorial: Adding new routes - // 1. Use router.HandleFunc to define the path and handler - // 2. Wrap the handler function, defined in package routes, with CatchError for error handling - // 3. Specify the HTTP method(s) using .Methods() - // 4. For URL parameters, use curly braces in the path, e.g., "/users/{id}" - // 5. Group similar routes together for better organization - // - // Example (if auditor is used): - // router.HandleFunc("/new/route/{param}", CatchError(WrapWithAuditor(auditor, routes.NewHandler))).Methods("GET", "POST") - // - // Example (if auditor is not used): - // router.HandleFunc("/new/route/{param}", CatchError(routes.NewHandler)).Methods("GET", "POST") - - // 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) { - url := r.URL - url.Path = url.Path[0 : len(url.Path)-1] - // @iacore: i think this won't have open redirect vuln - 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 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(WrapWithAuditor(auditor, routes.IndexPage))).Methods("GET") - router.HandleFunc("/about", CatchError(routes.AboutPage)).Methods("GET") - router.HandleFunc("/newest", CatchError(WrapWithAuditor(auditor, routes.NewestPage))).Methods("GET") - router.HandleFunc("/discovery", CatchError(WrapWithAuditor(auditor, routes.DiscoveryPage))).Methods("GET") - router.HandleFunc("/discovery/novel", CatchError(WrapWithAuditor(auditor, routes.NovelDiscoveryPage))).Methods("GET") - - // Ranking related routes - router.HandleFunc("/ranking", CatchError(WrapWithAuditor(auditor, routes.RankingPage))).Methods("GET") - router.HandleFunc("/rankingCalendar", CatchError(WrapWithAuditor(auditor, 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(WrapWithAuditor(auditor, routes.UserAtomFeed))).Methods("GET") - router.HandleFunc("/users/{id}/{category}.atom.xml", CatchError(WrapWithAuditor(auditor, routes.UserAtomFeed))).Methods("GET") - router.HandleFunc("/users/{id}", CatchError(WrapWithAuditor(auditor, routes.UserPage))).Methods("GET") - router.HandleFunc("/users/{id}/{category}", CatchError(WrapWithAuditor(auditor, routes.UserPage))).Methods("GET") - - // Artwork related routes - router.HandleFunc("/artworks/{id}", CatchError(WrapWithAuditor(auditor, routes.ArtworkPage))).Methods("GET") - router.HandleFunc("/artworks-multi/{ids}", CatchError(WrapWithAuditor(auditor, routes.ArtworkMultiPage))).Methods("GET") - // 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") - - // Manga related routes - router.HandleFunc("/users/{id}/series/{sid}", CatchError(WrapWithAuditor(auditor, routes.MangaSeriesPage))).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(WrapWithAuditor(auditor, routes.NovelPage))).Methods("GET") - router.HandleFunc("/novel/series/{id}", CatchError(WrapWithAuditor(auditor, 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(WrapWithAuditor(auditor, routes.SettingsPost))).Methods("POST") - - // User action routes (login, bookmarks, likes, etc.) - router.HandleFunc("/self", CatchError(routes.LoginUserPage)).Methods("GET") - router.HandleFunc("/self/followingWorks", CatchError(WrapWithAuditor(auditor, routes.FollowingWorksPage))).Methods("GET") - router.HandleFunc("/self/bookmarks", CatchError(routes.LoginBookmarkPage)).Methods("GET") - router.HandleFunc("/self/addBookmark/{id}", CatchError(WrapWithAuditor(auditor, routes.AddBookmarkRoute))).Methods("POST") - router.HandleFunc("/self/deleteBookmark/{id}", CatchError(WrapWithAuditor(auditor, routes.DeleteBookmarkRoute))).Methods("POST") - router.HandleFunc("/self/like/{id}", CatchError(WrapWithAuditor(auditor, routes.LikeRoute))).Methods("POST") - router.HandleFunc("/self/followUser/{id}", CatchError(WrapWithAuditor(auditor, routes.FollowUserRoute))).Methods("POST") - router.HandleFunc("/self/unfollowUser/{id}", CatchError(WrapWithAuditor(auditor, routes.UnfollowUserRoute))).Methods("POST") - - // oEmbed endpoint for embedding Pixiv content - router.HandleFunc("/oembed", CatchError(routes.Oembed)).Methods("GET") - - // Tag related routes - router.HandleFunc("/tags/{name}", CatchError(WrapWithAuditor(auditor, routes.TagPage))).Methods("GET") - router.HandleFunc("/tags/{name}", CatchError(WrapWithAuditor(auditor, routes.TagPage))).Methods("POST") - router.HandleFunc("/tags", CatchError(WrapWithAuditor(auditor, routes.TagPage))).Methods("GET") - router.HandleFunc("/tags", CatchError(routes.AdvancedTagPost)).Methods("POST") - - // Although we have ParsePixivRedirect for template. here's a fallback to catch all - router.HandleFunc("/jump.php", func(w http.ResponseWriter, r *http.Request) { - escaped, err := url.QueryUnescape(r.URL.RawQuery) - if err != nil { - routes.ErrorPage(w, r, i18n.Errorf("Failed to redirect to %s", r.URL.RawQuery), http.StatusNotFound) - } else { - http.Redirect(w, r, escaped, http.StatusPermanentRedirect) - } - }).Methods("GET") - - // 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) - // 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, i18n.Error("Route not found"), http.StatusNotFound) - }) - - return router -} diff --git a/server/router/router.go b/server/router/router.go new file mode 100644 index 0000000..21ad0f8 --- /dev/null +++ b/server/router/router.go @@ -0,0 +1,168 @@ +package router + +import ( + "net/http" + "net/url" + "strings" + + "github.com/gorilla/mux" + + "codeberg.org/vnpower/pixivfe/v2/audit" + "codeberg.org/vnpower/pixivfe/v2/i18n" + "codeberg.org/vnpower/pixivfe/v2/server/middleware" + "codeberg.org/vnpower/pixivfe/v2/server/routes" +) + +// AuditorHandler is a type for routes that require an auditor +type AuditorHandler func(auditor *audit.Auditor, w http.ResponseWriter, r *http.Request) error + +// WrapWithAuditor is a helper function to wrap AuditorHandler into middleware.CatchError +func WrapWithAuditor(auditor *audit.Auditor, handler AuditorHandler) func(w http.ResponseWriter, r *http.Request) error { + return func(w http.ResponseWriter, r *http.Request) error { + return handler(auditor, w, r) + } +} + +// 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(auditor *audit.Auditor) *mux.Router { + router := mux.NewRouter() + + // Tutorial: Adding new routes + // 1. Use router.HandleFunc to define the path and handler + // 2. Wrap the handler function, defined in package routes, with middleware.CatchError for error handling + // 3. Specify the HTTP method(s) using .Methods() + // 4. For URL parameters, use curly braces in the path, e.g., "/users/{id}" + // 5. Group similar routes together for better organization + // + // Example (if auditor is used): + // router.HandleFunc("/new/route/{param}", middleware.CatchError(WrapWithAuditor(auditor, routes.NewHandler))).Methods("GET", "POST") + // + // Example (if auditor is not used): + // router.HandleFunc("/new/route/{param}", middleware.CatchError(routes.NewHandler)).Methods("GET", "POST") + + // 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) { + url := r.URL + url.Path = url.Path[0 : len(url.Path)-1] + // @iacore: i think this won't have open redirect vuln + 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 for handling image requests + // These routes maintain cache headers set by upstream servers + handleStripPrefix(router, "/proxy/i.pximg.net/", middleware.CatchError(routes.IPximgProxy)).Methods("GET") + handleStripPrefix(router, "/proxy/s.pximg.net/", middleware.CatchError(routes.SPximgProxy)).Methods("GET") + handleStripPrefix(router, "/proxy/ugoira.com/", middleware.CatchError(routes.UgoiraProxy)).Methods("GET") + + // Main application routes + router.HandleFunc("/", middleware.CatchError(WrapWithAuditor(auditor, routes.IndexPage))).Methods("GET") + router.HandleFunc("/about", middleware.CatchError(routes.AboutPage)).Methods("GET") + router.HandleFunc("/newest", middleware.CatchError(WrapWithAuditor(auditor, routes.NewestPage))).Methods("GET") + router.HandleFunc("/discovery", middleware.CatchError(WrapWithAuditor(auditor, routes.DiscoveryPage))).Methods("GET") + router.HandleFunc("/discovery/novel", middleware.CatchError(WrapWithAuditor(auditor, routes.NovelDiscoveryPage))).Methods("GET") + + // Ranking related routes + router.HandleFunc("/ranking", middleware.CatchError(WrapWithAuditor(auditor, routes.RankingPage))).Methods("GET") + router.HandleFunc("/rankingCalendar", middleware.CatchError(WrapWithAuditor(auditor, routes.RankingCalendarPage))).Methods("GET") + router.HandleFunc("/rankingCalendar", middleware.CatchError(routes.RankingCalendarPicker)).Methods("POST") + + // User related routes, including Atom feeds + router.HandleFunc("/users/{id}.atom.xml", middleware.CatchError(WrapWithAuditor(auditor, routes.UserAtomFeed))).Methods("GET") + router.HandleFunc("/users/{id}/{category}.atom.xml", middleware.CatchError(WrapWithAuditor(auditor, routes.UserAtomFeed))).Methods("GET") + router.HandleFunc("/users/{id}", middleware.CatchError(WrapWithAuditor(auditor, routes.UserPage))).Methods("GET") + router.HandleFunc("/users/{id}/{category}", middleware.CatchError(WrapWithAuditor(auditor, routes.UserPage))).Methods("GET") + + // Artwork related routes + router.HandleFunc("/artworks/{id}", middleware.CatchError(WrapWithAuditor(auditor, routes.ArtworkPage))).Methods("GET") + router.HandleFunc("/artworks-multi/{ids}", middleware.CatchError(WrapWithAuditor(auditor, routes.ArtworkMultiPage))).Methods("GET") + // 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") + + // Manga related routes + router.HandleFunc("/users/{id}/series/{sid}", middleware.CatchError(WrapWithAuditor(auditor, routes.MangaSeriesPage))).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}", middleware.CatchError(WrapWithAuditor(auditor, routes.NovelPage))).Methods("GET") + router.HandleFunc("/novel/series/{id}", middleware.CatchError(WrapWithAuditor(auditor, routes.NovelSeriesPage))).Methods("GET") + + // Pixivision related routes + router.HandleFunc("/pixivision", middleware.CatchError(routes.PixivisionHomePage)).Methods("GET") + router.HandleFunc("/pixivision/a/{id}", middleware.CatchError(routes.PixivisionArticlePage)).Methods("GET") + router.HandleFunc("/pixivision/c/{id}", middleware.CatchError(routes.PixivisionCategoryPage)).Methods("GET") + router.HandleFunc("/pixivision/t/{id}", middleware.CatchError(routes.PixivisionTagPage)).Methods("GET") + + // Settings related routes + router.HandleFunc("/settings", middleware.CatchError(routes.SettingsPage)).Methods("GET") + router.HandleFunc("/settings/{type}", middleware.CatchError(WrapWithAuditor(auditor, routes.SettingsPost))).Methods("POST") + + // User action routes (login, bookmarks, likes, etc.) + router.HandleFunc("/self", middleware.CatchError(routes.LoginUserPage)).Methods("GET") + router.HandleFunc("/self/followingWorks", middleware.CatchError(WrapWithAuditor(auditor, routes.FollowingWorksPage))).Methods("GET") + router.HandleFunc("/self/bookmarks", middleware.CatchError(routes.LoginBookmarkPage)).Methods("GET") + router.HandleFunc("/self/addBookmark/{id}", middleware.CatchError(WrapWithAuditor(auditor, routes.AddBookmarkRoute))).Methods("POST") + router.HandleFunc("/self/deleteBookmark/{id}", middleware.CatchError(WrapWithAuditor(auditor, routes.DeleteBookmarkRoute))).Methods("POST") + router.HandleFunc("/self/like/{id}", middleware.CatchError(WrapWithAuditor(auditor, routes.LikeRoute))).Methods("POST") + router.HandleFunc("/self/followUser/{id}", middleware.CatchError(WrapWithAuditor(auditor, routes.FollowUserRoute))).Methods("POST") + router.HandleFunc("/self/unfollowUser/{id}", middleware.CatchError(WrapWithAuditor(auditor, routes.UnfollowUserRoute))).Methods("POST") + + // oEmbed endpoint for embedding Pixiv content + router.HandleFunc("/oembed", middleware.CatchError(routes.Oembed)).Methods("GET") + + // Tag related routes + router.HandleFunc("/tags/{name}", middleware.CatchError(WrapWithAuditor(auditor, routes.TagPage))).Methods("GET") + router.HandleFunc("/tags/{name}", middleware.CatchError(WrapWithAuditor(auditor, routes.TagPage))).Methods("POST") + router.HandleFunc("/tags", middleware.CatchError(WrapWithAuditor(auditor, routes.TagPage))).Methods("GET") + router.HandleFunc("/tags", middleware.CatchError(routes.AdvancedTagPost)).Methods("POST") + + // Although we have ParsePixivRedirect for template. here's a fallback to catch all + router.HandleFunc("/jump.php", func(w http.ResponseWriter, r *http.Request) { + escaped, err := url.QueryUnescape(r.URL.RawQuery) + if err != nil { + routes.ErrorPage(w, r, i18n.Errorf("Failed to redirect to %s", r.URL.RawQuery), http.StatusNotFound) + } else { + http.Redirect(w, r, escaped, http.StatusPermanentRedirect) + } + }).Methods("GET") + + // Diagnostic routes for monitoring and debugging + router.HandleFunc("/diagnostics", middleware.CatchError(routes.Diagnostics)).Methods("GET") + router.HandleFunc("/diagnostics/spans.json", middleware.CatchError(routes.DiagnosticsData)).Methods("GET") + router.HandleFunc("/diagnostics/reset", routes.ResetDiagnosticsData) + + // 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, i18n.Error("Route not found"), http.StatusNotFound) + }) + + return router +}