Files
x/handler/http/util_test.go
T
ginuerzh 3246b39c93 refactor(handler/http): split handler monolith into 7 focused files with 96 unit tests
Extract the 1024-line handler.go into concern-focused files:
  util.go       — 5 pure functions (decodeServerName, basicProxyAuth, upgradeType,
                  normalizeHostPort, buildConnectResponse) — 100% coverage
  auth.go       — authenticate (5 probe-resistance strategies + knock) + checkRateLimit
  connect.go    — handleConnect, sniffAndHandle, dial, setupTrafficLimiter
  proxy.go      — handleProxy keep-alive loop, proxyRoundTrip, handleUpgradeResponse
  websocket.go  — WebSocket frame sniffing/recording with rate-limited sampling
  udp.go        — UDP-over-HTTP relay with SOCKS5 tunnel
  metadata.go   — metadata struct, parseMetadata, probeResistance type

Key improvements:
- Transport injection (http.RoundTripper field) enables testing without real network
- Nil Router guards in dial, handleUDP, and nil Addr guard in checkRateLimit
- Pure functions converted from methods to package-level (no state dependency)
- setupTrafficLimiter returns cleanup closure to preserve defer lifetime
- handleConnect accepts caller's resp for correct recorder status capture
- Comprehensive package doc with full request-flow documentation

96 tests: 100% on pure functions, 86% on metadata parsing, 55% overall (integration
paths verified by e2e tests).
2026-05-28 23:06:37 +08:00

342 lines
7.5 KiB
Go

