starnet/tls_test.go
starainrt 732e81316c
fix(starnet): 重构请求执行链路并补齐代理/重试/trace边界
- 分离 Request 的配置态与执行态,修复二次 Do、raw 模式网络配置失效和 body 来源互斥问题
  - 新增 starnet trace 抽象,补齐 DNS/连接/TLS/重试事件,并优化动态 transport 缓存与代理解析路径
  - 收紧非法代理为 fail-fast,多目标目标回退仅限幂等请求,修复 Host/TLS/SNI 等语义边界
  - 补充防御性拷贝、专项回归测试、本地代理/TLS 用例与 README 行为说明
2026-04-19 15:39:51 +08:00

337 lines
8.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 starnet
import (
"context"
"crypto/tls"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestRequestSkipTLSVerify(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}))
defer server.Close()
// Without skip verify (should fail)
req := NewSimpleRequest(server.URL, "GET")
_, err := req.Do()
if err == nil {
t.Error("Expected TLS error without skip verify, got nil")
}
// With skip verify (should succeed)
req2 := NewSimpleRequest(server.URL, "GET").SetSkipTLSVerify(true)
resp, err := req2.Do()
if err != nil {
t.Fatalf("Do() with skip verify error: %v", err)
}
defer resp.Close()
body, _ := resp.Body().String()
if body != "OK" {
t.Errorf("Body = %v; want OK", body)
}
}
func TestRequestCustomTLSConfig(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}))
defer server.Close()
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
}
req := NewSimpleRequest(server.URL, "GET").SetTLSConfig(tlsConfig)
resp, err := req.Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
defer resp.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("StatusCode = %v; want %v", resp.StatusCode, http.StatusOK)
}
}
func TestClientDefaultTLSConfig(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
client := NewClientNoErr()
client.SetDefaultSkipTLSVerify(true)
resp, err := client.Get(server.URL)
if err != nil {
t.Fatalf("Get() error: %v", err)
}
defer resp.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("StatusCode = %v; want %v", resp.StatusCode, http.StatusOK)
}
}
func TestRequestLevelTLSOverride(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
// Client level: skip verify = false
client := NewClientNoErr()
client.SetDefaultSkipTLSVerify(false)
// Request level: skip verify = true (should override)
resp, err := client.Get(server.URL, WithSkipTLSVerify(true))
if err != nil {
t.Fatalf("Get() error: %v", err)
}
defer resp.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("StatusCode = %v; want %v", resp.StatusCode, http.StatusOK)
}
}
func TestRequestTls(t *testing.T) {
var requestCount int
server, pool := newTrustedIPv4TLSServer(t, "localhost", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestCount++
switch requestCount {
case 1:
if r.Header.Get("Hello") != "" {
t.Fatalf("unexpected hello header on first request: %q", r.Header.Get("Hello"))
}
if auth := r.Header.Get("Authorization"); auth != "" {
t.Fatalf("unexpected authorization on first request: %q", auth)
}
case 2:
if got := r.Header.Get("Hello"); got != "world" {
t.Fatalf("hello header=%q; want world", got)
}
if got := r.Header.Get("Authorization"); got != "Bearer ddddddd" {
t.Fatalf("authorization=%q; want bearer token", got)
}
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
}))
defer server.Close()
localURL := httpsURLForHost(t, server, "localhost")
resp, err := NewSimpleRequest(localURL, "GET").
SetTLSConfig(&tls.Config{RootCAs: pool}).
Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
defer resp.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("StatusCode = %v; want %v", resp.StatusCode, http.StatusOK)
}
t.Logf("Response: %v", resp.Body().MustString())
client, err := NewClient()
if err != nil {
t.Fatalf("NewClient() error: %v", err)
}
resp, err = client.NewSimpleRequest(localURL, "GET",
WithTLSConfig(&tls.Config{RootCAs: pool}),
WithHeader("hello", "world"),
WithContext(context.Background()),
WithBearerToken("ddddddd")).Do()
if err != nil {
t.Fatalf("Do() error: %v", err)
}
defer resp.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("StatusCode = %v; want %v", resp.StatusCode, http.StatusOK)
}
t.Logf("Response: %v", resp.Body().MustString())
}
func TestTLSWithProxyPath(t *testing.T) {
server, pool := newTrustedIPv4TLSServer(t, "localhost", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("proxied"))
}))
defer server.Close()
proxy := newIPv4ConnectProxyServer(t, nil)
defer proxy.Close()
client, err := NewClient()
if err != nil {
t.Fatal(err)
}
req, err := client.NewRequest(httpsURLForHost(t, server, "localhost"), "GET",
WithTimeout(10*time.Second),
WithProxy(proxy.URL),
WithTLSConfig(&tls.Config{RootCAs: pool}),
)
if err != nil {
t.Fatal(err)
}
resp, err := req.Do()
if err != nil {
t.Fatalf("Do error: %v", err)
}
defer resp.Close()
if targets := proxy.Targets(); len(targets) != 1 {
t.Fatalf("proxy targets=%v; want 1 target", targets)
}
t.Log(resp.Status)
}
func TestTLSWithProxyBug(t *testing.T) {
server, pool := newTrustedIPv4TLSServer(t, "proxy-bug.test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}))
defer server.Close()
proxy := newIPv4ConnectProxyServer(t, nil)
defer proxy.Close()
client, err := NewClient()
if err != nil {
t.Fatal(err)
}
// 关键:使用 WithProxy 触发 needsDynamicTransport
// 即使 proxy 是空串或无效地址,只要设置了就会走 buildDynamicTransport 分支
req, err := client.NewRequest(httpsURLForHost(t, server, "proxy-bug.test"), "GET",
WithTimeout(10*time.Second),
WithProxy(proxy.URL),
WithCustomIP([]string{"127.0.0.1"}),
WithTLSConfig(&tls.Config{RootCAs: pool}),
)
if err != nil {
t.Fatal(err)
}
resp, err := req.Do()
if err != nil {
// 修复前会报tls: either ServerName or InsecureSkipVerify must be specified
t.Fatalf("Do error: %v", err)
}
defer resp.Close()
if targets := proxy.Targets(); len(targets) != 1 || targets[0] == "" {
t.Fatalf("proxy targets=%v", targets)
}
t.Logf("Status: %s", resp.Status)
}
// 更精准的复现:直接测试有问题的分支
func TestTLSDialWithoutServerName(t *testing.T) {
server, pool := newTrustedIPv4TLSServer(t, "custom-ip.test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}))
defer server.Close()
client, err := NewClient()
if err != nil {
t.Fatal(err)
}
// 使用 WithCustomIP 也能触发 defaultDialTLSFunc
req, err := client.NewRequest(httpsURLForHost(t, server, "custom-ip.test"), "GET",
WithTimeout(10*time.Second),
WithCustomIP([]string{"127.0.0.1"}),
WithTLSConfig(&tls.Config{RootCAs: pool}),
)
if err != nil {
t.Fatal(err)
}
resp, err := req.Do()
if err != nil {
t.Fatalf("Do error: %v", err)
}
defer resp.Close()
t.Logf("Status: %s", resp.Status)
}
// 最小复现:只要触发 needsDynamicTransport 即可
func TestMinimalTLSBug(t *testing.T) {
server, pool := newTrustedIPv4TLSServer(t, "localhost", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}))
defer server.Close()
client, err := NewClient()
if err != nil {
t.Fatal(err)
}
// WithDialTimeout 也会触发动态 transport
req, err := client.NewRequest(httpsURLForHost(t, server, "localhost"), "GET",
WithDialTimeout(5*time.Second),
WithTLSConfig(&tls.Config{RootCAs: pool}),
)
if err != nil {
t.Fatal(err)
}
resp, err := req.Do()
if err != nil {
// 修复前必现tls handshake: tls: either ServerName or InsecureSkipVerify must be specified
t.Fatalf("Do error: %v", err)
}
defer resp.Close()
t.Logf("Status: %s", resp.Status)
}
func TestTLSWithSOCKS5ProxyPath(t *testing.T) {
server, pool := newTrustedIPv4TLSServer(t, "socks5-proxy.test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}))
defer server.Close()
proxy := newSOCKS5ProxyServer(t, nil)
defer proxy.Close()
client, err := NewClient()
if err != nil {
t.Fatal(err)
}
req, err := client.NewRequest(httpsURLForHost(t, server, "socks5-proxy.test"), "GET",
WithTimeout(10*time.Second),
WithProxy(proxy.URL()),
WithCustomIP([]string{"127.0.0.1"}),
WithTLSConfig(&tls.Config{RootCAs: pool}),
)
if err != nil {
t.Fatal(err)
}
resp, err := req.Do()
if err != nil {
t.Fatalf("Do error: %v", err)
}
defer resp.Close()
if targets := proxy.Targets(); len(targets) != 1 || targets[0] == "" {
t.Fatalf("socks5 targets=%v", targets)
}
t.Logf("Status: %s", resp.Status)
}