github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/fshttp/http.go (about)

     1  // Package fshttp contains the common http parts of the config, Transport and Client
     2  package fshttp
     3  
     4  import (
     5  	"bytes"
     6  	"context"
     7  	"crypto/tls"
     8  	"crypto/x509"
     9  	"log"
    10  	"net"
    11  	"net/http"
    12  	"net/http/cookiejar"
    13  	"net/http/httputil"
    14  	"os"
    15  	"sync"
    16  	"time"
    17  
    18  	"github.com/rclone/rclone/fs"
    19  	"github.com/rclone/rclone/fs/accounting"
    20  	"github.com/rclone/rclone/lib/structs"
    21  	"golang.org/x/net/publicsuffix"
    22  )
    23  
    24  const (
    25  	separatorReq  = ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
    26  	separatorResp = "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"
    27  )
    28  
    29  var (
    30  	transport    http.RoundTripper
    31  	noTransport  = new(sync.Once)
    32  	cookieJar, _ = cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
    33  	logMutex     sync.Mutex
    34  )
    35  
    36  // ResetTransport resets the existing transport, allowing it to take new settings.
    37  // Should only be used for testing.
    38  func ResetTransport() {
    39  	noTransport = new(sync.Once)
    40  }
    41  
    42  // NewTransportCustom returns an http.RoundTripper with the correct timeouts.
    43  // The customize function is called if set to give the caller an opportunity to
    44  // customize any defaults in the Transport.
    45  func NewTransportCustom(ctx context.Context, customize func(*http.Transport)) http.RoundTripper {
    46  	ci := fs.GetConfig(ctx)
    47  	// Start with a sensible set of defaults then override.
    48  	// This also means we get new stuff when it gets added to go
    49  	t := new(http.Transport)
    50  	structs.SetDefaults(t, http.DefaultTransport.(*http.Transport))
    51  	t.Proxy = http.ProxyFromEnvironment
    52  	t.MaxIdleConnsPerHost = 2 * (ci.Checkers + ci.Transfers + 1)
    53  	t.MaxIdleConns = 2 * t.MaxIdleConnsPerHost
    54  	t.TLSHandshakeTimeout = ci.ConnectTimeout
    55  	t.ResponseHeaderTimeout = ci.Timeout
    56  	t.DisableKeepAlives = ci.DisableHTTPKeepAlives
    57  
    58  	// TLS Config
    59  	t.TLSClientConfig = &tls.Config{
    60  		InsecureSkipVerify: ci.InsecureSkipVerify,
    61  	}
    62  
    63  	// Load client certs
    64  	if ci.ClientCert != "" || ci.ClientKey != "" {
    65  		if ci.ClientCert == "" || ci.ClientKey == "" {
    66  			log.Fatalf("Both --client-cert and --client-key must be set")
    67  		}
    68  		cert, err := tls.LoadX509KeyPair(ci.ClientCert, ci.ClientKey)
    69  		if err != nil {
    70  			log.Fatalf("Failed to load --client-cert/--client-key pair: %v", err)
    71  		}
    72  		t.TLSClientConfig.Certificates = []tls.Certificate{cert}
    73  	}
    74  
    75  	// Load CA certs
    76  	if len(ci.CaCert) != 0 {
    77  
    78  		caCertPool := x509.NewCertPool()
    79  
    80  		for _, cert := range ci.CaCert {
    81  			caCert, err := os.ReadFile(cert)
    82  			if err != nil {
    83  				log.Fatalf("Failed to read --ca-cert file %q : %v", cert, err)
    84  			}
    85  			ok := caCertPool.AppendCertsFromPEM(caCert)
    86  			if !ok {
    87  				log.Fatalf("Failed to add certificates from --ca-cert file %q", cert)
    88  			}
    89  		}
    90  		t.TLSClientConfig.RootCAs = caCertPool
    91  	}
    92  
    93  	t.DisableCompression = ci.NoGzip
    94  	t.DialContext = func(reqCtx context.Context, network, addr string) (net.Conn, error) {
    95  		return NewDialer(ctx).DialContext(reqCtx, network, addr)
    96  	}
    97  	t.IdleConnTimeout = 60 * time.Second
    98  	t.ExpectContinueTimeout = ci.ExpectContinueTimeout
    99  
   100  	if ci.Dump&(fs.DumpHeaders|fs.DumpBodies|fs.DumpAuth|fs.DumpRequests|fs.DumpResponses) != 0 {
   101  		fs.Debugf(nil, "You have specified to dump information. Please be noted that the "+
   102  			"Accept-Encoding as shown may not be correct in the request and the response may not show "+
   103  			"Content-Encoding if the go standard libraries auto gzip encoding was in effect. In this case"+
   104  			" the body of the request will be gunzipped before showing it.")
   105  	}
   106  
   107  	if ci.DisableHTTP2 {
   108  		t.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
   109  	}
   110  
   111  	// customize the transport if required
   112  	if customize != nil {
   113  		customize(t)
   114  	}
   115  
   116  	// Wrap that http.Transport in our own transport
   117  	return newTransport(ci, t)
   118  }
   119  
   120  // NewTransport returns an http.RoundTripper with the correct timeouts
   121  func NewTransport(ctx context.Context) http.RoundTripper {
   122  	(*noTransport).Do(func() {
   123  		transport = NewTransportCustom(ctx, nil)
   124  	})
   125  	return transport
   126  }
   127  
   128  // NewClient returns an http.Client with the correct timeouts
   129  func NewClient(ctx context.Context) *http.Client {
   130  	ci := fs.GetConfig(ctx)
   131  	client := &http.Client{
   132  		Transport: NewTransport(ctx),
   133  	}
   134  	if ci.Cookie {
   135  		client.Jar = cookieJar
   136  	}
   137  	return client
   138  }
   139  
   140  // Transport is our http Transport which wraps an http.Transport
   141  // * Sets the User Agent
   142  // * Does logging
   143  // * Updates metrics
   144  type Transport struct {
   145  	*http.Transport
   146  	dump          fs.DumpFlags
   147  	filterRequest func(req *http.Request)
   148  	userAgent     string
   149  	headers       []*fs.HTTPOption
   150  	metrics       *Metrics
   151  }
   152  
   153  // newTransport wraps the http.Transport passed in and logs all
   154  // roundtrips including the body if logBody is set.
   155  func newTransport(ci *fs.ConfigInfo, transport *http.Transport) *Transport {
   156  	return &Transport{
   157  		Transport: transport,
   158  		dump:      ci.Dump,
   159  		userAgent: ci.UserAgent,
   160  		headers:   ci.Headers,
   161  		metrics:   DefaultMetrics,
   162  	}
   163  }
   164  
   165  // SetRequestFilter sets a filter to be used on each request
   166  func (t *Transport) SetRequestFilter(f func(req *http.Request)) {
   167  	t.filterRequest = f
   168  }
   169  
   170  // A mutex to protect this map
   171  var checkedHostMu sync.RWMutex
   172  
   173  // A map of servers we have checked for time
   174  var checkedHost = make(map[string]struct{}, 1)
   175  
   176  // Check the server time is the same as ours, once for each server
   177  func checkServerTime(req *http.Request, resp *http.Response) {
   178  	host := req.URL.Host
   179  	if req.Host != "" {
   180  		host = req.Host
   181  	}
   182  	checkedHostMu.RLock()
   183  	_, ok := checkedHost[host]
   184  	checkedHostMu.RUnlock()
   185  	if ok {
   186  		return
   187  	}
   188  	dateString := resp.Header.Get("Date")
   189  	if dateString == "" {
   190  		return
   191  	}
   192  	date, err := http.ParseTime(dateString)
   193  	if err != nil {
   194  		fs.Debugf(nil, "Couldn't parse Date: from server %s: %q: %v", host, dateString, err)
   195  		return
   196  	}
   197  	dt := time.Since(date)
   198  	const window = 5 * 60 * time.Second
   199  	if dt > window || dt < -window {
   200  		fs.Logf(nil, "Time may be set wrong - time from %q is %v different from this computer", host, dt)
   201  	}
   202  	checkedHostMu.Lock()
   203  	checkedHost[host] = struct{}{}
   204  	checkedHostMu.Unlock()
   205  }
   206  
   207  // cleanAuth gets rid of one authBuf header within the first 4k
   208  func cleanAuth(buf, authBuf []byte) []byte {
   209  	// Find how much buffer to check
   210  	n := 4096
   211  	if len(buf) < n {
   212  		n = len(buf)
   213  	}
   214  	// See if there is an Authorization: header
   215  	i := bytes.Index(buf[:n], authBuf)
   216  	if i < 0 {
   217  		return buf
   218  	}
   219  	i += len(authBuf)
   220  	// Overwrite the next 4 chars with 'X'
   221  	for j := 0; i < len(buf) && j < 4; j++ {
   222  		if buf[i] == '\n' {
   223  			break
   224  		}
   225  		buf[i] = 'X'
   226  		i++
   227  	}
   228  	// Snip out to the next '\n'
   229  	j := bytes.IndexByte(buf[i:], '\n')
   230  	if j < 0 {
   231  		return buf[:i]
   232  	}
   233  	n = copy(buf[i:], buf[i+j:])
   234  	return buf[:i+n]
   235  }
   236  
   237  var authBufs = [][]byte{
   238  	[]byte("Authorization: "),
   239  	[]byte("X-Auth-Token: "),
   240  }
   241  
   242  // cleanAuths gets rid of all the possible Auth headers
   243  func cleanAuths(buf []byte) []byte {
   244  	for _, authBuf := range authBufs {
   245  		buf = cleanAuth(buf, authBuf)
   246  	}
   247  	return buf
   248  }
   249  
   250  // RoundTrip implements the RoundTripper interface.
   251  func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
   252  	// Limit transactions per second if required
   253  	accounting.LimitTPS(req.Context())
   254  	// Force user agent
   255  	req.Header.Set("User-Agent", t.userAgent)
   256  	// Set user defined headers
   257  	for _, option := range t.headers {
   258  		req.Header.Set(option.Key, option.Value)
   259  	}
   260  	// Filter the request if required
   261  	if t.filterRequest != nil {
   262  		t.filterRequest(req)
   263  	}
   264  	// Logf request
   265  	if t.dump&(fs.DumpHeaders|fs.DumpBodies|fs.DumpAuth|fs.DumpRequests|fs.DumpResponses) != 0 {
   266  		buf, _ := httputil.DumpRequestOut(req, t.dump&(fs.DumpBodies|fs.DumpRequests) != 0)
   267  		if t.dump&fs.DumpAuth == 0 {
   268  			buf = cleanAuths(buf)
   269  		}
   270  		logMutex.Lock()
   271  		fs.Debugf(nil, "%s", separatorReq)
   272  		fs.Debugf(nil, "%s (req %p)", "HTTP REQUEST", req)
   273  		fs.Debugf(nil, "%s", string(buf))
   274  		fs.Debugf(nil, "%s", separatorReq)
   275  		logMutex.Unlock()
   276  	}
   277  	// Do round trip
   278  	resp, err = t.Transport.RoundTrip(req)
   279  	// Logf response
   280  	if t.dump&(fs.DumpHeaders|fs.DumpBodies|fs.DumpAuth|fs.DumpRequests|fs.DumpResponses) != 0 {
   281  		logMutex.Lock()
   282  		fs.Debugf(nil, "%s", separatorResp)
   283  		fs.Debugf(nil, "%s (req %p)", "HTTP RESPONSE", req)
   284  		if err != nil {
   285  			fs.Debugf(nil, "Error: %v", err)
   286  		} else {
   287  			buf, _ := httputil.DumpResponse(resp, t.dump&(fs.DumpBodies|fs.DumpResponses) != 0)
   288  			fs.Debugf(nil, "%s", string(buf))
   289  		}
   290  		fs.Debugf(nil, "%s", separatorResp)
   291  		logMutex.Unlock()
   292  	}
   293  	// Update metrics
   294  	t.metrics.onResponse(req, resp)
   295  
   296  	if err == nil {
   297  		checkServerTime(req, resp)
   298  	}
   299  	return resp, err
   300  }