package http
import (
"encoding/base64"
"encoding/binary"
"hash/crc32"
"net/http"
"testing"
)
func TestDecodeServerName(t *testing.T) {
// Helper: encode a hostname using the GOST v2 encoding scheme
encodeName := func(name string) string {
v := []byte(name)
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, crc32.ChecksumIEEE(v))
inner := base64.RawURLEncoding.EncodeToString(v)
return base64.RawURLEncoding.EncodeToString(append(b, []byte(inner)...))
}
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "valid hostname",
input: encodeName("example.com:8080"),
want: "example.com:8080",
},
{
name: "valid IP address",
input: encodeName("1.2.3.4:443"),
want: "1.2.3.4:443",
},
{
name: "empty string",
input: "",
wantErr: true,
},
{
name: "invalid base64",
input: "!!!not-valid-base64!!!",
wantErr: true,
},
{
name: "too short (less than 4 bytes)",
input: base64.RawURLEncoding.EncodeToString([]byte("ab")),
wantErr: true,
},
{
name: "valid base64 but inner is garbage",
input: base64.RawURLEncoding.EncodeToString([]byte("0000!!!!")),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := decodeServerName(tt.input)
if tt.wantErr {
if err == nil {
t.Errorf("decodeServerName(%q) = (%q, nil), want error", tt.input, got)
}
return
}
if err != nil {
t.Errorf("decodeServerName(%q) error = %v", tt.input, err)
return
}
if got != tt.want {
t.Errorf("decodeServerName(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestDecodeServerName_CRC32Mismatch(t *testing.T) {
name := "test.example.com"
v := []byte(name)
// Use wrong CRC32
badCRC := crc32.ChecksumIEEE(v) ^ 0xFFFFFFFF
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, badCRC)
inner := base64.RawURLEncoding.EncodeToString(v)
s := base64.RawURLEncoding.EncodeToString(append(b, []byte(inner)...))
_, err := decodeServerName(s)
if err == nil {
t.Error("expected error for CRC32 mismatch")
}
}
func TestBasicProxyAuth(t *testing.T) {
encode := func(u, p string) string {
return "Basic " + base64.StdEncoding.EncodeToString([]byte(u+":"+p))
}
tests := []struct {
name string
proxyAuth string
wantUser string
wantPass string
wantOK bool
}{
{
name: "valid basic auth",
proxyAuth: encode("admin", "secret"),
wantUser: "admin",
wantPass: "secret",
wantOK: true,
},
{
name: "username only, no password",
proxyAuth: "Basic " + base64.StdEncoding.EncodeToString([]byte("user:")),
wantUser: "user",
wantPass: "",
wantOK: true,
},
{
name: "password only, no username",
proxyAuth: "Basic " + base64.StdEncoding.EncodeToString([]byte(":pass")),
wantUser: "",
wantPass: "pass",
wantOK: true,
},
{
name: "password contains colon",
proxyAuth: "Basic " + base64.StdEncoding.EncodeToString([]byte("user:a:b:c")),
wantUser: "user",
wantPass: "a:b:c",
wantOK: true,
},
{
name: "empty string",
proxyAuth: "",
},
{
name: "not Basic scheme",
proxyAuth: "Bearer token123",
},
{
name: "invalid base64",
proxyAuth: "Basic not-valid-base64!",
},
{
name: "no colon separator",
proxyAuth: "Basic " + base64.StdEncoding.EncodeToString([]byte("username")),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
user, pass, ok := basicProxyAuth(tt.proxyAuth)
if ok != tt.wantOK {
t.Errorf("basicProxyAuth(%q) ok = %v, want %v", tt.proxyAuth, ok, tt.wantOK)
return
}
if ok {
if user != tt.wantUser {
t.Errorf("basicProxyAuth(%q) user = %q, want %q", tt.proxyAuth, user, tt.wantUser)
}
if pass != tt.wantPass {
t.Errorf("basicProxyAuth(%q) pass = %q, want %q", tt.proxyAuth, pass, tt.wantPass)
}
}
})
}
}
func TestUpgradeType(t *testing.T) {
tests := []struct {
name string
header http.Header
want string
}{
{
name: "nil header",
header: nil,
want: "",
},
{
name: "websocket upgrade",
header: http.Header{
"Connection": {"Upgrade"},
"Upgrade": {"websocket"},
},
want: "websocket",
},
{
name: "http2 upgrade",
header: http.Header{
"Connection": {"Upgrade"},
"Upgrade": {"h2c"},
},
want: "h2c",
},
{
name: "no connection upgrade header",
header: http.Header{
"Upgrade": {"websocket"},
},
want: "",
},
{
name: "empty header",
header: http.Header{},
want: "",
},
{
name: "connection has upgrade but no upgrade header",
header: http.Header{
"Connection": {"Upgrade"},
},
want: "",
},
{
name: "connection upgrade in multi-value",
header: http.Header{
"Connection": {"keep-alive", "Upgrade"},
"Upgrade": {"websocket"},
},
want: "websocket",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := upgradeType(tt.header); got != tt.want {
t.Errorf("upgradeType() = %q, want %q", got, tt.want)
}
})
}
}
func TestNormalizeHostPort(t *testing.T) {
tests := []struct {
name string
host string
defaultPort string
want string
}{
{
name: "hostname without port",
host: "example.com",
defaultPort: "80",
want: "example.com:80",
},
{
name: "hostname with port",
host: "example.com:8080",
defaultPort: "80",
want: "example.com:8080",
},
{
name: "IPv4 without port",
host: "1.2.3.4",
defaultPort: "80",
want: "1.2.3.4:80",
},
{
name: "IPv4 with port",
host: "1.2.3.4:443",
defaultPort: "80",
want: "1.2.3.4:443",
},
{
name: "IPv6 without port (bracketed)",
host: "[::1]",
defaultPort: "80",
want: "[::1]:80",
},
{
name: "IPv6 with port",
host: "[::1]:443",
defaultPort: "80",
want: "[::1]:443",
},
{
name: "empty string",
host: "",
defaultPort: "80",
want: ":80",
},
{
name: "custom default port",
host: "example.com",
defaultPort: "443",
want: "example.com:443",
},
{
name: "bare IPv6 without brackets or port",
host: "::1",
defaultPort: "80",
want: "[::1]:80", // net.JoinHostPort wraps IPv6 in brackets
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := normalizeHostPort(tt.host, tt.defaultPort); got != tt.want {
t.Errorf("normalizeHostPort(%q, %q) = %q, want %q", tt.host, tt.defaultPort, got, tt.want)
}
})
}
}
func TestBuildConnectResponse(t *testing.T) {
got := buildConnectResponse("gost/3.0")
if len(got) == 0 {
t.Error("buildConnectResponse returned empty")
}
s := string(got)
if !contains(s, "200") {
t.Errorf("expected 200 status in response: %s", s)
}
if !contains(s, "Connection established") {
t.Errorf("expected Connection established in response: %s", s)
}
if !contains(s, "gost/3.0") {
t.Errorf("expected proxy agent in response: %s", s)
}
}
func TestBuildConnectResponse_CustomAgent(t *testing.T) {
got := buildConnectResponse("custom-agent/1.0")
s := string(got)
if !contains(s, "custom-agent/1.0") {
t.Errorf("expected custom agent in response: %s", s)
}
}
func contains(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}