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  }