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
This commit is contained in:
yusing
2026-02-18 19:12:07 +08:00
parent f7676b2dbd
commit a12bdeaf55
3 changed files with 185 additions and 20 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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")