2011-04-15 08:13:52 -07:00
|
|
|
// Copyright 2011 The Go Authors. All rights reserved.
|
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
|
|
// Reverse proxy tests.
|
|
|
|
|
|
2011-11-03 15:54:08 -07:00
|
|
|
package httputil
|
2011-04-15 08:13:52 -07:00
|
|
|
|
|
|
|
|
import (
|
2018-12-04 00:56:04 +00:00
|
|
|
"bufio"
|
|
|
|
|
"bytes"
|
2019-03-28 23:37:54 -04:00
|
|
|
"context"
|
2018-12-04 00:56:04 +00:00
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2014-12-30 12:19:43 +00:00
|
|
|
"log"
|
2011-11-08 15:41:54 -08:00
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
2021-04-07 14:36:40 +02:00
|
|
|
"net/http/internal/ascii"
|
2011-11-08 15:41:54 -08:00
|
|
|
"net/url"
|
2018-12-04 00:56:04 +00:00
|
|
|
"os"
|
2015-07-06 15:57:35 -07:00
|
|
|
"reflect"
|
2019-05-14 15:11:30 +00:00
|
|
|
"sort"
|
2018-12-04 00:56:04 +00:00
|
|
|
"strconv"
|
2012-07-31 08:38:49 +10:00
|
|
|
"strings"
|
2015-04-27 18:10:20 -07:00
|
|
|
"sync"
|
2018-12-04 00:56:04 +00:00
|
|
|
"testing"
|
|
|
|
|
"time"
|
2011-04-15 08:13:52 -07:00
|
|
|
)
|
|
|
|
|
|
2014-01-27 15:24:58 -08:00
|
|
|
const fakeHopHeader = "X-Fake-Hop-Header-For-Test"
|
|
|
|
|
|
|
|
|
|
func init() {
|
2018-07-09 22:29:00 +00:00
|
|
|
inOurTests = true
|
2014-01-27 15:24:58 -08:00
|
|
|
hopHeaders = append(hopHeaders, fakeHopHeader)
|
|
|
|
|
}
|
|
|
|
|
|
2011-04-15 08:13:52 -07:00
|
|
|
func TestReverseProxy(t *testing.T) {
|
|
|
|
|
const backendResponse = "I am the backend"
|
|
|
|
|
const backendStatus = 404
|
2011-11-03 15:54:08 -07:00
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2016-03-31 04:03:57 -07:00
|
|
|
if r.Method == "GET" && r.FormValue("mode") == "hangup" {
|
|
|
|
|
c, _, _ := w.(http.Hijacker).Hijack()
|
|
|
|
|
c.Close()
|
|
|
|
|
return
|
|
|
|
|
}
|
2011-06-24 16:46:14 -07:00
|
|
|
if len(r.TransferEncoding) > 0 {
|
|
|
|
|
t.Errorf("backend got unexpected TransferEncoding: %v", r.TransferEncoding)
|
|
|
|
|
}
|
2011-04-15 08:13:52 -07:00
|
|
|
if r.Header.Get("X-Forwarded-For") == "" {
|
|
|
|
|
t.Errorf("didn't get X-Forwarded-For header")
|
|
|
|
|
}
|
2011-10-26 15:27:29 +09:00
|
|
|
if c := r.Header.Get("Connection"); c != "" {
|
|
|
|
|
t.Errorf("handler got Connection header value %q", c)
|
|
|
|
|
}
|
2018-05-29 22:08:32 +00:00
|
|
|
if c := r.Header.Get("Te"); c != "trailers" {
|
|
|
|
|
t.Errorf("handler got Te header value %q; want 'trailers'", c)
|
|
|
|
|
}
|
2013-03-11 10:32:32 -07:00
|
|
|
if c := r.Header.Get("Upgrade"); c != "" {
|
2013-03-11 13:23:47 -07:00
|
|
|
t.Errorf("handler got Upgrade header value %q", c)
|
2013-03-11 10:32:32 -07:00
|
|
|
}
|
2016-02-03 21:35:03 +00:00
|
|
|
if c := r.Header.Get("Proxy-Connection"); c != "" {
|
|
|
|
|
t.Errorf("handler got Proxy-Connection header value %q", c)
|
|
|
|
|
}
|
2011-05-27 11:06:53 -07:00
|
|
|
if g, e := r.Host, "some-name"; g != e {
|
|
|
|
|
t.Errorf("backend got Host header %q, want %q", g, e)
|
|
|
|
|
}
|
2016-02-01 15:58:34 +00:00
|
|
|
w.Header().Set("Trailers", "not a special header field name")
|
2015-07-06 15:57:35 -07:00
|
|
|
w.Header().Set("Trailer", "X-Trailer")
|
2011-04-15 08:13:52 -07:00
|
|
|
w.Header().Set("X-Foo", "bar")
|
2014-01-27 15:24:58 -08:00
|
|
|
w.Header().Set("Upgrade", "foo")
|
|
|
|
|
w.Header().Set(fakeHopHeader, "foo")
|
|
|
|
|
w.Header().Add("X-Multi-Value", "foo")
|
|
|
|
|
w.Header().Add("X-Multi-Value", "bar")
|
2011-11-03 15:54:08 -07:00
|
|
|
http.SetCookie(w, &http.Cookie{Name: "flavor", Value: "chocolateChip"})
|
2011-04-15 08:13:52 -07:00
|
|
|
w.WriteHeader(backendStatus)
|
|
|
|
|
w.Write([]byte(backendResponse))
|
2015-07-06 15:57:35 -07:00
|
|
|
w.Header().Set("X-Trailer", "trailer_value")
|
2017-05-20 14:50:06 +01:00
|
|
|
w.Header().Set(http.TrailerPrefix+"X-Unannounced-Trailer", "unannounced_trailer_value")
|
2011-04-15 08:13:52 -07:00
|
|
|
}))
|
|
|
|
|
defer backend.Close()
|
2011-08-17 13:36:02 +10:00
|
|
|
backendURL, err := url.Parse(backend.URL)
|
2011-04-15 08:13:52 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
proxyHandler := NewSingleHostReverseProxy(backendURL)
|
2020-10-16 00:49:02 -04:00
|
|
|
proxyHandler.ErrorLog = log.New(io.Discard, "", 0) // quiet for tests
|
2011-04-15 08:13:52 -07:00
|
|
|
frontend := httptest.NewServer(proxyHandler)
|
|
|
|
|
defer frontend.Close()
|
2017-03-04 18:24:44 +00:00
|
|
|
frontendClient := frontend.Client()
|
2011-04-15 08:13:52 -07:00
|
|
|
|
2011-11-03 15:54:08 -07:00
|
|
|
getReq, _ := http.NewRequest("GET", frontend.URL, nil)
|
2011-05-27 11:06:53 -07:00
|
|
|
getReq.Host = "some-name"
|
2021-05-21 14:02:30 -04:00
|
|
|
getReq.Header.Set("Connection", "close, TE")
|
|
|
|
|
getReq.Header.Add("Te", "foo")
|
|
|
|
|
getReq.Header.Add("Te", "bar, trailers")
|
2016-02-03 21:35:03 +00:00
|
|
|
getReq.Header.Set("Proxy-Connection", "should be deleted")
|
2013-03-11 10:32:32 -07:00
|
|
|
getReq.Header.Set("Upgrade", "foo")
|
2011-10-26 15:27:29 +09:00
|
|
|
getReq.Close = true
|
2017-03-04 18:24:44 +00:00
|
|
|
res, err := frontendClient.Do(getReq)
|
2011-04-15 08:13:52 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Get: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if g, e := res.StatusCode, backendStatus; g != e {
|
|
|
|
|
t.Errorf("got res.StatusCode %d; expected %d", g, e)
|
|
|
|
|
}
|
|
|
|
|
if g, e := res.Header.Get("X-Foo"), "bar"; g != e {
|
|
|
|
|
t.Errorf("got X-Foo %q; expected %q", g, e)
|
|
|
|
|
}
|
2014-01-27 15:24:58 -08:00
|
|
|
if c := res.Header.Get(fakeHopHeader); c != "" {
|
|
|
|
|
t.Errorf("got %s header value %q", fakeHopHeader, c)
|
|
|
|
|
}
|
2016-02-01 15:58:34 +00:00
|
|
|
if g, e := res.Header.Get("Trailers"), "not a special header field name"; g != e {
|
|
|
|
|
t.Errorf("header Trailers = %q; want %q", g, e)
|
|
|
|
|
}
|
2014-01-27 15:24:58 -08:00
|
|
|
if g, e := len(res.Header["X-Multi-Value"]), 2; g != e {
|
|
|
|
|
t.Errorf("got %d X-Multi-Value header values; expected %d", g, e)
|
|
|
|
|
}
|
2011-06-16 13:02:28 -07:00
|
|
|
if g, e := len(res.Header["Set-Cookie"]), 1; g != e {
|
2011-05-27 11:06:53 -07:00
|
|
|
t.Fatalf("got %d SetCookies, want %d", g, e)
|
|
|
|
|
}
|
2015-07-06 15:57:35 -07:00
|
|
|
if g, e := res.Trailer, (http.Header{"X-Trailer": nil}); !reflect.DeepEqual(g, e) {
|
|
|
|
|
t.Errorf("before reading body, Trailer = %#v; want %#v", g, e)
|
|
|
|
|
}
|
2011-06-16 13:02:28 -07:00
|
|
|
if cookie := res.Cookies()[0]; cookie.Name != "flavor" {
|
2011-05-27 11:06:53 -07:00
|
|
|
t.Errorf("unexpected cookie %q", cookie.Name)
|
|
|
|
|
}
|
2020-10-16 00:49:02 -04:00
|
|
|
bodyBytes, _ := io.ReadAll(res.Body)
|
2011-04-15 08:13:52 -07:00
|
|
|
if g, e := string(bodyBytes), backendResponse; g != e {
|
|
|
|
|
t.Errorf("got body %q; expected %q", g, e)
|
|
|
|
|
}
|
2015-07-06 15:57:35 -07:00
|
|
|
if g, e := res.Trailer.Get("X-Trailer"), "trailer_value"; g != e {
|
|
|
|
|
t.Errorf("Trailer(X-Trailer) = %q ; want %q", g, e)
|
|
|
|
|
}
|
2017-05-20 14:50:06 +01:00
|
|
|
if g, e := res.Trailer.Get("X-Unannounced-Trailer"), "unannounced_trailer_value"; g != e {
|
|
|
|
|
t.Errorf("Trailer(X-Unannounced-Trailer) = %q ; want %q", g, e)
|
|
|
|
|
}
|
2016-03-31 04:03:57 -07:00
|
|
|
|
|
|
|
|
// Test that a backend failing to be reached or one which doesn't return
|
|
|
|
|
// a response results in a StatusBadGateway.
|
|
|
|
|
getReq, _ = http.NewRequest("GET", frontend.URL+"/?mode=hangup", nil)
|
|
|
|
|
getReq.Close = true
|
2017-03-04 18:24:44 +00:00
|
|
|
res, err = frontendClient.Do(getReq)
|
2016-03-31 04:03:57 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
if res.StatusCode != http.StatusBadGateway {
|
|
|
|
|
t.Errorf("request to bad proxy = %v; want 502 StatusBadGateway", res.Status)
|
|
|
|
|
}
|
|
|
|
|
|
2011-04-15 08:13:52 -07:00
|
|
|
}
|
2012-02-07 18:00:30 -08:00
|
|
|
|
2016-08-27 20:46:25 +04:30
|
|
|
// Issue 16875: remove any proxied headers mentioned in the "Connection"
|
|
|
|
|
// header value.
|
|
|
|
|
func TestReverseProxyStripHeadersPresentInConnection(t *testing.T) {
|
|
|
|
|
const fakeConnectionToken = "X-Fake-Connection-Token"
|
|
|
|
|
const backendResponse = "I am the backend"
|
2018-10-29 23:21:40 +00:00
|
|
|
|
|
|
|
|
// someConnHeader is some arbitrary header to be declared as a hop-by-hop header
|
|
|
|
|
// in the Request's Connection header.
|
|
|
|
|
const someConnHeader = "X-Some-Conn-Header"
|
|
|
|
|
|
2016-08-27 20:46:25 +04:30
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2019-05-14 15:11:30 +00:00
|
|
|
if c := r.Header.Get("Connection"); c != "" {
|
|
|
|
|
t.Errorf("handler got header %q = %q; want empty", "Connection", c)
|
|
|
|
|
}
|
2016-08-27 20:46:25 +04:30
|
|
|
if c := r.Header.Get(fakeConnectionToken); c != "" {
|
|
|
|
|
t.Errorf("handler got header %q = %q; want empty", fakeConnectionToken, c)
|
|
|
|
|
}
|
2018-10-29 23:21:40 +00:00
|
|
|
if c := r.Header.Get(someConnHeader); c != "" {
|
|
|
|
|
t.Errorf("handler got header %q = %q; want empty", someConnHeader, c)
|
2016-08-27 20:46:25 +04:30
|
|
|
}
|
2019-05-14 15:11:30 +00:00
|
|
|
w.Header().Add("Connection", "Upgrade, "+fakeConnectionToken)
|
|
|
|
|
w.Header().Add("Connection", someConnHeader)
|
2018-10-29 23:21:40 +00:00
|
|
|
w.Header().Set(someConnHeader, "should be deleted")
|
2016-09-08 11:39:12 +04:30
|
|
|
w.Header().Set(fakeConnectionToken, "should be deleted")
|
2016-08-27 20:46:25 +04:30
|
|
|
io.WriteString(w, backendResponse)
|
|
|
|
|
}))
|
|
|
|
|
defer backend.Close()
|
|
|
|
|
backendURL, err := url.Parse(backend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
proxyHandler := NewSingleHostReverseProxy(backendURL)
|
2016-09-04 12:20:14 +04:30
|
|
|
frontend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
proxyHandler.ServeHTTP(w, r)
|
2019-05-14 15:11:30 +00:00
|
|
|
if c := r.Header.Get(someConnHeader); c != "should be deleted" {
|
|
|
|
|
t.Errorf("handler modified header %q = %q; want %q", someConnHeader, c, "should be deleted")
|
|
|
|
|
}
|
|
|
|
|
if c := r.Header.Get(fakeConnectionToken); c != "should be deleted" {
|
|
|
|
|
t.Errorf("handler modified header %q = %q; want %q", fakeConnectionToken, c, "should be deleted")
|
|
|
|
|
}
|
|
|
|
|
c := r.Header["Connection"]
|
|
|
|
|
var cf []string
|
|
|
|
|
for _, f := range c {
|
|
|
|
|
for _, sf := range strings.Split(f, ",") {
|
|
|
|
|
if sf = strings.TrimSpace(sf); sf != "" {
|
|
|
|
|
cf = append(cf, sf)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sort.Strings(cf)
|
|
|
|
|
expectedValues := []string{"Upgrade", someConnHeader, fakeConnectionToken}
|
|
|
|
|
sort.Strings(expectedValues)
|
|
|
|
|
if !reflect.DeepEqual(cf, expectedValues) {
|
|
|
|
|
t.Errorf("handler modified header %q = %q; want %q", "Connection", cf, expectedValues)
|
2016-09-04 12:20:14 +04:30
|
|
|
}
|
|
|
|
|
}))
|
2016-08-27 20:46:25 +04:30
|
|
|
defer frontend.Close()
|
|
|
|
|
|
|
|
|
|
getReq, _ := http.NewRequest("GET", frontend.URL, nil)
|
2019-05-14 15:11:30 +00:00
|
|
|
getReq.Header.Add("Connection", "Upgrade, "+fakeConnectionToken)
|
|
|
|
|
getReq.Header.Add("Connection", someConnHeader)
|
|
|
|
|
getReq.Header.Set(someConnHeader, "should be deleted")
|
2016-08-27 20:46:25 +04:30
|
|
|
getReq.Header.Set(fakeConnectionToken, "should be deleted")
|
2017-03-04 18:24:44 +00:00
|
|
|
res, err := frontend.Client().Do(getReq)
|
2016-08-27 20:46:25 +04:30
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Get: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer res.Body.Close()
|
2020-10-16 00:49:02 -04:00
|
|
|
bodyBytes, err := io.ReadAll(res.Body)
|
2016-08-27 20:46:25 +04:30
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("reading body: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got, want := string(bodyBytes), backendResponse; got != want {
|
|
|
|
|
t.Errorf("got body %q; want %q", got, want)
|
|
|
|
|
}
|
2019-05-14 15:11:30 +00:00
|
|
|
if c := res.Header.Get("Connection"); c != "" {
|
|
|
|
|
t.Errorf("handler got header %q = %q; want empty", "Connection", c)
|
|
|
|
|
}
|
2018-10-29 23:21:40 +00:00
|
|
|
if c := res.Header.Get(someConnHeader); c != "" {
|
|
|
|
|
t.Errorf("handler got header %q = %q; want empty", someConnHeader, c)
|
2016-09-08 11:39:12 +04:30
|
|
|
}
|
|
|
|
|
if c := res.Header.Get(fakeConnectionToken); c != "" {
|
|
|
|
|
t.Errorf("handler got header %q = %q; want empty", fakeConnectionToken, c)
|
|
|
|
|
}
|
2016-08-27 20:46:25 +04:30
|
|
|
}
|
|
|
|
|
|
2021-05-21 14:02:30 -04:00
|
|
|
func TestReverseProxyStripEmptyConnection(t *testing.T) {
|
|
|
|
|
// See Issue 46313.
|
|
|
|
|
const backendResponse = "I am the backend"
|
|
|
|
|
|
|
|
|
|
// someConnHeader is some arbitrary header to be declared as a hop-by-hop header
|
|
|
|
|
// in the Request's Connection header.
|
|
|
|
|
const someConnHeader = "X-Some-Conn-Header"
|
|
|
|
|
|
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if c := r.Header.Values("Connection"); len(c) != 0 {
|
|
|
|
|
t.Errorf("handler got header %q = %v; want empty", "Connection", c)
|
|
|
|
|
}
|
|
|
|
|
if c := r.Header.Get(someConnHeader); c != "" {
|
|
|
|
|
t.Errorf("handler got header %q = %q; want empty", someConnHeader, c)
|
|
|
|
|
}
|
|
|
|
|
w.Header().Add("Connection", "")
|
|
|
|
|
w.Header().Add("Connection", someConnHeader)
|
|
|
|
|
w.Header().Set(someConnHeader, "should be deleted")
|
|
|
|
|
io.WriteString(w, backendResponse)
|
|
|
|
|
}))
|
|
|
|
|
defer backend.Close()
|
|
|
|
|
backendURL, err := url.Parse(backend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
proxyHandler := NewSingleHostReverseProxy(backendURL)
|
|
|
|
|
frontend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
proxyHandler.ServeHTTP(w, r)
|
|
|
|
|
if c := r.Header.Get(someConnHeader); c != "should be deleted" {
|
|
|
|
|
t.Errorf("handler modified header %q = %q; want %q", someConnHeader, c, "should be deleted")
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
defer frontend.Close()
|
|
|
|
|
|
|
|
|
|
getReq, _ := http.NewRequest("GET", frontend.URL, nil)
|
|
|
|
|
getReq.Header.Add("Connection", "")
|
|
|
|
|
getReq.Header.Add("Connection", someConnHeader)
|
|
|
|
|
getReq.Header.Set(someConnHeader, "should be deleted")
|
|
|
|
|
res, err := frontend.Client().Do(getReq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Get: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
bodyBytes, err := io.ReadAll(res.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("reading body: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got, want := string(bodyBytes), backendResponse; got != want {
|
|
|
|
|
t.Errorf("got body %q; want %q", got, want)
|
|
|
|
|
}
|
|
|
|
|
if c := res.Header.Get("Connection"); c != "" {
|
|
|
|
|
t.Errorf("handler got header %q = %q; want empty", "Connection", c)
|
|
|
|
|
}
|
|
|
|
|
if c := res.Header.Get(someConnHeader); c != "" {
|
|
|
|
|
t.Errorf("handler got header %q = %q; want empty", someConnHeader, c)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-07-31 08:38:49 +10:00
|
|
|
func TestXForwardedFor(t *testing.T) {
|
|
|
|
|
const prevForwardedFor = "client ip"
|
|
|
|
|
const backendResponse = "I am the backend"
|
|
|
|
|
const backendStatus = 404
|
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Header.Get("X-Forwarded-For") == "" {
|
|
|
|
|
t.Errorf("didn't get X-Forwarded-For header")
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(r.Header.Get("X-Forwarded-For"), prevForwardedFor) {
|
|
|
|
|
t.Errorf("X-Forwarded-For didn't contain prior data")
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(backendStatus)
|
|
|
|
|
w.Write([]byte(backendResponse))
|
|
|
|
|
}))
|
|
|
|
|
defer backend.Close()
|
|
|
|
|
backendURL, err := url.Parse(backend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
proxyHandler := NewSingleHostReverseProxy(backendURL)
|
|
|
|
|
frontend := httptest.NewServer(proxyHandler)
|
|
|
|
|
defer frontend.Close()
|
|
|
|
|
|
|
|
|
|
getReq, _ := http.NewRequest("GET", frontend.URL, nil)
|
|
|
|
|
getReq.Host = "some-name"
|
|
|
|
|
getReq.Header.Set("Connection", "close")
|
|
|
|
|
getReq.Header.Set("X-Forwarded-For", prevForwardedFor)
|
|
|
|
|
getReq.Close = true
|
2017-03-04 18:24:44 +00:00
|
|
|
res, err := frontend.Client().Do(getReq)
|
2012-07-31 08:38:49 +10:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Get: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if g, e := res.StatusCode, backendStatus; g != e {
|
|
|
|
|
t.Errorf("got res.StatusCode %d; expected %d", g, e)
|
|
|
|
|
}
|
2020-10-16 00:49:02 -04:00
|
|
|
bodyBytes, _ := io.ReadAll(res.Body)
|
2012-07-31 08:38:49 +10:00
|
|
|
if g, e := string(bodyBytes), backendResponse; g != e {
|
|
|
|
|
t.Errorf("got body %q; expected %q", g, e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-29 11:00:23 -07:00
|
|
|
// Issue 38079: don't append to X-Forwarded-For if it's present but nil
|
|
|
|
|
func TestXForwardedFor_Omit(t *testing.T) {
|
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if v := r.Header.Get("X-Forwarded-For"); v != "" {
|
|
|
|
|
t.Errorf("got X-Forwarded-For header: %q", v)
|
|
|
|
|
}
|
|
|
|
|
w.Write([]byte("hi"))
|
|
|
|
|
}))
|
|
|
|
|
defer backend.Close()
|
|
|
|
|
backendURL, err := url.Parse(backend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
proxyHandler := NewSingleHostReverseProxy(backendURL)
|
|
|
|
|
frontend := httptest.NewServer(proxyHandler)
|
|
|
|
|
defer frontend.Close()
|
|
|
|
|
|
|
|
|
|
oldDirector := proxyHandler.Director
|
|
|
|
|
proxyHandler.Director = func(r *http.Request) {
|
|
|
|
|
r.Header["X-Forwarded-For"] = nil
|
|
|
|
|
oldDirector(r)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getReq, _ := http.NewRequest("GET", frontend.URL, nil)
|
|
|
|
|
getReq.Host = "some-name"
|
|
|
|
|
getReq.Close = true
|
|
|
|
|
res, err := frontend.Client().Do(getReq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Get: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
}
|
|
|
|
|
|
2012-02-07 18:00:30 -08:00
|
|
|
var proxyQueryTests = []struct {
|
|
|
|
|
baseSuffix string // suffix to add to backend URL
|
|
|
|
|
reqSuffix string // suffix to add to frontend's request URL
|
|
|
|
|
want string // what backend should see for final request URL (without ?)
|
|
|
|
|
}{
|
|
|
|
|
{"", "", ""},
|
|
|
|
|
{"?sta=tic", "?us=er", "sta=tic&us=er"},
|
|
|
|
|
{"", "?us=er", "us=er"},
|
|
|
|
|
{"?sta=tic", "", "sta=tic"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestReverseProxyQuery(t *testing.T) {
|
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("X-Got-Query", r.URL.RawQuery)
|
|
|
|
|
w.Write([]byte("hi"))
|
|
|
|
|
}))
|
|
|
|
|
defer backend.Close()
|
|
|
|
|
|
|
|
|
|
for i, tt := range proxyQueryTests {
|
|
|
|
|
backendURL, err := url.Parse(backend.URL + tt.baseSuffix)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
frontend := httptest.NewServer(NewSingleHostReverseProxy(backendURL))
|
|
|
|
|
req, _ := http.NewRequest("GET", frontend.URL+tt.reqSuffix, nil)
|
|
|
|
|
req.Close = true
|
2017-03-04 18:24:44 +00:00
|
|
|
res, err := frontend.Client().Do(req)
|
2012-02-07 18:00:30 -08:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("%d. Get: %v", i, err)
|
|
|
|
|
}
|
|
|
|
|
if g, e := res.Header.Get("X-Got-Query"), tt.want; g != e {
|
|
|
|
|
t.Errorf("%d. got query %q; expected %q", i, g, e)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
frontend.Close()
|
|
|
|
|
}
|
|
|
|
|
}
|
2012-04-18 11:33:02 -07:00
|
|
|
|
|
|
|
|
func TestReverseProxyFlushInterval(t *testing.T) {
|
|
|
|
|
const expected = "hi"
|
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Write([]byte(expected))
|
|
|
|
|
}))
|
|
|
|
|
defer backend.Close()
|
|
|
|
|
|
|
|
|
|
backendURL, err := url.Parse(backend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
proxyHandler := NewSingleHostReverseProxy(backendURL)
|
|
|
|
|
proxyHandler.FlushInterval = time.Microsecond
|
|
|
|
|
|
|
|
|
|
frontend := httptest.NewServer(proxyHandler)
|
|
|
|
|
defer frontend.Close()
|
|
|
|
|
|
2012-04-20 09:31:23 -07:00
|
|
|
req, _ := http.NewRequest("GET", frontend.URL, nil)
|
|
|
|
|
req.Close = true
|
2017-03-04 18:24:44 +00:00
|
|
|
res, err := frontend.Client().Do(req)
|
2012-04-20 09:31:23 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Get: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer res.Body.Close()
|
2020-10-16 00:49:02 -04:00
|
|
|
if bodyBytes, _ := io.ReadAll(res.Body); string(bodyBytes) != expected {
|
2012-04-20 09:31:23 -07:00
|
|
|
t.Errorf("got body %q; expected %q", bodyBytes, expected)
|
2012-04-18 11:33:02 -07:00
|
|
|
}
|
|
|
|
|
}
|
2014-12-30 12:19:43 +00:00
|
|
|
|
2019-03-28 23:37:54 -04:00
|
|
|
func TestReverseProxyFlushIntervalHeaders(t *testing.T) {
|
|
|
|
|
const expected = "hi"
|
|
|
|
|
stopCh := make(chan struct{})
|
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Add("MyHeader", expected)
|
|
|
|
|
w.WriteHeader(200)
|
|
|
|
|
w.(http.Flusher).Flush()
|
|
|
|
|
<-stopCh
|
|
|
|
|
}))
|
|
|
|
|
defer backend.Close()
|
|
|
|
|
defer close(stopCh)
|
|
|
|
|
|
|
|
|
|
backendURL, err := url.Parse(backend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
proxyHandler := NewSingleHostReverseProxy(backendURL)
|
|
|
|
|
proxyHandler.FlushInterval = time.Microsecond
|
|
|
|
|
|
|
|
|
|
frontend := httptest.NewServer(proxyHandler)
|
|
|
|
|
defer frontend.Close()
|
|
|
|
|
|
|
|
|
|
req, _ := http.NewRequest("GET", frontend.URL, nil)
|
|
|
|
|
req.Close = true
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
req = req.WithContext(ctx)
|
|
|
|
|
|
|
|
|
|
res, err := frontend.Client().Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Get: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
|
|
if res.Header.Get("MyHeader") != expected {
|
|
|
|
|
t.Errorf("got header %q; expected %q", res.Header.Get("MyHeader"), expected)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-25 21:39:19 +00:00
|
|
|
func TestReverseProxyCancellation(t *testing.T) {
|
2014-12-30 12:19:43 +00:00
|
|
|
const backendResponse = "I am the backend"
|
|
|
|
|
|
|
|
|
|
reqInFlight := make(chan struct{})
|
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2016-09-02 05:00:05 +00:00
|
|
|
close(reqInFlight) // cause the client to cancel its request
|
2014-12-30 12:19:43 +00:00
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case <-time.After(10 * time.Second):
|
|
|
|
|
// Note: this should only happen in broken implementations, and the
|
|
|
|
|
// closenotify case should be instantaneous.
|
2016-09-02 05:00:05 +00:00
|
|
|
t.Error("Handler never saw CloseNotify")
|
|
|
|
|
return
|
2014-12-30 12:19:43 +00:00
|
|
|
case <-w.(http.CloseNotifier).CloseNotify():
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
w.Write([]byte(backendResponse))
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
defer backend.Close()
|
|
|
|
|
|
2020-10-16 00:49:02 -04:00
|
|
|
backend.Config.ErrorLog = log.New(io.Discard, "", 0)
|
2014-12-30 12:19:43 +00:00
|
|
|
|
|
|
|
|
backendURL, err := url.Parse(backend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
proxyHandler := NewSingleHostReverseProxy(backendURL)
|
|
|
|
|
|
|
|
|
|
// Discards errors of the form:
|
|
|
|
|
// http: proxy error: read tcp 127.0.0.1:44643: use of closed network connection
|
2020-10-16 00:49:02 -04:00
|
|
|
proxyHandler.ErrorLog = log.New(io.Discard, "", 0)
|
2014-12-30 12:19:43 +00:00
|
|
|
|
|
|
|
|
frontend := httptest.NewServer(proxyHandler)
|
|
|
|
|
defer frontend.Close()
|
2017-03-04 18:24:44 +00:00
|
|
|
frontendClient := frontend.Client()
|
2014-12-30 12:19:43 +00:00
|
|
|
|
|
|
|
|
getReq, _ := http.NewRequest("GET", frontend.URL, nil)
|
|
|
|
|
go func() {
|
|
|
|
|
<-reqInFlight
|
2017-03-04 18:24:44 +00:00
|
|
|
frontendClient.Transport.(*http.Transport).CancelRequest(getReq)
|
2014-12-30 12:19:43 +00:00
|
|
|
}()
|
2017-03-04 18:24:44 +00:00
|
|
|
res, err := frontendClient.Do(getReq)
|
2014-12-30 12:19:43 +00:00
|
|
|
if res != nil {
|
2016-09-04 14:52:59 -07:00
|
|
|
t.Errorf("got response %v; want nil", res.Status)
|
2014-12-30 12:19:43 +00:00
|
|
|
}
|
|
|
|
|
if err == nil {
|
|
|
|
|
// This should be an error like:
|
2019-08-28 15:50:31 +02:00
|
|
|
// Get "http://127.0.0.1:58079": read tcp 127.0.0.1:58079:
|
2014-12-30 12:19:43 +00:00
|
|
|
// use of closed network connection
|
2017-03-04 18:24:44 +00:00
|
|
|
t.Error("Server.Client().Do() returned nil error; want non-nil error")
|
2014-12-30 12:19:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
2015-08-26 10:53:59 -07:00
|
|
|
|
|
|
|
|
func req(t *testing.T, v string) *http.Request {
|
|
|
|
|
req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(v)))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return req
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Issue 12344
|
|
|
|
|
func TestNilBody(t *testing.T) {
|
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Write([]byte("hi"))
|
|
|
|
|
}))
|
|
|
|
|
defer backend.Close()
|
|
|
|
|
|
|
|
|
|
frontend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
|
|
|
backURL, _ := url.Parse(backend.URL)
|
|
|
|
|
rp := NewSingleHostReverseProxy(backURL)
|
|
|
|
|
r := req(t, "GET / HTTP/1.0\r\n\r\n")
|
|
|
|
|
r.Body = nil // this accidentally worked in Go 1.4 and below, so keep it working
|
|
|
|
|
rp.ServeHTTP(w, r)
|
|
|
|
|
}))
|
|
|
|
|
defer frontend.Close()
|
|
|
|
|
|
|
|
|
|
res, err := http.Get(frontend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
defer res.Body.Close()
|
2020-10-16 00:49:02 -04:00
|
|
|
slurp, err := io.ReadAll(res.Body)
|
2015-08-26 10:53:59 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if string(slurp) != "hi" {
|
|
|
|
|
t.Errorf("Got %q; want %q", slurp, "hi")
|
|
|
|
|
}
|
|
|
|
|
}
|
2015-04-27 18:10:20 -07:00
|
|
|
|
2016-05-16 15:30:28 +03:00
|
|
|
// Issue 15524
|
|
|
|
|
func TestUserAgentHeader(t *testing.T) {
|
|
|
|
|
const explicitUA = "explicit UA"
|
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.URL.Path == "/noua" {
|
|
|
|
|
if c := r.Header.Get("User-Agent"); c != "" {
|
|
|
|
|
t.Errorf("handler got non-empty User-Agent header %q", c)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if c := r.Header.Get("User-Agent"); c != explicitUA {
|
|
|
|
|
t.Errorf("handler got unexpected User-Agent header %q", c)
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
defer backend.Close()
|
|
|
|
|
backendURL, err := url.Parse(backend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
proxyHandler := NewSingleHostReverseProxy(backendURL)
|
2020-10-16 00:49:02 -04:00
|
|
|
proxyHandler.ErrorLog = log.New(io.Discard, "", 0) // quiet for tests
|
2016-05-16 15:30:28 +03:00
|
|
|
frontend := httptest.NewServer(proxyHandler)
|
|
|
|
|
defer frontend.Close()
|
2017-03-04 18:24:44 +00:00
|
|
|
frontendClient := frontend.Client()
|
2016-05-16 15:30:28 +03:00
|
|
|
|
|
|
|
|
getReq, _ := http.NewRequest("GET", frontend.URL, nil)
|
|
|
|
|
getReq.Header.Set("User-Agent", explicitUA)
|
|
|
|
|
getReq.Close = true
|
2017-03-04 18:24:44 +00:00
|
|
|
res, err := frontendClient.Do(getReq)
|
2016-05-16 15:30:28 +03:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Get: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
getReq, _ = http.NewRequest("GET", frontend.URL+"/noua", nil)
|
|
|
|
|
getReq.Header.Set("User-Agent", "")
|
|
|
|
|
getReq.Close = true
|
2017-03-04 18:24:44 +00:00
|
|
|
res, err = frontendClient.Do(getReq)
|
2016-05-16 15:30:28 +03:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Get: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-27 18:10:20 -07:00
|
|
|
type bufferPool struct {
|
|
|
|
|
get func() []byte
|
|
|
|
|
put func([]byte)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (bp bufferPool) Get() []byte { return bp.get() }
|
|
|
|
|
func (bp bufferPool) Put(v []byte) { bp.put(v) }
|
|
|
|
|
|
|
|
|
|
func TestReverseProxyGetPutBuffer(t *testing.T) {
|
|
|
|
|
const msg = "hi"
|
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
io.WriteString(w, msg)
|
|
|
|
|
}))
|
|
|
|
|
defer backend.Close()
|
|
|
|
|
|
|
|
|
|
backendURL, err := url.Parse(backend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
log []string
|
|
|
|
|
)
|
|
|
|
|
addLog := func(event string) {
|
|
|
|
|
mu.Lock()
|
|
|
|
|
defer mu.Unlock()
|
|
|
|
|
log = append(log, event)
|
|
|
|
|
}
|
|
|
|
|
rp := NewSingleHostReverseProxy(backendURL)
|
|
|
|
|
const size = 1234
|
|
|
|
|
rp.BufferPool = bufferPool{
|
|
|
|
|
get: func() []byte {
|
|
|
|
|
addLog("getBuf")
|
|
|
|
|
return make([]byte, size)
|
|
|
|
|
},
|
|
|
|
|
put: func(p []byte) {
|
|
|
|
|
addLog("putBuf-" + strconv.Itoa(len(p)))
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
frontend := httptest.NewServer(rp)
|
|
|
|
|
defer frontend.Close()
|
|
|
|
|
|
|
|
|
|
req, _ := http.NewRequest("GET", frontend.URL, nil)
|
|
|
|
|
req.Close = true
|
2017-03-04 18:24:44 +00:00
|
|
|
res, err := frontend.Client().Do(req)
|
2015-04-27 18:10:20 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Get: %v", err)
|
|
|
|
|
}
|
2020-10-16 00:49:02 -04:00
|
|
|
slurp, err := io.ReadAll(res.Body)
|
2015-04-27 18:10:20 -07:00
|
|
|
res.Body.Close()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("reading body: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if string(slurp) != msg {
|
|
|
|
|
t.Errorf("msg = %q; want %q", slurp, msg)
|
|
|
|
|
}
|
|
|
|
|
wantLog := []string{"getBuf", "putBuf-" + strconv.Itoa(size)}
|
|
|
|
|
mu.Lock()
|
|
|
|
|
defer mu.Unlock()
|
|
|
|
|
if !reflect.DeepEqual(log, wantLog) {
|
|
|
|
|
t.Errorf("Log events = %q; want %q", log, wantLog)
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-01-04 20:38:20 +00:00
|
|
|
|
|
|
|
|
func TestReverseProxy_Post(t *testing.T) {
|
|
|
|
|
const backendResponse = "I am the backend"
|
|
|
|
|
const backendStatus = 200
|
|
|
|
|
var requestBody = bytes.Repeat([]byte("a"), 1<<20)
|
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2020-10-16 00:49:02 -04:00
|
|
|
slurp, err := io.ReadAll(r.Body)
|
2016-01-04 20:38:20 +00:00
|
|
|
if err != nil {
|
2016-01-07 09:36:52 +09:00
|
|
|
t.Errorf("Backend body read = %v", err)
|
2016-01-04 20:38:20 +00:00
|
|
|
}
|
|
|
|
|
if len(slurp) != len(requestBody) {
|
|
|
|
|
t.Errorf("Backend read %d request body bytes; want %d", len(slurp), len(requestBody))
|
|
|
|
|
}
|
|
|
|
|
if !bytes.Equal(slurp, requestBody) {
|
|
|
|
|
t.Error("Backend read wrong request body.") // 1MB; omitting details
|
|
|
|
|
}
|
|
|
|
|
w.Write([]byte(backendResponse))
|
|
|
|
|
}))
|
|
|
|
|
defer backend.Close()
|
|
|
|
|
backendURL, err := url.Parse(backend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
proxyHandler := NewSingleHostReverseProxy(backendURL)
|
|
|
|
|
frontend := httptest.NewServer(proxyHandler)
|
|
|
|
|
defer frontend.Close()
|
|
|
|
|
|
|
|
|
|
postReq, _ := http.NewRequest("POST", frontend.URL, bytes.NewReader(requestBody))
|
2017-03-04 18:24:44 +00:00
|
|
|
res, err := frontend.Client().Do(postReq)
|
2016-01-04 20:38:20 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Do: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if g, e := res.StatusCode, backendStatus; g != e {
|
|
|
|
|
t.Errorf("got res.StatusCode %d; expected %d", g, e)
|
|
|
|
|
}
|
2020-10-16 00:49:02 -04:00
|
|
|
bodyBytes, _ := io.ReadAll(res.Body)
|
2016-01-04 20:38:20 +00:00
|
|
|
if g, e := string(bodyBytes), backendResponse; g != e {
|
|
|
|
|
t.Errorf("got body %q; expected %q", g, e)
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-09-02 05:00:05 +00:00
|
|
|
|
|
|
|
|
type RoundTripperFunc func(*http.Request) (*http.Response, error)
|
|
|
|
|
|
|
|
|
|
func (fn RoundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
|
|
|
return fn(req)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Issue 16036: send a Request with a nil Body when possible
|
|
|
|
|
func TestReverseProxy_NilBody(t *testing.T) {
|
|
|
|
|
backendURL, _ := url.Parse("http://fake.tld/")
|
|
|
|
|
proxyHandler := NewSingleHostReverseProxy(backendURL)
|
2020-10-16 00:49:02 -04:00
|
|
|
proxyHandler.ErrorLog = log.New(io.Discard, "", 0) // quiet for tests
|
2016-09-02 05:00:05 +00:00
|
|
|
proxyHandler.Transport = RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
|
|
|
|
if req.Body != nil {
|
|
|
|
|
t.Error("Body != nil; want a nil Body")
|
|
|
|
|
}
|
|
|
|
|
return nil, errors.New("done testing the interesting part; so force a 502 Gateway error")
|
|
|
|
|
})
|
|
|
|
|
frontend := httptest.NewServer(proxyHandler)
|
|
|
|
|
defer frontend.Close()
|
|
|
|
|
|
2017-03-04 18:24:44 +00:00
|
|
|
res, err := frontend.Client().Get(frontend.URL)
|
2016-09-02 05:00:05 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
if res.StatusCode != 502 {
|
|
|
|
|
t.Errorf("status code = %v; want 502 (Gateway Error)", res.Status)
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-10-08 06:57:49 -04:00
|
|
|
|
2019-07-16 17:40:20 -07:00
|
|
|
// Issue 33142: always allocate the request headers
|
|
|
|
|
func TestReverseProxy_AllocatedHeader(t *testing.T) {
|
|
|
|
|
proxyHandler := new(ReverseProxy)
|
2020-10-16 00:49:02 -04:00
|
|
|
proxyHandler.ErrorLog = log.New(io.Discard, "", 0) // quiet for tests
|
|
|
|
|
proxyHandler.Director = func(*http.Request) {} // noop
|
2019-07-16 17:40:20 -07:00
|
|
|
proxyHandler.Transport = RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
|
|
|
|
if req.Header == nil {
|
|
|
|
|
t.Error("Header == nil; want a non-nil Header")
|
|
|
|
|
}
|
|
|
|
|
return nil, errors.New("done testing the interesting part; so force a 502 Gateway error")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
proxyHandler.ServeHTTP(httptest.NewRecorder(), &http.Request{
|
|
|
|
|
Method: "GET",
|
|
|
|
|
URL: &url.URL{Scheme: "http", Host: "fake.tld", Path: "/"},
|
|
|
|
|
Proto: "HTTP/1.0",
|
|
|
|
|
ProtoMajor: 1,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-28 08:11:39 -07:00
|
|
|
// Issue 14237. Test ModifyResponse and that an error from it
|
|
|
|
|
// causes the proxy to return StatusBadGateway, or StatusOK otherwise.
|
|
|
|
|
func TestReverseProxyModifyResponse(t *testing.T) {
|
|
|
|
|
backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Add("X-Hit-Mod", fmt.Sprintf("%v", r.URL.Path == "/mod"))
|
|
|
|
|
}))
|
|
|
|
|
defer backendServer.Close()
|
|
|
|
|
|
|
|
|
|
rpURL, _ := url.Parse(backendServer.URL)
|
|
|
|
|
rproxy := NewSingleHostReverseProxy(rpURL)
|
2020-10-16 00:49:02 -04:00
|
|
|
rproxy.ErrorLog = log.New(io.Discard, "", 0) // quiet for tests
|
2016-10-28 08:11:39 -07:00
|
|
|
rproxy.ModifyResponse = func(resp *http.Response) error {
|
|
|
|
|
if resp.Header.Get("X-Hit-Mod") != "true" {
|
|
|
|
|
return fmt.Errorf("tried to by-pass proxy")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
frontendProxy := httptest.NewServer(rproxy)
|
|
|
|
|
defer frontendProxy.Close()
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
url string
|
|
|
|
|
wantCode int
|
|
|
|
|
}{
|
|
|
|
|
{frontendProxy.URL + "/mod", http.StatusOK},
|
|
|
|
|
{frontendProxy.URL + "/schedule", http.StatusBadGateway},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i, tt := range tests {
|
|
|
|
|
resp, err := http.Get(tt.url)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to reach proxy: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if g, e := resp.StatusCode, tt.wantCode; g != e {
|
|
|
|
|
t.Errorf("#%d: got res.StatusCode %d; expected %d", i, g, e)
|
|
|
|
|
}
|
|
|
|
|
resp.Body.Close()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-13 23:32:07 +01:00
|
|
|
type failingRoundTripper struct{}
|
|
|
|
|
|
|
|
|
|
func (failingRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
|
|
|
|
|
return nil, errors.New("some error")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type staticResponseRoundTripper struct{ res *http.Response }
|
|
|
|
|
|
|
|
|
|
func (rt staticResponseRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
|
|
|
|
|
return rt.res, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestReverseProxyErrorHandler(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
wantCode int
|
|
|
|
|
errorHandler func(http.ResponseWriter, *http.Request, error)
|
|
|
|
|
transport http.RoundTripper // defaults to failingRoundTripper
|
|
|
|
|
modifyResponse func(*http.Response) error
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "default",
|
|
|
|
|
wantCode: http.StatusBadGateway,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "errorhandler",
|
|
|
|
|
wantCode: http.StatusTeapot,
|
|
|
|
|
errorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { rw.WriteHeader(http.StatusTeapot) },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "modifyresponse_noerr",
|
|
|
|
|
transport: staticResponseRoundTripper{
|
|
|
|
|
&http.Response{StatusCode: 345, Body: http.NoBody},
|
|
|
|
|
},
|
|
|
|
|
modifyResponse: func(res *http.Response) error {
|
|
|
|
|
res.StatusCode++
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
errorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { rw.WriteHeader(http.StatusTeapot) },
|
|
|
|
|
wantCode: 346,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "modifyresponse_err",
|
|
|
|
|
transport: staticResponseRoundTripper{
|
|
|
|
|
&http.Response{StatusCode: 345, Body: http.NoBody},
|
|
|
|
|
},
|
|
|
|
|
modifyResponse: func(res *http.Response) error {
|
|
|
|
|
res.StatusCode++
|
|
|
|
|
return errors.New("some error to trigger errorHandler")
|
|
|
|
|
},
|
|
|
|
|
errorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { rw.WriteHeader(http.StatusTeapot) },
|
|
|
|
|
wantCode: http.StatusTeapot,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
target := &url.URL{
|
|
|
|
|
Scheme: "http",
|
|
|
|
|
Host: "dummy.tld",
|
|
|
|
|
Path: "/",
|
|
|
|
|
}
|
|
|
|
|
rproxy := NewSingleHostReverseProxy(target)
|
|
|
|
|
rproxy.Transport = tt.transport
|
|
|
|
|
rproxy.ModifyResponse = tt.modifyResponse
|
|
|
|
|
if rproxy.Transport == nil {
|
|
|
|
|
rproxy.Transport = failingRoundTripper{}
|
|
|
|
|
}
|
2020-10-16 00:49:02 -04:00
|
|
|
rproxy.ErrorLog = log.New(io.Discard, "", 0) // quiet for tests
|
2017-11-13 23:32:07 +01:00
|
|
|
if tt.errorHandler != nil {
|
|
|
|
|
rproxy.ErrorHandler = tt.errorHandler
|
|
|
|
|
}
|
|
|
|
|
frontendProxy := httptest.NewServer(rproxy)
|
|
|
|
|
defer frontendProxy.Close()
|
|
|
|
|
|
|
|
|
|
resp, err := http.Get(frontendProxy.URL + "/test")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to reach proxy: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if g, e := resp.StatusCode, tt.wantCode; g != e {
|
|
|
|
|
t.Errorf("got res.StatusCode %d; expected %d", g, e)
|
|
|
|
|
}
|
|
|
|
|
resp.Body.Close()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-08 06:57:49 -04:00
|
|
|
// Issue 16659: log errors from short read
|
|
|
|
|
func TestReverseProxy_CopyBuffer(t *testing.T) {
|
|
|
|
|
backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
out := "this call was relayed by the reverse proxy"
|
|
|
|
|
// Coerce a wrong content length to induce io.UnexpectedEOF
|
|
|
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(out)*2))
|
|
|
|
|
fmt.Fprintln(w, out)
|
|
|
|
|
}))
|
|
|
|
|
defer backendServer.Close()
|
|
|
|
|
|
|
|
|
|
rpURL, err := url.Parse(backendServer.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var proxyLog bytes.Buffer
|
|
|
|
|
rproxy := NewSingleHostReverseProxy(rpURL)
|
|
|
|
|
rproxy.ErrorLog = log.New(&proxyLog, "", log.Lshortfile)
|
2018-02-02 13:45:19 -05:00
|
|
|
donec := make(chan bool, 1)
|
|
|
|
|
frontendProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
defer func() { donec <- true }()
|
|
|
|
|
rproxy.ServeHTTP(w, r)
|
|
|
|
|
}))
|
2016-10-08 06:57:49 -04:00
|
|
|
defer frontendProxy.Close()
|
|
|
|
|
|
2018-02-02 13:45:19 -05:00
|
|
|
if _, err = frontendProxy.Client().Get(frontendProxy.URL); err == nil {
|
2016-10-08 06:57:49 -04:00
|
|
|
t.Fatalf("want non-nil error")
|
|
|
|
|
}
|
2018-02-02 13:45:19 -05:00
|
|
|
// The race detector complains about the proxyLog usage in logf in copyBuffer
|
|
|
|
|
// and our usage below with proxyLog.Bytes() so we're explicitly using a
|
|
|
|
|
// channel to ensure that the ReverseProxy's ServeHTTP is done before we
|
|
|
|
|
// continue after Get.
|
|
|
|
|
<-donec
|
|
|
|
|
|
2016-10-08 06:57:49 -04:00
|
|
|
expected := []string{
|
|
|
|
|
"EOF",
|
|
|
|
|
"read",
|
|
|
|
|
}
|
|
|
|
|
for _, phrase := range expected {
|
|
|
|
|
if !bytes.Contains(proxyLog.Bytes(), []byte(phrase)) {
|
|
|
|
|
t.Errorf("expected log to contain phrase %q", phrase)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-01-23 11:03:03 -08:00
|
|
|
|
|
|
|
|
type staticTransport struct {
|
|
|
|
|
res *http.Response
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *staticTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
|
|
|
return t.res, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func BenchmarkServeHTTP(b *testing.B) {
|
|
|
|
|
res := &http.Response{
|
|
|
|
|
StatusCode: 200,
|
2020-10-16 00:49:02 -04:00
|
|
|
Body: io.NopCloser(strings.NewReader("")),
|
2017-01-23 11:03:03 -08:00
|
|
|
}
|
|
|
|
|
proxy := &ReverseProxy{
|
|
|
|
|
Director: func(*http.Request) {},
|
|
|
|
|
Transport: &staticTransport{res},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
r := httptest.NewRequest("GET", "/", nil)
|
|
|
|
|
|
|
|
|
|
b.ReportAllocs()
|
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
|
|
|
proxy.ServeHTTP(w, r)
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-04-21 11:49:19 -06:00
|
|
|
|
|
|
|
|
func TestServeHTTPDeepCopy(t *testing.T) {
|
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Write([]byte("Hello Gopher!"))
|
|
|
|
|
}))
|
|
|
|
|
defer backend.Close()
|
|
|
|
|
backendURL, err := url.Parse(backend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type result struct {
|
|
|
|
|
before, after string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resultChan := make(chan result, 1)
|
|
|
|
|
proxyHandler := NewSingleHostReverseProxy(backendURL)
|
|
|
|
|
frontend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
before := r.URL.String()
|
|
|
|
|
proxyHandler.ServeHTTP(w, r)
|
|
|
|
|
after := r.URL.String()
|
|
|
|
|
resultChan <- result{before: before, after: after}
|
|
|
|
|
}))
|
|
|
|
|
defer frontend.Close()
|
|
|
|
|
|
|
|
|
|
want := result{before: "/", after: "/"}
|
|
|
|
|
|
|
|
|
|
res, err := frontend.Client().Get(frontend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Do: %v", err)
|
|
|
|
|
}
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
got := <-resultChan
|
|
|
|
|
if got != want {
|
|
|
|
|
t.Errorf("got = %+v; want = %+v", got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-06-26 19:07:24 +00:00
|
|
|
|
|
|
|
|
// Issue 18327: verify we always do a deep copy of the Request.Header map
|
|
|
|
|
// before any mutations.
|
|
|
|
|
func TestClonesRequestHeaders(t *testing.T) {
|
2020-10-16 00:49:02 -04:00
|
|
|
log.SetOutput(io.Discard)
|
2018-05-30 11:35:11 -04:00
|
|
|
defer log.SetOutput(os.Stderr)
|
2017-06-26 19:07:24 +00:00
|
|
|
req, _ := http.NewRequest("GET", "http://foo.tld/", nil)
|
|
|
|
|
req.RemoteAddr = "1.2.3.4:56789"
|
|
|
|
|
rp := &ReverseProxy{
|
|
|
|
|
Director: func(req *http.Request) {
|
|
|
|
|
req.Header.Set("From-Director", "1")
|
|
|
|
|
},
|
|
|
|
|
Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
|
|
|
|
if v := req.Header.Get("From-Director"); v != "1" {
|
|
|
|
|
t.Errorf("From-Directory value = %q; want 1", v)
|
|
|
|
|
}
|
|
|
|
|
return nil, io.EOF
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
rp.ServeHTTP(httptest.NewRecorder(), req)
|
|
|
|
|
|
|
|
|
|
if req.Header.Get("From-Director") == "1" {
|
|
|
|
|
t.Error("Director header mutation modified caller's request")
|
|
|
|
|
}
|
|
|
|
|
if req.Header.Get("X-Forwarded-For") != "" {
|
|
|
|
|
t.Error("X-Forward-For header mutation modified caller's request")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type roundTripperFunc func(req *http.Request) (*http.Response, error)
|
|
|
|
|
|
|
|
|
|
func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
|
|
|
return fn(req)
|
|
|
|
|
}
|
2017-11-11 10:10:14 +02:00
|
|
|
|
|
|
|
|
func TestModifyResponseClosesBody(t *testing.T) {
|
|
|
|
|
req, _ := http.NewRequest("GET", "http://foo.tld/", nil)
|
|
|
|
|
req.RemoteAddr = "1.2.3.4:56789"
|
|
|
|
|
closeCheck := new(checkCloser)
|
|
|
|
|
logBuf := new(bytes.Buffer)
|
|
|
|
|
outErr := errors.New("ModifyResponse error")
|
|
|
|
|
rp := &ReverseProxy{
|
|
|
|
|
Director: func(req *http.Request) {},
|
|
|
|
|
Transport: &staticTransport{&http.Response{
|
|
|
|
|
StatusCode: 200,
|
|
|
|
|
Body: closeCheck,
|
|
|
|
|
}},
|
|
|
|
|
ErrorLog: log.New(logBuf, "", 0),
|
|
|
|
|
ModifyResponse: func(*http.Response) error {
|
|
|
|
|
return outErr
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
|
rp.ServeHTTP(rec, req)
|
|
|
|
|
res := rec.Result()
|
|
|
|
|
if g, e := res.StatusCode, http.StatusBadGateway; g != e {
|
|
|
|
|
t.Errorf("got res.StatusCode %d; expected %d", g, e)
|
|
|
|
|
}
|
|
|
|
|
if !closeCheck.closed {
|
|
|
|
|
t.Errorf("body should have been closed")
|
|
|
|
|
}
|
|
|
|
|
if g, e := logBuf.String(), outErr.Error(); !strings.Contains(g, e) {
|
|
|
|
|
t.Errorf("ErrorLog %q does not contain %q", g, e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type checkCloser struct {
|
|
|
|
|
closed bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (cc *checkCloser) Close() error {
|
|
|
|
|
cc.closed = true
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (cc *checkCloser) Read(b []byte) (int, error) {
|
|
|
|
|
return len(b), nil
|
|
|
|
|
}
|
2018-02-02 13:45:19 -05:00
|
|
|
|
|
|
|
|
// Issue 23643: panic on body copy error
|
|
|
|
|
func TestReverseProxy_PanicBodyError(t *testing.T) {
|
2020-10-16 00:49:02 -04:00
|
|
|
log.SetOutput(io.Discard)
|
2018-05-30 11:35:11 -04:00
|
|
|
defer log.SetOutput(os.Stderr)
|
2018-02-02 13:45:19 -05:00
|
|
|
backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
out := "this call was relayed by the reverse proxy"
|
|
|
|
|
// Coerce a wrong content length to induce io.ErrUnexpectedEOF
|
|
|
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(out)*2))
|
|
|
|
|
fmt.Fprintln(w, out)
|
|
|
|
|
}))
|
|
|
|
|
defer backendServer.Close()
|
|
|
|
|
|
|
|
|
|
rpURL, err := url.Parse(backendServer.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rproxy := NewSingleHostReverseProxy(rpURL)
|
|
|
|
|
|
|
|
|
|
// Ensure that the handler panics when the body read encounters an
|
|
|
|
|
// io.ErrUnexpectedEOF
|
|
|
|
|
defer func() {
|
|
|
|
|
err := recover()
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("handler should have panicked")
|
|
|
|
|
}
|
|
|
|
|
if err != http.ErrAbortHandler {
|
|
|
|
|
t.Fatal("expected ErrAbortHandler, got", err)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
req, _ := http.NewRequest("GET", "http://foo.tld/", nil)
|
|
|
|
|
rproxy.ServeHTTP(httptest.NewRecorder(), req)
|
|
|
|
|
}
|
2018-09-25 16:13:17 +00:00
|
|
|
|
2021-07-07 16:34:34 -07:00
|
|
|
// Issue #46866: panic without closing incoming request body causes a panic
|
|
|
|
|
func TestReverseProxy_PanicClosesIncomingBody(t *testing.T) {
|
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
out := "this call was relayed by the reverse proxy"
|
|
|
|
|
// Coerce a wrong content length to induce io.ErrUnexpectedEOF
|
|
|
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(out)*2))
|
|
|
|
|
fmt.Fprintln(w, out)
|
|
|
|
|
}))
|
|
|
|
|
defer backend.Close()
|
|
|
|
|
backendURL, err := url.Parse(backend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
proxyHandler := NewSingleHostReverseProxy(backendURL)
|
|
|
|
|
proxyHandler.ErrorLog = log.New(io.Discard, "", 0) // quiet for tests
|
|
|
|
|
frontend := httptest.NewServer(proxyHandler)
|
|
|
|
|
defer frontend.Close()
|
|
|
|
|
frontendClient := frontend.Client()
|
|
|
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
for i := 0; i < 2; i++ {
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
for j := 0; j < 10; j++ {
|
|
|
|
|
const reqLen = 6 * 1024 * 1024
|
|
|
|
|
req, _ := http.NewRequest("POST", frontend.URL, &io.LimitedReader{R: neverEnding('x'), N: reqLen})
|
|
|
|
|
req.ContentLength = reqLen
|
|
|
|
|
resp, _ := frontendClient.Transport.RoundTrip(req)
|
|
|
|
|
if resp != nil {
|
|
|
|
|
io.Copy(io.Discard, resp.Body)
|
|
|
|
|
resp.Body.Close()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
wg.Wait()
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-25 16:13:17 +00:00
|
|
|
func TestSelectFlushInterval(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
p *ReverseProxy
|
|
|
|
|
res *http.Response
|
|
|
|
|
want time.Duration
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "default",
|
|
|
|
|
res: &http.Response{},
|
|
|
|
|
p: &ReverseProxy{FlushInterval: 123},
|
|
|
|
|
want: 123,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "server-sent events overrides non-zero",
|
|
|
|
|
res: &http.Response{
|
|
|
|
|
Header: http.Header{
|
|
|
|
|
"Content-Type": {"text/event-stream"},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
p: &ReverseProxy{FlushInterval: 123},
|
|
|
|
|
want: -1,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "server-sent events overrides zero",
|
|
|
|
|
res: &http.Response{
|
|
|
|
|
Header: http.Header{
|
|
|
|
|
"Content-Type": {"text/event-stream"},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
p: &ReverseProxy{FlushInterval: 0},
|
|
|
|
|
want: -1,
|
|
|
|
|
},
|
2020-10-08 20:32:50 +00:00
|
|
|
{
|
|
|
|
|
name: "Content-Length: -1, overrides non-zero",
|
|
|
|
|
res: &http.Response{
|
|
|
|
|
ContentLength: -1,
|
|
|
|
|
},
|
|
|
|
|
p: &ReverseProxy{FlushInterval: 123},
|
|
|
|
|
want: -1,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "Content-Length: -1, overrides zero",
|
|
|
|
|
res: &http.Response{
|
|
|
|
|
ContentLength: -1,
|
|
|
|
|
},
|
|
|
|
|
p: &ReverseProxy{FlushInterval: 0},
|
|
|
|
|
want: -1,
|
|
|
|
|
},
|
2018-09-25 16:13:17 +00:00
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
2020-10-08 20:32:50 +00:00
|
|
|
got := tt.p.flushInterval(tt.res)
|
2018-09-25 16:13:17 +00:00
|
|
|
if got != tt.want {
|
|
|
|
|
t.Errorf("flushLatency = %v; want %v", got, tt.want)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-10-29 23:21:40 +00:00
|
|
|
|
|
|
|
|
func TestReverseProxyWebSocket(t *testing.T) {
|
|
|
|
|
backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if upgradeType(r.Header) != "websocket" {
|
|
|
|
|
t.Error("unexpected backend request")
|
|
|
|
|
http.Error(w, "unexpected request", 400)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c, _, err := w.(http.Hijacker).Hijack()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Error(err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer c.Close()
|
|
|
|
|
io.WriteString(c, "HTTP/1.1 101 Switching Protocols\r\nConnection: upgrade\r\nUpgrade: WebSocket\r\n\r\n")
|
|
|
|
|
bs := bufio.NewScanner(c)
|
|
|
|
|
if !bs.Scan() {
|
|
|
|
|
t.Errorf("backend failed to read line from client: %v", bs.Err())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
fmt.Fprintf(c, "backend got %q\n", bs.Text())
|
|
|
|
|
}))
|
|
|
|
|
defer backendServer.Close()
|
|
|
|
|
|
|
|
|
|
backURL, _ := url.Parse(backendServer.URL)
|
|
|
|
|
rproxy := NewSingleHostReverseProxy(backURL)
|
2020-10-16 00:49:02 -04:00
|
|
|
rproxy.ErrorLog = log.New(io.Discard, "", 0) // quiet for tests
|
2019-01-09 15:06:20 +00:00
|
|
|
rproxy.ModifyResponse = func(res *http.Response) error {
|
|
|
|
|
res.Header.Add("X-Modified", "true")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2018-10-29 23:21:40 +00:00
|
|
|
|
2019-01-02 16:29:49 +00:00
|
|
|
handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
|
rw.Header().Set("X-Header", "X-Value")
|
|
|
|
|
rproxy.ServeHTTP(rw, req)
|
2020-09-26 13:21:41 +08:00
|
|
|
if got, want := rw.Header().Get("X-Modified"), "true"; got != want {
|
|
|
|
|
t.Errorf("response writer X-Modified header = %q; want %q", got, want)
|
|
|
|
|
}
|
2019-01-02 16:29:49 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
frontendProxy := httptest.NewServer(handler)
|
2018-10-29 23:21:40 +00:00
|
|
|
defer frontendProxy.Close()
|
|
|
|
|
|
|
|
|
|
req, _ := http.NewRequest("GET", frontendProxy.URL, nil)
|
|
|
|
|
req.Header.Set("Connection", "Upgrade")
|
|
|
|
|
req.Header.Set("Upgrade", "websocket")
|
|
|
|
|
|
|
|
|
|
c := frontendProxy.Client()
|
|
|
|
|
res, err := c.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if res.StatusCode != 101 {
|
|
|
|
|
t.Fatalf("status = %v; want 101", res.Status)
|
|
|
|
|
}
|
2019-01-02 16:29:49 +00:00
|
|
|
|
|
|
|
|
got := res.Header.Get("X-Header")
|
|
|
|
|
want := "X-Value"
|
|
|
|
|
if got != want {
|
|
|
|
|
t.Errorf("Header(XHeader) = %q; want %q", got, want)
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-07 14:36:40 +02:00
|
|
|
if !ascii.EqualFold(upgradeType(res.Header), "websocket") {
|
2018-10-29 23:21:40 +00:00
|
|
|
t.Fatalf("not websocket upgrade; got %#v", res.Header)
|
|
|
|
|
}
|
|
|
|
|
rwc, ok := res.Body.(io.ReadWriteCloser)
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("response body is of type %T; does not implement ReadWriteCloser", res.Body)
|
|
|
|
|
}
|
|
|
|
|
defer rwc.Close()
|
|
|
|
|
|
2019-01-09 15:06:20 +00:00
|
|
|
if got, want := res.Header.Get("X-Modified"), "true"; got != want {
|
|
|
|
|
t.Errorf("response X-Modified header = %q; want %q", got, want)
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-29 23:21:40 +00:00
|
|
|
io.WriteString(rwc, "Hello\n")
|
|
|
|
|
bs := bufio.NewScanner(rwc)
|
|
|
|
|
if !bs.Scan() {
|
|
|
|
|
t.Fatalf("Scan: %v", bs.Err())
|
|
|
|
|
}
|
2019-01-02 16:29:49 +00:00
|
|
|
got = bs.Text()
|
|
|
|
|
want = `backend got "Hello"`
|
2018-10-29 23:21:40 +00:00
|
|
|
if got != want {
|
|
|
|
|
t.Errorf("got %#q, want %#q", got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-12-03 20:46:23 +00:00
|
|
|
|
2021-05-07 10:47:28 +00:00
|
|
|
func TestReverseProxyWebSocketCancellation(t *testing.T) {
|
2020-04-26 09:11:35 +00:00
|
|
|
n := 5
|
|
|
|
|
triggerCancelCh := make(chan bool, n)
|
|
|
|
|
nthResponse := func(i int) string {
|
|
|
|
|
return fmt.Sprintf("backend response #%d\n", i)
|
|
|
|
|
}
|
|
|
|
|
terminalMsg := "final message"
|
|
|
|
|
|
|
|
|
|
cst := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if g, ws := upgradeType(r.Header), "websocket"; g != ws {
|
|
|
|
|
t.Errorf("Unexpected upgrade type %q, want %q", g, ws)
|
|
|
|
|
http.Error(w, "Unexpected request", 400)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
conn, bufrw, err := w.(http.Hijacker).Hijack()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Error(err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
|
|
|
|
upgradeMsg := "HTTP/1.1 101 Switching Protocols\r\nConnection: upgrade\r\nUpgrade: WebSocket\r\n\r\n"
|
|
|
|
|
if _, err := io.WriteString(conn, upgradeMsg); err != nil {
|
|
|
|
|
t.Error(err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if _, _, err := bufrw.ReadLine(); err != nil {
|
|
|
|
|
t.Errorf("Failed to read line from client: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i := 0; i < n; i++ {
|
|
|
|
|
if _, err := bufrw.WriteString(nthResponse(i)); err != nil {
|
2020-05-04 17:50:17 -07:00
|
|
|
select {
|
|
|
|
|
case <-triggerCancelCh:
|
|
|
|
|
default:
|
|
|
|
|
t.Errorf("Writing response #%d failed: %v", i, err)
|
|
|
|
|
}
|
|
|
|
|
return
|
2020-04-26 09:11:35 +00:00
|
|
|
}
|
|
|
|
|
bufrw.Flush()
|
|
|
|
|
time.Sleep(time.Second)
|
|
|
|
|
}
|
|
|
|
|
if _, err := bufrw.WriteString(terminalMsg); err != nil {
|
2020-05-04 17:50:17 -07:00
|
|
|
select {
|
|
|
|
|
case <-triggerCancelCh:
|
|
|
|
|
default:
|
|
|
|
|
t.Errorf("Failed to write terminal message: %v", err)
|
|
|
|
|
}
|
2020-04-26 09:11:35 +00:00
|
|
|
}
|
|
|
|
|
bufrw.Flush()
|
|
|
|
|
}))
|
|
|
|
|
defer cst.Close()
|
|
|
|
|
|
|
|
|
|
backendURL, _ := url.Parse(cst.URL)
|
|
|
|
|
rproxy := NewSingleHostReverseProxy(backendURL)
|
2020-10-16 00:49:02 -04:00
|
|
|
rproxy.ErrorLog = log.New(io.Discard, "", 0) // quiet for tests
|
2020-04-26 09:11:35 +00:00
|
|
|
rproxy.ModifyResponse = func(res *http.Response) error {
|
|
|
|
|
res.Header.Add("X-Modified", "true")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
|
rw.Header().Set("X-Header", "X-Value")
|
|
|
|
|
ctx, cancel := context.WithCancel(req.Context())
|
|
|
|
|
go func() {
|
|
|
|
|
<-triggerCancelCh
|
|
|
|
|
cancel()
|
|
|
|
|
}()
|
|
|
|
|
rproxy.ServeHTTP(rw, req.WithContext(ctx))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
frontendProxy := httptest.NewServer(handler)
|
|
|
|
|
defer frontendProxy.Close()
|
|
|
|
|
|
|
|
|
|
req, _ := http.NewRequest("GET", frontendProxy.URL, nil)
|
|
|
|
|
req.Header.Set("Connection", "Upgrade")
|
|
|
|
|
req.Header.Set("Upgrade", "websocket")
|
|
|
|
|
|
|
|
|
|
res, err := frontendProxy.Client().Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Dialing to frontend proxy: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
if g, w := res.StatusCode, 101; g != w {
|
|
|
|
|
t.Fatalf("Switching protocols failed, got: %d, want: %d", g, w)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if g, w := res.Header.Get("X-Header"), "X-Value"; g != w {
|
|
|
|
|
t.Errorf("X-Header mismatch\n\tgot: %q\n\twant: %q", g, w)
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-07 14:36:40 +02:00
|
|
|
if g, w := upgradeType(res.Header), "websocket"; !ascii.EqualFold(g, w) {
|
2020-04-26 09:11:35 +00:00
|
|
|
t.Fatalf("Upgrade header mismatch\n\tgot: %q\n\twant: %q", g, w)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rwc, ok := res.Body.(io.ReadWriteCloser)
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("Response body type mismatch, got %T, want io.ReadWriteCloser", res.Body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if got, want := res.Header.Get("X-Modified"), "true"; got != want {
|
|
|
|
|
t.Errorf("response X-Modified header = %q; want %q", got, want)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := io.WriteString(rwc, "Hello\n"); err != nil {
|
|
|
|
|
t.Fatalf("Failed to write first message: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read loop.
|
|
|
|
|
|
|
|
|
|
br := bufio.NewReader(rwc)
|
|
|
|
|
for {
|
|
|
|
|
line, err := br.ReadString('\n')
|
|
|
|
|
switch {
|
|
|
|
|
case line == terminalMsg: // this case before "err == io.EOF"
|
|
|
|
|
t.Fatalf("The websocket request was not canceled, unfortunately!")
|
|
|
|
|
|
|
|
|
|
case err == io.EOF:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
case err != nil:
|
|
|
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
|
|
|
|
|
|
|
|
case line == nthResponse(0): // We've gotten the first response back
|
|
|
|
|
// Let's trigger a cancel.
|
|
|
|
|
close(triggerCancelCh)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-03 20:46:23 +00:00
|
|
|
func TestUnannouncedTrailer(t *testing.T) {
|
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
w.(http.Flusher).Flush()
|
|
|
|
|
w.Header().Set(http.TrailerPrefix+"X-Unannounced-Trailer", "unannounced_trailer_value")
|
|
|
|
|
}))
|
|
|
|
|
defer backend.Close()
|
|
|
|
|
backendURL, err := url.Parse(backend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
proxyHandler := NewSingleHostReverseProxy(backendURL)
|
2020-10-16 00:49:02 -04:00
|
|
|
proxyHandler.ErrorLog = log.New(io.Discard, "", 0) // quiet for tests
|
2018-12-03 20:46:23 +00:00
|
|
|
frontend := httptest.NewServer(proxyHandler)
|
|
|
|
|
defer frontend.Close()
|
|
|
|
|
frontendClient := frontend.Client()
|
|
|
|
|
|
|
|
|
|
res, err := frontendClient.Get(frontend.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Get: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-16 00:49:02 -04:00
|
|
|
io.ReadAll(res.Body)
|
2018-12-03 20:46:23 +00:00
|
|
|
|
|
|
|
|
if g, w := res.Trailer.Get("X-Unannounced-Trailer"), "unannounced_trailer_value"; g != w {
|
|
|
|
|
t.Errorf("Trailer(X-Unannounced-Trailer) = %q; want %q", g, w)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
2018-12-04 00:56:04 +00:00
|
|
|
|
|
|
|
|
func TestSingleJoinSlash(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
slasha string
|
|
|
|
|
slashb string
|
|
|
|
|
expected string
|
|
|
|
|
}{
|
|
|
|
|
{"https://www.google.com/", "/favicon.ico", "https://www.google.com/favicon.ico"},
|
|
|
|
|
{"https://www.google.com", "/favicon.ico", "https://www.google.com/favicon.ico"},
|
|
|
|
|
{"https://www.google.com", "favicon.ico", "https://www.google.com/favicon.ico"},
|
|
|
|
|
{"https://www.google.com", "", "https://www.google.com/"},
|
|
|
|
|
{"", "favicon.ico", "/favicon.ico"},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
if got := singleJoiningSlash(tt.slasha, tt.slashb); got != tt.expected {
|
net/http/httputil: handle escaped paths in SingleHostReverseProxy
When forwarding a request, a SingleHostReverseProxy appends the
request's path to the target URL's path. However, if certain path
elements are encoded, (such as %2F for slash in either the request or
target path), simply joining the URL.Path elements is not sufficient,
since the field holds the decoded path.
Since 87a605, the RawPath field was added which holds a decoding
hint for the URL. When joining URL paths, this decoding hint needs
to be taken into consideration.
As an example, if the target URL.Path is /a/b, and URL.RawPath
is /a%2Fb, joining the path with /c should result in /a/b/c
in URL.Path, and /a%2Fb/c in RawPath.
The added joinURLPath function combines the two URL's Paths,
while taking into account escaping, and replaces the previously used
singleJoiningSlash in NewSingleHostReverseProxy.
Fixes #35908
Change-Id: I45886aee548431fe4031883ab1629a41e35f1727
GitHub-Last-Rev: 7be6b8d421c63928639f499b984a821585992c2b
GitHub-Pull-Request: golang/go#36378
Reviewed-on: https://go-review.googlesource.com/c/go/+/213257
Run-TryBot: Ian Lance Taylor <iant@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
2020-01-07 02:16:40 +00:00
|
|
|
t.Errorf("singleJoiningSlash(%q,%q) want %q got %q",
|
2018-12-04 00:56:04 +00:00
|
|
|
tt.slasha,
|
|
|
|
|
tt.slashb,
|
|
|
|
|
tt.expected,
|
|
|
|
|
got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
net/http/httputil: handle escaped paths in SingleHostReverseProxy
When forwarding a request, a SingleHostReverseProxy appends the
request's path to the target URL's path. However, if certain path
elements are encoded, (such as %2F for slash in either the request or
target path), simply joining the URL.Path elements is not sufficient,
since the field holds the decoded path.
Since 87a605, the RawPath field was added which holds a decoding
hint for the URL. When joining URL paths, this decoding hint needs
to be taken into consideration.
As an example, if the target URL.Path is /a/b, and URL.RawPath
is /a%2Fb, joining the path with /c should result in /a/b/c
in URL.Path, and /a%2Fb/c in RawPath.
The added joinURLPath function combines the two URL's Paths,
while taking into account escaping, and replaces the previously used
singleJoiningSlash in NewSingleHostReverseProxy.
Fixes #35908
Change-Id: I45886aee548431fe4031883ab1629a41e35f1727
GitHub-Last-Rev: 7be6b8d421c63928639f499b984a821585992c2b
GitHub-Pull-Request: golang/go#36378
Reviewed-on: https://go-review.googlesource.com/c/go/+/213257
Run-TryBot: Ian Lance Taylor <iant@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
2020-01-07 02:16:40 +00:00
|
|
|
|
|
|
|
|
func TestJoinURLPath(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
a *url.URL
|
|
|
|
|
b *url.URL
|
|
|
|
|
wantPath string
|
|
|
|
|
wantRaw string
|
|
|
|
|
}{
|
|
|
|
|
{&url.URL{Path: "/a/b"}, &url.URL{Path: "/c"}, "/a/b/c", ""},
|
|
|
|
|
{&url.URL{Path: "/a/b", RawPath: "badpath"}, &url.URL{Path: "c"}, "/a/b/c", "/a/b/c"},
|
|
|
|
|
{&url.URL{Path: "/a/b", RawPath: "/a%2Fb"}, &url.URL{Path: "/c"}, "/a/b/c", "/a%2Fb/c"},
|
|
|
|
|
{&url.URL{Path: "/a/b", RawPath: "/a%2Fb"}, &url.URL{Path: "/c"}, "/a/b/c", "/a%2Fb/c"},
|
|
|
|
|
{&url.URL{Path: "/a/b/", RawPath: "/a%2Fb%2F"}, &url.URL{Path: "c"}, "/a/b//c", "/a%2Fb%2F/c"},
|
|
|
|
|
{&url.URL{Path: "/a/b/", RawPath: "/a%2Fb/"}, &url.URL{Path: "/c/d", RawPath: "/c%2Fd"}, "/a/b/c/d", "/a%2Fb/c%2Fd"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
p, rp := joinURLPath(tt.a, tt.b)
|
|
|
|
|
if p != tt.wantPath || rp != tt.wantRaw {
|
|
|
|
|
t.Errorf("joinURLPath(URL(%q,%q),URL(%q,%q)) want (%q,%q) got (%q,%q)",
|
|
|
|
|
tt.a.Path, tt.a.RawPath,
|
|
|
|
|
tt.b.Path, tt.b.RawPath,
|
|
|
|
|
tt.wantPath, tt.wantRaw,
|
|
|
|
|
p, rp)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|