github.com/yandex/pandora@v0.5.32/components/guns/http/client.go (about) 1 package phttp 2 3 import ( 4 "crypto/tls" 5 "net" 6 "net/http" 7 "strings" 8 "time" 9 10 "github.com/pkg/errors" 11 "github.com/yandex/pandora/lib/netutil" 12 "go.uber.org/zap" 13 "golang.org/x/net/http2" 14 ) 15 16 //go:generate mockery --name=Client --case=underscore --inpackage --testonly 17 18 type Client interface { 19 Do(req *http.Request) (*http.Response, error) 20 CloseIdleConnections() // We should close idle conns after gun close. 21 } 22 23 type ClientConfig struct { 24 Redirect bool // When true, follow HTTP redirects. 25 Dialer DialerConfig `config:"dial"` 26 Transport TransportConfig `config:",squash"` 27 ConnectSSL bool `config:"connect-ssl"` // Defines if tunnel encrypted. 28 } 29 30 type ClientConstructor func(clientConfig ClientConfig, target string) Client 31 32 func DefaultClientConfig() ClientConfig { 33 return ClientConfig{ 34 Transport: DefaultTransportConfig(), 35 Dialer: DefaultDialerConfig(), 36 Redirect: false, 37 } 38 } 39 40 // DialerConfig can be mapped on net.Dialer. 41 // Set net.Dialer for details. 42 type DialerConfig struct { 43 DNSCache bool `config:"dns-cache" map:"-"` 44 45 Timeout time.Duration `config:"timeout"` 46 DualStack bool `config:"dual-stack"` 47 48 // IPv4/IPv6 settings should not matter really, 49 // because target should be dialed using pre-resolved addr. 50 FallbackDelay time.Duration `config:"fallback-delay"` 51 KeepAlive time.Duration `config:"keep-alive"` 52 } 53 54 func DefaultDialerConfig() DialerConfig { 55 return DialerConfig{ 56 DNSCache: true, 57 DualStack: true, 58 Timeout: 3 * time.Second, 59 KeepAlive: 120 * time.Second, 60 } 61 } 62 63 func NewDialer(conf DialerConfig) netutil.Dialer { 64 d := &net.Dialer{ 65 Timeout: conf.Timeout, 66 DualStack: conf.DualStack, 67 FallbackDelay: conf.FallbackDelay, 68 KeepAlive: conf.KeepAlive, 69 } 70 if !conf.DNSCache { 71 return d 72 } 73 return netutil.NewDNSCachingDialer(d, netutil.DefaultDNSCache) 74 } 75 76 // TransportConfig can be mapped on http.Transport. 77 // See http.Transport for details. 78 type TransportConfig struct { 79 TLSHandshakeTimeout time.Duration `config:"tls-handshake-timeout"` 80 DisableKeepAlives bool `config:"disable-keep-alives"` 81 DisableCompression bool `config:"disable-compression"` 82 MaxIdleConns int `config:"max-idle-conns"` 83 MaxIdleConnsPerHost int `config:"max-idle-conns-per-host"` 84 IdleConnTimeout time.Duration `config:"idle-conn-timeout"` 85 ResponseHeaderTimeout time.Duration `config:"response-header-timeout"` 86 ExpectContinueTimeout time.Duration `config:"expect-continue-timeout"` 87 } 88 89 func DefaultTransportConfig() TransportConfig { 90 return TransportConfig{ 91 MaxIdleConns: 0, // No limit. 92 IdleConnTimeout: 90 * time.Second, 93 TLSHandshakeTimeout: 1 * time.Second, 94 ExpectContinueTimeout: 1 * time.Second, 95 DisableCompression: true, 96 } 97 } 98 99 func NewTransport(conf TransportConfig, dial netutil.DialerFunc, target string) *http.Transport { 100 tr := &http.Transport{ 101 TLSHandshakeTimeout: conf.TLSHandshakeTimeout, 102 DisableKeepAlives: conf.DisableKeepAlives, 103 DisableCompression: conf.DisableCompression, 104 MaxIdleConns: conf.MaxIdleConns, 105 MaxIdleConnsPerHost: conf.MaxIdleConnsPerHost, 106 IdleConnTimeout: conf.IdleConnTimeout, 107 ResponseHeaderTimeout: conf.ResponseHeaderTimeout, 108 ExpectContinueTimeout: conf.ExpectContinueTimeout, 109 } 110 host, _, err := net.SplitHostPort(target) 111 if err != nil { 112 zap.L().Panic("HTTP transport configure fail", zap.Error(err)) 113 } 114 tr.TLSClientConfig = &tls.Config{ 115 InsecureSkipVerify: true, // We should not spend time for this stuff. 116 NextProtos: []string{"http/1.1"}, // Disable HTTP/2. Use HTTP/2 transport explicitly, if needed. 117 ServerName: host, 118 } 119 tr.DialContext = dial 120 return tr 121 } 122 123 func NewHTTP2Transport(conf TransportConfig, dial netutil.DialerFunc, target string) *http.Transport { 124 tr := NewTransport(conf, dial, target) 125 err := http2.ConfigureTransport(tr) 126 if err != nil { 127 zap.L().Panic("HTTP/2 transport configure fail", zap.Error(err)) 128 } 129 tr.TLSClientConfig.NextProtos = []string{"h2"} 130 return tr 131 } 132 133 func NewRedirectingClient(tr *http.Transport, redirect bool) Client { 134 if redirect { 135 return redirectClient{&http.Client{Transport: tr}} 136 } 137 return noRedirectClient{tr} 138 } 139 140 type redirectClient struct{ *http.Client } 141 142 func (c redirectClient) CloseIdleConnections() { 143 c.Transport.(*http.Transport).CloseIdleConnections() 144 } 145 146 type noRedirectClient struct{ *http.Transport } 147 148 func (c noRedirectClient) Do(req *http.Request) (*http.Response, error) { 149 return c.Transport.RoundTrip(req) 150 } 151 152 // Used to cancel shooting in HTTP/2 gun, when target doesn't support HTTP/2 153 type panicOnHTTP1Client struct { 154 Client 155 } 156 157 const notHTTP2PanicMsg = "Non HTTP/2 connection established. Seems that target doesn't support HTTP/2." 158 159 func (c *panicOnHTTP1Client) Do(req *http.Request) (*http.Response, error) { 160 res, err := c.Client.Do(req) 161 if err != nil { 162 var opError *net.OpError 163 // Unfortunately, Go doesn't expose tls.alert (https://github.com/golang/go/issues/35234), so we make decisions based on the error message 164 if errors.As(err, &opError) && opError.Op == "remote error" && strings.Contains(err.Error(), "no application protocol") { 165 zap.L().Panic(notHTTP2PanicMsg, zap.Error(err)) 166 } 167 return nil, err 168 } 169 err = checkHTTP2(res.TLS) 170 if err != nil { 171 zap.L().Panic(notHTTP2PanicMsg, zap.Error(err)) 172 } 173 return res, nil 174 } 175 176 func checkHTTP2(state *tls.ConnectionState) error { 177 if state == nil { 178 return errors.New("http2: non TLS connection") 179 } 180 if p := state.NegotiatedProtocol; p != http2.NextProtoTLS { 181 return errors.Errorf("http2: unexpected ALPN protocol %q; want %q", p, http2.NextProtoTLS) 182 } 183 if !state.NegotiatedProtocolIsMutual { 184 return errors.New("http2: could not negotiate protocol mutually") 185 } 186 return nil 187 } 188 189 func getHostWithoutPort(target string) string { 190 host, _, err := net.SplitHostPort(target) 191 if err != nil { 192 host = target 193 } 194 return host 195 }