diff --git a/README.md b/README.md index d8016eaf1bc43b8c842a35f48492b212a02e2eef..ab2ac6329316fb7b77b71e96afb2907d0741d7da 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,34 @@ We use `gorilla/handlers.ProxyHeaders` middleware. For more information please r > NOTE: This middleware should only be used when behind a reverse proxy like nginx, HAProxy or Apache. Reverse proxies that don't (or are configured not to) strip these headers from client requests, or where these headers are accepted "as is" from a remote client (e.g. when Go is not behind a proxy), can manifest as a vulnerability if your application uses these headers for validating the 'trustworthiness' of a request. +### PROXY protocol for HTTPS + +The above `listen-proxy` option only works for plaintext HTTP, where the reverse +proxy was already able to parse the incoming HTTP traffic and inject a header for +the remote client IP. + +This does not work for HTTPS which is generally proxied at the TCP level. In +order to propagate the remote client IP in this case, you can use the +[PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). +This is supported by HAProxy and some third party services such as Cloudflare. + +To configure PROXY protocol support, run `gitlab-pages` with the +`listen-https-proxyv2` flag. + +If you are using HAProxy as your TCP load balancer, you can configure the backend +with the `send-proxy-v2` option, like so: + +``` +frontend fe + bind 127.0.0.1:12340 + mode tcp + default_backend be + +backend be + mode tcp + server app1 127.0.0.1:1234 send-proxy-v2 +``` + ### GitLab access control GitLab access control is configured with properties `auth-client-id`, `auth-client-secret`, `auth-redirect-uri`, `auth-server` and `auth-secret`. Client ID, secret and redirect uri are configured in the GitLab and should match. `auth-server` points to a GitLab instance used for authentication. `auth-redirect-uri` should be `http(s)://pages-domain/auth`. Note that if the pages-domain is not handled by GitLab pages, then the `auth-redirect-uri` should use some reserved namespace prefix (such as `http(s)://projects.pages-domain/auth`). Using HTTPS is _strongly_ encouraged. `auth-secret` is used to encrypt the session cookie, and it should be strong enough. diff --git a/app.go b/app.go index 218c1be4f483e0deb86f33c00f79943ef3b0fd3a..ca4950734fb62bb5e44cce03eae9ef3e42fd52cb 100644 --- a/app.go +++ b/app.go @@ -369,6 +369,11 @@ func (a *theApp) Run() { a.listenProxyFD(&wg, fd, proxyHandler, limiter) } + // Listen for HTTPS PROXYv2 requests + for _, fd := range a.ListenHTTPSProxyv2 { + a.ListenHTTPSProxyv2FD(&wg, fd, proxyHandler, limiter) + } + // Serve metrics for Prometheus if a.ListenMetrics != 0 { a.listenMetricsFD(&wg, a.ListenMetrics) @@ -383,7 +388,7 @@ func (a *theApp) listenHTTPFD(wg *sync.WaitGroup, fd uintptr, httpHandler http.H wg.Add(1) go func() { defer wg.Done() - err := listenAndServe(fd, httpHandler, a.HTTP2, nil, limiter) + err := listenAndServe(fd, httpHandler, a.HTTP2, nil, limiter, false) if err != nil { capturingFatal(err, errortracking.WithField("listener", request.SchemeHTTP)) } @@ -399,7 +404,7 @@ func (a *theApp) listenHTTPSFD(wg *sync.WaitGroup, fd uintptr, httpHandler http. capturingFatal(err, errortracking.WithField("listener", request.SchemeHTTPS)) } - err = listenAndServe(fd, httpHandler, a.HTTP2, tlsConfig, limiter) + err = listenAndServe(fd, httpHandler, a.HTTP2, tlsConfig, limiter, false) if err != nil { capturingFatal(err, errortracking.WithField("listener", request.SchemeHTTPS)) } @@ -412,7 +417,7 @@ func (a *theApp) listenProxyFD(wg *sync.WaitGroup, fd uintptr, proxyHandler http wg.Add(1) go func(fd uintptr) { defer wg.Done() - err := listenAndServe(fd, proxyHandler, a.HTTP2, nil, limiter) + err := listenAndServe(fd, proxyHandler, a.HTTP2, nil, limiter, false) if err != nil { capturingFatal(err, errortracking.WithField("listener", "http proxy")) } @@ -420,6 +425,23 @@ func (a *theApp) listenProxyFD(wg *sync.WaitGroup, fd uintptr, proxyHandler http }() } +// https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt +func (a *theApp) ListenHTTPSProxyv2FD(wg *sync.WaitGroup, fd uintptr, httpHandler http.Handler, limiter *netutil.Limiter) { + wg.Add(1) + go func() { + defer wg.Done() + tlsConfig, err := a.TLSConfig() + if err != nil { + capturingFatal(err, errortracking.WithField("listener", request.SchemeHTTPS)) + } + + err = listenAndServe(fd, httpHandler, a.HTTP2, tlsConfig, limiter, true) + if err != nil { + capturingFatal(err, errortracking.WithField("listener", request.SchemeHTTPS)) + } + }() +} + func (a *theApp) listenMetricsFD(wg *sync.WaitGroup, fd uintptr) { wg.Add(1) go func() { diff --git a/app_config.go b/app_config.go index 3bc2197b1d1b9acff43e55e046f6c10d526dda9c..bb4aa917891f298bd75fe86814c2c36351f9b911 100644 --- a/app_config.go +++ b/app_config.go @@ -10,13 +10,14 @@ type appConfig struct { RootKey []byte MaxConns int - ListenHTTP []uintptr - ListenHTTPS []uintptr - ListenProxy []uintptr - ListenMetrics uintptr - InsecureCiphers bool - TLSMinVersion uint16 - TLSMaxVersion uint16 + ListenHTTP []uintptr + ListenHTTPS []uintptr + ListenProxy []uintptr + ListenHTTPSProxyv2 []uintptr + ListenMetrics uintptr + InsecureCiphers bool + TLSMinVersion uint16 + TLSMaxVersion uint16 HTTP2 bool RedirectHTTP bool diff --git a/daemon.go b/daemon.go index 11fa3e9e0fd4469ddb69ab6ec70b4de979341b13..c2404e05b0c2cab7ae0373f0474fdd166688c28e 100644 --- a/daemon.go +++ b/daemon.go @@ -330,6 +330,7 @@ func updateFds(config *appConfig, cmd *exec.Cmd) { config.ListenHTTP, config.ListenHTTPS, config.ListenProxy, + config.ListenHTTPSProxyv2, } { daemonUpdateFds(cmd, fds) } diff --git a/go.mod b/go.mod index a2cce75f46e08e1e79496258a4f40413974c7df5..00864c0424339a59b7c020ed72f2e2fac95bce89 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/namsral/flag v1.7.4-pre github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/pires/go-proxyproto v0.2.0 github.com/prometheus/client_golang v1.6.0 github.com/rs/cors v1.7.0 github.com/sirupsen/logrus v1.4.2 diff --git a/go.sum b/go.sum index 9c3086267dcf102839396bb59328dd2ab3a62f21..4f69bd34faa524ddfba45683372f4caf1ce083c7 100644 --- a/go.sum +++ b/go.sum @@ -245,6 +245,8 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pires/go-proxyproto v0.2.0 h1:WyYKlv9pkt77b+LjMvPfwrsAxviaGCFhG4KDIy1ofLY= +github.com/pires/go-proxyproto v0.2.0/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= @@ -342,10 +344,10 @@ github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FB github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -gitlab.com/gitlab-org/labkit v0.0.0-20201014124351-eb1fe6499318 h1:3xX/pl8dQjEtBZzHPCkex4Bwr7SGmVea/Zu4JdbZrKs= -gitlab.com/gitlab-org/labkit v0.0.0-20201014124351-eb1fe6499318/go.mod h1:SNfxkfUwVNECgtmluVayv0GWFgEjjBs5AzgsowPQuo0= gitlab.com/gitlab-org/go-mimedb v1.45.0 h1:PO8dx6HEWzPYU6MQTYnCbpQEJzhJLW/Bh43+2VUHTgc= gitlab.com/gitlab-org/go-mimedb v1.45.0/go.mod h1:wa9y/zOSFKmTXLyBs4clz2FNVhZQmmEQM9TxslPAjZ0= +gitlab.com/gitlab-org/labkit v0.0.0-20201014124351-eb1fe6499318 h1:3xX/pl8dQjEtBZzHPCkex4Bwr7SGmVea/Zu4JdbZrKs= +gitlab.com/gitlab-org/labkit v0.0.0-20201014124351-eb1fe6499318/go.mod h1:SNfxkfUwVNECgtmluVayv0GWFgEjjBs5AzgsowPQuo0= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= diff --git a/main.go b/main.go index 1d3979225f68ac00671b3bde6816ef5eae47b92a..7defd281c2b242bc61bab7a970151f5a2d3156df 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,7 @@ func init() { flag.Var(&listenHTTP, "listen-http", "The address(es) to listen on for HTTP requests") flag.Var(&listenHTTPS, "listen-https", "The address(es) to listen on for HTTPS requests") flag.Var(&listenProxy, "listen-proxy", "The address(es) to listen on for proxy requests") + flag.Var(&ListenHTTPSProxyv2, "listen-https-proxyv2", "The address(es) to listen on for HTTPS PROXYv2 requests (https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)") flag.Var(&header, "header", "The additional http header(s) that should be send to the client") } @@ -78,9 +79,10 @@ var ( disableCrossOriginRequests = flag.Bool("disable-cross-origin-requests", false, "Disable cross-origin requests") // See init() - listenHTTP MultiStringFlag - listenHTTPS MultiStringFlag - listenProxy MultiStringFlag + listenHTTP MultiStringFlag + listenHTTPS MultiStringFlag + listenProxy MultiStringFlag + ListenHTTPSProxyv2 MultiStringFlag header MultiStringFlag ) @@ -274,6 +276,7 @@ func loadConfig() appConfig { "listen-http": strings.Join(listenHTTP, ","), "listen-https": strings.Join(listenHTTPS, ","), "listen-proxy": strings.Join(listenProxy, ","), + "listen-https-proxyv2": strings.Join(ListenHTTPSProxyv2, ","), "log-format": *logFormat, "metrics-address": *metricsAddress, "pages-domain": *pagesDomain, @@ -389,6 +392,17 @@ func createAppListeners(config *appConfig) []io.Closer { config.ListenProxy = append(config.ListenProxy, f.Fd()) } + for _, addr := range ListenHTTPSProxyv2.Split() { + l, f := createSocket(addr) + closers = append(closers, l, f) + + log.WithFields(log.Fields{ + "listener": addr, + }).Debug("Set up https proxyv2 listener") + + config.ListenHTTPSProxyv2 = append(config.ListenHTTPSProxyv2, f.Fd()) + } + return closers } diff --git a/server.go b/server.go index 04ba818ad1586b0ea4d0aa70dfc2b83d39773bbc..678367a3e615a134931ec1865dd9e52ba8cbdafe 100644 --- a/server.go +++ b/server.go @@ -9,6 +9,7 @@ import ( "time" "github.com/gorilla/context" + proxyproto "github.com/pires/go-proxyproto" "golang.org/x/net/http2" "gitlab.com/gitlab-org/gitlab-pages/internal/netutil" @@ -36,7 +37,7 @@ func (ln *keepAliveListener) Accept() (net.Conn, error) { return conn, nil } -func listenAndServe(fd uintptr, handler http.Handler, useHTTP2 bool, tlsConfig *tls.Config, limiter *netutil.Limiter) error { +func listenAndServe(fd uintptr, handler http.Handler, useHTTP2 bool, tlsConfig *tls.Config, limiter *netutil.Limiter, proxyv2 bool) error { // create server server := &http.Server{Handler: context.ClearHandler(handler), TLSConfig: tlsConfig} @@ -56,9 +57,20 @@ func listenAndServe(fd uintptr, handler http.Handler, useHTTP2 bool, tlsConfig * l = netutil.SharedLimitListener(l, limiter) } + l = &keepAliveListener{l} + + if proxyv2 { + l = &proxyproto.Listener{ + Listener: l, + Policy: func(upstream net.Addr) (proxyproto.Policy, error) { + return proxyproto.REQUIRE, nil + }, + } + } + if tlsConfig != nil { - tlsListener := tls.NewListener(&keepAliveListener{l}, server.TLSConfig) - return server.Serve(tlsListener) + l = tls.NewListener(l, server.TLSConfig) } - return server.Serve(&keepAliveListener{l}) + + return server.Serve(l) } diff --git a/shared/lookups/zip-malformed.gitlab.io.json b/shared/lookups/zip-malformed.gitlab.io.json index 37ad1dddb5969d26bf40f41fa51ea14b83ca9c1e..8c0185dacd7db8fc885cd88c56f0e755f9d1f197 100644 --- a/shared/lookups/zip-malformed.gitlab.io.json +++ b/shared/lookups/zip-malformed.gitlab.io.json @@ -8,7 +8,7 @@ "prefix": "/", "project_id": 123, "source": { - "path": "http://127.0.0.1:37003/malformed.zip", + "path": "http://127.0.0.1:38001/malformed.zip", "type": "zip" } } diff --git a/shared/lookups/zip-not-found.gitlab.io.json b/shared/lookups/zip-not-found.gitlab.io.json index 94de4a902e8fe4b8d9af723592393352db888191..514b8ff2b454199664c73fae46f4fc9d0a37bfa2 100644 --- a/shared/lookups/zip-not-found.gitlab.io.json +++ b/shared/lookups/zip-not-found.gitlab.io.json @@ -8,7 +8,7 @@ "prefix": "/", "project_id": 123, "source": { - "path": "http://127.0.0.1:37003/not-found.zip", + "path": "http://127.0.0.1:38001/not-found.zip", "type": "zip" } } diff --git a/shared/lookups/zip.gitlab.io.json b/shared/lookups/zip.gitlab.io.json index cf755a5821e01d0db33e7b4d9b3700fd3acc2c0c..0549adc827edb4b0cc8364de2587c1c460886394 100644 --- a/shared/lookups/zip.gitlab.io.json +++ b/shared/lookups/zip.gitlab.io.json @@ -8,7 +8,7 @@ "prefix": "/", "project_id": 123, "source": { - "path": "http://127.0.0.1:37003/public.zip", + "path": "http://127.0.0.1:38001/public.zip", "type": "zip" } } diff --git a/test/acceptance/acceptance_test.go b/test/acceptance/acceptance_test.go index e155ce8b2f910fe7800ea95f5544666255b2c7c5..9921076ea0090895a99bd28a70cbd024fe0344a1 100644 --- a/test/acceptance/acceptance_test.go +++ b/test/acceptance/acceptance_test.go @@ -11,7 +11,7 @@ import ( ) const ( - objectStorageMockServer = "127.0.0.1:37003" + objectStorageMockServer = "127.0.0.1:38001" ) var ( @@ -27,11 +27,14 @@ var ( {"https", "::1", "37001"}, {"proxy", "127.0.0.1", "37002"}, {"proxy", "::1", "37002"}, + {"https-proxyv2", "127.0.0.1", "37003"}, + {"https-proxyv2", "::1", "37003"}, } - httpListener = listeners[0] - httpsListener = listeners[2] - proxyListener = listeners[4] + httpListener = listeners[0] + httpsListener = listeners[2] + proxyListener = listeners[4] + httpsProxyv2Listener = listeners[6] ) func TestMain(m *testing.M) { diff --git a/test/acceptance/helpers_test.go b/test/acceptance/helpers_test.go index 5412f6d5091dbcd7f186df326829f754241dcee2..3506e2362667574960dfc92cc56aae0b418b5ae0 100644 --- a/test/acceptance/helpers_test.go +++ b/test/acceptance/helpers_test.go @@ -2,6 +2,7 @@ package acceptance_test import ( "bytes" + "context" "crypto/tls" "crypto/x509" "fmt" @@ -14,14 +15,18 @@ import ( "os/exec" "path" "strings" + "sync" "testing" "time" + proxyproto "github.com/pires/go-proxyproto" "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitlab-pages/internal/request" ) +// The HTTPS certificate isn't signed by anyone. This http client is set up +// so it can talk to servers using it. var ( // The HTTPS certificate isn't signed by anyone. This http client is set up // so it can talk to servers using it. @@ -40,8 +45,49 @@ var ( }, } + // Proxyv2 client + TestProxyv2Client = &http.Client{ + Transport: &http.Transport{ + DialContext: Proxyv2DialContext, + TLSClientConfig: &tls.Config{RootCAs: TestCertPool}, + }, + } + + QuickTimeoutProxyv2Client = &http.Client{ + Transport: &http.Transport{ + DialContext: Proxyv2DialContext, + TLSClientConfig: &tls.Config{RootCAs: TestCertPool}, + ResponseHeaderTimeout: 100 * time.Millisecond, + }, + } + TestCertPool = x509.NewCertPool() + // Proxyv2 will create a dummy request with src 10.1.1.1:1000 + // and dst 20.2.2.2:2000 + Proxyv2DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + var d net.Dialer + + conn, err := d.DialContext(ctx, network, addr) + if err != nil { + return nil, err + } + + header := &proxyproto.Header{ + Version: 2, + Command: proxyproto.PROXY, + TransportProtocol: proxyproto.TCPv4, + SourceAddress: net.ParseIP("10.1.1.1"), + SourcePort: 1000, + DestinationAddress: net.ParseIP("20.2.2.2"), + DestinationPort: 2000, + } + + _, err = header.WriteTo(conn) + + return conn, err + } + existingAcmeTokenPath = "/.well-known/acme-challenge/existingtoken" notExistingAcmeTokenPath = "/.well-known/acme-challenge/notexistingtoken" ) @@ -56,6 +102,36 @@ func (t *tWriter) Write(b []byte) (int, error) { return len(b), nil } +type LogCaptureBuffer struct { + b bytes.Buffer + m sync.Mutex +} + +func (b *LogCaptureBuffer) Read(p []byte) (n int, err error) { + b.m.Lock() + defer b.m.Unlock() + + return b.b.Read(p) +} +func (b *LogCaptureBuffer) Write(p []byte) (n int, err error) { + b.m.Lock() + defer b.m.Unlock() + + return b.b.Write(p) +} +func (b *LogCaptureBuffer) String() string { + b.m.Lock() + defer b.m.Unlock() + + return b.b.String() +} +func (b *LogCaptureBuffer) Reset() { + b.m.Lock() + defer b.m.Unlock() + + b.b.Reset() +} + // ListenSpec is used to point at a gitlab-pages http server, preserving the // type of port it is (http, https, proxy) type ListenSpec struct { @@ -66,7 +142,7 @@ type ListenSpec struct { func (l ListenSpec) URL(suffix string) string { scheme := request.SchemeHTTP - if l.Type == request.SchemeHTTPS { + if l.Type == request.SchemeHTTPS || l.Type == "https-proxyv2" { scheme = request.SchemeHTTPS } @@ -90,7 +166,12 @@ func (l ListenSpec) WaitUntilRequestSucceeds(done chan struct{}) error { return err } - response, err := QuickTimeoutHTTPSClient.Transport.RoundTrip(req) + client := QuickTimeoutHTTPSClient + if l.Type == "https-proxyv2" { + client = QuickTimeoutProxyv2Client + } + + response, err := client.Transport.RoundTrip(req) if err != nil { time.Sleep(100 * time.Millisecond) continue @@ -117,19 +198,27 @@ func (l ListenSpec) JoinHostPort() string { // // If run as root via sudo, the gitlab-pages process will drop privileges func RunPagesProcess(t *testing.T, pagesBinary string, listeners []ListenSpec, promPort string, extraArgs ...string) (teardown func()) { - return runPagesProcess(t, true, pagesBinary, listeners, promPort, nil, extraArgs...) + _, cleanup := runPagesProcess(t, true, pagesBinary, listeners, promPort, nil, extraArgs...) + return cleanup } func RunPagesProcessWithoutWait(t *testing.T, pagesBinary string, listeners []ListenSpec, promPort string, extraArgs ...string) (teardown func()) { - return runPagesProcess(t, false, pagesBinary, listeners, promPort, nil, extraArgs...) + _, cleanup := runPagesProcess(t, false, pagesBinary, listeners, promPort, nil, extraArgs...) + return cleanup } func RunPagesProcessWithSSLCertFile(t *testing.T, pagesBinary string, listeners []ListenSpec, promPort string, sslCertFile string, extraArgs ...string) (teardown func()) { - return runPagesProcess(t, true, pagesBinary, listeners, promPort, []string{"SSL_CERT_FILE=" + sslCertFile}, extraArgs...) + _, cleanup := runPagesProcess(t, true, pagesBinary, listeners, promPort, []string{"SSL_CERT_FILE=" + sslCertFile}, extraArgs...) + return cleanup } func RunPagesProcessWithEnvs(t *testing.T, wait bool, pagesBinary string, listeners []ListenSpec, promPort string, envs []string, extraArgs ...string) (teardown func()) { - return runPagesProcess(t, wait, pagesBinary, listeners, promPort, envs, extraArgs...) + _, cleanup := runPagesProcess(t, wait, pagesBinary, listeners, promPort, envs, extraArgs...) + return cleanup +} + +func RunPagesProcessWithOutput(t *testing.T, pagesBinary string, listeners []ListenSpec, promPort string, extraArgs ...string) (out *LogCaptureBuffer, teardown func()) { + return runPagesProcess(t, true, pagesBinary, listeners, promPort, nil, extraArgs...) } func RunPagesProcessWithStubGitLabServer(t *testing.T, wait bool, pagesBinary string, listeners []ListenSpec, promPort string, envs []string, extraArgs ...string) (teardown func()) { @@ -139,7 +228,7 @@ func RunPagesProcessWithStubGitLabServer(t *testing.T, wait bool, pagesBinary st gitLabAPISecretKey := CreateGitLabAPISecretKeyFixtureFile(t) pagesArgs := append([]string{"-gitlab-server", source.URL, "-api-secret-key", gitLabAPISecretKey, "-domain-config-source", "gitlab"}, extraArgs...) - cleanup := runPagesProcess(t, wait, pagesBinary, listeners, promPort, envs, pagesArgs...) + _, cleanup := runPagesProcess(t, wait, pagesBinary, listeners, promPort, envs, pagesArgs...) return func() { source.Close() @@ -153,9 +242,10 @@ func RunPagesProcessWithAuth(t *testing.T, pagesBinary string, listeners []Liste "auth-redirect-uri=https://projects.gitlab-example.com/auth") defer cleanup() - return runPagesProcess(t, true, pagesBinary, listeners, promPort, nil, + _, cleanup2 := runPagesProcess(t, true, pagesBinary, listeners, promPort, nil, "-config="+configFile, ) + return cleanup2 } func RunPagesProcessWithAuthServer(t *testing.T, pagesBinary string, listeners []ListenSpec, promPort string, authServer string) func() { @@ -191,21 +281,25 @@ func runPagesProcessWithAuthServer(t *testing.T, pagesBinary string, listeners [ "auth-redirect-uri=https://projects.gitlab-example.com/auth") defer cleanup() - return runPagesProcess(t, true, pagesBinary, listeners, promPort, extraEnv, + _, cleanup2 := runPagesProcess(t, true, pagesBinary, listeners, promPort, extraEnv, "-config="+configFile) + return cleanup2 } -func runPagesProcess(t *testing.T, wait bool, pagesBinary string, listeners []ListenSpec, promPort string, extraEnv []string, extraArgs ...string) (teardown func()) { +func runPagesProcess(t *testing.T, wait bool, pagesBinary string, listeners []ListenSpec, promPort string, extraEnv []string, extraArgs ...string) (*LogCaptureBuffer, func()) { t.Helper() _, err := os.Stat(pagesBinary) require.NoError(t, err) + logBuf := &LogCaptureBuffer{} + out := io.MultiWriter(&tWriter{t}, logBuf) + args, tempfiles := getPagesArgs(t, listeners, promPort, extraArgs) cmd := exec.Command(pagesBinary, args...) cmd.Env = append(os.Environ(), extraEnv...) - cmd.Stdout = &tWriter{t} - cmd.Stderr = &tWriter{t} + cmd.Stdout = out + cmd.Stderr = out require.NoError(t, cmd.Start()) t.Logf("Running %s %v", pagesBinary, args) @@ -232,7 +326,7 @@ func runPagesProcess(t *testing.T, wait bool, pagesBinary string, listeners []Li } } - return cleanup + return logBuf, cleanup } func getPagesArgs(t *testing.T, listeners []ListenSpec, promPort string, extraArgs []string) (args, tempfiles []string) { @@ -329,7 +423,7 @@ func GetPageFromListenerWithCookie(t *testing.T, spec ListenSpec, host, urlsuffi req.Host = host - return DoPagesRequest(t, req) + return DoPagesRequest(t, spec, req) } func GetCompressedPageFromListener(t *testing.T, spec ListenSpec, host, urlsuffix string, encoding string) (*http.Response, error) { @@ -341,7 +435,7 @@ func GetCompressedPageFromListener(t *testing.T, spec ListenSpec, host, urlsuffi req.Host = host req.Header.Set("Accept-Encoding", encoding) - return DoPagesRequest(t, req) + return DoPagesRequest(t, spec, req) } func GetProxiedPageFromListener(t *testing.T, spec ListenSpec, host, xForwardedHost, urlsuffix string) (*http.Response, error) { @@ -354,12 +448,16 @@ func GetProxiedPageFromListener(t *testing.T, spec ListenSpec, host, xForwardedH req.Host = host req.Header.Set("X-Forwarded-Host", xForwardedHost) - return DoPagesRequest(t, req) + return DoPagesRequest(t, spec, req) } -func DoPagesRequest(t *testing.T, req *http.Request) (*http.Response, error) { +func DoPagesRequest(t *testing.T, spec ListenSpec, req *http.Request) (*http.Response, error) { t.Logf("curl -X %s -H'Host: %s' %s", req.Method, req.Host, req.URL) + if spec.Type == "https-proxyv2" { + return TestProxyv2Client.Do(req) + } + return TestHTTPSClient.Do(req) } @@ -395,6 +493,10 @@ func GetRedirectPageWithHeaders(t *testing.T, spec ListenSpec, host, urlsuffix s req.Host = host + if spec.Type == "https-proxyv2" { + return TestProxyv2Client.Transport.RoundTrip(req) + } + return TestHTTPSClient.Transport.RoundTrip(req) } @@ -416,7 +518,12 @@ func waitForRoundtrips(t *testing.T, listeners []ListenSpec, timeout time.Durati t.Fatal(err) } - if response, err := QuickTimeoutHTTPSClient.Transport.RoundTrip(req); err == nil { + client := QuickTimeoutHTTPSClient + if spec.Type == "https-proxyv2" { + client = QuickTimeoutProxyv2Client + } + + if response, err := client.Transport.RoundTrip(req); err == nil { nListening++ response.Body.Close() break diff --git a/test/acceptance/proxyv2_test.go b/test/acceptance/proxyv2_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c407ea194d6c99833fad0115854938f55ab484e2 --- /dev/null +++ b/test/acceptance/proxyv2_test.go @@ -0,0 +1,52 @@ +package acceptance_test + +import ( + "io/ioutil" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestProxyv2(t *testing.T) { + skipUnlessEnabled(t) + + logBuf, teardown := RunPagesProcessWithOutput(t, *pagesBinary, listeners, "") + defer teardown() + + // the dummy client IP 10.1.1.1 is set by TestProxyv2Client + tests := map[string]struct { + host string + urlSuffix string + expectedStatusCode int + expectedContent string + expectedLog string + }{ + "basic_proxyv2_request": { + host: "group.gitlab-example.com", + urlSuffix: "project/", + expectedStatusCode: http.StatusOK, + expectedContent: "project-subdir\n", + expectedLog: "group.gitlab-example.com 10.1.1.1", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + logBuf.Reset() + + response, err := GetPageFromListener(t, httpsProxyv2Listener, tt.host, tt.urlSuffix) + require.NoError(t, err) + defer response.Body.Close() + + require.Equal(t, tt.expectedStatusCode, response.StatusCode) + + body, err := ioutil.ReadAll(response.Body) + require.NoError(t, err) + + require.Contains(t, string(body), tt.expectedContent, "content mismatch") + + require.Contains(t, logBuf.String(), tt.expectedLog, "log mismatch") + }) + } +} diff --git a/test/acceptance/serving_test.go b/test/acceptance/serving_test.go index 0193594694cbb72a0e12c7f48f145da4fe478dd0..4ccdd8f40bc2abfb1908c3b5c3ef04209aa57bfb 100644 --- a/test/acceptance/serving_test.go +++ b/test/acceptance/serving_test.go @@ -213,7 +213,7 @@ func TestCORSWhenDisabled(t *testing.T) { for _, spec := range listeners { for _, method := range []string{"GET", "OPTIONS"} { - rsp := doCrossOriginRequest(t, method, method, spec.URL("project/")) + rsp := doCrossOriginRequest(t, spec, method, method, spec.URL("project/")) require.Equal(t, http.StatusOK, rsp.StatusCode) require.Equal(t, "", rsp.Header.Get("Access-Control-Allow-Origin")) @@ -229,7 +229,7 @@ func TestCORSAllowsGET(t *testing.T) { for _, spec := range listeners { for _, method := range []string{"GET", "OPTIONS"} { - rsp := doCrossOriginRequest(t, method, method, spec.URL("project/")) + rsp := doCrossOriginRequest(t, spec, method, method, spec.URL("project/")) require.Equal(t, http.StatusOK, rsp.StatusCode) require.Equal(t, "*", rsp.Header.Get("Access-Control-Allow-Origin")) @@ -245,7 +245,7 @@ func TestCORSForbidsPOST(t *testing.T) { defer teardown() for _, spec := range listeners { - rsp := doCrossOriginRequest(t, "OPTIONS", "POST", spec.URL("project/")) + rsp := doCrossOriginRequest(t, spec, "OPTIONS", "POST", spec.URL("project/")) require.Equal(t, http.StatusOK, rsp.StatusCode) require.Equal(t, "", rsp.Header.Get("Access-Control-Allow-Origin")) @@ -502,7 +502,7 @@ func TestKnownHostInReverseProxySetupReturns200(t *testing.T) { } } -func doCrossOriginRequest(t *testing.T, method, reqMethod, url string) *http.Response { +func doCrossOriginRequest(t *testing.T, spec ListenSpec, method, reqMethod, url string) *http.Response { req, err := http.NewRequest(method, url, nil) require.NoError(t, err) @@ -513,7 +513,7 @@ func doCrossOriginRequest(t *testing.T, method, reqMethod, url string) *http.Res var rsp *http.Response err = fmt.Errorf("no request was made") for start := time.Now(); time.Since(start) < 1*time.Second; { - rsp, err = DoPagesRequest(t, req) + rsp, err = DoPagesRequest(t, spec, req) if err == nil { break }