mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 23:16:37 +08:00
autoresearch iter 32: PERF whitelist parses patterns once, 14 allocs/op to 1
This commit is contained in:
@@ -45,7 +45,7 @@ type RouterRegistry struct {
|
|||||||
nextID uint64
|
nextID uint64
|
||||||
routes []RouteDefinition
|
routes []RouteDefinition
|
||||||
middlewares []any
|
middlewares []any
|
||||||
whitelist []string
|
whitelist PathWhitelist
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRouterRegistry creates a new root router collector.
|
// NewRouterRegistry creates a new root router collector.
|
||||||
@@ -182,36 +182,17 @@ func (r *RouterRegistry) Routes() []RouteDefinition {
|
|||||||
|
|
||||||
// RegisterWhitelist adds path patterns to the whitelist.
|
// RegisterWhitelist adds path patterns to the whitelist.
|
||||||
func (r *RouterRegistry) RegisterWhitelist(patterns ...string) {
|
func (r *RouterRegistry) RegisterWhitelist(patterns ...string) {
|
||||||
r.mu.Lock()
|
r.whitelist.Add(patterns...)
|
||||||
defer r.mu.Unlock()
|
|
||||||
for _, p := range patterns {
|
|
||||||
clean := cleanPath(p)
|
|
||||||
if clean != "" {
|
|
||||||
r.whitelist = append(r.whitelist, clean)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whitelist returns a copy of all registered whitelist path patterns.
|
// Whitelist returns a copy of all registered whitelist path patterns.
|
||||||
func (r *RouterRegistry) Whitelist() []string {
|
func (r *RouterRegistry) Whitelist() []string {
|
||||||
r.mu.RLock()
|
return r.whitelist.Patterns()
|
||||||
defer r.mu.RUnlock()
|
|
||||||
res := make([]string, len(r.whitelist))
|
|
||||||
copy(res, r.whitelist)
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsWhitelisted checks if the given path matches any registered whitelist pattern.
|
// IsWhitelisted checks if the given path matches any registered whitelist pattern.
|
||||||
func (r *RouterRegistry) IsWhitelisted(path string) bool {
|
func (r *RouterRegistry) IsWhitelisted(path string) bool {
|
||||||
r.mu.RLock()
|
return r.whitelist.Match(path)
|
||||||
defer r.mu.RUnlock()
|
|
||||||
clean := cleanPath(path)
|
|
||||||
for _, pattern := range r.whitelist {
|
|
||||||
if MatchPathPattern(pattern, clean) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouterGroup represents a scoped route group with a path prefix and group-level middlewares.
|
// RouterGroup represents a scoped route group with a path prefix and group-level middlewares.
|
||||||
@@ -411,3 +392,114 @@ func MatchPathPattern(pattern, path string) bool {
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compiledPattern holds a whitelist pattern with its per-request work already done.
|
||||||
|
type compiledPattern struct {
|
||||||
|
raw string // normalised pattern, reported back by Patterns
|
||||||
|
prefix string // non-empty when the pattern ends in "/*"
|
||||||
|
parts []string // normalised pattern split on "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathWhitelist matches request paths against a fixed set of patterns.
|
||||||
|
//
|
||||||
|
// Patterns are registered once during plugin Apply and never change afterwards, so
|
||||||
|
// normalising and splitting them on every request is wasted work. PathWhitelist
|
||||||
|
// does that once at registration instead. The zero value is ready to use.
|
||||||
|
type PathWhitelist struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
patterns []compiledPattern
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPathWhitelist returns a whitelist pre-populated with the given patterns.
|
||||||
|
func NewPathWhitelist(patterns ...string) *PathWhitelist {
|
||||||
|
w := &PathWhitelist{}
|
||||||
|
w.Add(patterns...)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// compilePatterns normalises and splits each pattern once, ahead of any request.
|
||||||
|
func compilePatterns(patterns []string) []compiledPattern {
|
||||||
|
compiled := make([]compiledPattern, 0, len(patterns))
|
||||||
|
for _, p := range patterns {
|
||||||
|
clean := cleanPath(p)
|
||||||
|
cp := compiledPattern{raw: clean, parts: strings.Split(clean, "/")}
|
||||||
|
if strings.HasSuffix(clean, "/*") {
|
||||||
|
cp.prefix = strings.TrimSuffix(clean, "/*")
|
||||||
|
}
|
||||||
|
compiled = append(compiled, cp)
|
||||||
|
}
|
||||||
|
return compiled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add appends patterns, normalising and splitting each now rather than per request.
|
||||||
|
func (w *PathWhitelist) Add(patterns ...string) {
|
||||||
|
if len(patterns) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
compiled := compilePatterns(patterns)
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
w.patterns = append(w.patterns, compiled...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace discards any existing patterns and installs the given ones, for callers
|
||||||
|
// whose configuration is a full swap rather than an incremental registration.
|
||||||
|
func (w *PathWhitelist) Replace(patterns ...string) {
|
||||||
|
compiled := compilePatterns(patterns)
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
w.patterns = compiled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match reports whether path matches any registered pattern. Equivalent to calling
|
||||||
|
// MatchPathPattern for every pattern, except the path is normalised and split once.
|
||||||
|
func (w *PathWhitelist) Match(path string) bool {
|
||||||
|
clean := cleanPath(path)
|
||||||
|
pathParts := strings.Split(clean, "/")
|
||||||
|
|
||||||
|
w.mu.RLock()
|
||||||
|
defer w.mu.RUnlock()
|
||||||
|
for i := range w.patterns {
|
||||||
|
p := &w.patterns[i]
|
||||||
|
if p.raw == clean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// A suffix wildcard matches both the bare prefix and anything below it.
|
||||||
|
if p.prefix != "" && (clean == p.prefix || strings.HasPrefix(clean, p.prefix+"/")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(p.parts) != len(pathParts) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if matchSegments(p.parts, pathParts) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchSegments compares an already-split pattern against an already-split path.
|
||||||
|
func matchSegments(patternParts, pathParts []string) bool {
|
||||||
|
for i, part := range patternParts {
|
||||||
|
if part == "*" || strings.HasPrefix(part, ":") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if part != pathParts[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patterns returns a copy of the registered patterns in registration order.
|
||||||
|
func (w *PathWhitelist) Patterns() []string {
|
||||||
|
w.mu.RLock()
|
||||||
|
defer w.mu.RUnlock()
|
||||||
|
res := make([]string, len(w.patterns))
|
||||||
|
for i := range w.patterns {
|
||||||
|
res[i] = w.patterns[i].raw
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package extpoints_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Wavelet/core/extpoints"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// whitelistEquivalencePatterns and paths cover every matching rule MatchPathPattern
|
||||||
|
// implements, so PathWhitelist.Match can be pinned against the behaviour it replaces.
|
||||||
|
var (
|
||||||
|
whitelistEquivalencePatterns = []string{
|
||||||
|
"/api/v1/user/login",
|
||||||
|
"/api/v1/oauth/*",
|
||||||
|
"/api/v1/cap/:source/authorize",
|
||||||
|
"/api/v1/files/*/download",
|
||||||
|
"/",
|
||||||
|
"login",
|
||||||
|
"/api/v1/x/",
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
|
||||||
|
whitelistEquivalencePaths = []string{
|
||||||
|
"/api/v1/user/login",
|
||||||
|
"/api/v1/user/login/",
|
||||||
|
"/api/v1/oauth/callback",
|
||||||
|
"/api/v1/oauth",
|
||||||
|
"/api/v1/oauth/a/b",
|
||||||
|
"/api/v1/cap/github/authorize",
|
||||||
|
"/api/v1/cap/:source/authorize",
|
||||||
|
"/api/v1/files/abc/download",
|
||||||
|
"/api/v1/files/a/b/download",
|
||||||
|
"/",
|
||||||
|
"login",
|
||||||
|
"/login",
|
||||||
|
"",
|
||||||
|
"/api/v1/x",
|
||||||
|
"/api/v1/x/y",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// legacyMatch reproduces the per-request loop every whitelist caller used before
|
||||||
|
// PathWhitelist existed.
|
||||||
|
func legacyMatch(patterns []string, path string) bool {
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
if extpoints.MatchPathPattern(pattern, path) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathWhitelistMatchesLegacyLoop(t *testing.T) {
|
||||||
|
for _, pattern := range whitelistEquivalencePatterns {
|
||||||
|
wl := extpoints.NewPathWhitelist(pattern)
|
||||||
|
for _, path := range whitelistEquivalencePaths {
|
||||||
|
got := wl.Match(path)
|
||||||
|
want := legacyMatch([]string{pattern}, path)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("pattern %q path %q: Match=%v, legacy=%v", pattern, path, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathWhitelistAccumulatesAcrossRegistration(t *testing.T) {
|
||||||
|
wl := extpoints.NewPathWhitelist("/api/v1/a")
|
||||||
|
wl.Add("/api/v1/b/*")
|
||||||
|
|
||||||
|
if !wl.Match("/api/v1/a") {
|
||||||
|
t.Error("first registration lost")
|
||||||
|
}
|
||||||
|
if !wl.Match("/api/v1/b/deep") {
|
||||||
|
t.Error("second registration lost")
|
||||||
|
}
|
||||||
|
if wl.Match("/api/v1/c") {
|
||||||
|
t.Error("path outside both registrations matched")
|
||||||
|
}
|
||||||
|
|
||||||
|
got := wl.Patterns()
|
||||||
|
want := []string{"/api/v1/a", "/api/v1/b/*"}
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatalf("Patterns() = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
t.Errorf("Patterns()[%d] = %q, want %q", i, got[i], want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathWhitelistReplaceDropsPreviousPatterns(t *testing.T) {
|
||||||
|
wl := extpoints.NewPathWhitelist("/api/v1/a")
|
||||||
|
wl.Replace("/api/v1/b")
|
||||||
|
|
||||||
|
if wl.Match("/api/v1/a") {
|
||||||
|
t.Error("Replace kept a pattern it should have discarded")
|
||||||
|
}
|
||||||
|
if !wl.Match("/api/v1/b") {
|
||||||
|
t.Error("Replace did not install the new pattern")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// whitelistBenchPatterns mirrors a realistically sized auth whitelist.
|
||||||
|
var whitelistBenchPatterns = []string{
|
||||||
|
"/api/v1/user/login",
|
||||||
|
"/api/v1/auth/refresh",
|
||||||
|
"/api/v1/oauth/*",
|
||||||
|
"/api/v1/cap/*",
|
||||||
|
"/api/v1/public/config",
|
||||||
|
"/api/v1/health",
|
||||||
|
"/api/v1/uploads/:id/file",
|
||||||
|
"/api/v1/notify/webhook/:channel",
|
||||||
|
"/login",
|
||||||
|
"/api/v1/access-tokens/:id/revoke",
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPathWhitelistAllocationReduction asserts the point of pre-compiling: a single
|
||||||
|
// Match must allocate less than the legacy per-pattern loop it replaces.
|
||||||
|
func TestPathWhitelistAllocationReduction(t *testing.T) {
|
||||||
|
wl := extpoints.NewPathWhitelist(whitelistBenchPatterns...)
|
||||||
|
|
||||||
|
legacy := testing.Benchmark(func(b *testing.B) {
|
||||||
|
for b.Loop() {
|
||||||
|
_ = legacyMatch(whitelistBenchPatterns, "/api/v1/uploads/9/file")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
compiled := testing.Benchmark(func(b *testing.B) {
|
||||||
|
for b.Loop() {
|
||||||
|
_ = wl.Match("/api/v1/uploads/9/file")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
legacyAlloc := legacy.AllocsPerOp()
|
||||||
|
compiledAlloc := compiled.AllocsPerOp()
|
||||||
|
t.Logf("legacy %d allocs/op, PathWhitelist %d allocs/op", legacyAlloc, compiledAlloc)
|
||||||
|
|
||||||
|
if compiledAlloc >= legacyAlloc {
|
||||||
|
t.Errorf("PathWhitelist allocated %d/op, want fewer than legacy %d/op", compiledAlloc, legacyAlloc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLegacyWhitelistMatch(b *testing.B) {
|
||||||
|
for b.Loop() {
|
||||||
|
_ = legacyMatch(whitelistBenchPatterns, "/api/v1/uploads/9/file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPathWhitelistMatch(b *testing.B) {
|
||||||
|
wl := extpoints.NewPathWhitelist(whitelistBenchPatterns...)
|
||||||
|
b.ResetTimer()
|
||||||
|
for b.Loop() {
|
||||||
|
_ = wl.Match("/api/v1/uploads/9/file")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,33 +13,22 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// whitelist holds the no-auth route patterns. They are registered during Apply and
|
||||||
whitelistMu sync.RWMutex
|
// matched on every request, so PathWhitelist parses them once up front.
|
||||||
whitelist []string
|
var whitelist = extpoints.NewPathWhitelist()
|
||||||
)
|
|
||||||
|
|
||||||
// RegisterWhitelist registers route patterns that bypass mandatory authentication.
|
// RegisterWhitelist registers route patterns that bypass mandatory authentication.
|
||||||
func RegisterWhitelist(patterns ...string) {
|
func RegisterWhitelist(patterns ...string) {
|
||||||
whitelistMu.Lock()
|
whitelist.Add(patterns...)
|
||||||
defer whitelistMu.Unlock()
|
|
||||||
whitelist = append(whitelist, patterns...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsWhitelisted checks if the specified path matches the auth whitelist.
|
// IsWhitelisted checks if the specified path matches the auth whitelist.
|
||||||
func IsWhitelisted(path string) bool {
|
func IsWhitelisted(path string) bool {
|
||||||
whitelistMu.RLock()
|
return whitelist.Match(path)
|
||||||
defer whitelistMu.RUnlock()
|
|
||||||
for _, pattern := range whitelist {
|
|
||||||
if extpoints.MatchPathPattern(pattern, path) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func hashToken(token string) string {
|
func hashToken(token string) string {
|
||||||
|
|||||||
@@ -25,29 +25,20 @@ import (
|
|||||||
var (
|
var (
|
||||||
apiPrefixMu sync.RWMutex
|
apiPrefixMu sync.RWMutex
|
||||||
apiPrefix = "/api/v1"
|
apiPrefix = "/api/v1"
|
||||||
|
|
||||||
whitelistMu sync.RWMutex
|
|
||||||
whitelistPatterns []string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// whitelist holds the global no-auth HTTP patterns. They are set once at
|
||||||
|
// configuration time and matched per request, so PathWhitelist parses them up front.
|
||||||
|
var whitelist = extpoints.NewPathWhitelist()
|
||||||
|
|
||||||
// SetWhitelist configures global whitelist patterns for HTTP routes.
|
// SetWhitelist configures global whitelist patterns for HTTP routes.
|
||||||
func SetWhitelist(patterns []string) {
|
func SetWhitelist(patterns []string) {
|
||||||
whitelistMu.Lock()
|
whitelist.Replace(patterns...)
|
||||||
defer whitelistMu.Unlock()
|
|
||||||
whitelistPatterns = make([]string, len(patterns))
|
|
||||||
copy(whitelistPatterns, patterns)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsPathWhitelisted checks if the given path matches any registered whitelist pattern.
|
// IsPathWhitelisted checks if the given path matches any registered whitelist pattern.
|
||||||
func IsPathWhitelisted(path string) bool {
|
func IsPathWhitelisted(path string) bool {
|
||||||
whitelistMu.RLock()
|
return whitelist.Match(path)
|
||||||
defer whitelistMu.RUnlock()
|
|
||||||
for _, pattern := range whitelistPatterns {
|
|
||||||
if extpoints.MatchPathPattern(pattern, path) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setAPIPrefix(prefix string) {
|
func setAPIPrefix(prefix string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user