package httpc import ( "errors" "net" "net/http" "net/url" "reflect" "strconv" "strings" "time" "git.yuntuo.hk/k2safe/go-omni/internal/configdiag" ) // ProxyMode controls the owned transport's proxy selection. Empty preserves // DefaultTransport's policy (normally environment proxies). type ProxyMode string const ( ProxyEnvironment ProxyMode = "environment" ProxyDirect ProxyMode = "direct" ProxyURL ProxyMode = "url" ) type ProxyConfig struct { Mode ProxyMode `json:"mode"` URL string `json:"url"` // May contain credentials; never log the configuration. } // Config owns one private connection pool unless Transport is supplied. // New duration/pool fields are optional: zero retains existing defaults, positive // values override them. Use a custom Transport for disabling timeouts, custom // dialers, mTLS and other protocol-specific settings. type Config struct { Timeout time.Duration `json:"timeout"` MaxRetries int `json:"maxRetries"` Backoff time.Duration `json:"backoff"` MaxBackoff time.Duration `json:"maxBackoff"` MaxConnsPerHost int `json:"maxConnsPerHost"` MaxIdleConns int `json:"maxIdleConns"` MaxIdleConnsPerHost int `json:"maxIdleConnsPerHost"` DialTimeout time.Duration `json:"dialTimeout"` DialKeepAlive time.Duration `json:"dialKeepAlive"` TLSHandshakeTimeout time.Duration `json:"tlsHandshakeTimeout"` ResponseHeaderTimeout time.Duration `json:"responseHeaderTimeout"` ExpectContinueTimeout time.Duration `json:"expectContinueTimeout"` IdleConnTimeout time.Duration `json:"idleConnTimeout"` Proxy ProxyConfig `json:"proxy"` MaxResponseBytes int64 `json:"maxResponseBytes"` DisableRequestID bool `json:"disableRequestId"` DisableLogging bool `json:"disableLogging"` Transport http.RoundTripper `json:"-"` // Borrowed; never modified or closed. } const maxResponseBytes int64 = 1 << 30 // Validate checks values without creating pools or making network requests. func (cfg Config) Validate() error { _, err := normalizeConfig(cfg); return err } func normalizeConfig(cfg Config) (Config, error) { for _, field := range []struct { name string value time.Duration }{ {"timeout", cfg.Timeout}, {"backoff", cfg.Backoff}, {"maxBackoff", cfg.MaxBackoff}, {"dialTimeout", cfg.DialTimeout}, {"dialKeepAlive", cfg.DialKeepAlive}, {"tlsHandshakeTimeout", cfg.TLSHandshakeTimeout}, {"responseHeaderTimeout", cfg.ResponseHeaderTimeout}, {"expectContinueTimeout", cfg.ExpectContinueTimeout}, {"idleConnTimeout", cfg.IdleConnTimeout}, } { if field.value < 0 { return Config{}, configdiag.New(field.name, "must be nonnegative") } } if cfg.MaxRetries < 0 || cfg.MaxRetries > 5 { return Config{}, configdiag.New("maxRetries", "must be between 0 and 5") } for _, f := range []struct { name string value, max int }{{"maxConnsPerHost", cfg.MaxConnsPerHost, 65536}, {"maxIdleConnsPerHost", cfg.MaxIdleConnsPerHost, 65536}, {"maxIdleConns", cfg.MaxIdleConns, 131072}} { if f.value < 0 || f.value > f.max { return Config{}, configdiag.New(f.name, "must be between 0 and "+strconv.Itoa(f.max)) } } if cfg.MaxResponseBytes < 0 || cfg.MaxResponseBytes > maxResponseBytes { return Config{}, configdiag.New("maxResponseBytes", "must be between 0 and 1 GiB") } if _, err := proxyFunction(cfg.Proxy); err != nil { return Config{}, err } if cfg.Transport != nil { value := reflect.ValueOf(cfg.Transport) switch value.Kind() { case reflect.Pointer, reflect.Map, reflect.Func, reflect.Slice, reflect.Interface, reflect.Chan: if value.IsNil() { return Config{}, configdiag.New("transport", "must not be a typed nil") } } if cfg.hasTransportOptions() { return Config{}, configdiag.New("transport", "cannot be combined with proxy, dial, TLS or connection pool options; configure the borrowed transport directly") } } if cfg.Timeout == 0 { cfg.Timeout = 10 * time.Second } if cfg.Backoff == 0 { cfg.Backoff = 100 * time.Millisecond } if cfg.MaxBackoff == 0 { cfg.MaxBackoff = 2 * time.Second } if cfg.Backoff > cfg.MaxBackoff { return Config{}, configdiag.New("backoff", "must not exceed maxBackoff") } if cfg.MaxConnsPerHost == 0 { cfg.MaxConnsPerHost = 64 } if cfg.MaxIdleConnsPerHost == 0 { cfg.MaxIdleConnsPerHost = cfg.MaxConnsPerHost } if cfg.MaxIdleConns == 0 { cfg.MaxIdleConns = cfg.MaxConnsPerHost * 2 } if cfg.MaxResponseBytes == 0 { cfg.MaxResponseBytes = 1 << 20 } return cfg, nil } func (c Config) hasTransportOptions() bool { return c.Proxy.Mode != "" || c.Proxy.URL != "" || c.MaxConnsPerHost != 0 || c.MaxIdleConns != 0 || c.MaxIdleConnsPerHost != 0 || c.DialTimeout != 0 || c.DialKeepAlive != 0 || c.TLSHandshakeTimeout != 0 || c.ResponseHeaderTimeout != 0 || c.ExpectContinueTimeout != 0 || c.IdleConnTimeout != 0 } func proxyFunction(c ProxyConfig) (func(*http.Request) (*url.URL, error), error) { if c.Mode != ProxyURL && c.URL != "" { return nil, configdiag.New("proxy.mode", "must be url when a URL is configured") } switch c.Mode { case "": return nil, nil case ProxyDirect: return nil, nil case ProxyEnvironment: return http.ProxyFromEnvironment, nil case ProxyURL: u, err := url.Parse(c.URL) if err != nil || u == nil || u.Hostname() == "" || u.Opaque != "" || u.RawQuery != "" || u.ForceQuery || u.Fragment != "" || (u.Path != "" && u.Path != "/") { return nil, configdiag.New("proxy.url", "must be an absolute proxy origin without path, query or fragment") } switch u.Scheme { case "http", "https", "socks5", "socks5h": default: return nil, configdiag.New("proxy.url", "scheme must be http, https, socks5 or socks5h") } if strings.HasSuffix(u.Host, ":") { return nil, configdiag.New("proxy.url", "port must be between 1 and 65535") } if port := u.Port(); port != "" { n, e := strconv.Atoi(port) if e != nil || n < 1 || n > 65535 { return nil, configdiag.New("proxy.url", "port must be between 1 and 65535") } } u.Path = "" u.RawPath = "" return http.ProxyURL(u), nil default: return nil, configdiag.New("proxy.mode", "must be environment, direct or url") } } func ownedTransport(cfg Config) (*http.Transport, error) { base, ok := http.DefaultTransport.(*http.Transport) if !ok || base == nil { return nil, errors.New("httpc: provide Transport when DefaultTransport is replaced") } if (base.DialTLSContext != nil || base.DialTLS != nil) && (cfg.DialTimeout > 0 || cfg.DialKeepAlive > 0 || cfg.TLSHandshakeTimeout > 0) { return nil, errors.New("httpc: provide Transport when DefaultTransport has a custom TLS dialer") } t := base.Clone() t.MaxConnsPerHost = cfg.MaxConnsPerHost t.MaxIdleConnsPerHost = cfg.MaxIdleConnsPerHost t.MaxIdleConns = cfg.MaxIdleConns if cfg.Proxy.Mode != "" { proxy, err := proxyFunction(cfg.Proxy) if err != nil { return nil, err } t.Proxy = proxy } if cfg.DialTimeout > 0 || cfg.DialKeepAlive > 0 { dialer := &net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second} if cfg.DialTimeout > 0 { dialer.Timeout = cfg.DialTimeout } if cfg.DialKeepAlive > 0 { dialer.KeepAlive = cfg.DialKeepAlive } t.DialContext = dialer.DialContext t.Dial = nil } if cfg.TLSHandshakeTimeout > 0 { t.TLSHandshakeTimeout = cfg.TLSHandshakeTimeout } if cfg.ResponseHeaderTimeout > 0 { t.ResponseHeaderTimeout = cfg.ResponseHeaderTimeout } if cfg.ExpectContinueTimeout > 0 { t.ExpectContinueTimeout = cfg.ExpectContinueTimeout } if cfg.IdleConnTimeout > 0 { t.IdleConnTimeout = cfg.IdleConnTimeout } return t, nil }