Files
xlgo-core/utils/http_test.go
T
杭州明婳科技 6595e94677 fix: 多模型评审终审收口 — P0安全 + 主线A并发治理 + 文档对齐
经 deepseek/GLM/Claude 三轮对抗性评审 + 交叉核验 + 终审(见
last_report.md、cross_review_assessment.md),逐条回读源码核实并修复。
go build / vet / test -race ./... 全绿。

安全(P0):
- JWT alg confusion:WithValidMethods 固定 HMAC + keyfunc 方法断言
- JWT 空密钥 / 不支持算法 fail-closed(ErrEmptySecret / ErrUnsupportedAlgorithm)
- 上传大小实测封顶(enforceUploadSize),不信任客户端 file.Size
- HTTP headers/cookies map 竞态(写锁+快照)+ SSRF 防护(opt-in dialer.Control)

并发/资源(主线A 闭合):
- 包级可变全局一律 atomic.Pointer:DefaultRedis / DefaultStorage /
  DefaultManager / Validator / DefaultCache 等,消除裸指针数据竞争
- trace.Close 去 sync.Once(防 exporter 泄漏);app OnReady 失败走 Shutdown
- ws.Hub.Stop stopOnce 防 double-close;cron 接入 App 生命周期(WithCron)
- ratelimit failClosed atomic.Bool;Recover Written() 守卫
- cron checkAndRun 锁内收集锁外 spawn(wg.Add 在锁内防 Stop 竞态)

终审剩余项收口(H-A~M-H + L 系列):
- 副本连接池 MaxOpenConns/2 截断修复(replicaMaxOpenConns)
- JWT 黑名单 1s 超时 + 显式错误;ratelimit GetRedis 取一次复用
- GetPage 深分页上限;HashFile 流式;ReadFile 去 TOCTOU
- config Clone() 深拷贝;cache 锁操作返 ErrRedisNotReady
- compress 解压残留清理;正则预编译;EqualsIgnoreCase→EqualFold
- 删 redisLimiters 死代码;CLI 输入校验 + 回滚

文档:
- README/GUIDE/CHANGELOG 对齐当前 API(含破坏性变更迁移说明)
- docs/README 索引修正;config 注释修正(RS256 已不支持)

破坏性变更详见 CHANGELOG.md [Unreleased]。项目处于研发初期、无下游用户。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:16:03 +08:00

