mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-23 09:18:51 +02:00
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:
@@ -2,16 +2,19 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/auth"
|
|
||||||
"github.com/yusing/godoxy/internal/route/rules"
|
"github.com/yusing/godoxy/internal/route/rules"
|
||||||
httputils "github.com/yusing/goutils/http"
|
httputils "github.com/yusing/goutils/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Bypass []rules.RuleOn
|
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 {
|
func (b Bypass) ShouldBypass(w http.ResponseWriter, r *http.Request) bool {
|
||||||
for _, rule := range b {
|
for _, rule := range b {
|
||||||
if rule.Check(w, r) {
|
if rule.Check(w, r) {
|
||||||
@@ -29,32 +32,93 @@ type checkBypass struct {
|
|||||||
modReq RequestModifier
|
modReq RequestModifier
|
||||||
modRes ResponseModifier
|
modRes ResponseModifier
|
||||||
|
|
||||||
// when request path matches any of these prefixes, bypass is not applied
|
modReqCheckEnforceFuncs []checkReqFunc
|
||||||
enforcedPathPrefixes []string
|
modReqCheckBypassFuncs []checkReqFunc
|
||||||
|
|
||||||
|
modResCheckEnforceFuncs []checkRespFunc
|
||||||
|
modResCheckBypassFuncs []checkRespFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *checkBypass) isEnforced(r *http.Request) bool {
|
var (
|
||||||
for _, prefix := range c.enforcedPathPrefixes {
|
_ RequestModifier = (*checkBypass)(nil)
|
||||||
if strings.HasPrefix(r.URL.Path, prefix) {
|
_ 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 true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
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) {
|
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
|
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)
|
return c.modReq.before(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// modifyResponse modifies the response if the response should be modified.
|
||||||
func (c *checkBypass) modifyResponse(resp *http.Response) error {
|
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
|
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)
|
return c.modRes.modifyResponse(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,23 +127,46 @@ func (m *Middleware) withCheckBypass() any {
|
|||||||
modReq, _ := m.impl.(RequestModifier)
|
modReq, _ := m.impl.(RequestModifier)
|
||||||
modRes, _ := m.impl.(ResponseModifier)
|
modRes, _ := m.impl.(ResponseModifier)
|
||||||
return &checkBypass{
|
return &checkBypass{
|
||||||
name: m.Name(),
|
name: m.Name(),
|
||||||
bypass: m.Bypass,
|
bypass: m.Bypass,
|
||||||
enforcedPathPrefixes: getEnforcedPathPrefixes(modReq, modRes),
|
modReq: modReq,
|
||||||
modReq: modReq,
|
modRes: modRes,
|
||||||
modRes: modRes,
|
modReqCheckEnforceFuncs: getModReqCheckEnforceFuncs(modReq),
|
||||||
|
modReqCheckBypassFuncs: getModReqCheckBypassFuncs(modReq),
|
||||||
|
modResCheckEnforceFuncs: getModResCheckEnforceFuncs(modRes),
|
||||||
|
modResCheckBypassFuncs: getModResCheckBypassFuncs(modRes),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m.impl
|
return m.impl
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEnforcedPathPrefixes(modReq RequestModifier, modRes ResponseModifier) []string {
|
func getModReqCheckEnforceFuncs(modReq RequestModifier) (checks []checkReqFunc) {
|
||||||
if modReq == nil && modRes == nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
switch modReq.(type) {
|
switch modReq.(type) {
|
||||||
case *oidcMiddleware:
|
case *oidcMiddleware, *forwardAuthMiddleware, *crowdsecMiddleware, *hCaptcha:
|
||||||
return []string{auth.OIDCAuthBasePath}
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
74
internal/net/gphttp/middleware/bypass_static.go
Normal file
74
internal/net/gphttp/middleware/bypass_static.go
Normal 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
|
||||||
|
}
|
||||||
@@ -28,6 +28,10 @@ type oidcMiddleware struct {
|
|||||||
|
|
||||||
var OIDC = NewMiddleware[oidcMiddleware]()
|
var OIDC = NewMiddleware[oidcMiddleware]()
|
||||||
|
|
||||||
|
func isOIDCAuthPath(r *http.Request) bool {
|
||||||
|
return strings.HasPrefix(r.URL.Path, auth.OIDCAuthBasePath)
|
||||||
|
}
|
||||||
|
|
||||||
func (amw *oidcMiddleware) finalize() error {
|
func (amw *oidcMiddleware) finalize() error {
|
||||||
if !auth.IsOIDCEnabled() {
|
if !auth.IsOIDCEnabled() {
|
||||||
log.Error().Msg("OIDC not enabled but OIDC middleware is used")
|
log.Error().Msg("OIDC not enabled but OIDC middleware is used")
|
||||||
|
|||||||
Reference in New Issue
Block a user