rclone/fs/http.go
Nick Craig-Wood 0cb9bb3b54 Redo http Transport code
* Insert User-Agent in Transport - fixes #199
  * Update timeouts to use Context
  * Modernise transport
2016-09-12 17:50:19 +01:00

169 lines
4.2 KiB
Go

// The HTTP based parts of the config, Transport and Client
package fs
import (
"crypto/tls"
"net"
"net/http"
"net/http/httputil"
"reflect"
"sync"
"time"
)
const (
separatorReq = ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
separatorResp = "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"
)
var (
transport http.RoundTripper
noTransport sync.Once
)
// A net.Conn that sets a deadline for every Read or Write operation
type timeoutConn struct {
net.Conn
readTimer *time.Timer
writeTimer *time.Timer
timeout time.Duration
_cancel func()
off time.Time
}
// create a timeoutConn using the timeout
func newTimeoutConn(conn net.Conn, timeout time.Duration) *timeoutConn {
return &timeoutConn{
Conn: conn,
timeout: timeout,
}
}
// Read bytes doing timeouts
func (c *timeoutConn) Read(b []byte) (n int, err error) {
err = c.Conn.SetReadDeadline(time.Now().Add(c.timeout))
if err != nil {
return n, err
}
n, err = c.Conn.Read(b)
cerr := c.Conn.SetReadDeadline(c.off)
if cerr != nil {
err = cerr
}
return n, err
}
// Write bytes doing timeouts
func (c *timeoutConn) Write(b []byte) (n int, err error) {
err = c.Conn.SetWriteDeadline(time.Now().Add(c.timeout))
if err != nil {
return n, err
}
n, err = c.Conn.Write(b)
cerr := c.Conn.SetWriteDeadline(c.off)
if cerr != nil {
err = cerr
}
return n, err
}
// setDefaults for a from b
//
// Copy the public members from b to a. We can't just use a struct
// copy as Transport contains a private mutex.
func setDefaults(a, b interface{}) {
pt := reflect.TypeOf(a)
t := pt.Elem()
va := reflect.ValueOf(a).Elem()
vb := reflect.ValueOf(b).Elem()
for i := 0; i < t.NumField(); i++ {
aField := va.Field(i)
// Set a from b if it is public
if aField.CanSet() {
bField := vb.Field(i)
aField.Set(bField)
}
}
}
// Transport returns an http.RoundTripper with the correct timeouts
func (ci *ConfigInfo) Transport() http.RoundTripper {
noTransport.Do(func() {
// Start with a sensible set of defaults then override.
// This also means we get new stuff when it gets added to go
t := new(http.Transport)
setDefaults(t, http.DefaultTransport.(*http.Transport))
t.Proxy = http.ProxyFromEnvironment
t.MaxIdleConnsPerHost = 4 * (ci.Checkers + ci.Transfers + 1)
t.TLSHandshakeTimeout = ci.ConnectTimeout
t.ResponseHeaderTimeout = ci.ConnectTimeout
t.TLSClientConfig = &tls.Config{InsecureSkipVerify: ci.InsecureSkipVerify}
t.DisableCompression = *noGzip
// Set in http_old.go initTransport
// t.Dial
// Set in http_new.go initTransport
// t.DialContext
// t.IdelConnTimeout
// t.ExpectContinueTimeout
ci.initTransport(t)
// Wrap that http.Transport in our own transport
transport = NewTransport(t, ci.DumpHeaders, ci.DumpBodies)
})
return transport
}
// Client returns an http.Client with the correct timeouts
func (ci *ConfigInfo) Client() *http.Client {
return &http.Client{
Transport: ci.Transport(),
}
}
// Transport is a our http Transport which wraps an http.Transport
// * Sets the User Agent
// * Does logging
type Transport struct {
*http.Transport
logHeader bool
logBody bool
}
// NewTransport wraps the http.Transport passed in and logs all
// roundtrips including the body if logBody is set.
func NewTransport(transport *http.Transport, logHeader, logBody bool) *Transport {
return &Transport{
Transport: transport,
logHeader: logHeader,
logBody: logBody,
}
}
// RoundTrip implements the RoundTripper interface.
func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
// Force user agent
req.Header.Set("User-Agent", UserAgent)
// Log request
if t.logHeader {
buf, _ := httputil.DumpRequestOut(req, t.logBody)
Debug(nil, "%s", separatorReq)
Debug(nil, "%s", "HTTP REQUEST")
Debug(nil, "%s", string(buf))
Debug(nil, "%s", separatorReq)
}
// Do round trip
resp, err = t.Transport.RoundTrip(req)
// Log response
if t.logHeader {
Debug(nil, "%s", separatorResp)
Debug(nil, "%s", "HTTP RESPONSE")
if err != nil {
Debug(nil, "Error: %v", err)
} else {
buf, _ := httputil.DumpResponse(resp, t.logBody)
Debug(nil, "%s", string(buf))
}
Debug(nil, "%s", separatorResp)
}
return resp, err
}