mirror of
				https://github.com/traefik/traefik.git
				synced 2025-11-04 02:11:15 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			338 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			338 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
 | 
						|
}
 |