ooni-probe-cli/internal/netxlite/http.go
Simone Basso 6895946a34
refactor: introduce factory for stdlib http transport (#413)
With this factory, we want to construct ourselves the TLS dialer
so that we can use a dialer wrapper that always sets timeouts when
reading, addressing https://github.com/ooni/probe/issues/1609.

As a result, we cannot immediately replace the i/e/netx factory
for creating a new HTTP transport, since the functions signatures
are not directly compatible.

Refactoring is part of https://github.com/ooni/probe/issues/1505.
2021-07-01 15:26:08 +02:00

120 lines
3.4 KiB
Go

package netxlite
import (
"context"
"crypto/tls"
"net"
"net/http"
"time"
)
// HTTPTransport is an http.Transport-like structure.
type HTTPTransport interface {
// RoundTrip performs the HTTP round trip.
RoundTrip(req *http.Request) (*http.Response, error)
// CloseIdleConnections closes idle connections.
CloseIdleConnections()
}
// HTTPTransportLogger is an HTTPTransport with logging.
type HTTPTransportLogger struct {
// HTTPTransport is the underlying HTTP transport.
HTTPTransport HTTPTransport
// Logger is the underlying logger.
Logger Logger
}
var _ HTTPTransport = &HTTPTransportLogger{}
// RoundTrip implements HTTPTransport.RoundTrip.
func (txp *HTTPTransportLogger) RoundTrip(req *http.Request) (*http.Response, error) {
host := req.Host
if host == "" {
host = req.URL.Host
}
req.Header.Set("Host", host) // anticipate what Go would do
return txp.logTrip(req)
}
// logTrip is an HTTP round trip with logging.
func (txp *HTTPTransportLogger) logTrip(req *http.Request) (*http.Response, error) {
txp.Logger.Debugf("> %s %s", req.Method, req.URL.String())
for key, values := range req.Header {
for _, value := range values {
txp.Logger.Debugf("> %s: %s", key, value)
}
}
txp.Logger.Debug(">")
resp, err := txp.HTTPTransport.RoundTrip(req)
if err != nil {
txp.Logger.Debugf("< %s", err)
return nil, err
}
txp.Logger.Debugf("< %d", resp.StatusCode)
for key, values := range resp.Header {
for _, value := range values {
txp.Logger.Debugf("< %s: %s", key, value)
}
}
txp.Logger.Debug("<")
return resp, nil
}
// CloseIdleConnections implement HTTPTransport.CloseIdleConnections.
func (txp *HTTPTransportLogger) CloseIdleConnections() {
txp.HTTPTransport.CloseIdleConnections()
}
// NewHTTPTransport creates a new HTTP transport using Go stdlib.
func NewHTTPTransport(dialer Dialer, tlsConfig *tls.Config,
handshaker TLSHandshaker) HTTPTransport {
txp := http.DefaultTransport.(*http.Transport).Clone()
dialer = &httpDialerWithReadTimeout{dialer}
txp.DialContext = dialer.DialContext
txp.DialTLSContext = (&TLSDialer{
Config: tlsConfig,
Dialer: dialer,
TLSHandshaker: handshaker,
}).DialTLSContext
// Better for Cloudflare DNS and also better because we have less
// noisy events and we can better understand what happened.
txp.MaxConnsPerHost = 1
// The following (1) reduces the number of headers that Go will
// automatically send for us and (2) ensures that we always receive
// back the true headers, such as Content-Length. This change is
// functional to OONI's goal of observing the network.
txp.DisableCompression = true
return txp
}
// httpDialerWithReadTimeout enforces a read timeout for all HTTP
// connections. See https://github.com/ooni/probe/issues/1609.
type httpDialerWithReadTimeout struct {
Dialer
}
// DialContext implements Dialer.DialContext.
func (d *httpDialerWithReadTimeout) DialContext(
ctx context.Context, network, address string) (net.Conn, error) {
conn, err := d.Dialer.DialContext(ctx, network, address)
if err != nil {
return nil, err
}
return &httpConnWithReadTimeout{conn}, nil
}
// httpConnWithReadTimeout enforces a read timeout for all HTTP
// connections. See https://github.com/ooni/probe/issues/1609.
type httpConnWithReadTimeout struct {
net.Conn
}
// Read implements Conn.Read.
func (c *httpConnWithReadTimeout) Read(b []byte) (int, error) {
c.Conn.SetReadDeadline(time.Now().Add(30 * time.Second))
defer c.Conn.SetReadDeadline(time.Time{})
return c.Conn.Read(b)
}