diff options
Diffstat (limited to 'test')
-rw-r--r-- | test/acceptance/acme_test.go | 6 | ||||
-rw-r--r-- | test/acceptance/artifacts_test.go | 19 | ||||
-rw-r--r-- | test/acceptance/auth_test.go | 70 | ||||
-rw-r--r-- | test/acceptance/encodings_test.go | 6 | ||||
-rw-r--r-- | test/acceptance/helpers_test.go | 167 | ||||
-rw-r--r-- | test/acceptance/metrics_test.go | 6 | ||||
-rw-r--r-- | test/acceptance/proxyv2_test.go | 4 | ||||
-rw-r--r-- | test/acceptance/ratelimiter_test.go | 128 | ||||
-rw-r--r-- | test/acceptance/redirects_test.go | 7 | ||||
-rw-r--r-- | test/acceptance/rewrites_test.go | 3 | ||||
-rw-r--r-- | test/acceptance/serving_test.go | 32 | ||||
-rw-r--r-- | test/acceptance/status_test.go | 4 | ||||
-rw-r--r-- | test/acceptance/unknown_http_method_test.go | 4 | ||||
-rw-r--r-- | test/acceptance/zip_test.go | 13 |
14 files changed, 306 insertions, 163 deletions
diff --git a/test/acceptance/acme_test.go b/test/acceptance/acme_test.go index 77c4d6c0..d743a4e1 100644 --- a/test/acceptance/acme_test.go +++ b/test/acceptance/acme_test.go @@ -7,6 +7,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" ) const ( @@ -41,8 +43,8 @@ func TestAcmeChallengesWhenItIsNotConfigured(t *testing.T) { rsp, err := GetRedirectPage(t, httpListener, "withacmechallenge.domain.com", test.token) + testhelpers.Close(t, rsp.Body) require.NoError(t, err) - defer rsp.Body.Close() require.Equal(t, test.expectedStatus, rsp.StatusCode) body, err := io.ReadAll(rsp.Body) require.NoError(t, err) @@ -82,8 +84,8 @@ func TestAcmeChallengesWhenItIsConfigured(t *testing.T) { rsp, err := GetRedirectPage(t, httpListener, "withacmechallenge.domain.com", test.token) + testhelpers.Close(t, rsp.Body) require.NoError(t, err) - defer rsp.Body.Close() require.Equal(t, test.expectedStatus, rsp.StatusCode) body, err := io.ReadAll(rsp.Body) require.NoError(t, err) diff --git a/test/acceptance/artifacts_test.go b/test/acceptance/artifacts_test.go index f087581c..65018c2c 100644 --- a/test/acceptance/artifacts_test.go +++ b/test/acceptance/artifacts_test.go @@ -11,12 +11,11 @@ import ( "time" "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" ) func TestArtifactProxyRequest(t *testing.T) { - transport := (TestHTTPSClient.Transport).(*http.Transport).Clone() - transport.ResponseHeaderTimeout = 5 * time.Second - content := "<!DOCTYPE html><html><head><title>Title of the document</title></head><body></body></html>" contentLength := int64(len(content)) testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -133,7 +132,7 @@ func TestArtifactProxyRequest(t *testing.T) { resp, err := GetPageFromListener(t, httpListener, tt.host, tt.path) require.NoError(t, err) - defer resp.Body.Close() + testhelpers.Close(t, resp.Body) require.Equal(t, tt.status, resp.StatusCode) require.Equal(t, tt.contentType, resp.Header.Get("Content-Type")) @@ -150,8 +149,6 @@ func TestArtifactProxyRequest(t *testing.T) { } func TestPrivateArtifactProxyRequest(t *testing.T) { - setupTransport(t) - testServer := NewGitlabUnstartedServerStub(t, &stubOpts{}) keyFile, certFile := CreateHTTPSFixtureFiles(t) @@ -229,7 +226,7 @@ func TestPrivateArtifactProxyRequest(t *testing.T) { resp, err := GetRedirectPage(t, httpsListener, tt.host, tt.path) require.NoError(t, err) - defer resp.Body.Close() + testhelpers.Close(t, resp.Body) require.Equal(t, http.StatusFound, resp.StatusCode) @@ -245,7 +242,7 @@ func TestPrivateArtifactProxyRequest(t *testing.T) { resp, err = GetRedirectPage(t, httpsListener, url.Host, url.Path+"?"+url.RawQuery) require.NoError(t, err) - defer resp.Body.Close() + testhelpers.Close(t, resp.Body) require.Equal(t, http.StatusFound, resp.StatusCode) pagesDomainCookie := resp.Header.Get("Set-Cookie") @@ -255,7 +252,7 @@ func TestPrivateArtifactProxyRequest(t *testing.T) { state, pagesDomainCookie) require.NoError(t, err) - defer authrsp.Body.Close() + testhelpers.Close(t, authrsp.Body) // Will redirect auth callback to correct host url, err = url.Parse(authrsp.Header.Get("Location")) @@ -266,7 +263,7 @@ func TestPrivateArtifactProxyRequest(t *testing.T) { // Request auth callback in project domain authrsp, err = GetRedirectPageWithCookie(t, httpsListener, url.Host, url.Path+"?"+url.RawQuery, cookie) require.NoError(t, err) - defer authrsp.Body.Close() + testhelpers.Close(t, authrsp.Body) // server returns the ticket, user will be redirected to the project page require.Equal(t, http.StatusFound, authrsp.StatusCode) @@ -276,7 +273,7 @@ func TestPrivateArtifactProxyRequest(t *testing.T) { require.Equal(t, tt.status, resp.StatusCode) require.NoError(t, err) - defer resp.Body.Close() + testhelpers.Close(t, resp.Body) }) } } diff --git a/test/acceptance/auth_test.go b/test/acceptance/auth_test.go index 18b73161..d7677622 100644 --- a/test/acceptance/auth_test.go +++ b/test/acceptance/auth_test.go @@ -8,6 +8,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" ) func TestWhenAuthIsDisabledPrivateIsNotAccessible(t *testing.T) { @@ -33,7 +35,7 @@ func TestWhenAuthIsEnabledPrivateWillRedirectToAuthorize(t *testing.T) { rsp, err := GetRedirectPage(t, httpsListener, "group.auth.gitlab-example.com", "private.project/") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusFound, rsp.StatusCode) require.Equal(t, 1, len(rsp.Header["Location"])) @@ -41,7 +43,7 @@ func TestWhenAuthIsEnabledPrivateWillRedirectToAuthorize(t *testing.T) { require.NoError(t, err) rsp, err = GetRedirectPage(t, httpsListener, url.Host, url.Path+"?"+url.RawQuery) require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusFound, rsp.StatusCode) require.Equal(t, 1, len(rsp.Header["Location"])) @@ -69,7 +71,7 @@ func TestWhenAuthDeniedWillCauseUnauthorized(t *testing.T) { rsp, err := GetPageFromListener(t, httpsListener, "projects.gitlab-example.com", "/auth?error=access_denied") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusUnauthorized, rsp.StatusCode) } @@ -84,13 +86,13 @@ func TestWhenLoginCallbackWithWrongStateShouldFail(t *testing.T) { rsp, err := GetRedirectPage(t, httpsListener, "group.auth.gitlab-example.com", "private.project/") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) // Go to auth page with wrong state will cause failure authrsp, err := GetPageFromListener(t, httpsListener, "projects.gitlab-example.com", "/auth?code=0&state=0") require.NoError(t, err) - defer authrsp.Body.Close() + testhelpers.Close(t, authrsp.Body) require.Equal(t, http.StatusUnauthorized, authrsp.StatusCode) } @@ -106,7 +108,7 @@ func TestWhenLoginCallbackWithUnencryptedCode(t *testing.T) { rsp, err := GetRedirectPage(t, httpsListener, "group.auth.gitlab-example.com", "private.project/") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) cookie := rsp.Header.Get("Set-Cookie") @@ -122,7 +124,7 @@ func TestWhenLoginCallbackWithUnencryptedCode(t *testing.T) { url.Query().Get("state"), header) require.NoError(t, err) - defer authrsp.Body.Close() + testhelpers.Close(t, authrsp.Body) // Will cause 500 because the code is not encrypted require.Equal(t, http.StatusInternalServerError, authrsp.StatusCode) @@ -153,7 +155,7 @@ func TestAccessControlUnderCustomDomain(t *testing.T) { t.Run(name, func(t *testing.T) { rsp, err := GetRedirectPage(t, httpListener, tt.domain, tt.path) require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) cookie := rsp.Header.Get("Set-Cookie") @@ -165,7 +167,7 @@ func TestAccessControlUnderCustomDomain(t *testing.T) { pagesrsp, err := GetRedirectPage(t, httpListener, url.Host, url.Path+"?"+url.RawQuery) require.NoError(t, err) - defer pagesrsp.Body.Close() + testhelpers.Close(t, pagesrsp.Body) pagescookie := pagesrsp.Header.Get("Set-Cookie") @@ -174,7 +176,7 @@ func TestAccessControlUnderCustomDomain(t *testing.T) { state, pagescookie) require.NoError(t, err) - defer authrsp.Body.Close() + testhelpers.Close(t, authrsp.Body) url, err = url.Parse(authrsp.Header.Get("Location")) require.NoError(t, err) @@ -188,7 +190,7 @@ func TestAccessControlUnderCustomDomain(t *testing.T) { state, cookie) require.NoError(t, err) - defer authrsp.Body.Close() + testhelpers.Close(t, authrsp.Body) // Will redirect to the page cookie = authrsp.Header.Get("Set-Cookie") @@ -203,7 +205,7 @@ func TestAccessControlUnderCustomDomain(t *testing.T) { // Fetch page in custom domain authrsp, err = GetRedirectPageWithCookie(t, httpListener, tt.domain, tt.path, cookie) require.NoError(t, err) - defer authrsp.Body.Close() + testhelpers.Close(t, authrsp.Body) require.Equal(t, http.StatusOK, authrsp.StatusCode) }) } @@ -254,7 +256,7 @@ func TestCustomErrorPageWithAuth(t *testing.T) { t.Run(tt.name, func(t *testing.T) { rsp, err := GetRedirectPage(t, httpListener, tt.domain, tt.path) require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) cookie := rsp.Header.Get("Set-Cookie") @@ -266,7 +268,7 @@ func TestCustomErrorPageWithAuth(t *testing.T) { pagesrsp, err := GetRedirectPage(t, httpListener, url.Host, url.Path+"?"+url.RawQuery) require.NoError(t, err) - defer pagesrsp.Body.Close() + testhelpers.Close(t, pagesrsp.Body) pagescookie := pagesrsp.Header.Get("Set-Cookie") @@ -275,7 +277,7 @@ func TestCustomErrorPageWithAuth(t *testing.T) { state, pagescookie) require.NoError(t, err) - defer authrsp.Body.Close() + testhelpers.Close(t, authrsp.Body) url, err = url.Parse(authrsp.Header.Get("Location")) require.NoError(t, err) @@ -292,7 +294,7 @@ func TestCustomErrorPageWithAuth(t *testing.T) { state, cookie) require.NoError(t, err) - defer authrsp.Body.Close() + testhelpers.Close(t, authrsp.Body) // Will redirect to the page groupCookie := authrsp.Header.Get("Set-Cookie") @@ -307,7 +309,7 @@ func TestCustomErrorPageWithAuth(t *testing.T) { // Fetch page in custom domain anotherResp, err := GetRedirectPageWithCookie(t, httpListener, tt.domain, tt.path, groupCookie) require.NoError(t, err) - defer anotherResp.Body.Close() + testhelpers.Close(t, anotherResp.Body) require.Equal(t, http.StatusNotFound, anotherResp.StatusCode) @@ -328,7 +330,7 @@ func TestAccessControlUnderCustomDomainWithHTTPSProxy(t *testing.T) { rsp, err := GetProxyRedirectPageWithCookie(t, proxyListener, "private.domain.com", "/", "", true) require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) cookie := rsp.Header.Get("Set-Cookie") @@ -339,7 +341,7 @@ func TestAccessControlUnderCustomDomainWithHTTPSProxy(t *testing.T) { require.Equal(t, url.Query().Get("domain"), "https://private.domain.com") pagesrsp, err := GetProxyRedirectPageWithCookie(t, proxyListener, url.Host, url.Path+"?"+url.RawQuery, "", true) require.NoError(t, err) - defer pagesrsp.Body.Close() + testhelpers.Close(t, pagesrsp.Body) pagescookie := pagesrsp.Header.Get("Set-Cookie") @@ -349,7 +351,7 @@ func TestAccessControlUnderCustomDomainWithHTTPSProxy(t *testing.T) { pagescookie, true) require.NoError(t, err) - defer authrsp.Body.Close() + testhelpers.Close(t, authrsp.Body) url, err = url.Parse(authrsp.Header.Get("Location")) require.NoError(t, err) @@ -366,7 +368,7 @@ func TestAccessControlUnderCustomDomainWithHTTPSProxy(t *testing.T) { "/auth?code="+code+"&state="+state, cookie, true) require.NoError(t, err) - defer authrsp.Body.Close() + testhelpers.Close(t, authrsp.Body) // Will redirect to the page cookie = authrsp.Header.Get("Set-Cookie") @@ -381,7 +383,7 @@ func TestAccessControlUnderCustomDomainWithHTTPSProxy(t *testing.T) { authrsp, err = GetProxyRedirectPageWithCookie(t, proxyListener, "private.domain.com", "/", cookie, true) require.NoError(t, err) - defer authrsp.Body.Close() + testhelpers.Close(t, authrsp.Body) require.Equal(t, http.StatusOK, authrsp.StatusCode) } @@ -395,7 +397,7 @@ func TestAccessControlGroupDomain404RedirectsAuth(t *testing.T) { rsp, err := GetRedirectPage(t, httpListener, "group.gitlab-example.com", "/nonexistent/") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusFound, rsp.StatusCode) // Redirects to the projects under gitlab pages domain for authentication flow url, err := url.Parse(rsp.Header.Get("Location")) @@ -414,15 +416,13 @@ func TestAccessControlProject404DoesNotRedirect(t *testing.T) { rsp, err := GetRedirectPage(t, httpListener, "group.gitlab-example.com", "/project/nonexistent/") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusNotFound, rsp.StatusCode) } type runPagesFunc func(t *testing.T, listeners []ListenSpec, sslCertFile string) func testAccessControl(t *testing.T, runPages runPagesFunc) { - setupTransport(t) - _, certFile := CreateHTTPSFixtureFiles(t) tests := map[string]struct { @@ -488,7 +488,7 @@ func testAccessControl(t *testing.T, runPages runPagesFunc) { t.Run(tn, func(t *testing.T) { rsp1, err1 := GetRedirectPage(t, httpsListener, tt.host, tt.path) require.NoError(t, err1) - defer rsp1.Body.Close() + testhelpers.Close(t, rsp1.Body) require.Equal(t, http.StatusFound, rsp1.StatusCode) cookie := rsp1.Header.Get("Set-Cookie") @@ -502,7 +502,7 @@ func testAccessControl(t *testing.T, runPages runPagesFunc) { rsp2, err2 := GetRedirectPage(t, httpsListener, loc1.Host, loc1.Path+"?"+loc1.RawQuery) require.NoError(t, err2) - defer rsp2.Body.Close() + testhelpers.Close(t, rsp2.Body) require.Equal(t, http.StatusFound, rsp2.StatusCode) pagesDomainCookie := rsp2.Header.Get("Set-Cookie") @@ -511,7 +511,7 @@ func testAccessControl(t *testing.T, runPages runPagesFunc) { authrsp1, err := GetRedirectPageWithCookie(t, httpsListener, "projects.gitlab-example.com", "/auth?code=1&state="+ state, pagesDomainCookie) require.NoError(t, err) - defer authrsp1.Body.Close() + testhelpers.Close(t, authrsp1.Body) // Will redirect auth callback to correct host authLoc, err := url.Parse(authrsp1.Header.Get("Location")) @@ -522,7 +522,7 @@ func testAccessControl(t *testing.T, runPages runPagesFunc) { // Request auth callback in project domain authrsp2, err := GetRedirectPageWithCookie(t, httpsListener, authLoc.Host, authLoc.Path+"?"+authLoc.RawQuery, cookie) require.NoError(t, err) - defer authrsp2.Body.Close() + testhelpers.Close(t, authrsp2.Body) // server returns the ticket, user will be redirected to the project page require.Equal(t, http.StatusFound, authrsp2.StatusCode) @@ -530,7 +530,7 @@ func testAccessControl(t *testing.T, runPages runPagesFunc) { rsp3, err3 := GetRedirectPageWithCookie(t, httpsListener, tt.host, tt.path, cookie) require.NoError(t, err3) - defer rsp3.Body.Close() + testhelpers.Close(t, rsp3.Body) require.Equal(t, tt.status, rsp3.StatusCode) @@ -580,7 +580,7 @@ func TestHijackedCode(t *testing.T) { hackedURL := fmt.Sprintf("/auth?domain=http://%s&state=%s", attackersDomain, "irrelevant") maliciousResp, err := GetProxyRedirectPageWithCookie(t, proxyListener, "projects.gitlab-example.com", hackedURL, "", true) require.NoError(t, err) - defer maliciousResp.Body.Close() + testhelpers.Close(t, maliciousResp.Body) pagesCookie := maliciousResp.Header.Get("Set-Cookie") @@ -597,7 +597,7 @@ func TestHijackedCode(t *testing.T) { pagesCookie, true) require.NoError(t, err) - defer authrsp.Body.Close() + testhelpers.Close(t, authrsp.Body) /****ATTACKER******/ // Target is redirected to attacker's domain and attacker receives the proper code @@ -614,7 +614,7 @@ func TestHijackedCode(t *testing.T) { impersonatingRes, err := GetProxyRedirectPageWithCookie(t, proxyListener, targetDomain, "/auth?code="+hijackedCode+"&state="+attackerState, attackerCookie, true) require.NoError(t, err) - defer impersonatingRes.Body.Close() + testhelpers.Close(t, impersonatingRes.Body) require.Equal(t, impersonatingRes.StatusCode, http.StatusInternalServerError, "should fail to decode code") } @@ -626,7 +626,7 @@ func getValidCookieAndState(t *testing.T, domain string) (string, string) { // visit https://<domain>/ rsp, err := GetProxyRedirectPageWithCookie(t, proxyListener, domain, "/", "", true) require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) cookie := rsp.Header.Get("Set-Cookie") require.NotEmpty(t, cookie) diff --git a/test/acceptance/encodings_test.go b/test/acceptance/encodings_test.go index 18f2c492..c3532bd9 100644 --- a/test/acceptance/encodings_test.go +++ b/test/acceptance/encodings_test.go @@ -6,6 +6,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" ) func TestMIMETypes(t *testing.T) { @@ -27,7 +29,7 @@ func TestMIMETypes(t *testing.T) { t.Run(name, func(t *testing.T) { rsp, err := GetPageFromListener(t, httpListener, "group.gitlab-example.com", "project/"+tt.file) require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusOK, rsp.StatusCode) mt, _, err := mime.ParseMediaType(rsp.Header.Get("Content-Type")) @@ -69,7 +71,7 @@ func TestCompressedEncoding(t *testing.T) { } rsp, err := GetPageFromListenerWithHeaders(t, httpListener, "group.gitlab-example.com", "index.html", header) require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusOK, rsp.StatusCode) require.Equal(t, tt.encoding, rsp.Header.Get("Content-Encoding")) diff --git a/test/acceptance/helpers_test.go b/test/acceptance/helpers_test.go index 62a5e344..1b514a85 100644 --- a/test/acceptance/helpers_test.go +++ b/test/acceptance/helpers_test.go @@ -34,65 +34,10 @@ import ( // 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. - TestHTTPSClient = &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{RootCAs: TestCertPool}, - }, - } - - // Use HTTP with a very short timeout to repeatedly check for the server to be - // up. Again, ignore HTTP - QuickTimeoutHTTPSClient = &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{RootCAs: TestCertPool}, - ResponseHeaderTimeout: 100 * time.Millisecond, - }, - } - - // 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 - } ) type tWriter struct { @@ -151,15 +96,84 @@ func supportedListeners() []ListenSpec { return listeners } -func (l ListenSpec) URL(suffix string) string { - scheme := request.SchemeHTTP +func (l ListenSpec) Scheme() string { if l.Type == request.SchemeHTTPS || l.Type == "https-proxyv2" { - scheme = request.SchemeHTTPS + return request.SchemeHTTPS } + return request.SchemeHTTP +} + +func (l ListenSpec) URL(suffix string) string { suffix = strings.TrimPrefix(suffix, "/") - return fmt.Sprintf("%s://%s/%s", scheme, l.JoinHostPort(), suffix) + return fmt.Sprintf("%s://%s/%s", l.Scheme(), l.JoinHostPort(), suffix) +} + +type dialContext func(ctx context.Context, network, addr string) (net.Conn, error) + +func (l ListenSpec) proxyV2DialContext() dialContext { + return func(ctx context.Context, network, addr string) (net.Conn, error) { + var d net.Dialer + + // bypass DNS resolution by going directly to host and port + conn, err := d.DialContext(ctx, network, l.JoinHostPort()) + 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 + } +} + +func (l ListenSpec) httpsDialContext() dialContext { + return func(ctx context.Context, network, addr string) (net.Conn, error) { + var d net.Dialer + + // bypass DNS resolution by going directly to host and port + return d.DialContext(ctx, network, l.JoinHostPort()) + } +} + +func (l ListenSpec) dialContext() dialContext { + if l.Type == "https-proxyv2" { + return l.proxyV2DialContext() + } + + return l.httpsDialContext() +} + +func (l ListenSpec) Client() *http.Client { + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{RootCAs: TestCertPool}, + DialContext: l.dialContext(), + ResponseHeaderTimeout: 5 * time.Second, + }, + } +} + +// Use a very short timeout to repeatedly check for the server to be up. +func (l ListenSpec) QuickTimeoutClient() *http.Client { + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{RootCAs: TestCertPool}, + DialContext: l.dialContext(), + ResponseHeaderTimeout: 100 * time.Millisecond, + }, + } } // Returns only once this spec points at a working TCP server @@ -177,12 +191,7 @@ func (l ListenSpec) WaitUntilRequestSucceeds(done chan struct{}) error { return err } - client := QuickTimeoutHTTPSClient - if l.Type == "https-proxyv2" { - client = QuickTimeoutProxyv2Client - } - - response, err := client.Transport.RoundTrip(req) + response, err := l.QuickTimeoutClient().Transport.RoundTrip(req) if err != nil { time.Sleep(100 * time.Millisecond) continue @@ -380,11 +389,7 @@ func GetPageFromListenerWithHeaders(t *testing.T, spec ListenSpec, host, urlSuff 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) + return spec.Client().Do(req) } func GetRedirectPage(t *testing.T, spec ListenSpec, host, urlsuffix string) (*http.Response, error) { @@ -419,11 +424,7 @@ 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) + return spec.Client().Transport.RoundTrip(req) } func ClientWithConfig(tlsConfig *tls.Config) (*http.Client, func()) { @@ -602,12 +603,14 @@ func copyFile(dest, src string) error { return err } -func setupTransport(t *testing.T) { - t.Helper() +// RequireMetricEqual requests prometheus metrics and makes sure metric is there +func RequireMetricEqual(t *testing.T, metricsAddress, metricWithValue string) { + resp, err := http.Get(fmt.Sprintf("http://%s/metrics", metricsAddress)) + require.NoError(t, err) + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) - transport := (TestHTTPSClient.Transport).(*http.Transport) - defer func(t time.Duration) { - transport.ResponseHeaderTimeout = t - }(transport.ResponseHeaderTimeout) - transport.ResponseHeaderTimeout = 5 * time.Second + require.Contains(t, string(body), metricWithValue) } diff --git a/test/acceptance/metrics_test.go b/test/acceptance/metrics_test.go index 32a36638..d1e8a1b6 100644 --- a/test/acceptance/metrics_test.go +++ b/test/acceptance/metrics_test.go @@ -6,6 +6,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" ) func TestPrometheusMetricsCanBeScraped(t *testing.T) { @@ -20,13 +22,13 @@ func TestPrometheusMetricsCanBeScraped(t *testing.T) { res, err := GetPageFromListener(t, httpListener, "zip.gitlab.io", "/symlink.html") require.NoError(t, err) - defer res.Body.Close() + testhelpers.Close(t, res.Body) require.Equal(t, http.StatusOK, res.StatusCode) resp, err := http.Get("http://127.0.0.1:42345/metrics") require.NoError(t, err) - defer resp.Body.Close() + testhelpers.Close(t, resp.Body) body, err := io.ReadAll(resp.Body) require.NoError(t, err) diff --git a/test/acceptance/proxyv2_test.go b/test/acceptance/proxyv2_test.go index 81a7ff94..45bdcb89 100644 --- a/test/acceptance/proxyv2_test.go +++ b/test/acceptance/proxyv2_test.go @@ -7,6 +7,8 @@ import ( "time" "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" ) func TestProxyv2(t *testing.T) { @@ -37,7 +39,7 @@ func TestProxyv2(t *testing.T) { response, err := GetPageFromListener(t, httpsProxyv2Listener, tt.host, tt.urlSuffix) require.NoError(t, err) - defer response.Body.Close() + testhelpers.Close(t, response.Body) require.Equal(t, tt.expectedStatusCode, response.StatusCode) diff --git a/test/acceptance/ratelimiter_test.go b/test/acceptance/ratelimiter_test.go index 02ba54f5..a97fdfb1 100644 --- a/test/acceptance/ratelimiter_test.go +++ b/test/acceptance/ratelimiter_test.go @@ -3,6 +3,7 @@ package acceptance_test import ( "fmt" "net/http" + "strconv" "testing" "time" @@ -77,7 +78,7 @@ func TestIPRateLimits(t *testing.T) { } } -func TestDomainateLimits(t *testing.T) { +func TestDomainRateLimits(t *testing.T) { testhelpers.StubFeatureFlagValue(t, feature.EnforceDomainRateLimits.EnvVariable, true) for name, tc := range ratelimitedListeners { @@ -112,6 +113,131 @@ func TestDomainateLimits(t *testing.T) { } } +func TestTLSRateLimits(t *testing.T) { + tests := map[string]struct { + spec ListenSpec + domainLimit bool + sourceIP string + enforceEnabled bool + }{ + "https_with_domain_limit": { + spec: httpsListener, + domainLimit: true, + sourceIP: "127.0.0.1", + enforceEnabled: true, + }, + "https_with_domain_limit_not_enforced": { + spec: httpsListener, + domainLimit: true, + sourceIP: "127.0.0.1", + enforceEnabled: false, + }, + "https_with_ip_limit": { + spec: httpsListener, + sourceIP: "127.0.0.1", + enforceEnabled: true, + }, + "https_with_ip_limit_not_enforced": { + spec: httpsListener, + sourceIP: "127.0.0.1", + enforceEnabled: false, + }, + "proxyv2_with_domain_limit": { + spec: httpsProxyv2Listener, + domainLimit: true, + sourceIP: "10.1.1.1", + enforceEnabled: true, + }, + "proxyv2_with_domain_limit_not_enforced": { + spec: httpsProxyv2Listener, + domainLimit: true, + sourceIP: "10.1.1.1", + enforceEnabled: false, + }, + "proxyv2_with_ip_limit": { + spec: httpsProxyv2Listener, + sourceIP: "10.1.1.1", + enforceEnabled: true, + }, + "proxyv2_with_ip_limit_not_enforced": { + spec: httpsProxyv2Listener, + sourceIP: "10.1.1.1", + enforceEnabled: false, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + rateLimit := 5 + + options := []processOption{ + withListeners([]ListenSpec{tt.spec}), + withExtraArgument("metrics-address", ":42345"), + } + + featureName := feature.EnforceIPTLSRateLimits.EnvVariable + limitName := "tls_connections_by_source_ip" + + if tt.domainLimit { + options = append(options, + withExtraArgument("rate-limit-tls-domain", fmt.Sprint(rateLimit)), + withExtraArgument("rate-limit-tls-domain-burst", fmt.Sprint(rateLimit))) + + featureName = feature.EnforceDomainTLSRateLimits.EnvVariable + limitName = "tls_connections_by_domain" + } else { + options = append(options, + withExtraArgument("rate-limit-tls-source-ip", fmt.Sprint(rateLimit)), + withExtraArgument("rate-limit-tls-source-ip-burst", fmt.Sprint(rateLimit))) + } + + testhelpers.StubFeatureFlagValue(t, featureName, tt.enforceEnabled) + logBuf := RunPagesProcess(t, options...) + + // when we start the process we make 1 requests to verify that process is up + // it gets counted in the rate limit for IP, but host is different + if !tt.domainLimit { + rateLimit-- + } + + for i := 0; i < 10; i++ { + rsp, err := makeTLSRequest(t, tt.spec) + + if i >= rateLimit { + assertLogFound(t, logBuf, []string{ + "TLS connection rate-limited", + "\"req_host\":\"group.gitlab-example.com\"", + fmt.Sprintf("\"source_ip\":\"%s\"", tt.sourceIP), + "\"enforced\":" + strconv.FormatBool(tt.enforceEnabled)}) + + if tt.enforceEnabled { + require.Error(t, err) + require.Contains(t, err.Error(), "remote error: tls: internal error") + } + + continue + } + + require.NoError(t, err, "request: %d failed", i) + require.NoError(t, rsp.Body.Close()) + require.Equal(t, http.StatusOK, rsp.StatusCode, "request: %d failed", i) + } + expectedMetric := fmt.Sprintf( + "gitlab_pages_rate_limit_blocked_count{enforced=\"%t\",limit_name=\"%s\"} %v", + tt.enforceEnabled, limitName, 10-rateLimit) + + RequireMetricEqual(t, "127.0.0.1:42345", expectedMetric) + }) + } +} + +func makeTLSRequest(t *testing.T, spec ListenSpec) (*http.Response, error) { + req, err := http.NewRequest("GET", "https://group.gitlab-example.com/project", nil) + require.NoError(t, err) + + return spec.Client().Do(req) +} + func assertLogFound(t *testing.T, logBuf *LogCaptureBuffer, expectedLogs []string) { t.Helper() diff --git a/test/acceptance/redirects_test.go b/test/acceptance/redirects_test.go index 6c1158a4..5846d2cd 100644 --- a/test/acceptance/redirects_test.go +++ b/test/acceptance/redirects_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitlab-pages/internal/feature" + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" ) func TestRedirectStatusPage(t *testing.T) { @@ -22,7 +23,7 @@ func TestRedirectStatusPage(t *testing.T) { body, err := io.ReadAll(rsp.Body) require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Contains(t, string(body), "14 rules") require.Equal(t, http.StatusOK, rsp.StatusCode) @@ -37,7 +38,7 @@ func TestRedirect(t *testing.T) { // Test that serving a file still works with redirects enabled rsp, err := GetRedirectPage(t, httpListener, "group.redirects.gitlab-example.com", "/project-redirects/index.html") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusOK, rsp.StatusCode) @@ -95,7 +96,7 @@ func TestRedirect(t *testing.T) { t.Run(fmt.Sprintf("%s%s -> %s (%d)", tt.host, tt.path, tt.expectedLocation, tt.expectedStatus), func(t *testing.T) { rsp, err := GetRedirectPage(t, httpListener, tt.host, tt.path) require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, tt.expectedLocation, rsp.Header.Get("Location")) require.Equal(t, tt.expectedStatus, rsp.StatusCode) diff --git a/test/acceptance/rewrites_test.go b/test/acceptance/rewrites_test.go index aa105789..eefb1e82 100644 --- a/test/acceptance/rewrites_test.go +++ b/test/acceptance/rewrites_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitlab-pages/internal/feature" + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" ) func TestRewrites(t *testing.T) { @@ -47,7 +48,7 @@ func TestRewrites(t *testing.T) { t.Run(name, func(t *testing.T) { rsp, err := GetPageFromListener(t, httpListener, tt.host, tt.path) require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) body, err := io.ReadAll(rsp.Body) require.NoError(t, err) diff --git a/test/acceptance/serving_test.go b/test/acceptance/serving_test.go index 8b01f5b2..bab69357 100644 --- a/test/acceptance/serving_test.go +++ b/test/acceptance/serving_test.go @@ -10,6 +10,8 @@ import ( "time" "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" ) func TestUnknownHostReturnsNotFound(t *testing.T) { @@ -29,7 +31,7 @@ func TestUnknownProjectReturnsNotFound(t *testing.T) { rsp, err := GetPageFromListener(t, httpListener, "group.gitlab-example.com", "/nonexistent/") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusNotFound, rsp.StatusCode) } @@ -38,7 +40,7 @@ func TestGroupDomainReturns200(t *testing.T) { rsp, err := GetPageFromListener(t, httpListener, "group.gitlab-example.com", "/") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusOK, rsp.StatusCode) body, err := io.ReadAll(rsp.Body) @@ -154,7 +156,7 @@ func TestCustom404(t *testing.T) { rsp, err := GetPageFromListener(t, spec, test.host, test.path) require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusNotFound, rsp.StatusCode) page, err := io.ReadAll(rsp.Body) @@ -171,7 +173,7 @@ func TestCORSWhenDisabled(t *testing.T) { for _, spec := range supportedListeners() { for _, method := range []string{http.MethodGet, http.MethodHead, http.MethodOptions} { rsp := doCrossOriginRequest(t, spec, method, method, spec.URL("project/")) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusOK, rsp.StatusCode) require.Equal(t, "", rsp.Header.Get("Access-Control-Allow-Origin")) @@ -219,7 +221,7 @@ func TestCORSAllowsMethod(t *testing.T) { t.Run(tt.name, func(t *testing.T) { for _, spec := range supportedListeners() { rsp := doCrossOriginRequest(t, spec, tt.method, tt.method, spec.URL("project/")) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, tt.expectedStatus, rsp.StatusCode) require.Equal(t, tt.expectedOrigin, rsp.Header.Get("Access-Control-Allow-Origin")) @@ -237,7 +239,7 @@ func TestCustomHeaders(t *testing.T) { for _, spec := range supportedListeners() { rsp, err := GetPageFromListener(t, spec, "group.gitlab-example.com:", "project/") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusOK, rsp.StatusCode) require.Equal(t, "Testing1", rsp.Header.Get("X-Test1")) require.Equal(t, "Testing2", rsp.Header.Get("X-Test2")) @@ -261,12 +263,12 @@ func TestHttpToHttpsRedirectDisabled(t *testing.T) { rsp, err := GetRedirectPage(t, httpListener, "group.gitlab-example.com", "project/") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusOK, rsp.StatusCode) rsp, err = GetPageFromListener(t, httpsListener, "group.gitlab-example.com", "project/") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusOK, rsp.StatusCode) } @@ -275,14 +277,14 @@ func TestHttpToHttpsRedirectEnabled(t *testing.T) { rsp, err := GetRedirectPage(t, httpListener, "group.gitlab-example.com", "project/") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusTemporaryRedirect, rsp.StatusCode) require.Equal(t, 1, len(rsp.Header["Location"])) require.Equal(t, "https://group.gitlab-example.com/project/", rsp.Header.Get("Location")) rsp, err = GetPageFromListener(t, httpsListener, "group.gitlab-example.com", "project/") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusOK, rsp.StatusCode) } @@ -408,7 +410,7 @@ func TestDomainResolverError(t *testing.T) { response, err := GetPageFromListener(t, httpListener, domainName, "/my/pages/project/") require.NoError(t, err) - defer response.Body.Close() + testhelpers.Close(t, response.Body) require.True(t, opts.getAPICalled(), "api must have been called") @@ -450,7 +452,7 @@ func TestQueryStringPersistedInSlashRewrite(t *testing.T) { rsp, err := GetRedirectPage(t, httpsListener, "group.gitlab-example.com", "project?q=test") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusFound, rsp.StatusCode) require.Equal(t, 1, len(rsp.Header["Location"])) @@ -458,7 +460,7 @@ func TestQueryStringPersistedInSlashRewrite(t *testing.T) { rsp, err = GetPageFromListener(t, httpsListener, "group.gitlab-example.com", "project/?q=test") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusOK, rsp.StatusCode) } @@ -488,7 +490,7 @@ func TestServerRepliesWithHeaders(t *testing.T) { rsp, err := GetPageFromListener(t, httpListener, "group.gitlab-example.com", "/") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusOK, rsp.StatusCode) @@ -540,7 +542,7 @@ func TestDiskDisabledFailsToServeFileAndLocalContent(t *testing.T) { t.Run(host, func(t *testing.T) { rsp, err := GetPageFromListener(t, httpListener, host, suffix) require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusInternalServerError, rsp.StatusCode) }) diff --git a/test/acceptance/status_test.go b/test/acceptance/status_test.go index ef01c692..c48aaff7 100644 --- a/test/acceptance/status_test.go +++ b/test/acceptance/status_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" ) func TestStatusPage(t *testing.T) { @@ -15,6 +17,6 @@ func TestStatusPage(t *testing.T) { rsp, err := GetPageFromListener(t, httpListener, "group.gitlab-example.com", "@statuscheck") require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, http.StatusOK, rsp.StatusCode) } diff --git a/test/acceptance/unknown_http_method_test.go b/test/acceptance/unknown_http_method_test.go index dfe9c82f..7e96c5e2 100644 --- a/test/acceptance/unknown_http_method_test.go +++ b/test/acceptance/unknown_http_method_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" ) func TestUnknownHTTPMethod(t *testing.T) { @@ -18,7 +20,7 @@ func TestUnknownHTTPMethod(t *testing.T) { resp, err := DoPagesRequest(t, httpListener, req) require.NoError(t, err) - defer resp.Body.Close() + testhelpers.Close(t, resp.Body) require.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) } diff --git a/test/acceptance/zip_test.go b/test/acceptance/zip_test.go index dcb831e7..5bb2a0d1 100644 --- a/test/acceptance/zip_test.go +++ b/test/acceptance/zip_test.go @@ -10,6 +10,8 @@ import ( "time" "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" ) func TestZipServing(t *testing.T) { @@ -85,8 +87,7 @@ func TestZipServing(t *testing.T) { t.Run(name, func(t *testing.T) { response, err := GetPageFromListener(t, httpListener, tt.host, tt.urlSuffix) require.NoError(t, err) - defer response.Body.Close() - + testhelpers.Close(t, response.Body) require.Equal(t, tt.expectedStatusCode, response.StatusCode) if tt.expectedStatusCode == http.StatusOK { @@ -217,7 +218,7 @@ func TestZipServingCache(t *testing.T) { // send a request to get the ETag response, err := GetPageFromListener(t, httpListener, tt.host, tt.urlSuffix) require.NoError(t, err) - defer response.Body.Close() + testhelpers.Close(t, response.Body) require.Equal(t, http.StatusOK, response.StatusCode) etag := response.Header.Get("ETag") @@ -231,7 +232,7 @@ func TestZipServingCache(t *testing.T) { body, err := io.ReadAll(rsp.Body) require.NoError(t, err) - defer rsp.Body.Close() + testhelpers.Close(t, rsp.Body) require.Equal(t, tt.expectedContent, string(body), "content mismatch") }) } @@ -308,7 +309,7 @@ func TestZipServingFromDisk(t *testing.T) { t.Run(name, func(t *testing.T) { response, err := GetPageFromListener(t, httpListener, tt.host, tt.urlSuffix) require.NoError(t, err) - defer response.Body.Close() + testhelpers.Close(t, response.Body) require.Equal(t, tt.expectedStatusCode, response.StatusCode) @@ -333,7 +334,7 @@ func TestZipServingConfigShortTimeout(t *testing.T) { response, err := GetPageFromListener(t, httpListener, "zip.gitlab.io", "/") require.NoError(t, err) - defer response.Body.Close() + testhelpers.Close(t, response.Body) require.Equal(t, http.StatusInternalServerError, response.StatusCode, "should fail to serve") } |