206 lines
7.4 KiB
Go
206 lines
7.4 KiB
Go
package httpc
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// 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{}, fmt.Errorf("httpc: %s must not be negative", field.name)
|
|
}
|
|
}
|
|
if cfg.MaxRetries < 0 || cfg.MaxRetries > 5 {
|
|
return Config{}, errors.New("httpc: maxRetries must be 0..5")
|
|
}
|
|
if cfg.MaxConnsPerHost < 0 || cfg.MaxConnsPerHost > 65536 || cfg.MaxIdleConnsPerHost < 0 || cfg.MaxIdleConnsPerHost > 65536 || cfg.MaxIdleConns < 0 || cfg.MaxIdleConns > 131072 {
|
|
return Config{}, errors.New("httpc: invalid connection pool limits")
|
|
}
|
|
if cfg.MaxResponseBytes < 0 || cfg.MaxResponseBytes > maxResponseBytes {
|
|
return Config{}, errors.New("httpc: maxResponseBytes must be 0..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{}, errors.New("httpc: nil Transport")
|
|
}
|
|
}
|
|
if cfg.hasTransportOptions() {
|
|
return Config{}, errors.New("httpc: 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{}, errors.New("httpc: backoff exceeds 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, errors.New("httpc: proxy URL requires mode url")
|
|
}
|
|
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, errors.New("httpc: proxy URL must be an absolute proxy origin without path, query or fragment")
|
|
}
|
|
switch u.Scheme {
|
|
case "http", "https", "socks5", "socks5h":
|
|
default:
|
|
return nil, errors.New("httpc: proxy scheme must be http, https, socks5 or socks5h")
|
|
}
|
|
if strings.HasSuffix(u.Host, ":") {
|
|
return nil, errors.New("httpc: proxy port must be 1..65535")
|
|
}
|
|
if port := u.Port(); port != "" {
|
|
n, e := strconv.Atoi(port)
|
|
if e != nil || n < 1 || n > 65535 {
|
|
return nil, errors.New("httpc: proxy port must be 1..65535")
|
|
}
|
|
}
|
|
u.Path = ""
|
|
u.RawPath = ""
|
|
return http.ProxyURL(u), nil
|
|
default:
|
|
return nil, errors.New("httpc: 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
|
|
}
|