move router to separate package

This commit is contained in:
perennial
2024-10-25 15:47:37 +11:00
parent 5c6a21baa9
commit 0d7f22afac
4 changed files with 172 additions and 170 deletions
+2 -2
View File
@@ -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.",
+2 -1
View File
@@ -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.*()
-167
View File
@@ -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
}
+168
View File
@@ -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
}