Files
go-omni/httpc/config.go

213 lines
7.6 KiB
Go

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
}