diff --git a/fs/fshttp/http.go b/fs/fshttp/http.go index 5fb939ecb..c46b8f615 100644 --- a/fs/fshttp/http.go +++ b/fs/fshttp/http.go @@ -25,6 +25,7 @@ import ( "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/accounting" "github.com/rclone/rclone/fs/config/obscure" + "github.com/rclone/rclone/lib/rest" "github.com/rclone/rclone/lib/structs" "github.com/youmark/pkcs8" "golang.org/x/net/publicsuffix" @@ -591,6 +592,22 @@ func newClientTrace(req *http.Request) *httptrace.ClientTrace { } } +// redirectLeavesHost reports whether req is a redirect hop in a +// chain which has visited a host other than that of the original +// request at any point. +func redirectLeavesHost(req *http.Request) bool { + origin := req + for origin.Response != nil && origin.Response.Request != nil { + origin = origin.Response.Request + } + for hop := req; hop != origin; hop = hop.Response.Request { + if !rest.SameHost(hop.URL, origin.URL) { + return true + } + } + return false +} + // RoundTrip implements the RoundTripper interface. func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { // Check if certificates are being used and the certificates are expired @@ -602,9 +619,15 @@ func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error accounting.LimitTPS(req.Context()) // Force user agent req.Header.Set("User-Agent", t.userAgent) - // Set user defined headers - for _, option := range t.headers { - req.Header.Set(option.Key, option.Value) + // Set user defined headers, unless redirected elsewhere + if redirectLeavesHost(req) { + for _, option := range t.headers { + req.Header.Del(option.Key) + } + } else { + for _, option := range t.headers { + req.Header.Set(option.Key, option.Value) + } } // Filter the request if required if t.filterRequest != nil { diff --git a/fs/fshttp/http_test.go b/fs/fshttp/http_test.go index 084099c77..1d887dabf 100644 --- a/fs/fshttp/http_test.go +++ b/fs/fshttp/http_test.go @@ -174,6 +174,12 @@ func TestCertificates(t *testing.T) { // create a test cert/key pair and write it to the files ctx := context.TODO() ci := fs.GetConfig(ctx) + // Restore the global config for later tests as the temp files + // are removed when this test finishes + oldCert, oldKey := ci.ClientCert, ci.ClientKey + t.Cleanup(func() { + ci.ClientCert, ci.ClientKey = oldCert, oldKey + }) // Create a test certificate and write it to a temp file ci.ClientCert = t.TempDir() + "client.cert" ci.ClientKey = t.TempDir() + "client.key" @@ -202,3 +208,59 @@ func TestCertificates(t *testing.T) { _, err = client.Get(ts.URL) assert.NoError(t, err) } + +// TestRedirectStripsGlobalHeaders checks the headers set with --header +// are sent to the requested host and any redirect on it but not to +// another host the request is redirected to +func TestRedirectStripsGlobalHeaders(t *testing.T) { + var got http.Header + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got = r.Header.Clone() + })) + defer target.Close() + + var ts *httptest.Server + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/same": + http.Redirect(w, r, ts.URL+"/moved", http.StatusFound) + case "/other": + http.Redirect(w, r, target.URL+"/moved", http.StatusFound) + case "/back": + // Chain via the other host and back again + http.Redirect(w, r, target.URL+"/bounce", http.StatusFound) + default: + got = r.Header.Clone() + } + })) + defer ts.Close() + // The other host bounces /bounce back to the original host + target.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/bounce" { + http.Redirect(w, r, ts.URL+"/moved", http.StatusFound) + return + } + got = r.Header.Clone() + }) + + ctx, ci := fs.AddConfig(context.Background()) + ci.Headers = []*fs.HTTPOption{{Key: "X-Potato", Value: "sausage"}} + client := NewClient(ctx) + + for _, test := range []struct { + path string + want string + }{ + {"/direct", "sausage"}, + {"/same", "sausage"}, + {"/other", ""}, + {"/back", ""}, + } { + got = nil + resp, err := client.Get(ts.URL + test.path) + require.NoError(t, err, test.path) + require.NoError(t, resp.Body.Close()) + require.NotNil(t, got, test.path) + assert.Equal(t, test.want, got.Get("X-Potato"), test.path) + } +}