feat(安全): 添加安全开关并完善测试流程

实现安全开关默认关闭与响应头透传逻辑
- URL 校验与响应头过滤支持开关并覆盖流式路径
- 非流式 Content-Type 透传/默认值按配置生效
- 接入 go test、golangci-lint 与前端 lint/typecheck
- 补充相关测试与配置/文档说明
This commit is contained in:
yangjianbo
2026-01-05 13:54:43 +08:00
parent c8e5455df0
commit 794a9f969b
24 changed files with 1811 additions and 14 deletions

View File

@@ -42,6 +42,9 @@ var hopByHopHeaders = map[string]struct{}{
}
func FilterHeaders(src http.Header, cfg config.ResponseHeaderConfig) http.Header {
if !cfg.Enabled {
return passThroughHeaders(src)
}
allowed := make(map[string]struct{}, len(defaultAllowed)+len(cfg.AdditionalAllowed))
for key := range defaultAllowed {
allowed[key] = struct{}{}
@@ -91,3 +94,17 @@ func WriteFilteredHeaders(dst http.Header, src http.Header, cfg config.ResponseH
}
}
}
func passThroughHeaders(src http.Header) http.Header {
filtered := make(http.Header, len(src))
for key, values := range src {
lower := strings.ToLower(key)
if _, isHopByHop := hopByHopHeaders[lower]; isHopByHop {
continue
}
for _, value := range values {
filtered.Add(key, value)
}
}
return filtered
}

View File

@@ -0,0 +1,67 @@
package responseheaders
import (
"net/http"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
)
func TestFilterHeadersDisabledPassThrough(t *testing.T) {
src := http.Header{}
src.Add("Content-Type", "application/json")
src.Add("X-Test", "ok")
src.Add("X-Remove", "keep")
src.Add("Connection", "keep-alive")
src.Add("Content-Length", "123")
cfg := config.ResponseHeaderConfig{
Enabled: false,
ForceRemove: []string{"x-test"},
}
filtered := FilterHeaders(src, cfg)
if filtered.Get("Content-Type") != "application/json" {
t.Fatalf("expected Content-Type passthrough, got %q", filtered.Get("Content-Type"))
}
if filtered.Get("X-Test") != "ok" {
t.Fatalf("expected X-Test passthrough, got %q", filtered.Get("X-Test"))
}
if filtered.Get("X-Remove") != "keep" {
t.Fatalf("expected X-Remove passthrough, got %q", filtered.Get("X-Remove"))
}
if filtered.Get("Connection") != "" {
t.Fatalf("expected Connection to be removed, got %q", filtered.Get("Connection"))
}
if filtered.Get("Content-Length") != "" {
t.Fatalf("expected Content-Length to be removed, got %q", filtered.Get("Content-Length"))
}
}
func TestFilterHeadersEnabledUsesAllowlist(t *testing.T) {
src := http.Header{}
src.Add("Content-Type", "application/json")
src.Add("X-Extra", "ok")
src.Add("X-Remove", "nope")
src.Add("X-Blocked", "nope")
cfg := config.ResponseHeaderConfig{
Enabled: true,
AdditionalAllowed: []string{"x-extra"},
ForceRemove: []string{"x-remove"},
}
filtered := FilterHeaders(src, cfg)
if filtered.Get("Content-Type") != "application/json" {
t.Fatalf("expected Content-Type allowed, got %q", filtered.Get("Content-Type"))
}
if filtered.Get("X-Extra") != "ok" {
t.Fatalf("expected X-Extra allowed, got %q", filtered.Get("X-Extra"))
}
if filtered.Get("X-Remove") != "" {
t.Fatalf("expected X-Remove removed, got %q", filtered.Get("X-Remove"))
}
if filtered.Get("X-Blocked") != "" {
t.Fatalf("expected X-Blocked removed, got %q", filtered.Get("X-Blocked"))
}
}