feat: simplify HTTP client proxy pools and authenticated JSON calls
This commit is contained in:
205
httpc/config.go
Normal file
205
httpc/config.go
Normal file
@@ -0,0 +1,205 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user