249 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package utils_test
import (
"crypto/tls"
"errors"
"net/http"
"net/http/httptest"
"sync"
"testing"
"github.com/EthanCodeCraft/xlgo-core/utils"
)
// 回归 H2:默认 HTTPClient 必须 校验 TLSInsecureSkipVerify=false),
// 访问自签证书的 https server 应失败。旧实现 DefaultHTTPClientConfig.SkipTLSVerify=true
// HTTPGet/Post 默认可被 MITM。
func TestHTTPClientDefaultVerifiesTLS(t *testing.T) {
// 启动一个使用自签证书的 TLS server(httptest 自动生成证书,未被客户端信任)。
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
defer srv.Close()
// 默认客户端(H2 修复后 SkipTLSVerify=false)应因证书校验失败而报错。
c := utils.NewHTTPClient()
if _, err := c.Get(srv.URL, nil); err == nil {
t.Error("default HTTPClient should fail TLS verification against self-signed cert (H2: was InsecureSkipVerify=true)")
}
}
// 回归 H2HTTPGet 包级函数(经 DefaultHTTPClient)默认同样校验 TLS。
func TestHTTPGetDefaultVerifiesTLS(t *testing.T) {
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
defer srv.Close()
if _, err := utils.HTTPGet(srv.URL, nil); err == nil {
t.Error("HTTPGet should fail TLS verification against self-signed cert by default")
}
}
// 回归 H2:显式 SetSkipTLS(true) 后可访问自签证书 serveropt-in 跳过校验)。
func TestHTTPClientSkipTLSOptIn(t *testing.T) {
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
defer srv.Close()
c := utils.NewHTTPClient()
c.SetSkipTLS(true)
body, err := c.Get(srv.URL, nil)
if err != nil {
t.Fatalf("Get with SkipTLS=true should succeed against self-signed cert, got: %v", err)
}
if string(body) != "ok" {
t.Errorf("body = %q, want ok", string(body))
}
}
// 回归 H2NewHTTPClientWithConfig 显式设 SkipTLSVerify=true 可跳过校验。
func TestHTTPClientWithConfigSkipTLS(t *testing.T) {
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
defer srv.Close()
c := utils.NewHTTPClientWithConfig(utils.HTTPClientConfig{
SkipTLSVerify: true,
})
body, err := c.Get(srv.URL, nil)
if err != nil {
t.Fatalf("Get with config SkipTLSVerify=true should succeed, got: %v", err)
}
if string(body) != "ok" {
t.Errorf("body = %q, want ok", string(body))
}
}
// 回归 H2DefaultHTTPClientConfig.SkipTLSVerify 默认 false。
func TestDefaultHTTPClientConfigNoSkipTLS(t *testing.T) {
if utils.DefaultHTTPClientConfig.SkipTLSVerify {
t.Error("DefaultHTTPClientConfig.SkipTLSVerify = true, want false (H2: default must verify TLS)")
}
}
// 回归 H2:默认 transport 的 TLSClientConfig.InsecureSkipVerify 为 false。
// 直接构造一个对 https 的请求,确认未跳过校验。用 errors 判断是否为 x509 校验类错误。
func TestDefaultClientTransportVerifiesTLS(t *testing.T) {
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
defer srv.Close()
c := utils.NewHTTPClient()
// 用自定义 client 配自定义 transport 不可行(封装),直接调 Get 验证错误类型。
_, err := c.Get(srv.URL, nil)
if err == nil {
t.Fatal("expected TLS verification error, got nil")
}
// 错误应包含证书校验相关提示(如 x509 / certificate / tls)。
if !isTLSError(err) {
t.Logf("err = %v (not strictly x509 classified, but default did reject — acceptable)", err)
}
}
// isTLSError 粗略判断错误是否为 TLS 证书校验失败。
func isTLSError(err error) bool {
if err == nil {
return false
}
var ve *tls.CertificateVerificationError
return errors.As(err, &ve)
}
// TestHTTPClientSetSkipTLSConcurrent H-12 回归:并发 SetSkipTLS 与 Get 请求不应触发
// 数据竞争,且 SetSkipTLS(true) 后能成功访问自签 TLS server。
// 修复前 SetSkipTLS 直接覆盖 transport.TLSClientConfig 指针,与并发 Do 读该字段竞态。
func TestHTTPClientSetSkipTLSConcurrent(t *testing.T) {
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
defer srv.Close()
c := utils.NewHTTPClient()
defer c.Close()
var wg sync.WaitGroup
// 并发切换 TLS 策略
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
c.SetSkipTLS(i%2 == 0)
}(i)
}
// 并发发请求(自签 server,仅 skip=true 时成功,skip=false 时 TLS 失败,均不应 panic/竞态)
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, _ = c.Get(srv.URL, nil)
}()
}
wg.Wait()
// 最终切到 skip=true,应能成功访问自签 server(验证 SetSkipTLS 重建后功能正常)
c.SetSkipTLS(true)
if _, err := c.Get(srv.URL, nil); err != nil {
t.Errorf("after SetSkipTLS(true), GET should succeed against self-signed server, got %v", err)
}
}
// ===== P0headers/cookies map 并发安全 =====
// 回归 P0:并发 SetHeader/SetCookie 与 Get 请求不应触发数据竞争(-race)。
// 修复前 Set* 无锁写 map、do 无锁读 map,并发即竞态可 panic。
func TestHTTPClientConcurrentHeadersNoRace(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
defer srv.Close()
c := utils.NewHTTPClient()
defer c.Close()
var wg sync.WaitGroup
for i := 0; i < 20; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
c.SetHeader("X-Req", string(rune('A'+i%26)))
c.SetCookie("sid", string(rune('0'+i%10)))
c.SetHeaders(map[string]string{"X-Batch": "v"})
}(i)
}
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, _ = c.Get(srv.URL, nil)
}()
}
wg.Wait()
}
// ===== P0SSRF 防护 =====
// 回归 P0:启用 SSRF 防护后,连接回环地址(httptest server 监听 127.0.0.1)必须被拒绝,
// 返回 ErrSSRFBlocked。
func TestHTTPClientSSRFBlocksLoopback(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("secret-internal"))
}))
defer srv.Close()
c := utils.NewSSRFSafeHTTPClient()
defer c.Close()
_, err := c.Get(srv.URL, nil)
if err == nil {
t.Fatal("SSRF-safe client should refuse to connect to loopback address")
}
if !errors.Is(err, utils.ErrSSRFBlocked) {
t.Errorf("err = %v, want ErrSSRFBlocked", err)
}
}
// 回归 P0:未启用 SSRF 防护时(默认),回环连接照常放行(兼容性回归)。
func TestHTTPClientSSRFDisabledAllowsLoopback(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
defer srv.Close()
c := utils.NewHTTPClient() // 默认不启用 SSRF 防护
defer c.Close()
body, err := c.Get(srv.URL, nil)
if err != nil {
t.Fatalf("default client should allow loopback, got %v", err)
}
if string(body) != "ok" {
t.Errorf("body = %q, want ok", string(body))
}
}
// 回归 P0SetBlockPrivateNetworks 运行期开关生效——开启后回环被拒。
func TestHTTPClientSetBlockPrivateNetworks(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
defer srv.Close()
c := utils.NewHTTPClient()
defer c.Close()
// 开启前放行
if _, err := c.Get(srv.URL, nil); err != nil {
t.Fatalf("before enabling guard, GET should succeed, got %v", err)
}
// 开启后拒绝
c.SetBlockPrivateNetworks(true)
if _, err := c.Get(srv.URL, nil); !errors.Is(err, utils.ErrSSRFBlocked) {
t.Errorf("after enabling guard, err = %v, want ErrSSRFBlocked", err)
}
}