Files
x/handler/http/metadata.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

153 lines
5.8 KiB
Go

package http
import (
"crypto"
"crypto/tls"
"crypto/x509"
"net/http"
"strings"
"time"
"github.com/go-gost/core/bypass"
mdata "github.com/go-gost/core/metadata"
mdutil "github.com/go-gost/x/metadata/util"
"github.com/go-gost/x/registry"
)
const (
defaultRealm = "gost"
defaultProxyAgent = "gost/3.0"
)
// metadata holds all configuration parsed from the handler's metadata map.
// Fields are populated by parseMetadata and read throughout the handler's
// request-processing methods.
type metadata struct {
readTimeout time.Duration // read timeout for upstream responses; default 15s, negative disables
idleTimeout time.Duration // idle timeout for pipe forwarding; 0 means disabled
keepalive bool // enable HTTP keep-alive on the upstream transport
compression bool // enable HTTP compression on the upstream transport
probeResistance *probeResistance // decoy response config for auth failures (nil = disabled)
enableUDP bool // allow UDP relay over HTTP
udpBufferSize int // UDP relay buffer size in bytes
header http.Header // additional response headers added to every proxy response
hash string // hash strategy for hop selection ("host" or empty)
authBasicRealm string // custom realm for Basic auth 407 responses
proxyAgent string // Proxy-Agent header value; default "gost/3.0"
observerPeriod time.Duration // stats reporting interval; default 5s, min 1s
observerResetTraffic bool // reset traffic counters after each observation
sniffing bool // enable protocol sniffing on CONNECT tunnels
sniffingTimeout time.Duration // deadline for the initial sniff read
sniffingWebsocket bool // enable WebSocket frame recording
sniffingWebsocketSampleRate float64 // max frames recorded per second
certificate *x509.Certificate // MITM CA certificate (for TLS decryption)
privateKey crypto.PrivateKey // MITM CA private key
alpn string // ALPN protocol to negotiate during MITM
mitmBypass bypass.Bypass // bypass rules that skip MITM decryption
limiterRefreshInterval time.Duration // traffic limiter cache refresh interval
limiterCleanupInterval time.Duration // traffic limiter cache cleanup interval
}
// parseMetadata reads all configuration values from the metadata map into
// the handler's metadata struct. It applies defaults for readTimeout (15s),
// observerPeriod (5s, min 1s), and proxyAgent ("gost/3.0"). MITM TLS is
// enabled when both mitm.certFile and mitm.keyFile are provided.
func (h *httpHandler) parseMetadata(md mdata.Metadata) error {
h.md.readTimeout = mdutil.GetDuration(md, "readTimeout")
if h.md.readTimeout == 0 {
h.md.readTimeout = 15 * time.Second
}
if h.md.readTimeout < 0 {
h.md.readTimeout = 0
}
h.md.idleTimeout = mdutil.GetDuration(md, "idleTimeout")
if h.md.idleTimeout < 0 {
h.md.idleTimeout = 0
}
if m := mdutil.GetStringMapString(md, "http.header", "header"); len(m) > 0 {
hd := http.Header{}
for k, v := range m {
hd.Add(k, v)
}
h.md.header = hd
}
h.md.keepalive = mdutil.GetBool(md, "http.keepalive", "keepalive")
h.md.compression = mdutil.GetBool(md, "http.compression", "compression")
// probeResist format: "type:value" (e.g. "code:404", "web:example.com",
// "host:192.168.1.1:80", "file:/var/www/index.html").
if pr := mdutil.GetString(md, "probeResist", "probe_resist"); pr != "" {
if ss := strings.SplitN(pr, ":", 2); len(ss) == 2 {
h.md.probeResistance = &probeResistance{
Type: ss[0],
Value: ss[1],
Knock: mdutil.GetString(md, "knock"),
}
}
}
h.md.enableUDP = mdutil.GetBool(md, "udp")
h.md.udpBufferSize = mdutil.GetInt(md, "udp.bufferSize", "udpBufferSize")
h.md.hash = mdutil.GetString(md, "hash")
h.md.authBasicRealm = mdutil.GetString(md, "authBasicRealm")
h.md.observerPeriod = mdutil.GetDuration(md, "observePeriod", "observer.period", "observer.observePeriod")
if h.md.observerPeriod == 0 {
h.md.observerPeriod = 5 * time.Second
}
if h.md.observerPeriod < time.Second {
h.md.observerPeriod = time.Second
}
h.md.observerResetTraffic = mdutil.GetBool(md, "observer.resetTraffic")
h.md.proxyAgent = mdutil.GetString(md, "http.proxyAgent", "proxyAgent")
if h.md.proxyAgent == "" {
h.md.proxyAgent = defaultProxyAgent
}
h.md.sniffing = mdutil.GetBool(md, "sniffing")
h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout")
h.md.sniffingWebsocket = mdutil.GetBool(md, "sniffing.websocket")
h.md.sniffingWebsocketSampleRate = mdutil.GetFloat(md, "sniffing.websocket.sampleRate")
certFile := mdutil.GetString(md, "mitm.certFile", "mitm.caCertFile")
keyFile := mdutil.GetString(md, "mitm.keyFile", "mitm.caKeyFile")
if certFile != "" && keyFile != "" {
tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return err
}
h.md.certificate, err = x509.ParseCertificate(tlsCert.Certificate[0])
if err != nil {
return err
}
h.md.privateKey = tlsCert.PrivateKey
}
h.md.alpn = mdutil.GetString(md, "mitm.alpn")
h.md.mitmBypass = registry.BypassRegistry().Get(mdutil.GetString(md, "mitm.bypass"))
h.md.limiterRefreshInterval = mdutil.GetDuration(md, "limiter.refreshInterval")
h.md.limiterCleanupInterval = mdutil.GetDuration(md, "limiter.cleanupInterval")
return nil
}
// probeResistance configures a decoy response that hides the proxy from
// unauthorised clients. When authentication fails, instead of returning
// 407 Proxy-Auth-Required, the handler returns a fake response that makes
// the port appear to run a different service.
type probeResistance struct {
Type string // strategy: "code", "web", "host", or "file"
Value string // strategy-specific parameter (status code, URL, address, path)
Knock string // optional hostname; when set, probe resistance only fires for non-matching hosts
}