traefik/pkg/proxy/fast/proxy_test.go
2024-10-17 09:12:04 +02:00

337 lines
7.9 KiB
Go

package fast
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"testing"
"time"
"github.com/armon/go-socks5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/config/static"
"github.com/traefik/traefik/v3/pkg/testhelpers"
"github.com/traefik/traefik/v3/pkg/tls/generate"
)
const (
proxyHTTP = "http"
proxyHTTPS = "https"
proxySocks5 = "socks"
)
type authCreds struct {
user string
password string
}
func TestProxyFromEnvironment(t *testing.T) {
testCases := []struct {
desc string
proxyType string
tls bool
auth *authCreds
}{
{
desc: "Proxy HTTP with HTTP Backend",
proxyType: proxyHTTP,
},
{
desc: "Proxy HTTP with HTTP backend and proxy auth",
proxyType: proxyHTTP,
tls: false,
auth: &authCreds{
user: "user",
password: "password",
},
},
{
desc: "Proxy HTTP with HTTPS backend",
proxyType: proxyHTTP,
tls: true,
},
{
desc: "Proxy HTTP with HTTPS backend and proxy auth",
proxyType: proxyHTTP,
tls: true,
auth: &authCreds{
user: "user",
password: "password",
},
},
{
desc: "Proxy HTTPS with HTTP backend",
proxyType: proxyHTTPS,
},
{
desc: "Proxy HTTPS with HTTP backend and proxy auth",
proxyType: proxyHTTPS,
tls: false,
auth: &authCreds{
user: "user",
password: "password",
},
},
{
desc: "Proxy HTTPS with HTTPS backend",
proxyType: proxyHTTPS,
tls: true,
},
{
desc: "Proxy HTTPS with HTTPS backend and proxy auth",
proxyType: proxyHTTPS,
tls: true,
auth: &authCreds{
user: "user",
password: "password",
},
},
{
desc: "Proxy Socks5 with HTTP backend",
proxyType: proxySocks5,
},
{
desc: "Proxy Socks5 with HTTP backend and proxy auth",
proxyType: proxySocks5,
auth: &authCreds{
user: "user",
password: "password",
},
},
{
desc: "Proxy Socks5 with HTTPS backend",
proxyType: proxySocks5,
tls: true,
},
{
desc: "Proxy Socks5 with HTTPS backend and proxy auth",
proxyType: proxySocks5,
tls: true,
auth: &authCreds{
user: "user",
password: "password",
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
backendURL, backendCert := newBackendServer(t, test.tls, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
_, _ = rw.Write([]byte("backend"))
}))
var proxyCalled bool
proxyHandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
proxyCalled = true
if test.auth != nil {
proxyAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(test.auth.user+":"+test.auth.password))
require.Equal(t, proxyAuth, req.Header.Get("Proxy-Authorization"))
}
if req.Method != http.MethodConnect {
proxy := httputil.NewSingleHostReverseProxy(testhelpers.MustParseURL("http://" + req.Host))
proxy.ServeHTTP(rw, req)
return
}
// CONNECT method
conn, err := net.Dial("tcp", req.Host)
require.NoError(t, err)
hj, ok := rw.(http.Hijacker)
require.True(t, ok)
rw.WriteHeader(http.StatusOK)
connHj, _, err := hj.Hijack()
require.NoError(t, err)
go func() { _, _ = io.Copy(connHj, conn) }()
_, _ = io.Copy(conn, connHj)
})
var proxyURL string
var proxyCert *x509.Certificate
switch test.proxyType {
case proxySocks5:
ln, err := net.Listen("tcp", ":0")
require.NoError(t, err)
proxyURL = fmt.Sprintf("socks5://%s", ln.Addr())
go func() {
conn, err := ln.Accept()
require.NoError(t, err)
proxyCalled = true
conf := &socks5.Config{}
if test.auth != nil {
conf.Credentials = socks5.StaticCredentials{test.auth.user: test.auth.password}
}
server, err := socks5.New(conf)
require.NoError(t, err)
// We are not checking the error, because ServeConn is blocked until the client or the backend
// connection is closed which, in some cases, raises a connection reset by peer error.
_ = server.ServeConn(conn)
err = ln.Close()
require.NoError(t, err)
}()
case proxyHTTP:
proxyServer := httptest.NewServer(proxyHandler)
t.Cleanup(proxyServer.Close)
proxyURL = proxyServer.URL
case proxyHTTPS:
proxyServer := httptest.NewServer(proxyHandler)
t.Cleanup(proxyServer.Close)
proxyURL = proxyServer.URL
proxyCert = proxyServer.Certificate()
}
certPool := x509.NewCertPool()
if proxyCert != nil {
certPool.AddCert(proxyCert)
}
if backendCert != nil {
cert, err := x509.ParseCertificate(backendCert.Certificate[0])
require.NoError(t, err)
certPool.AddCert(cert)
}
builder := NewProxyBuilder(&transportManagerMock{tlsConfig: &tls.Config{RootCAs: certPool}}, static.FastProxyConfig{})
builder.proxy = func(req *http.Request) (*url.URL, error) {
u, err := url.Parse(proxyURL)
if err != nil {
return nil, err
}
if test.auth != nil {
u.User = url.UserPassword(test.auth.user, test.auth.password)
}
return u, nil
}
reverseProxy, err := builder.Build("foo", testhelpers.MustParseURL(backendURL), false, false)
require.NoError(t, err)
reverseProxyServer := httptest.NewServer(reverseProxy)
t.Cleanup(reverseProxyServer.Close)
client := http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(reverseProxyServer.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, "backend", string(body))
assert.True(t, proxyCalled)
})
}
}
func TestPreservePath(t *testing.T) {
var callCount int
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
callCount++
assert.Equal(t, "/base/foo/bar", req.URL.Path)
assert.Equal(t, "/base/foo%2Fbar", req.URL.RawPath)
}))
t.Cleanup(server.Close)
builder := NewProxyBuilder(&transportManagerMock{}, static.FastProxyConfig{})
serverURL, err := url.JoinPath(server.URL, "base")
require.NoError(t, err)
proxyHandler, err := builder.Build("", testhelpers.MustParseURL(serverURL), true, true)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "/foo%2Fbar", http.NoBody)
res := httptest.NewRecorder()
proxyHandler.ServeHTTP(res, req)
assert.Equal(t, 1, callCount)
assert.Equal(t, http.StatusOK, res.Code)
}
func newCertificate(t *testing.T, domain string) *tls.Certificate {
t.Helper()
certPEM, keyPEM, err := generate.KeyPair(domain, time.Time{})
require.NoError(t, err)
certificate, err := tls.X509KeyPair(certPEM, keyPEM)
require.NoError(t, err)
return &certificate
}
func newBackendServer(t *testing.T, isTLS bool, handler http.Handler) (string, *tls.Certificate) {
t.Helper()
var ln net.Listener
var err error
var cert *tls.Certificate
scheme := "http"
domain := "backend.localhost"
if isTLS {
scheme = "https"
cert = newCertificate(t, domain)
ln, err = tls.Listen("tcp", ":0", &tls.Config{Certificates: []tls.Certificate{*cert}})
require.NoError(t, err)
} else {
ln, err = net.Listen("tcp", ":0")
require.NoError(t, err)
}
srv := &http.Server{Handler: handler}
go func() { _ = srv.Serve(ln) }()
t.Cleanup(func() { _ = srv.Close() })
_, port, err := net.SplitHostPort(ln.Addr().String())
require.NoError(t, err)
backendURL := fmt.Sprintf("%s://%s:%s", scheme, domain, port)
return backendURL, cert
}
type transportManagerMock struct {
tlsConfig *tls.Config
}
func (r *transportManagerMock) GetTLSConfig(_ string) (*tls.Config, error) {
return r.tlsConfig, nil
}
func (r *transportManagerMock) Get(_ string) (*dynamic.ServersTransport, error) {
return &dynamic.ServersTransport{}, nil
}