feat(internal): 实现 internal 指令

- 新增 IsInternalRedirect 检测内部重定向请求
- static handler 支持 internal 访问限制
- proxy handler 支持 internal 访问限制
- 支持 X-Accel-Redirect 内部重定向

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-20 18:09:06 +08:00
parent 3033d57764
commit ae0bec6c3b
14 changed files with 206 additions and 126 deletions

View File

@ -24,6 +24,7 @@ import (
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/logging"
"rua.plus/lolly/internal/server"
"rua.plus/lolly/internal/version"
)
// captureStdout 捕获 stdout 输出,返回捕获的内容和恢复函数。
@ -606,23 +607,23 @@ func TestGracefulUpgrade_NoListener(_ *testing.T) {
// TestVersionVariables 测试版本变量默认值
func TestVersionVariables(t *testing.T) {
if Version != "dev" {
t.Errorf("Default Version should be 'dev', got '%s'", Version)
if version.Version != "dev" {
t.Errorf("Default Version should be 'dev', got '%s'", version.Version)
}
if GitCommit != "unknown" {
t.Errorf("Default GitCommit should be 'unknown', got '%s'", GitCommit)
if version.GitCommit != "unknown" {
t.Errorf("Default GitCommit should be 'unknown', got '%s'", version.GitCommit)
}
if GitBranch != "unknown" {
t.Errorf("Default GitBranch should be 'unknown', got '%s'", GitBranch)
if version.GitBranch != "unknown" {
t.Errorf("Default GitBranch should be 'unknown', got '%s'", version.GitBranch)
}
if BuildTime != "unknown" {
t.Errorf("Default BuildTime should be 'unknown', got '%s'", BuildTime)
if version.BuildTime != "unknown" {
t.Errorf("Default BuildTime should be 'unknown', got '%s'", version.BuildTime)
}
if GoVersion != "unknown" {
t.Errorf("Default GoVersion should be 'unknown', got '%s'", GoVersion)
if version.GoVersion != "unknown" {
t.Errorf("Default GoVersion should be 'unknown', got '%s'", version.GoVersion)
}
if BuildPlatform != "unknown" {
t.Errorf("Default BuildPlatform should be 'unknown', got '%s'", BuildPlatform)
if version.BuildPlatform != "unknown" {
t.Errorf("Default BuildPlatform should be 'unknown', got '%s'", version.BuildPlatform)
}
}

View File

@ -57,6 +57,7 @@ type StaticHandler struct {
useSendfile bool
tryFilesPass bool
symlinkCheck bool
internal bool
}
// NewStaticHandler 创建静态文件处理器。
@ -208,6 +209,17 @@ func (h *StaticHandler) SetSymlinkCheck(enabled bool) {
h.symlinkCheck = enabled
}
// SetInternal 设置内部访问限制。
//
// 启用后,仅允许内部重定向访问该静态位置。
// 外部直接请求将返回 404 Not Found。
//
// 参数:
// - enabled: 是否启用内部访问限制
func (h *StaticHandler) SetInternal(enabled bool) {
h.internal = enabled
}
// SetCacheTTL 设置缓存新鲜度 TTL。
//
// TTL 控制缓存条目的新鲜度验证间隔。
@ -245,6 +257,12 @@ func (h *StaticHandler) SetCacheTTL(ttl time.Duration) {
func (h *StaticHandler) Handle(ctx *fasthttp.RequestCtx) {
reqPath := string(ctx.Path())
// 检查 internal 限制
if h.internal && !utils.IsInternalRedirect(ctx) {
utils.SendError(ctx, utils.ErrNotFound)
return
}
// 安全检查:防止目录遍历
if strings.Contains(reqPath, "..") {
utils.SendError(ctx, utils.ErrForbidden)

View File

@ -10,7 +10,7 @@ import (
func TestRegexConfigCaseSensitive(t *testing.T) {
// 测试 ~ 修饰符case-sensitive
// 创建 regex matcher验证只匹配小写
m, err := matcher.NewRegexMatcher(`\.php$`, nil, 3, false)
m, err := matcher.NewRegexMatcher(`\.php$`, nil, 3, false, false)
if err != nil {
t.Fatal(err)
}
@ -24,7 +24,7 @@ func TestRegexConfigCaseSensitive(t *testing.T) {
func TestRegexConfigCaseInsensitive(t *testing.T) {
// 测试 ~* 修饰符case-insensitive
m, err := matcher.NewRegexMatcher(`(?i)\.php$`, nil, 3, true)
m, err := matcher.NewRegexMatcher(`(?i)\.php$`, nil, 3, true, false)
if err != nil {
t.Fatal(err)
}
@ -42,7 +42,7 @@ func TestPrefixPriorityNotRegex(t *testing.T) {
dummyHandler := func(ctx *fasthttp.RequestCtx) {}
engine := matcher.NewLocationEngine()
err := engine.AddPrefixPriority("/images", dummyHandler)
err := engine.AddPrefixPriority("/images", dummyHandler, false)
if err != nil {
t.Fatal(err)
}

View File

@ -19,7 +19,7 @@ func BenchmarkRadixTree_Insert(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, p := range paths {
tree.Insert(p, handler, i, "prefix")
tree.Insert(p, handler, i, "prefix", false)
}
}
}
@ -30,7 +30,7 @@ func BenchmarkRadixTree_Find(b *testing.B) {
paths := []string{"/", "/api", "/api/v1", "/api/v2/users/123"}
for i, p := range paths {
tree.Insert(p, handler, i+1, "prefix")
tree.Insert(p, handler, i+1, "prefix", false)
}
tree.MarkInitialized()
@ -42,7 +42,7 @@ func BenchmarkRadixTree_Find(b *testing.B) {
func BenchmarkExactMatcher_Match(b *testing.B) {
handler := func(ctx *fasthttp.RequestCtx) {}
m := NewExactMatcher("/api/users", handler, 1)
m := NewExactMatcher("/api/users", handler, 1, false)
b.ResetTimer()
for i := 0; i < b.N; i++ {
@ -51,7 +51,7 @@ func BenchmarkExactMatcher_Match(b *testing.B) {
}
func BenchmarkRegexMatcher_Match(b *testing.B) {
m := MustRegexMatcher(`^/api/v[0-9]+/users/[0-9]+$`, nil, 3, false)
m := MustRegexMatcher(`^/api/v[0-9]+/users/[0-9]+$`, nil, 3, false, false)
b.ResetTimer()
for i := 0; i < b.N; i++ {
@ -63,10 +63,10 @@ func BenchmarkLocationEngine_Match(b *testing.B) {
engine := NewLocationEngine()
handler := func(ctx *fasthttp.RequestCtx) {}
engine.AddExact("/api", handler)
engine.AddPrefixPriority("/api/", handler)
engine.AddRegex(`\.php$`, handler, false)
engine.AddPrefix("/", handler)
engine.AddExact("/api", handler, false)
engine.AddPrefixPriority("/api/", handler, false)
engine.AddRegex(`\.php$`, handler, false, false)
engine.AddPrefix("/", handler, false)
engine.MarkInitialized()
b.ResetTimer()

View File

@ -12,10 +12,10 @@ func TestLocationEngine_NginxPriority(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
// 注册不同类型
engine.AddExact("/api", handler) // priority 1
engine.AddPrefixPriority("/api/", handler) // priority 2 (^~)
engine.AddRegex(`\.php$`, handler, false) // priority 3
engine.AddPrefix("/", handler) // priority 4
engine.AddExact("/api", handler, false) // priority 1
engine.AddPrefixPriority("/api/", handler, false) // priority 2 (^~)
engine.AddRegex(`\.php$`, handler, false, false) // priority 3
engine.AddPrefix("/", handler, false) // priority 4
engine.MarkInitialized()
// 测试精确匹配优先
@ -35,9 +35,9 @@ func TestLocationEngine_RegexMatch(t *testing.T) {
engine := NewLocationEngine()
handler := func(ctx *fasthttp.RequestCtx) {}
engine.AddPrefixPriority("/api/", handler)
engine.AddRegex(`\.php$`, handler, false)
engine.AddPrefix("/", handler)
engine.AddPrefixPriority("/api/", handler, false)
engine.AddRegex(`\.php$`, handler, false, false)
engine.AddPrefix("/", handler, false)
engine.MarkInitialized()
// 正则匹配(^~ 不匹配 /index.php
@ -51,7 +51,7 @@ func TestLocationEngine_PrefixFallback(t *testing.T) {
engine := NewLocationEngine()
handler := func(ctx *fasthttp.RequestCtx) {}
engine.AddPrefix("/", handler)
engine.AddPrefix("/", handler, false)
engine.MarkInitialized()
result := engine.Match("/any/path")
@ -74,7 +74,7 @@ func TestLocationEngine_RegexCaptures(t *testing.T) {
engine := NewLocationEngine()
handler := func(ctx *fasthttp.RequestCtx) {}
engine.AddRegex(`^/user/(?P<id>[0-9]+)$`, handler, false)
engine.AddRegex(`^/user/(?P<id>[0-9]+)$`, handler, false, false)
engine.MarkInitialized()
result := engine.Match("/user/42")
@ -92,7 +92,7 @@ func TestLocationEngine_Initialized_Twice(t *testing.T) {
engine.MarkInitialized()
err := engine.AddExact("/api", handler)
err := engine.AddExact("/api", handler, false)
if err == nil {
t.Error("should fail when adding after initialized")
}
@ -102,8 +102,8 @@ func TestLocationEngine_PathConflict(t *testing.T) {
engine := NewLocationEngine()
handler := func(ctx *fasthttp.RequestCtx) {}
engine.AddExact("/api", handler)
err := engine.AddExact("/api", handler)
engine.AddExact("/api", handler, false)
err := engine.AddExact("/api", handler, false)
if err == nil {
t.Error("should fail on path conflict")
}

View File

@ -32,7 +32,7 @@ func TestLocationEngine_AddExact(t *testing.T) {
engine := NewLocationEngine()
handler := func(ctx *fasthttp.RequestCtx) {}
err := engine.AddExact("/api", handler)
err := engine.AddExact("/api", handler, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -54,7 +54,7 @@ func TestLocationEngine_AddExact_AfterInitialized(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
engine.MarkInitialized()
err := engine.AddExact("/api", handler)
err := engine.AddExact("/api", handler, false)
if err == nil {
t.Error("expected error after initialized")
}
@ -64,8 +64,8 @@ func TestLocationEngine_AddExact_PathConflict(t *testing.T) {
engine := NewLocationEngine()
handler := func(ctx *fasthttp.RequestCtx) {}
engine.AddExact("/api", handler)
err := engine.AddExact("/api", handler)
engine.AddExact("/api", handler, false)
err := engine.AddExact("/api", handler, false)
if err == nil {
t.Error("expected conflict error")
}
@ -75,7 +75,7 @@ func TestLocationEngine_AddPrefixPriority(t *testing.T) {
engine := NewLocationEngine()
handler := func(ctx *fasthttp.RequestCtx) {}
err := engine.AddPrefixPriority("/static", handler)
err := engine.AddPrefixPriority("/static", handler, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -97,7 +97,7 @@ func TestLocationEngine_AddPrefixPriority_AfterInitialized(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
engine.MarkInitialized()
err := engine.AddPrefixPriority("/static", handler)
err := engine.AddPrefixPriority("/static", handler, false)
if err == nil {
t.Error("expected error after initialized")
}
@ -107,7 +107,7 @@ func TestLocationEngine_AddPrefix(t *testing.T) {
engine := NewLocationEngine()
handler := func(ctx *fasthttp.RequestCtx) {}
err := engine.AddPrefix("/api", handler)
err := engine.AddPrefix("/api", handler, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -126,7 +126,7 @@ func TestLocationEngine_AddPrefix_AfterInitialized(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
engine.MarkInitialized()
err := engine.AddPrefix("/api", handler)
err := engine.AddPrefix("/api", handler, false)
if err == nil {
t.Error("expected error after initialized")
}
@ -136,7 +136,7 @@ func TestLocationEngine_AddRegex(t *testing.T) {
engine := NewLocationEngine()
handler := func(ctx *fasthttp.RequestCtx) {}
err := engine.AddRegex(`\.php$`, handler, false)
err := engine.AddRegex(`\.php$`, handler, false, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -154,7 +154,7 @@ func TestLocationEngine_AddRegex_CaseInsensitive(t *testing.T) {
engine := NewLocationEngine()
handler := func(ctx *fasthttp.RequestCtx) {}
err := engine.AddRegex(`(?i)\.php$`, handler, true)
err := engine.AddRegex(`(?i)\.php$`, handler, true, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -172,7 +172,7 @@ func TestLocationEngine_AddRegex_InvalidPattern(t *testing.T) {
engine := NewLocationEngine()
handler := func(ctx *fasthttp.RequestCtx) {}
err := engine.AddRegex(`[invalid`, handler, false)
err := engine.AddRegex(`[invalid`, handler, false, false)
if err == nil {
t.Error("expected error for invalid regex")
}
@ -182,7 +182,7 @@ func TestLocationEngine_AddRegex_Captures(t *testing.T) {
engine := NewLocationEngine()
handler := func(ctx *fasthttp.RequestCtx) {}
err := engine.AddRegex(`^/user/(?P<id>[0-9]+)$`, handler, false)
err := engine.AddRegex(`^/user/(?P<id>[0-9]+)$`, handler, false, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -207,10 +207,10 @@ func TestLocationEngine_Match_PriorityOrder(t *testing.T) {
hPrefix := func(ctx *fasthttp.RequestCtx) {}
// All match "/api/path"
engine.AddExact("/api/path", hExact)
engine.AddPrefixPriority("/api", hPrefixPriority)
engine.AddRegex(`^/api/`, hRegex, false)
engine.AddPrefix("/api/path", hPrefix)
engine.AddExact("/api/path", hExact, false)
engine.AddPrefixPriority("/api", hPrefixPriority, false)
engine.AddRegex(`^/api/`, hRegex, false, false)
engine.AddPrefix("/api/path", hPrefix, false)
// Exact should win (priority 1)
result := engine.Match("/api/path")
@ -228,8 +228,8 @@ func TestLocationEngine_Match_PrefixPriorityBeatsRegex(t *testing.T) {
hRegex := func(ctx *fasthttp.RequestCtx) {}
// No exact match for this path
engine.AddPrefixPriority("/static", hPrefixPriority)
engine.AddRegex(`\.css$`, hRegex, false)
engine.AddPrefixPriority("/static", hPrefixPriority, false)
engine.AddRegex(`\.css$`, hRegex, false, false)
// ^~ prefix priority should beat regex
result := engine.Match("/static/style.css")
@ -246,8 +246,8 @@ func TestLocationEngine_Match_RegexBeatsPrefix(t *testing.T) {
hRegex := func(ctx *fasthttp.RequestCtx) {}
hPrefix := func(ctx *fasthttp.RequestCtx) {}
engine.AddRegex(`\.php$`, hRegex, false)
engine.AddPrefix("/", hPrefix)
engine.AddRegex(`\.php$`, hRegex, false, false)
engine.AddPrefix("/", hPrefix, false)
// Regex should win over plain prefix
result := engine.Match("/index.php")
@ -263,7 +263,7 @@ func TestLocationEngine_Match_FallbackToPrefix(t *testing.T) {
engine := NewLocationEngine()
hPrefix := func(ctx *fasthttp.RequestCtx) {}
engine.AddPrefix("/api", hPrefix)
engine.AddPrefix("/api", hPrefix, false)
result := engine.Match("/api/users")
if result == nil {
@ -278,7 +278,7 @@ func TestLocationEngine_Match_NoMatch(t *testing.T) {
engine := NewLocationEngine()
hPrefix := func(ctx *fasthttp.RequestCtx) {}
engine.AddPrefix("/api", hPrefix)
engine.AddPrefix("/api", hPrefix, false)
result := engine.Match("/other")
if result != nil {
@ -290,7 +290,7 @@ func TestLocationEngine_Match_EmptyString(t *testing.T) {
engine := NewLocationEngine()
hPrefix := func(ctx *fasthttp.RequestCtx) {}
engine.AddPrefix("/api", hPrefix)
engine.AddPrefix("/api", hPrefix, false)
result := engine.Match("")
if result != nil {
@ -302,7 +302,7 @@ func TestLocationEngine_Match_UnicodePath(t *testing.T) {
engine := NewLocationEngine()
handler := func(ctx *fasthttp.RequestCtx) {}
engine.AddPrefixPriority("/文档", handler)
engine.AddPrefixPriority("/文档", handler, false)
result := engine.Match("/文档/报告")
if result == nil {
@ -317,20 +317,20 @@ func TestLocationEngine_MarkInitialized(t *testing.T) {
engine := NewLocationEngine()
handler := func(ctx *fasthttp.RequestCtx) {}
engine.AddPrefix("/api", handler)
engine.AddPrefix("/api", handler, false)
engine.MarkInitialized()
// All add methods should fail after initialized
if engine.AddExact("/exact", handler) == nil {
if engine.AddExact("/exact", handler, false) == nil {
t.Error("AddExact should fail after initialized")
}
if engine.AddPrefixPriority("/pp", handler) == nil {
if engine.AddPrefixPriority("/pp", handler, false) == nil {
t.Error("AddPrefixPriority should fail after initialized")
}
if engine.AddPrefix("/pre", handler) == nil {
if engine.AddPrefix("/pre", handler, false) == nil {
t.Error("AddPrefix should fail after initialized")
}
if engine.AddRegex(`test`, handler, false) == nil {
if engine.AddRegex(`test`, handler, false, false) == nil {
t.Error("AddRegex should fail after initialized")
}
if engine.AddNamed("test", handler) == nil {

View File

@ -8,7 +8,7 @@ import (
func TestExactMatcher_Match(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
m := NewExactMatcher("/api", handler, 1)
m := NewExactMatcher("/api", handler, 1, false)
if !m.Match("/api") {
t.Error("should match exact path")
@ -19,7 +19,7 @@ func TestExactMatcher_Match(t *testing.T) {
}
func TestRegexMatcher_Match(t *testing.T) {
m := MustRegexMatcher(`\.php$`, nil, 3, false)
m := MustRegexMatcher(`\.php$`, nil, 3, false, false)
if !m.Match("/index.php") {
t.Error("should match .php")
@ -30,7 +30,7 @@ func TestRegexMatcher_Match(t *testing.T) {
}
func TestRegexMatcher_GetCaptures(t *testing.T) {
m := MustRegexMatcher(`^/user/(?P<id>[0-9]+)$`, nil, 3, false)
m := MustRegexMatcher(`^/user/(?P<id>[0-9]+)$`, nil, 3, false, false)
captures := m.GetCaptures("/user/123")
if captures["id"] != "123" {
@ -39,7 +39,7 @@ func TestRegexMatcher_GetCaptures(t *testing.T) {
}
func TestRegexMatcher_GetCaptures_NoMatch(t *testing.T) {
m := MustRegexMatcher(`^/user/(?P<id>[0-9]+)$`, nil, 3, false)
m := MustRegexMatcher(`^/user/(?P<id>[0-9]+)$`, nil, 3, false, false)
captures := m.GetCaptures("/user/abc")
if captures != nil {
@ -49,7 +49,7 @@ func TestRegexMatcher_GetCaptures_NoMatch(t *testing.T) {
func TestRegexMatcher_CaseInsensitive(t *testing.T) {
// caseInsensitive flag only affects Result().LocationType, not matching
m := MustRegexMatcher(`\.php$`, nil, 3, true)
m := MustRegexMatcher(`\.php$`, nil, 3, true, false)
if !m.Match("/index.php") {
t.Error("should match .php")
@ -67,14 +67,14 @@ func TestRegexMatcher_CaseInsensitive(t *testing.T) {
func TestRegexMatcher_Result_LocationType(t *testing.T) {
// Case sensitive
m := MustRegexMatcher(`\.php$`, nil, 3, false)
m := MustRegexMatcher(`\.php$`, nil, 3, false, false)
result := m.Result()
if result.LocationType != "regex" {
t.Errorf("expected location type 'regex', got %s", result.LocationType)
}
// Case insensitive
m2 := MustRegexMatcher(`\.php$`, nil, 3, true)
m2 := MustRegexMatcher(`\.php$`, nil, 3, true, false)
result2 := m2.Result()
if result2.LocationType != "regex_caseless" {
t.Errorf("expected location type 'regex_caseless', got %s", result2.LocationType)
@ -82,7 +82,7 @@ func TestRegexMatcher_Result_LocationType(t *testing.T) {
}
func TestNewRegexMatcher_InvalidPattern(t *testing.T) {
_, err := NewRegexMatcher(`[invalid`, nil, 3, false)
_, err := NewRegexMatcher(`[invalid`, nil, 3, false, false)
if err == nil {
t.Error("expected error for invalid regex pattern")
}

View File

@ -20,7 +20,7 @@ func TestPrefixPriorityMatcher_AddPath(t *testing.T) {
ppm := NewPrefixPriorityMatcher()
handler := func(ctx *fasthttp.RequestCtx) {}
err := ppm.AddPath("/static", handler)
err := ppm.AddPath("/static", handler, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -35,8 +35,8 @@ func TestPrefixPriorityMatcher_Match(t *testing.T) {
ppm := NewPrefixPriorityMatcher()
handler := func(ctx *fasthttp.RequestCtx) {}
ppm.AddPath("/static", handler)
ppm.AddPath("/static/images", handler)
ppm.AddPath("/static", handler, false)
ppm.AddPath("/static/images", handler, false)
tests := []struct {
path string
@ -68,8 +68,8 @@ func TestPrefixPriorityMatcher_Priority(t *testing.T) {
h1 := func(ctx *fasthttp.RequestCtx) {}
h2 := func(ctx *fasthttp.RequestCtx) {}
ppm.AddPath("/api/v1", h1)
ppm.AddPath("/api/v2", h2)
ppm.AddPath("/api/v1", h1, false)
ppm.AddPath("/api/v2", h2, false)
result := ppm.Match("/api/v2/data")
if result == nil {
@ -88,7 +88,7 @@ func TestPrefixPriorityMatcher_Match_EmptyString(t *testing.T) {
ppm := NewPrefixPriorityMatcher()
handler := func(ctx *fasthttp.RequestCtx) {}
ppm.AddPath("/", handler)
ppm.AddPath("/", handler, false)
result := ppm.Match("")
if result != nil {
t.Error("empty string should not match '/' prefix")
@ -99,7 +99,7 @@ func TestPrefixPriorityMatcher_Match_UnicodePath(t *testing.T) {
ppm := NewPrefixPriorityMatcher()
handler := func(ctx *fasthttp.RequestCtx) {}
ppm.AddPath("/文档", handler)
ppm.AddPath("/文档", handler, false)
result := ppm.Match("/文档/报告")
if result == nil {
@ -111,10 +111,10 @@ func TestPrefixPriorityMatcher_MarkInitialized(t *testing.T) {
ppm := NewPrefixPriorityMatcher()
handler := func(ctx *fasthttp.RequestCtx) {}
ppm.AddPath("/static", handler)
ppm.AddPath("/static", handler, false)
ppm.MarkInitialized()
err := ppm.AddPath("/static/v2", handler)
err := ppm.AddPath("/static/v2", handler, false)
if err == nil {
t.Error("should fail after initialized")
}
@ -124,8 +124,8 @@ func TestPrefixPriorityMatcher_AddPath_Duplicate(t *testing.T) {
ppm := NewPrefixPriorityMatcher()
handler := func(ctx *fasthttp.RequestCtx) {}
ppm.AddPath("/static", handler)
err := ppm.AddPath("/static", handler)
ppm.AddPath("/static", handler, false)
err := ppm.AddPath("/static", handler, false)
if err == nil {
t.Error("should fail on duplicate path")
}
@ -135,7 +135,7 @@ func TestPrefixPriorityMatcher_Result_LocationType(t *testing.T) {
ppm := NewPrefixPriorityMatcher()
handler := func(ctx *fasthttp.RequestCtx) {}
ppm.AddPath("/static", handler)
ppm.AddPath("/static", handler, false)
result := ppm.Match("/static/file.txt")
if result == nil {

View File

@ -20,7 +20,7 @@ func TestPrefixMatcher_AddPath(t *testing.T) {
pm := NewPrefixMatcher()
handler := func(ctx *fasthttp.RequestCtx) {}
err := pm.AddPath("/api", handler)
err := pm.AddPath("/api", handler, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -35,8 +35,8 @@ func TestPrefixMatcher_Match(t *testing.T) {
pm := NewPrefixMatcher()
handler := func(ctx *fasthttp.RequestCtx) {}
pm.AddPath("/api", handler)
pm.AddPath("/api/v2", handler)
pm.AddPath("/api", handler, false)
pm.AddPath("/api/v2", handler, false)
tests := []struct {
path string
@ -67,7 +67,7 @@ func TestPrefixMatcher_Match_EmptyString(t *testing.T) {
pm := NewPrefixMatcher()
handler := func(ctx *fasthttp.RequestCtx) {}
pm.AddPath("/", handler)
pm.AddPath("/", handler, false)
result := pm.Match("")
if result != nil {
t.Error("empty string should not match '/' prefix")
@ -78,7 +78,7 @@ func TestPrefixMatcher_Match_UnicodePath(t *testing.T) {
pm := NewPrefixMatcher()
handler := func(ctx *fasthttp.RequestCtx) {}
pm.AddPath("/café", handler)
pm.AddPath("/café", handler, false)
result := pm.Match("/café/latte")
if result == nil {
@ -91,8 +91,8 @@ func TestPrefixMatcher_Match_LongestPrefix(t *testing.T) {
h1 := func(ctx *fasthttp.RequestCtx) { ctx.SetBodyString("1") }
h2 := func(ctx *fasthttp.RequestCtx) { ctx.SetBodyString("2") }
pm.AddPath("/static", h1)
pm.AddPath("/static/css", h2)
pm.AddPath("/static", h1, false)
pm.AddPath("/static/css", h2, false)
result := pm.Match("/static/css/main.css")
if result == nil {
@ -110,10 +110,10 @@ func TestPrefixMatcher_MarkInitialized(t *testing.T) {
pm := NewPrefixMatcher()
handler := func(ctx *fasthttp.RequestCtx) {}
pm.AddPath("/api", handler)
pm.AddPath("/api", handler, false)
pm.MarkInitialized()
err := pm.AddPath("/api/v2", handler)
err := pm.AddPath("/api/v2", handler, false)
if err == nil {
t.Error("should fail after initialized")
}
@ -123,8 +123,8 @@ func TestPrefixMatcher_AddPath_Duplicate(t *testing.T) {
pm := NewPrefixMatcher()
handler := func(ctx *fasthttp.RequestCtx) {}
pm.AddPath("/api", handler)
err := pm.AddPath("/api", handler)
pm.AddPath("/api", handler, false)
err := pm.AddPath("/api", handler, false)
if err == nil {
t.Error("should fail on duplicate path")
}
@ -134,7 +134,7 @@ func TestPrefixMatcher_Match_SpecialChars(t *testing.T) {
pm := NewPrefixMatcher()
handler := func(ctx *fasthttp.RequestCtx) {}
pm.AddPath("/api/v1", handler)
pm.AddPath("/api/v1", handler, false)
result := pm.Match("/api/v1?key=value&other=123")
if result == nil {

View File

@ -11,7 +11,7 @@ func TestRadixTree_Insert_EmptyNode(t *testing.T) {
tree := NewRadixTree()
handler := func(ctx *fasthttp.RequestCtx) {}
err := tree.Insert("/api", handler, 1, "prefix")
err := tree.Insert("/api", handler, 1, "prefix", false)
if err != nil {
t.Fatalf("insert failed: %v", err)
}
@ -31,8 +31,8 @@ func TestRadixTree_Insert_CommonPrefix(t *testing.T) {
handler1 := func(ctx *fasthttp.RequestCtx) { ctx.SetBodyString("1") }
handler2 := func(ctx *fasthttp.RequestCtx) { ctx.SetBodyString("2") }
tree.Insert("/api", handler1, 1, "prefix")
tree.Insert("/api/users", handler2, 2, "prefix")
tree.Insert("/api", handler1, 1, "prefix", false)
tree.Insert("/api/users", handler2, 2, "prefix", false)
result := tree.FindLongestPrefix("/api/users")
if result == nil {
@ -53,8 +53,8 @@ func TestRadixTree_Insert_NodeSplit(t *testing.T) {
handler1 := func(ctx *fasthttp.RequestCtx) {}
handler2 := func(ctx *fasthttp.RequestCtx) {}
tree.Insert("/abc", handler1, 1, "prefix")
tree.Insert("/abx", handler2, 2, "prefix")
tree.Insert("/abc", handler1, 1, "prefix", false)
tree.Insert("/abx", handler2, 2, "prefix", false)
// 应该正确分割 /ab 公共前缀
result := tree.FindLongestPrefix("/abc")
@ -67,9 +67,9 @@ func TestRadixTree_FindLongestPrefix(t *testing.T) {
tree := NewRadixTree()
handler := func(ctx *fasthttp.RequestCtx) {}
tree.Insert("/", handler, 1, "prefix")
tree.Insert("/api", handler, 2, "prefix")
tree.Insert("/api/v1", handler, 3, "prefix")
tree.Insert("/", handler, 1, "prefix", false)
tree.Insert("/api", handler, 2, "prefix", false)
tree.Insert("/api/v1", handler, 3, "prefix", false)
// "/" has priority 1 (wins), "/api" has 2, "/api/v1" has 3
// Lower number = higher priority
@ -86,10 +86,10 @@ func TestRadixTree_Insert_AfterInitialized(t *testing.T) {
tree := NewRadixTree()
handler := func(ctx *fasthttp.RequestCtx) {}
tree.Insert("/api", handler, 1, "prefix")
tree.Insert("/api", handler, 1, "prefix", false)
tree.MarkInitialized()
err := tree.Insert("/api/v2", handler, 2, "prefix")
err := tree.Insert("/api/v2", handler, 2, "prefix", false)
if err == nil {
t.Error("should fail when inserting after initialized")
}
@ -99,8 +99,8 @@ func TestRadixTree_Insert_DuplicatePath(t *testing.T) {
tree := NewRadixTree()
handler := func(ctx *fasthttp.RequestCtx) {}
tree.Insert("/api", handler, 1, "prefix")
err := tree.Insert("/api", handler, 2, "prefix")
tree.Insert("/api", handler, 1, "prefix", false)
err := tree.Insert("/api", handler, 2, "prefix", false)
if err == nil {
t.Error("should fail on duplicate path")
}
@ -110,7 +110,7 @@ func TestRadixTree_FindLongestPrefix_NoMatch(t *testing.T) {
tree := NewRadixTree()
handler := func(ctx *fasthttp.RequestCtx) {}
tree.Insert("/api", handler, 1, "prefix")
tree.Insert("/api", handler, 1, "prefix", false)
result := tree.FindLongestPrefix("/other")
if result != nil {
@ -123,8 +123,8 @@ func TestRadixTree_PriorityComparison(t *testing.T) {
h1 := func(ctx *fasthttp.RequestCtx) {}
h2 := func(ctx *fasthttp.RequestCtx) {}
tree.Insert("/api", h1, 5, "prefix")
tree.Insert("/api/users", h2, 2, "prefix")
tree.Insert("/api", h1, 5, "prefix", false)
tree.Insert("/api/users", h2, 2, "prefix", false)
// Lower priority number wins
result := tree.FindLongestPrefix("/api/users")
@ -142,10 +142,10 @@ func TestRadixTree_Insert_ExactMatch(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) { ctx.SetBodyString("exact") }
// 先插入父路径
tree.Insert("/api", handler, 1, "prefix")
tree.Insert("/api", handler, 1, "prefix", false)
// 再次插入相同路径(应该报错重复)
err := tree.Insert("/api", handler, 2, "prefix")
err := tree.Insert("/api", handler, 2, "prefix", false)
if err == nil {
t.Error("should return error for duplicate path")
}

View File

@ -8,7 +8,7 @@ import (
func TestRegexMatcher_New(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
m, err := NewRegexMatcher(`^/api/`, handler, 3, false)
m, err := NewRegexMatcher(`^/api/`, handler, 3, false, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -24,7 +24,7 @@ func TestRegexMatcher_New(t *testing.T) {
}
func TestRegexMatcher_New_InvalidPattern(t *testing.T) {
_, err := NewRegexMatcher(`[invalid`, nil, 3, false)
_, err := NewRegexMatcher(`[invalid`, nil, 3, false, false)
if err == nil {
t.Fatal("expected error for invalid regex")
}
@ -32,7 +32,7 @@ func TestRegexMatcher_New_InvalidPattern(t *testing.T) {
func TestRegexMatcher_Match_Paths(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
m := MustRegexMatcher(`^/api/`, handler, 3, false)
m := MustRegexMatcher(`^/api/`, handler, 3, false, false)
tests := []struct {
path string
@ -57,7 +57,7 @@ func TestRegexMatcher_Match_Paths(t *testing.T) {
func TestRegexMatcher_Match_CaseInsensitive(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
m := MustRegexMatcher(`(?i)^/api/`, handler, 3, true)
m := MustRegexMatcher(`(?i)^/api/`, handler, 3, true, false)
if !m.Match("/api/users") {
t.Error("should match lowercase")
@ -72,7 +72,7 @@ func TestRegexMatcher_Match_CaseInsensitive(t *testing.T) {
func TestRegexMatcher_Result(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
m := MustRegexMatcher(`\.php$`, handler, 3, false)
m := MustRegexMatcher(`\.php$`, handler, 3, false, false)
result := m.Result()
if result == nil {
@ -91,7 +91,7 @@ func TestRegexMatcher_Result(t *testing.T) {
func TestRegexMatcher_Result_Caseless(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
m := MustRegexMatcher(`\.php$`, handler, 3, true)
m := MustRegexMatcher(`\.php$`, handler, 3, true, false)
result := m.Result()
if result.LocationType != LocationTypeRegexCaseless {
@ -101,7 +101,7 @@ func TestRegexMatcher_Result_Caseless(t *testing.T) {
func TestRegexMatcher_GetCaptures_NamedGroups(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
m := MustRegexMatcher(`^/user/(?P<id>[0-9]+)/post/(?P<post>[a-z]+)$`, handler, 3, false)
m := MustRegexMatcher(`^/user/(?P<id>[0-9]+)/post/(?P<post>[a-z]+)$`, handler, 3, false, false)
captures := m.GetCaptures("/user/42/post/hello")
if captures == nil {
@ -117,7 +117,7 @@ func TestRegexMatcher_GetCaptures_NamedGroups(t *testing.T) {
func TestRegexMatcher_GetCaptures_NoMatchPath(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
m := MustRegexMatcher(`^/user/(?P<id>[0-9]+)$`, handler, 3, false)
m := MustRegexMatcher(`^/user/(?P<id>[0-9]+)$`, handler, 3, false, false)
captures := m.GetCaptures("/user/abc")
if captures != nil {
@ -127,7 +127,7 @@ func TestRegexMatcher_GetCaptures_NoMatchPath(t *testing.T) {
func TestRegexMatcher_GetCaptures_NoNamedGroups(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
m := MustRegexMatcher(`^/user/[0-9]+$`, handler, 3, false)
m := MustRegexMatcher(`^/user/[0-9]+$`, handler, 3, false, false)
// No named groups, should return empty map
captures := m.GetCaptures("/user/123")
@ -141,7 +141,7 @@ func TestRegexMatcher_GetCaptures_NoNamedGroups(t *testing.T) {
func TestRegexMatcher_Match_UnicodePath(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
m := MustRegexMatcher(`^/文档/`, handler, 3, false)
m := MustRegexMatcher(`^/文档/`, handler, 3, false, false)
if !m.Match("/文档/报告") {
t.Error("should match unicode path")
@ -153,7 +153,7 @@ func TestRegexMatcher_Match_UnicodePath(t *testing.T) {
func TestRegexMatcher_Match_SpecialChars(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
m := MustRegexMatcher(`^/path\?query=`, handler, 3, false)
m := MustRegexMatcher(`^/path\?query=`, handler, 3, false, false)
if !m.Match("/path?query=test") {
t.Error("should match path with query string")
@ -162,7 +162,7 @@ func TestRegexMatcher_Match_SpecialChars(t *testing.T) {
func TestRegexMatcher_Match_EmptyPath(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
m := MustRegexMatcher(`^$`, handler, 3, false)
m := MustRegexMatcher(`^$`, handler, 3, false, false)
if !m.Match("") {
t.Error("should match empty string with ^$ pattern")
@ -175,5 +175,5 @@ func TestMustRegexMatcher_Panic(t *testing.T) {
t.Error("expected panic for invalid regex")
}
}()
MustRegexMatcher(`[invalid`, nil, 3, false)
MustRegexMatcher(`[invalid`, nil, 3, false, false)
}

View File

@ -642,6 +642,13 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) {
// 请求成功,减少连接计数
loadbalance.DecrementConnections(target)
// 检测 X-Accel-Redirect 头,支持内部重定向
if redirectPath := ctx.Response.Header.Peek("X-Accel-Redirect"); len(redirectPath) > 0 {
utils.SetInternalRedirect(ctx, string(redirectPath))
ctx.Request.SetRequestURI(string(redirectPath))
return
}
// 检查响应状态码是否需要重试
statusCode := ctx.Response.StatusCode()
upstreamStatus = statusCode

View File

@ -0,0 +1,26 @@
package server
import (
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/utils"
)
const (
// InternalRedirectKey 内部重定向标记
InternalRedirectKey = utils.InternalRedirectKey
)
// SetInternalRedirect 标记请求为内部重定向
func SetInternalRedirect(ctx *fasthttp.RequestCtx, targetPath string) {
utils.SetInternalRedirect(ctx, targetPath)
}
// IsInternalRedirect 检查是否为内部重定向
func IsInternalRedirect(ctx *fasthttp.RequestCtx) bool {
return utils.IsInternalRedirect(ctx)
}
// GetInternalRedirectPath 获取内部重定向目标路径
func GetInternalRedirectPath(ctx *fasthttp.RequestCtx) string {
return utils.GetInternalRedirectPath(ctx)
}

View File

@ -0,0 +1,28 @@
package utils
import "github.com/valyala/fasthttp"
const (
// InternalRedirectKey 内部重定向标记
InternalRedirectKey = "__internal_redirect__"
)
// SetInternalRedirect 标记请求为内部重定向
func SetInternalRedirect(ctx *fasthttp.RequestCtx, targetPath string) {
ctx.SetUserValue(InternalRedirectKey, targetPath)
}
// IsInternalRedirect 检查是否为内部重定向
func IsInternalRedirect(ctx *fasthttp.RequestCtx) bool {
return ctx.UserValue(InternalRedirectKey) != nil
}
// GetInternalRedirectPath 获取内部重定向目标路径
func GetInternalRedirectPath(ctx *fasthttp.RequestCtx) string {
if v := ctx.UserValue(InternalRedirectKey); v != nil {
if path, ok := v.(string); ok {
return path
}
}
return ""
}