From a12bdeaf55e449f0aa735efa7196c95604e8beee Mon Sep 17 00:00:00 2001 From: yusing Date: Wed, 18 Feb 2026 19:12:07 +0800 Subject: [PATCH] refactor(middleware): replace path prefix checks with function-based approach Replace simple path prefix-based enforcement/bypass mechanism with a more flexible function-based approach. This allows for more complex conditions to determine when middleware should be enforced or bypassed. - Add checkReqFunc and checkRespFunc types for flexible condition checking - Replace enforcedPathPrefixes with separate enforce and bypass check functions - Add static asset path detection for automatic bypassing - Separate request and response check logic for better granularity --- internal/net/gphttp/middleware/bypass.go | 127 +++++++++++++++--- .../net/gphttp/middleware/bypass_static.go | 74 ++++++++++ internal/net/gphttp/middleware/oidc.go | 4 + 3 files changed, 185 insertions(+), 20 deletions(-) create mode 100644 internal/net/gphttp/middleware/bypass_static.go diff --git a/internal/net/gphttp/middleware/bypass.go b/internal/net/gphttp/middleware/bypass.go index 83dab822..c598dff2 100644 --- a/internal/net/gphttp/middleware/bypass.go +++ b/internal/net/gphttp/middleware/bypass.go @@ -2,16 +2,19 @@ package middleware import ( "net/http" - "strings" "github.com/rs/zerolog/log" - "github.com/yusing/godoxy/internal/auth" "github.com/yusing/godoxy/internal/route/rules" httputils "github.com/yusing/goutils/http" ) type Bypass []rules.RuleOn +type ( + checkReqFunc func(r *http.Request) bool + checkRespFunc func(resp *http.Response) bool +) + func (b Bypass) ShouldBypass(w http.ResponseWriter, r *http.Request) bool { for _, rule := range b { if rule.Check(w, r) { @@ -29,32 +32,93 @@ type checkBypass struct { modReq RequestModifier modRes ResponseModifier - // when request path matches any of these prefixes, bypass is not applied - enforcedPathPrefixes []string + modReqCheckEnforceFuncs []checkReqFunc + modReqCheckBypassFuncs []checkReqFunc + + modResCheckEnforceFuncs []checkRespFunc + modResCheckBypassFuncs []checkRespFunc } -func (c *checkBypass) isEnforced(r *http.Request) bool { - for _, prefix := range c.enforcedPathPrefixes { - if strings.HasPrefix(r.URL.Path, prefix) { +var ( + _ RequestModifier = (*checkBypass)(nil) + _ ResponseModifier = (*checkBypass)(nil) +) + +// shouldModReqEnforce checks if the modify request should be enforced. +// +// Returns true if any of the check functions returns true. +func (c *checkBypass) shouldModReqEnforce(r *http.Request) bool { + for _, f := range c.modReqCheckEnforceFuncs { + if f(r) { return true } } return false } +// shouldModResEnforce checks if the modify response should be enforced. +// +// Returns true if any of the check functions returns true. +func (c *checkBypass) shouldModResEnforce(resp *http.Response) bool { + for _, f := range c.modResCheckEnforceFuncs { + if f(resp) { + return true + } + } + return false +} + +// shouldModReqBypass checks if the modify request should be bypassed. +// +// If enforce checks return true, the bypass checks are not performed. +// Otherwise, if any of the bypass checks returns true +// or user defined bypass rules return true, the request is bypassed. +func (c *checkBypass) shouldModReqBypass(w http.ResponseWriter, r *http.Request) bool { + if c.shouldModReqEnforce(r) { + return false + } + for _, f := range c.modReqCheckBypassFuncs { + if f(r) { + return true + } + } + return c.bypass.ShouldBypass(w, r) +} + +// shouldModResBypass checks if the modify response should be bypassed. +// +// If enforce checks return true, the bypass checks are not performed. +// Otherwise, if any of the bypass checks returns true +// or user defined bypass rules return true, the response is bypassed. +func (c *checkBypass) shouldModResBypass(resp *http.Response) bool { + if c.shouldModResEnforce(resp) { + return false + } + for _, f := range c.modResCheckBypassFuncs { + if f(resp) { + return true + } + } + return c.bypass.ShouldBypass(httputils.ResponseAsRW(resp), resp.Request) +} + +// before modifies the request if the request should be modified. +// +// Returns true if the request is not done, false otherwise. func (c *checkBypass) before(w http.ResponseWriter, r *http.Request) (proceedNext bool) { - if c.modReq == nil || (!c.isEnforced(r) && c.bypass.ShouldBypass(w, r)) { + if c.modReq == nil || c.shouldModReqBypass(w, r) { return true } - log.Debug().Str("middleware", c.name).Str("url", r.Host+r.URL.Path).Msg("modifying request") + // log.Debug().Str("middleware", c.name).Str("url", r.Host+r.URL.Path).Msg("modifying request") return c.modReq.before(w, r) } +// modifyResponse modifies the response if the response should be modified. func (c *checkBypass) modifyResponse(resp *http.Response) error { - if c.modRes == nil || (!c.isEnforced(resp.Request) && c.bypass.ShouldBypass(httputils.ResponseAsRW(resp), resp.Request)) { + if c.modRes == nil || c.shouldModResBypass(resp) { return nil } - log.Debug().Str("middleware", c.name).Str("url", resp.Request.Host+resp.Request.URL.Path).Msg("modifying response") + // log.Debug().Str("middleware", c.name).Str("url", resp.Request.Host+resp.Request.URL.Path).Msg("modifying response") return c.modRes.modifyResponse(resp) } @@ -63,23 +127,46 @@ func (m *Middleware) withCheckBypass() any { modReq, _ := m.impl.(RequestModifier) modRes, _ := m.impl.(ResponseModifier) return &checkBypass{ - name: m.Name(), - bypass: m.Bypass, - enforcedPathPrefixes: getEnforcedPathPrefixes(modReq, modRes), - modReq: modReq, - modRes: modRes, + name: m.Name(), + bypass: m.Bypass, + modReq: modReq, + modRes: modRes, + modReqCheckEnforceFuncs: getModReqCheckEnforceFuncs(modReq), + modReqCheckBypassFuncs: getModReqCheckBypassFuncs(modReq), + modResCheckEnforceFuncs: getModResCheckEnforceFuncs(modRes), + modResCheckBypassFuncs: getModResCheckBypassFuncs(modRes), } } return m.impl } -func getEnforcedPathPrefixes(modReq RequestModifier, modRes ResponseModifier) []string { - if modReq == nil && modRes == nil { +func getModReqCheckEnforceFuncs(modReq RequestModifier) (checks []checkReqFunc) { + if modReq == nil { + return nil + } + if _, ok := modReq.(*oidcMiddleware); ok { + checks = append(checks, isOIDCAuthPath) + } + return checks +} + +func getModReqCheckBypassFuncs(modReq RequestModifier) (checks []checkReqFunc) { + if modReq == nil { return nil } switch modReq.(type) { - case *oidcMiddleware: - return []string{auth.OIDCAuthBasePath} + case *oidcMiddleware, *forwardAuthMiddleware, *crowdsecMiddleware, *hCaptcha: + checks = append(checks, isStaticAssetPath) } + return checks +} + +func getModResCheckEnforceFuncs(modRes ResponseModifier) []checkRespFunc { + // TODO: add enforce checks for response modifiers if needed. + return nil +} + +func getModResCheckBypassFuncs(modRes ResponseModifier) []checkRespFunc { + // TODO: add bypass checks for response modifiers if needed. return nil } diff --git a/internal/net/gphttp/middleware/bypass_static.go b/internal/net/gphttp/middleware/bypass_static.go new file mode 100644 index 00000000..b69b3e5e --- /dev/null +++ b/internal/net/gphttp/middleware/bypass_static.go @@ -0,0 +1,74 @@ +package middleware + +import ( + "net/http" + + "github.com/yusing/godoxy/internal/route/rules" +) + +func must[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +} + +var staticAssetsPaths = map[string]struct{}{ + // Web app manifests + "/manifest.json": {}, + "/manifest.webmanifest": {}, + // Service workers + "/sw.js": {}, + "/registerSW.js": {}, + // Favicons + "/favicon.ico": {}, + "/favicon.png": {}, + "/favicon.svg": {}, + // Apple icons + "/apple-icon.png": {}, + "/apple-touch-icon.png": {}, + "/apple-touch-icon-precomposed.png": {}, + // Microsoft / browser config + "/browserconfig.xml": {}, + // Safari pinned tab + "/safari-pinned-tab.svg": {}, + // Crawlers / SEO + "/robots.txt": {}, + "/sitemap.xml": {}, + "/sitemap_index.xml": {}, + "/ads.txt": {}, +} + +var staticAssetsGlobs = []rules.Matcher{ + // Workbox (PWA) + must(rules.GlobMatcher("/workbox-window.prod.es5-*.js", false)), + must(rules.GlobMatcher("/workbox-*.js", false)), + // Favicon variants (e.g. favicon-32x32.png) + must(rules.GlobMatcher("/favicon-*.png", false)), + // Web app manifest icons + must(rules.GlobMatcher("/web-app-manifest-*.png", false)), + // Android Chrome icons + must(rules.GlobMatcher("/android-chrome-*.png", false)), + // Apple touch icon variants + must(rules.GlobMatcher("/apple-touch-icon-*.png", false)), + // Microsoft tile icons + must(rules.GlobMatcher("/mstile-*.png", false)), + // Generic PWA / app icons + must(rules.GlobMatcher("/pwa-*.png", false)), + must(rules.GlobMatcher("/icon-*.png", false)), + // Sitemaps (e.g. sitemap-1.xml, sitemap-posts.xml) + must(rules.GlobMatcher("/sitemap-*.xml", false)), +} + +func isStaticAssetPath(r *http.Request) bool { + if _, ok := staticAssetsPaths[r.URL.Path]; ok { + return true + } + + for _, matcher := range staticAssetsGlobs { + if matcher(r.URL.Path) { + return true + } + } + return false +} diff --git a/internal/net/gphttp/middleware/oidc.go b/internal/net/gphttp/middleware/oidc.go index bd8c74e9..e9e35509 100644 --- a/internal/net/gphttp/middleware/oidc.go +++ b/internal/net/gphttp/middleware/oidc.go @@ -28,6 +28,10 @@ type oidcMiddleware struct { var OIDC = NewMiddleware[oidcMiddleware]() +func isOIDCAuthPath(r *http.Request) bool { + return strings.HasPrefix(r.URL.Path, auth.OIDCAuthBasePath) +} + func (amw *oidcMiddleware) finalize() error { if !auth.IsOIDCEnabled() { log.Error().Msg("OIDC not enabled but OIDC middleware is used")