mirror of
https://github.com/caddyserver/caddy.git
synced 2024-11-26 10:13:39 +08:00
ecf852ea43
Because of this commit(6e36811c37399d60cbce587b7c48e611009c5aec) on go tip, it will probe the request's body to determine whether to use chunked transfer encoding which trailers depend on it. So we just offer a non empty body to make trailers work. fix issue #1359 Signed-off-by: Tw <tw19881113@gmail.com>
1198 lines
32 KiB
Go
1198 lines
32 KiB
Go
package proxy
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mholt/caddy/caddyfile"
|
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
|
|
|
"golang.org/x/net/websocket"
|
|
)
|
|
|
|
// This is a simple wrapper around httptest.NewTLSServer()
|
|
// which forcefully enables (among others) HTTP/2 support.
|
|
// The httptest package only supports HTTP/1.1 by default.
|
|
func newTLSServer(handler http.Handler) *httptest.Server {
|
|
ts := httptest.NewUnstartedServer(handler)
|
|
ts.TLS = new(tls.Config)
|
|
ts.TLS.NextProtos = []string{"h2"}
|
|
ts.StartTLS()
|
|
return ts
|
|
}
|
|
|
|
func TestReverseProxy(t *testing.T) {
|
|
log.SetOutput(ioutil.Discard)
|
|
defer log.SetOutput(os.Stderr)
|
|
|
|
verifyHeaders := func(headers http.Header, trailers http.Header) {
|
|
if headers.Get("X-Header") != "header-value" {
|
|
t.Error("Expected header 'X-Header' to be proxied properly")
|
|
}
|
|
|
|
if trailers == nil {
|
|
t.Error("Expected to receive trailers")
|
|
}
|
|
if trailers.Get("X-Trailer") != "trailer-value" {
|
|
t.Error("Expected header 'X-Trailer' to be proxied properly")
|
|
}
|
|
}
|
|
|
|
var requestReceived bool
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// read the body (even if it's empty) to make Go parse trailers
|
|
io.Copy(ioutil.Discard, r.Body)
|
|
verifyHeaders(r.Header, r.Trailer)
|
|
|
|
requestReceived = true
|
|
|
|
w.Header().Set("Trailer", "X-Trailer")
|
|
w.Header().Set("X-Header", "header-value")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("Hello, client"))
|
|
w.Header().Set("X-Trailer", "trailer-value")
|
|
}))
|
|
defer backend.Close()
|
|
|
|
// set up proxy
|
|
p := &Proxy{
|
|
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
|
|
Upstreams: []Upstream{newFakeUpstream(backend.URL, false)},
|
|
}
|
|
|
|
// create request and response recorder
|
|
r := httptest.NewRequest("GET", "/", strings.NewReader("test"))
|
|
w := httptest.NewRecorder()
|
|
|
|
r.ContentLength = -1 // force chunked encoding (required for trailers)
|
|
r.Header.Set("X-Header", "header-value")
|
|
r.Trailer = map[string][]string{
|
|
"X-Trailer": {"trailer-value"},
|
|
}
|
|
|
|
p.ServeHTTP(w, r)
|
|
|
|
if !requestReceived {
|
|
t.Error("Expected backend to receive request, but it didn't")
|
|
}
|
|
|
|
res := w.Result()
|
|
verifyHeaders(res.Header, res.Trailer)
|
|
|
|
// Make sure {upstream} placeholder is set
|
|
r.Body = ioutil.NopCloser(strings.NewReader("test"))
|
|
rr := httpserver.NewResponseRecorder(httptest.NewRecorder())
|
|
rr.Replacer = httpserver.NewReplacer(r, rr, "-")
|
|
|
|
p.ServeHTTP(rr, r)
|
|
|
|
if got, want := rr.Replacer.Replace("{upstream}"), backend.URL; got != want {
|
|
t.Errorf("Expected custom placeholder {upstream} to be set (%s), but it wasn't; got: %s", want, got)
|
|
}
|
|
}
|
|
|
|
func TestReverseProxyInsecureSkipVerify(t *testing.T) {
|
|
log.SetOutput(ioutil.Discard)
|
|
defer log.SetOutput(os.Stderr)
|
|
|
|
var requestReceived bool
|
|
var requestWasHTTP2 bool
|
|
backend := newTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requestReceived = true
|
|
requestWasHTTP2 = r.ProtoAtLeast(2, 0)
|
|
w.Write([]byte("Hello, client"))
|
|
}))
|
|
defer backend.Close()
|
|
|
|
// set up proxy
|
|
p := &Proxy{
|
|
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
|
|
Upstreams: []Upstream{newFakeUpstream(backend.URL, true)},
|
|
}
|
|
|
|
// create request and response recorder
|
|
r := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
p.ServeHTTP(w, r)
|
|
|
|
if !requestReceived {
|
|
t.Error("Even with insecure HTTPS, expected backend to receive request, but it didn't")
|
|
}
|
|
if !requestWasHTTP2 {
|
|
t.Error("Even with insecure HTTPS, expected proxy to use HTTP/2")
|
|
}
|
|
}
|
|
|
|
func TestWebSocketReverseProxyNonHijackerPanic(t *testing.T) {
|
|
// Capture the expected panic
|
|
defer func() {
|
|
r := recover()
|
|
if _, ok := r.(httpserver.NonHijackerError); !ok {
|
|
t.Error("not get the expected panic")
|
|
}
|
|
}()
|
|
|
|
var connCount int32
|
|
wsNop := httptest.NewServer(websocket.Handler(func(ws *websocket.Conn) { atomic.AddInt32(&connCount, 1) }))
|
|
defer wsNop.Close()
|
|
|
|
// Get proxy to use for the test
|
|
p := newWebSocketTestProxy(wsNop.URL, false)
|
|
|
|
// Create client request
|
|
r := httptest.NewRequest("GET", "/", nil)
|
|
|
|
r.Header = http.Header{
|
|
"Connection": {"Upgrade"},
|
|
"Upgrade": {"websocket"},
|
|
"Origin": {wsNop.URL},
|
|
"Sec-WebSocket-Key": {"x3JJHMbDL1EzLkh9GBhXDw=="},
|
|
"Sec-WebSocket-Version": {"13"},
|
|
}
|
|
|
|
nonHijacker := httptest.NewRecorder()
|
|
p.ServeHTTP(nonHijacker, r)
|
|
}
|
|
|
|
func TestWebSocketReverseProxyServeHTTPHandler(t *testing.T) {
|
|
// No-op websocket backend simply allows the WS connection to be
|
|
// accepted then it will be immediately closed. Perfect for testing.
|
|
var connCount int32
|
|
wsNop := httptest.NewServer(websocket.Handler(func(ws *websocket.Conn) { atomic.AddInt32(&connCount, 1) }))
|
|
defer wsNop.Close()
|
|
|
|
// Get proxy to use for the test
|
|
p := newWebSocketTestProxy(wsNop.URL, false)
|
|
|
|
// Create client request
|
|
r := httptest.NewRequest("GET", "/", nil)
|
|
|
|
r.Header = http.Header{
|
|
"Connection": {"Upgrade"},
|
|
"Upgrade": {"websocket"},
|
|
"Origin": {wsNop.URL},
|
|
"Sec-WebSocket-Key": {"x3JJHMbDL1EzLkh9GBhXDw=="},
|
|
"Sec-WebSocket-Version": {"13"},
|
|
}
|
|
|
|
// Capture the request
|
|
w := &recorderHijacker{httptest.NewRecorder(), new(fakeConn)}
|
|
|
|
// Booya! Do the test.
|
|
p.ServeHTTP(w, r)
|
|
|
|
// Make sure the backend accepted the WS connection.
|
|
// Mostly interested in the Upgrade and Connection response headers
|
|
// and the 101 status code.
|
|
expected := []byte("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=\r\n\r\n")
|
|
actual := w.fakeConn.writeBuf.Bytes()
|
|
if !bytes.Equal(actual, expected) {
|
|
t.Errorf("Expected backend to accept response:\n'%s'\nActually got:\n'%s'", expected, actual)
|
|
}
|
|
if atomic.LoadInt32(&connCount) != 1 {
|
|
t.Errorf("Expected 1 websocket connection, got %d", connCount)
|
|
}
|
|
}
|
|
|
|
func TestWebSocketReverseProxyFromWSClient(t *testing.T) {
|
|
// Echo server allows us to test that socket bytes are properly
|
|
// being proxied.
|
|
wsEcho := httptest.NewServer(websocket.Handler(func(ws *websocket.Conn) {
|
|
io.Copy(ws, ws)
|
|
}))
|
|
defer wsEcho.Close()
|
|
|
|
// Get proxy to use for the test
|
|
p := newWebSocketTestProxy(wsEcho.URL, false)
|
|
|
|
// This is a full end-end test, so the proxy handler
|
|
// has to be part of a server listening on a port. Our
|
|
// WS client will connect to this test server, not
|
|
// the echo client directly.
|
|
echoProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
p.ServeHTTP(w, r)
|
|
}))
|
|
defer echoProxy.Close()
|
|
|
|
// Set up WebSocket client
|
|
url := strings.Replace(echoProxy.URL, "http://", "ws://", 1)
|
|
ws, err := websocket.Dial(url, "", echoProxy.URL)
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ws.Close()
|
|
|
|
// Send test message
|
|
trialMsg := "Is it working?"
|
|
|
|
if sendErr := websocket.Message.Send(ws, trialMsg); sendErr != nil {
|
|
t.Fatal(sendErr)
|
|
}
|
|
|
|
// It should be echoed back to us
|
|
var actualMsg string
|
|
|
|
if rcvErr := websocket.Message.Receive(ws, &actualMsg); rcvErr != nil {
|
|
t.Fatal(rcvErr)
|
|
}
|
|
|
|
if actualMsg != trialMsg {
|
|
t.Errorf("Expected '%s' but got '%s' instead", trialMsg, actualMsg)
|
|
}
|
|
}
|
|
|
|
func TestWebSocketReverseProxyFromWSSClient(t *testing.T) {
|
|
wsEcho := newTLSServer(websocket.Handler(func(ws *websocket.Conn) {
|
|
io.Copy(ws, ws)
|
|
}))
|
|
defer wsEcho.Close()
|
|
|
|
p := newWebSocketTestProxy(wsEcho.URL, true)
|
|
|
|
echoProxy := newTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
p.ServeHTTP(w, r)
|
|
}))
|
|
defer echoProxy.Close()
|
|
|
|
// Set up WebSocket client
|
|
url := strings.Replace(echoProxy.URL, "https://", "wss://", 1)
|
|
wsCfg, err := websocket.NewConfig(url, echoProxy.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wsCfg.TlsConfig = &tls.Config{InsecureSkipVerify: true}
|
|
ws, err := websocket.DialConfig(wsCfg)
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ws.Close()
|
|
|
|
// Send test message
|
|
trialMsg := "Is it working?"
|
|
|
|
if sendErr := websocket.Message.Send(ws, trialMsg); sendErr != nil {
|
|
t.Fatal(sendErr)
|
|
}
|
|
|
|
// It should be echoed back to us
|
|
var actualMsg string
|
|
|
|
if rcvErr := websocket.Message.Receive(ws, &actualMsg); rcvErr != nil {
|
|
t.Fatal(rcvErr)
|
|
}
|
|
|
|
if actualMsg != trialMsg {
|
|
t.Errorf("Expected '%s' but got '%s' instead", trialMsg, actualMsg)
|
|
}
|
|
}
|
|
|
|
func TestUnixSocketProxy(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
return
|
|
}
|
|
|
|
trialMsg := "Is it working?"
|
|
|
|
var proxySuccess bool
|
|
|
|
// This is our fake "application" we want to proxy to
|
|
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Request was proxied when this is called
|
|
proxySuccess = true
|
|
|
|
fmt.Fprint(w, trialMsg)
|
|
}))
|
|
|
|
// Get absolute path for unix: socket
|
|
dir, err := ioutil.TempDir("", "caddy_proxytest")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make temp dir to contain unix socket. %v", err)
|
|
}
|
|
defer os.RemoveAll(dir)
|
|
socketPath := filepath.Join(dir, "test_socket")
|
|
|
|
// Change httptest.Server listener to listen to unix: socket
|
|
ln, err := net.Listen("unix", socketPath)
|
|
if err != nil {
|
|
t.Fatalf("Unable to listen: %v", err)
|
|
}
|
|
ts.Listener = ln
|
|
|
|
ts.Start()
|
|
defer ts.Close()
|
|
|
|
url := strings.Replace(ts.URL, "http://", "unix:", 1)
|
|
p := newWebSocketTestProxy(url, false)
|
|
|
|
echoProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
p.ServeHTTP(w, r)
|
|
}))
|
|
defer echoProxy.Close()
|
|
|
|
res, err := http.Get(echoProxy.URL)
|
|
if err != nil {
|
|
t.Fatalf("Unable to GET: %v", err)
|
|
}
|
|
|
|
greeting, err := ioutil.ReadAll(res.Body)
|
|
res.Body.Close()
|
|
if err != nil {
|
|
t.Fatalf("Unable to GET: %v", err)
|
|
}
|
|
|
|
actualMsg := fmt.Sprintf("%s", greeting)
|
|
|
|
if !proxySuccess {
|
|
t.Errorf("Expected request to be proxied, but it wasn't")
|
|
}
|
|
|
|
if actualMsg != trialMsg {
|
|
t.Errorf("Expected '%s' but got '%s' instead", trialMsg, actualMsg)
|
|
}
|
|
}
|
|
|
|
func GetHTTPProxy(messageFormat string, prefix string) (*Proxy, *httptest.Server) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, messageFormat, r.URL.String())
|
|
}))
|
|
|
|
return newPrefixedWebSocketTestProxy(ts.URL, prefix), ts
|
|
}
|
|
|
|
func GetSocketProxy(messageFormat string, prefix string) (*Proxy, *httptest.Server, string, error) {
|
|
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, messageFormat, r.URL.String())
|
|
}))
|
|
|
|
dir, err := ioutil.TempDir("", "caddy_proxytest")
|
|
if err != nil {
|
|
return nil, nil, dir, fmt.Errorf("Failed to make temp dir to contain unix socket. %v", err)
|
|
}
|
|
socketPath := filepath.Join(dir, "test_socket")
|
|
|
|
ln, err := net.Listen("unix", socketPath)
|
|
if err != nil {
|
|
os.RemoveAll(dir)
|
|
return nil, nil, dir, fmt.Errorf("Unable to listen: %v", err)
|
|
}
|
|
ts.Listener = ln
|
|
|
|
ts.Start()
|
|
|
|
tsURL := strings.Replace(ts.URL, "http://", "unix:", 1)
|
|
|
|
return newPrefixedWebSocketTestProxy(tsURL, prefix), ts, dir, nil
|
|
}
|
|
|
|
func GetTestServerMessage(p *Proxy, ts *httptest.Server, path string) (string, error) {
|
|
echoProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
p.ServeHTTP(w, r)
|
|
}))
|
|
|
|
// *httptest.Server is passed so it can be `defer`red properly
|
|
defer ts.Close()
|
|
defer echoProxy.Close()
|
|
|
|
res, err := http.Get(echoProxy.URL + path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Unable to GET: %v", err)
|
|
}
|
|
|
|
greeting, err := ioutil.ReadAll(res.Body)
|
|
res.Body.Close()
|
|
if err != nil {
|
|
return "", fmt.Errorf("Unable to read body: %v", err)
|
|
}
|
|
|
|
return fmt.Sprintf("%s", greeting), nil
|
|
}
|
|
|
|
func TestUnixSocketProxyPaths(t *testing.T) {
|
|
greeting := "Hello route %s"
|
|
|
|
tests := []struct {
|
|
url string
|
|
prefix string
|
|
expected string
|
|
}{
|
|
{"", "", fmt.Sprintf(greeting, "/")},
|
|
{"/hello", "", fmt.Sprintf(greeting, "/hello")},
|
|
{"/foo/bar", "", fmt.Sprintf(greeting, "/foo/bar")},
|
|
{"/foo?bar", "", fmt.Sprintf(greeting, "/foo?bar")},
|
|
{"/greet?name=john", "", fmt.Sprintf(greeting, "/greet?name=john")},
|
|
{"/world?wonderful&colorful", "", fmt.Sprintf(greeting, "/world?wonderful&colorful")},
|
|
{"/proxy/hello", "/proxy", fmt.Sprintf(greeting, "/hello")},
|
|
{"/proxy/foo/bar", "/proxy", fmt.Sprintf(greeting, "/foo/bar")},
|
|
{"/proxy/?foo=bar", "/proxy", fmt.Sprintf(greeting, "/?foo=bar")},
|
|
{"/queues/%2F/fetchtasks", "", fmt.Sprintf(greeting, "/queues/%2F/fetchtasks")},
|
|
{"/queues/%2F/fetchtasks?foo=bar", "", fmt.Sprintf(greeting, "/queues/%2F/fetchtasks?foo=bar")},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
p, ts := GetHTTPProxy(greeting, test.prefix)
|
|
|
|
actualMsg, err := GetTestServerMessage(p, ts, test.url)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Getting server message failed - %v", err)
|
|
}
|
|
|
|
if actualMsg != test.expected {
|
|
t.Errorf("Expected '%s' but got '%s' instead", test.expected, actualMsg)
|
|
}
|
|
}
|
|
|
|
if runtime.GOOS == "windows" {
|
|
return
|
|
}
|
|
|
|
for _, test := range tests {
|
|
p, ts, tmpdir, err := GetSocketProxy(greeting, test.prefix)
|
|
if err != nil {
|
|
t.Fatalf("Getting socket proxy failed - %v", err)
|
|
}
|
|
|
|
actualMsg, err := GetTestServerMessage(p, ts, test.url)
|
|
|
|
if err != nil {
|
|
os.RemoveAll(tmpdir)
|
|
t.Fatalf("Getting server message failed - %v", err)
|
|
}
|
|
|
|
if actualMsg != test.expected {
|
|
t.Errorf("Expected '%s' but got '%s' instead", test.expected, actualMsg)
|
|
}
|
|
|
|
os.RemoveAll(tmpdir)
|
|
}
|
|
}
|
|
|
|
func TestUpstreamHeadersUpdate(t *testing.T) {
|
|
log.SetOutput(ioutil.Discard)
|
|
defer log.SetOutput(os.Stderr)
|
|
|
|
var actualHeaders http.Header
|
|
var actualHost string
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("Hello, client"))
|
|
actualHeaders = r.Header
|
|
actualHost = r.Host
|
|
}))
|
|
defer backend.Close()
|
|
|
|
upstream := newFakeUpstream(backend.URL, false)
|
|
upstream.host.UpstreamHeaders = http.Header{
|
|
"Connection": {"{>Connection}"},
|
|
"Upgrade": {"{>Upgrade}"},
|
|
"+Merge-Me": {"Merge-Value"},
|
|
"+Add-Me": {"Add-Value"},
|
|
"+Add-Empty": {"{}"},
|
|
"-Remove-Me": {""},
|
|
"Replace-Me": {"{hostname}"},
|
|
"Clear-Me": {""},
|
|
"Host": {"{>Host}"},
|
|
}
|
|
// set up proxy
|
|
p := &Proxy{
|
|
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
|
|
Upstreams: []Upstream{upstream},
|
|
}
|
|
|
|
// create request and response recorder
|
|
r := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
const expectHost = "example.com"
|
|
//add initial headers
|
|
r.Header.Add("Merge-Me", "Initial")
|
|
r.Header.Add("Remove-Me", "Remove-Value")
|
|
r.Header.Add("Replace-Me", "Replace-Value")
|
|
r.Header.Add("Host", expectHost)
|
|
|
|
p.ServeHTTP(w, r)
|
|
|
|
replacer := httpserver.NewReplacer(r, nil, "")
|
|
|
|
for headerKey, expect := range map[string][]string{
|
|
"Merge-Me": {"Initial", "Merge-Value"},
|
|
"Add-Me": {"Add-Value"},
|
|
"Add-Empty": nil,
|
|
"Remove-Me": nil,
|
|
"Replace-Me": {replacer.Replace("{hostname}")},
|
|
"Clear-Me": nil,
|
|
} {
|
|
if got := actualHeaders[headerKey]; !reflect.DeepEqual(got, expect) {
|
|
t.Errorf("Upstream request does not contain expected %v header: expect %v, but got %v",
|
|
headerKey, expect, got)
|
|
}
|
|
}
|
|
|
|
if actualHost != expectHost {
|
|
t.Errorf("Request sent to upstream backend should have value of Host with %s, but got %s", expectHost, actualHost)
|
|
}
|
|
|
|
}
|
|
|
|
func TestDownstreamHeadersUpdate(t *testing.T) {
|
|
log.SetOutput(ioutil.Discard)
|
|
defer log.SetOutput(os.Stderr)
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Add("Merge-Me", "Initial")
|
|
w.Header().Add("Remove-Me", "Remove-Value")
|
|
w.Header().Add("Replace-Me", "Replace-Value")
|
|
w.Header().Add("Content-Type", "text/html")
|
|
w.Header().Add("Overwrite-Me", "Overwrite-Value")
|
|
w.Write([]byte("Hello, client"))
|
|
}))
|
|
defer backend.Close()
|
|
|
|
upstream := newFakeUpstream(backend.URL, false)
|
|
upstream.host.DownstreamHeaders = http.Header{
|
|
"+Merge-Me": {"Merge-Value"},
|
|
"+Add-Me": {"Add-Value"},
|
|
"-Remove-Me": {""},
|
|
"Replace-Me": {"{hostname}"},
|
|
}
|
|
// set up proxy
|
|
p := &Proxy{
|
|
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
|
|
Upstreams: []Upstream{upstream},
|
|
}
|
|
|
|
// create request and response recorder
|
|
r := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
// set a predefined skip header
|
|
w.Header().Set("Content-Type", "text/css")
|
|
// set a predefined overwritten header
|
|
w.Header().Set("Overwrite-Me", "Initial")
|
|
|
|
p.ServeHTTP(w, r)
|
|
|
|
replacer := httpserver.NewReplacer(r, nil, "")
|
|
actualHeaders := w.Header()
|
|
|
|
for headerKey, expect := range map[string][]string{
|
|
"Merge-Me": {"Initial", "Merge-Value"},
|
|
"Add-Me": {"Add-Value"},
|
|
"Remove-Me": nil,
|
|
"Replace-Me": {replacer.Replace("{hostname}")},
|
|
"Content-Type": {"text/css"},
|
|
"Overwrite-Me": {"Overwrite-Value"},
|
|
} {
|
|
if got := actualHeaders[headerKey]; !reflect.DeepEqual(got, expect) {
|
|
t.Errorf("Downstream response does not contain expected %s header: expect %v, but got %v",
|
|
headerKey, expect, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
var (
|
|
upstreamResp1 = []byte("Hello, /")
|
|
upstreamResp2 = []byte("Hello, /api/")
|
|
)
|
|
|
|
func newMultiHostTestProxy() *Proxy {
|
|
// No-op backends.
|
|
upstreamServer1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, "%s", upstreamResp1)
|
|
}))
|
|
upstreamServer2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, "%s", upstreamResp2)
|
|
}))
|
|
p := &Proxy{
|
|
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
|
|
Upstreams: []Upstream{
|
|
// The order is important; the short path should go first to ensure
|
|
// we choose the most specific route, not the first one.
|
|
&fakeUpstream{
|
|
name: upstreamServer1.URL,
|
|
from: "/",
|
|
},
|
|
&fakeUpstream{
|
|
name: upstreamServer2.URL,
|
|
from: "/api",
|
|
},
|
|
},
|
|
}
|
|
return p
|
|
}
|
|
|
|
func TestMultiReverseProxyFromClient(t *testing.T) {
|
|
p := newMultiHostTestProxy()
|
|
|
|
// This is a full end-end test, so the proxy handler.
|
|
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
p.ServeHTTP(w, r)
|
|
}))
|
|
defer proxy.Close()
|
|
|
|
// Table tests.
|
|
var multiProxy = []struct {
|
|
url string
|
|
body []byte
|
|
}{
|
|
{
|
|
"/",
|
|
upstreamResp1,
|
|
},
|
|
{
|
|
"/api/",
|
|
upstreamResp2,
|
|
},
|
|
{
|
|
"/messages/",
|
|
upstreamResp1,
|
|
},
|
|
{
|
|
"/api/messages/?text=cat",
|
|
upstreamResp2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range multiProxy {
|
|
// Create client request
|
|
reqURL := proxy.URL + tt.url
|
|
req, err := http.NewRequest("GET", reqURL, nil)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if err != nil {
|
|
t.Fatalf("Failed to read response: %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(body, tt.body) {
|
|
t.Errorf("Expected '%s' but got '%s' instead", tt.body, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHostSimpleProxyNoHeaderForward(t *testing.T) {
|
|
var requestHost string
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requestHost = r.Host
|
|
w.Write([]byte("Hello, client"))
|
|
}))
|
|
defer backend.Close()
|
|
|
|
// set up proxy
|
|
p := &Proxy{
|
|
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
|
|
Upstreams: []Upstream{newFakeUpstream(backend.URL, false)},
|
|
}
|
|
|
|
r := httptest.NewRequest("GET", "/", nil)
|
|
r.Host = "test.com"
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
p.ServeHTTP(w, r)
|
|
|
|
if !strings.Contains(backend.URL, "//") {
|
|
t.Fatalf("The URL of the backend server doesn't contains //: %s", backend.URL)
|
|
}
|
|
|
|
expectedHost := strings.Split(backend.URL, "//")
|
|
if expectedHost[1] != requestHost {
|
|
t.Fatalf("Expected %s as a Host header got %s\n", expectedHost[1], requestHost)
|
|
}
|
|
}
|
|
|
|
func TestHostHeaderReplacedUsingForward(t *testing.T) {
|
|
var requestHost string
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requestHost = r.Host
|
|
w.Write([]byte("Hello, client"))
|
|
}))
|
|
defer backend.Close()
|
|
|
|
upstream := newFakeUpstream(backend.URL, false)
|
|
proxyHostHeader := "test2.com"
|
|
upstream.host.UpstreamHeaders = http.Header{"Host": []string{proxyHostHeader}}
|
|
// set up proxy
|
|
p := &Proxy{
|
|
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
|
|
Upstreams: []Upstream{upstream},
|
|
}
|
|
|
|
r := httptest.NewRequest("GET", "/", nil)
|
|
r.Host = "test.com"
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
p.ServeHTTP(w, r)
|
|
|
|
if proxyHostHeader != requestHost {
|
|
t.Fatalf("Expected %s as a Host header got %s\n", proxyHostHeader, requestHost)
|
|
}
|
|
}
|
|
|
|
func TestBasicAuth(t *testing.T) {
|
|
basicAuthTestcase(t, nil, nil)
|
|
basicAuthTestcase(t, nil, url.UserPassword("username", "password"))
|
|
basicAuthTestcase(t, url.UserPassword("usename", "password"), nil)
|
|
basicAuthTestcase(t, url.UserPassword("unused", "unused"),
|
|
url.UserPassword("username", "password"))
|
|
}
|
|
|
|
func basicAuthTestcase(t *testing.T, upstreamUser, clientUser *url.Userinfo) {
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
u, p, ok := r.BasicAuth()
|
|
|
|
if ok {
|
|
w.Write([]byte(u))
|
|
}
|
|
if ok && p != "" {
|
|
w.Write([]byte(":"))
|
|
w.Write([]byte(p))
|
|
}
|
|
}))
|
|
defer backend.Close()
|
|
|
|
backURL, err := url.Parse(backend.URL)
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse URL: %v", err)
|
|
}
|
|
backURL.User = upstreamUser
|
|
|
|
p := &Proxy{
|
|
Next: httpserver.EmptyNext,
|
|
Upstreams: []Upstream{newFakeUpstream(backURL.String(), false)},
|
|
}
|
|
r, err := http.NewRequest("GET", "/foo", nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create request: %v", err)
|
|
}
|
|
if clientUser != nil {
|
|
u := clientUser.Username()
|
|
p, _ := clientUser.Password()
|
|
r.SetBasicAuth(u, p)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
|
|
p.ServeHTTP(w, r)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("Invalid response code: %d", w.Code)
|
|
}
|
|
body, _ := ioutil.ReadAll(w.Body)
|
|
|
|
if clientUser != nil {
|
|
if string(body) != clientUser.String() {
|
|
t.Fatalf("Invalid auth info: %s", string(body))
|
|
}
|
|
} else {
|
|
if upstreamUser != nil {
|
|
if string(body) != upstreamUser.String() {
|
|
t.Fatalf("Invalid auth info: %s", string(body))
|
|
}
|
|
} else {
|
|
if string(body) != "" {
|
|
t.Fatalf("Invalid auth info: %s", string(body))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProxyDirectorURL(t *testing.T) {
|
|
for i, c := range []struct {
|
|
requestURL string
|
|
targetURL string
|
|
without string
|
|
expectURL string
|
|
}{
|
|
{
|
|
requestURL: `http://localhost:2020/test`,
|
|
targetURL: `https://localhost:2021`,
|
|
expectURL: `https://localhost:2021/test`,
|
|
},
|
|
{
|
|
requestURL: `http://localhost:2020/test`,
|
|
targetURL: `https://localhost:2021/t`,
|
|
expectURL: `https://localhost:2021/t/test`,
|
|
},
|
|
{
|
|
requestURL: `http://localhost:2020/test?t=w`,
|
|
targetURL: `https://localhost:2021/t`,
|
|
expectURL: `https://localhost:2021/t/test?t=w`,
|
|
},
|
|
{
|
|
requestURL: `http://localhost:2020/test`,
|
|
targetURL: `https://localhost:2021/t?foo=bar`,
|
|
expectURL: `https://localhost:2021/t/test?foo=bar`,
|
|
},
|
|
{
|
|
requestURL: `http://localhost:2020/test?t=w`,
|
|
targetURL: `https://localhost:2021/t?foo=bar`,
|
|
expectURL: `https://localhost:2021/t/test?foo=bar&t=w`,
|
|
},
|
|
{
|
|
requestURL: `http://localhost:2020/test?t=w`,
|
|
targetURL: `https://localhost:2021/t?foo=bar`,
|
|
expectURL: `https://localhost:2021/t?foo=bar&t=w`,
|
|
without: "/test",
|
|
},
|
|
{
|
|
requestURL: `http://localhost:2020/test?t%3dw`,
|
|
targetURL: `https://localhost:2021/t?foo%3dbar`,
|
|
expectURL: `https://localhost:2021/t?foo%3dbar&t%3dw`,
|
|
without: "/test",
|
|
},
|
|
{
|
|
requestURL: `http://localhost:2020/test/`,
|
|
targetURL: `https://localhost:2021/t/`,
|
|
expectURL: `https://localhost:2021/t/test/`,
|
|
},
|
|
{
|
|
requestURL: `http://localhost:2020/test/mypath`,
|
|
targetURL: `https://localhost:2021/t/`,
|
|
expectURL: `https://localhost:2021/t/mypath`,
|
|
without: "/test",
|
|
},
|
|
} {
|
|
targetURL, err := url.Parse(c.targetURL)
|
|
if err != nil {
|
|
t.Errorf("case %d failed to parse target URL: %s", i, err)
|
|
continue
|
|
}
|
|
req, err := http.NewRequest("GET", c.requestURL, nil)
|
|
if err != nil {
|
|
t.Errorf("case %d failed to create request: %s", i, err)
|
|
continue
|
|
}
|
|
|
|
NewSingleHostReverseProxy(targetURL, c.without, 0).Director(req)
|
|
if expect, got := c.expectURL, req.URL.String(); expect != got {
|
|
t.Errorf("case %d url not equal: expect %q, but got %q",
|
|
i, expect, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReverseProxyRetry(t *testing.T) {
|
|
log.SetOutput(ioutil.Discard)
|
|
defer log.SetOutput(os.Stderr)
|
|
|
|
// set up proxy
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
io.Copy(w, r.Body)
|
|
r.Body.Close()
|
|
}))
|
|
defer backend.Close()
|
|
|
|
su, err := NewStaticUpstreams(caddyfile.NewDispenser("Testfile", strings.NewReader(`
|
|
proxy / localhost:65535 localhost:65534 `+backend.URL+` {
|
|
policy round_robin
|
|
fail_timeout 5s
|
|
max_fails 1
|
|
try_duration 5s
|
|
try_interval 250ms
|
|
}
|
|
`)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
p := &Proxy{
|
|
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
|
|
Upstreams: su,
|
|
}
|
|
|
|
// middle is required to simulate closable downstream request body
|
|
middle := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, err = p.ServeHTTP(w, r)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
}))
|
|
defer middle.Close()
|
|
|
|
testcase := "test content"
|
|
r, err := http.NewRequest("POST", middle.URL, bytes.NewBufferString(testcase))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resp, err := http.DefaultTransport.RoundTrip(r)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(b) != testcase {
|
|
t.Fatalf("string(b) = %s, want %s", string(b), testcase)
|
|
}
|
|
}
|
|
|
|
func TestReverseProxyLargeBody(t *testing.T) {
|
|
log.SetOutput(ioutil.Discard)
|
|
defer log.SetOutput(os.Stderr)
|
|
|
|
// set up proxy
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
io.Copy(ioutil.Discard, r.Body)
|
|
r.Body.Close()
|
|
}))
|
|
defer backend.Close()
|
|
|
|
su, err := NewStaticUpstreams(caddyfile.NewDispenser("Testfile", strings.NewReader(`proxy / `+backend.URL)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
p := &Proxy{
|
|
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
|
|
Upstreams: su,
|
|
}
|
|
|
|
// middle is required to simulate closable downstream request body
|
|
middle := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, err = p.ServeHTTP(w, r)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
}))
|
|
defer middle.Close()
|
|
|
|
// Our request body will be 100MB
|
|
bodySize := uint64(100 * 1000 * 1000)
|
|
|
|
// We want to see how much memory the proxy module requires for this request.
|
|
// So lets record the mem stats before we start it.
|
|
begMemstats := &runtime.MemStats{}
|
|
runtime.ReadMemStats(begMemstats)
|
|
|
|
r, err := http.NewRequest("POST", middle.URL, &noopReader{len: bodySize})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resp, err := http.DefaultTransport.RoundTrip(r)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
// Finally we need the mem stats after the request is done...
|
|
endMemstats := &runtime.MemStats{}
|
|
runtime.ReadMemStats(endMemstats)
|
|
|
|
// ...to calculate the total amount of allocated memory during the request.
|
|
totalAlloc := endMemstats.TotalAlloc - begMemstats.TotalAlloc
|
|
|
|
// If that's as much as the size of the body itself it's a serious sign that the
|
|
// request was not "streamed" to the upstream without buffering it first.
|
|
if totalAlloc >= bodySize {
|
|
t.Fatalf("proxy allocated too much memory: %d bytes", totalAlloc)
|
|
}
|
|
}
|
|
|
|
type noopReader struct {
|
|
len uint64
|
|
pos uint64
|
|
}
|
|
|
|
var _ io.Reader = &noopReader{}
|
|
|
|
func (r *noopReader) Read(b []byte) (int, error) {
|
|
if r.pos >= r.len {
|
|
return 0, io.EOF
|
|
}
|
|
n := int(r.len - r.pos)
|
|
if n > len(b) {
|
|
n = len(b)
|
|
}
|
|
for i := range b[:n] {
|
|
b[i] = 0
|
|
}
|
|
r.pos += uint64(n)
|
|
return n, nil
|
|
}
|
|
|
|
func newFakeUpstream(name string, insecure bool) *fakeUpstream {
|
|
uri, _ := url.Parse(name)
|
|
u := &fakeUpstream{
|
|
name: name,
|
|
from: "/",
|
|
host: &UpstreamHost{
|
|
Name: name,
|
|
ReverseProxy: NewSingleHostReverseProxy(uri, "", http.DefaultMaxIdleConnsPerHost),
|
|
},
|
|
}
|
|
if insecure {
|
|
u.host.ReverseProxy.UseInsecureTransport()
|
|
}
|
|
return u
|
|
}
|
|
|
|
type fakeUpstream struct {
|
|
name string
|
|
host *UpstreamHost
|
|
from string
|
|
without string
|
|
}
|
|
|
|
func (u *fakeUpstream) From() string {
|
|
return u.from
|
|
}
|
|
|
|
func (u *fakeUpstream) Select(r *http.Request) *UpstreamHost {
|
|
if u.host == nil {
|
|
uri, err := url.Parse(u.name)
|
|
if err != nil {
|
|
log.Fatalf("Unable to url.Parse %s: %v", u.name, err)
|
|
}
|
|
u.host = &UpstreamHost{
|
|
Name: u.name,
|
|
ReverseProxy: NewSingleHostReverseProxy(uri, u.without, http.DefaultMaxIdleConnsPerHost),
|
|
}
|
|
}
|
|
return u.host
|
|
}
|
|
|
|
func (u *fakeUpstream) AllowedPath(requestPath string) bool { return true }
|
|
func (u *fakeUpstream) GetTryDuration() time.Duration { return 1 * time.Second }
|
|
func (u *fakeUpstream) GetTryInterval() time.Duration { return 250 * time.Millisecond }
|
|
func (u *fakeUpstream) GetHostCount() int { return 1 }
|
|
|
|
// newWebSocketTestProxy returns a test proxy that will
|
|
// redirect to the specified backendAddr. The function
|
|
// also sets up the rules/environment for testing WebSocket
|
|
// proxy.
|
|
func newWebSocketTestProxy(backendAddr string, insecure bool) *Proxy {
|
|
return &Proxy{
|
|
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
|
|
Upstreams: []Upstream{&fakeWsUpstream{
|
|
name: backendAddr,
|
|
without: "",
|
|
insecure: insecure,
|
|
}},
|
|
}
|
|
}
|
|
|
|
func newPrefixedWebSocketTestProxy(backendAddr string, prefix string) *Proxy {
|
|
return &Proxy{
|
|
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
|
|
Upstreams: []Upstream{&fakeWsUpstream{name: backendAddr, without: prefix}},
|
|
}
|
|
}
|
|
|
|
type fakeWsUpstream struct {
|
|
name string
|
|
without string
|
|
insecure bool
|
|
}
|
|
|
|
func (u *fakeWsUpstream) From() string {
|
|
return "/"
|
|
}
|
|
|
|
func (u *fakeWsUpstream) Select(r *http.Request) *UpstreamHost {
|
|
uri, _ := url.Parse(u.name)
|
|
host := &UpstreamHost{
|
|
Name: u.name,
|
|
ReverseProxy: NewSingleHostReverseProxy(uri, u.without, http.DefaultMaxIdleConnsPerHost),
|
|
UpstreamHeaders: http.Header{
|
|
"Connection": {"{>Connection}"},
|
|
"Upgrade": {"{>Upgrade}"}},
|
|
}
|
|
if u.insecure {
|
|
host.ReverseProxy.UseInsecureTransport()
|
|
}
|
|
return host
|
|
}
|
|
|
|
func (u *fakeWsUpstream) AllowedPath(requestPath string) bool { return true }
|
|
func (u *fakeWsUpstream) GetTryDuration() time.Duration { return 1 * time.Second }
|
|
func (u *fakeWsUpstream) GetTryInterval() time.Duration { return 250 * time.Millisecond }
|
|
func (u *fakeWsUpstream) GetHostCount() int { return 1 }
|
|
|
|
// recorderHijacker is a ResponseRecorder that can
|
|
// be hijacked.
|
|
type recorderHijacker struct {
|
|
*httptest.ResponseRecorder
|
|
fakeConn *fakeConn
|
|
}
|
|
|
|
func (rh *recorderHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|
return rh.fakeConn, nil, nil
|
|
}
|
|
|
|
type fakeConn struct {
|
|
readBuf bytes.Buffer
|
|
writeBuf bytes.Buffer
|
|
}
|
|
|
|
func (c *fakeConn) LocalAddr() net.Addr { return nil }
|
|
func (c *fakeConn) RemoteAddr() net.Addr { return nil }
|
|
func (c *fakeConn) SetDeadline(t time.Time) error { return nil }
|
|
func (c *fakeConn) SetReadDeadline(t time.Time) error { return nil }
|
|
func (c *fakeConn) SetWriteDeadline(t time.Time) error { return nil }
|
|
func (c *fakeConn) Close() error { return nil }
|
|
func (c *fakeConn) Read(b []byte) (int, error) { return c.readBuf.Read(b) }
|
|
func (c *fakeConn) Write(b []byte) (int, error) { return c.writeBuf.Write(b) }
|
|
|
|
func BenchmarkProxy(b *testing.B) {
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("Hello, client"))
|
|
}))
|
|
defer backend.Close()
|
|
|
|
upstream := newFakeUpstream(backend.URL, false)
|
|
upstream.host.UpstreamHeaders = http.Header{
|
|
"Hostname": {"{hostname}"},
|
|
"Host": {"{host}"},
|
|
"X-Real-IP": {"{remote}"},
|
|
"X-Forwarded-Proto": {"{scheme}"},
|
|
}
|
|
// set up proxy
|
|
p := &Proxy{
|
|
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
|
|
Upstreams: []Upstream{upstream},
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
b.StopTimer()
|
|
// create request and response recorder
|
|
r, err := http.NewRequest("GET", "/", nil)
|
|
if err != nil {
|
|
b.Fatalf("Failed to create request: %v", err)
|
|
}
|
|
b.StartTimer()
|
|
p.ServeHTTP(w, r)
|
|
}
|
|
}
|