2026-03-08 20:19:40 +08:00
|
|
|
|
package starnet
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-08 21:38:45 +08:00
|
|
|
|
"context"
|
2026-03-08 20:19:40 +08:00
|
|
|
|
"crypto/tls"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
|
"testing"
|
2026-03-08 21:38:45 +08:00
|
|
|
|
"time"
|
2026-03-08 20:19:40 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-08 21:38:45 +08:00
|
|
|
|
|
|
|
|
|
|
func TestRequestTls(t *testing.T) {
|
2026-04-19 15:39:51 +08:00
|
|
|
|
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()
|
2026-03-08 21:38:45 +08:00
|
|
|
|
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())
|
2026-04-19 15:39:51 +08:00
|
|
|
|
|
2026-03-08 21:38:45 +08:00
|
|
|
|
client, err := NewClient()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("NewClient() error: %v", err)
|
|
|
|
|
|
}
|
2026-04-19 15:39:51 +08:00
|
|
|
|
resp, err = client.NewSimpleRequest(localURL, "GET",
|
|
|
|
|
|
WithTLSConfig(&tls.Config{RootCAs: pool}),
|
2026-03-08 21:38:45 +08:00
|
|
|
|
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) {
|
2026-04-19 15:39:51 +08:00
|
|
|
|
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()
|
|
|
|
|
|
|
2026-03-08 21:38:45 +08:00
|
|
|
|
client, err := NewClient()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-19 15:39:51 +08:00
|
|
|
|
req, err := client.NewRequest(httpsURLForHost(t, server, "localhost"), "GET",
|
2026-03-08 21:38:45 +08:00
|
|
|
|
WithTimeout(10*time.Second),
|
2026-04-19 15:39:51 +08:00
|
|
|
|
WithProxy(proxy.URL),
|
|
|
|
|
|
WithTLSConfig(&tls.Config{RootCAs: pool}),
|
2026-03-08 21:38:45 +08:00
|
|
|
|
)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := req.Do()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("Do error: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
defer resp.Close()
|
2026-04-19 15:39:51 +08:00
|
|
|
|
if targets := proxy.Targets(); len(targets) != 1 {
|
|
|
|
|
|
t.Fatalf("proxy targets=%v; want 1 target", targets)
|
|
|
|
|
|
}
|
2026-03-08 21:38:45 +08:00
|
|
|
|
t.Log(resp.Status)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestTLSWithProxyBug(t *testing.T) {
|
2026-04-19 15:39:51 +08:00
|
|
|
|
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()
|
|
|
|
|
|
|
2026-03-08 21:38:45 +08:00
|
|
|
|
client, err := NewClient()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关键:使用 WithProxy 触发 needsDynamicTransport
|
|
|
|
|
|
// 即使 proxy 是空串或无效地址,只要设置了就会走 buildDynamicTransport 分支
|
2026-04-19 15:39:51 +08:00
|
|
|
|
req, err := client.NewRequest(httpsURLForHost(t, server, "proxy-bug.test"), "GET",
|
2026-03-08 21:38:45 +08:00
|
|
|
|
WithTimeout(10*time.Second),
|
2026-04-19 15:39:51 +08:00
|
|
|
|
WithProxy(proxy.URL),
|
|
|
|
|
|
WithCustomIP([]string{"127.0.0.1"}),
|
|
|
|
|
|
WithTLSConfig(&tls.Config{RootCAs: pool}),
|
2026-03-08 21:38:45 +08:00
|
|
|
|
)
|
|
|
|
|
|
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()
|
2026-04-19 15:39:51 +08:00
|
|
|
|
if targets := proxy.Targets(); len(targets) != 1 || targets[0] == "" {
|
|
|
|
|
|
t.Fatalf("proxy targets=%v", targets)
|
|
|
|
|
|
}
|
2026-03-08 21:38:45 +08:00
|
|
|
|
t.Logf("Status: %s", resp.Status)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更精准的复现:直接测试有问题的分支
|
|
|
|
|
|
func TestTLSDialWithoutServerName(t *testing.T) {
|
2026-04-19 15:39:51 +08:00
|
|
|
|
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()
|
|
|
|
|
|
|
2026-03-08 21:38:45 +08:00
|
|
|
|
client, err := NewClient()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 WithCustomIP 也能触发 defaultDialTLSFunc
|
2026-04-19 15:39:51 +08:00
|
|
|
|
req, err := client.NewRequest(httpsURLForHost(t, server, "custom-ip.test"), "GET",
|
2026-03-08 21:38:45 +08:00
|
|
|
|
WithTimeout(10*time.Second),
|
2026-04-19 15:39:51 +08:00
|
|
|
|
WithCustomIP([]string{"127.0.0.1"}),
|
|
|
|
|
|
WithTLSConfig(&tls.Config{RootCAs: pool}),
|
2026-03-08 21:38:45 +08:00
|
|
|
|
)
|
|
|
|
|
|
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) {
|
2026-04-19 15:39:51 +08:00
|
|
|
|
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()
|
|
|
|
|
|
|
2026-03-08 21:38:45 +08:00
|
|
|
|
client, err := NewClient()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// WithDialTimeout 也会触发动态 transport
|
2026-04-19 15:39:51 +08:00
|
|
|
|
req, err := client.NewRequest(httpsURLForHost(t, server, "localhost"), "GET",
|
2026-03-08 21:38:45 +08:00
|
|
|
|
WithDialTimeout(5*time.Second),
|
2026-04-19 15:39:51 +08:00
|
|
|
|
WithTLSConfig(&tls.Config{RootCAs: pool}),
|
2026-03-08 21:38:45 +08:00
|
|
|
|
)
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
2026-04-19 15:39:51 +08:00
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|