Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-pages.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVladimir Shushlin <vshushlin@gitlab.com>2020-07-13 15:34:36 +0300
committerVladimir Shushlin <vshushlin@gitlab.com>2020-07-13 15:34:36 +0300
commitdc7bf0c3a847320b9370decc8792aff3712dd3e2 (patch)
treee00fc0ee840f3b787a213d6f9752778ac5cc21f1
parente1310e9432b9cdf8ac33304e4385e0564c2f023d (diff)
parent4c7d7872868361d79796e87cca2d4cf5d0e95824 (diff)
Merge branch '183-custom-error-page-for-public-projects' into 'master'
Serve custom 404.html file for namespace domains Closes #391 and #183 See merge request gitlab-org/gitlab-pages!263
-rw-r--r--acceptance_test.go170
-rw-r--r--app.go45
-rw-r--r--internal/auth/auth.go20
-rw-r--r--internal/auth/auth_test.go44
-rw-r--r--internal/domain/domain.go39
-rw-r--r--internal/domain/domain_test.go85
-rw-r--r--internal/source/disk/domain_test.go4
-rw-r--r--internal/source/disk/map_test.go1
-rw-r--r--shared/pages/group.404/domain.404/public/404.html2
-rw-r--r--shared/pages/group.404/group.404.gitlab-example.com/public/404.html1
-rw-r--r--shared/pages/group.404/private_project/config.json5
-rw-r--r--shared/pages/group.404/private_project/public/404.html1
-rw-r--r--shared/pages/group.404/private_unauthorized/config.json5
-rw-r--r--shared/pages/group.404/private_unauthorized/public/404.html1
-rw-r--r--shared/pages/group.auth/group.auth.gitlab-example.com/config.json1
-rw-r--r--shared/pages/group.auth/group.auth.gitlab-example.com/public/404.html1
-rw-r--r--shared/pages/group.auth/private.project.1/public/404.html1
17 files changed, 381 insertions, 45 deletions
diff --git a/acceptance_test.go b/acceptance_test.go
index e8f7fe1a..2b0a7800 100644
--- a/acceptance_test.go
+++ b/acceptance_test.go
@@ -197,6 +197,69 @@ func TestNestedSubgroups(t *testing.T) {
})
}
}
+
+func TestCustom404(t *testing.T) {
+ skipUnlessEnabled(t)
+ teardown := RunPagesProcess(t, *pagesBinary, listeners, "")
+ defer teardown()
+
+ tests := []struct {
+ host string
+ path string
+ content string
+ }{
+ {
+ host: "group.404.gitlab-example.com",
+ path: "project.404/not/existing-file",
+ content: "Custom 404 project page",
+ },
+ {
+ host: "group.404.gitlab-example.com",
+ path: "project.404/",
+ content: "Custom 404 project page",
+ },
+ {
+ host: "group.404.gitlab-example.com",
+ path: "not/existing-file",
+ content: "Custom 404 group page",
+ },
+ {
+ host: "group.404.gitlab-example.com",
+ path: "not-existing-file",
+ content: "Custom 404 group page",
+ },
+ {
+ host: "group.404.gitlab-example.com",
+ content: "Custom 404 group page",
+ },
+ {
+ host: "domain.404.com",
+ content: "Custom domain.404 page",
+ },
+ {
+ host: "group.404.gitlab-example.com",
+ path: "project.no.404/not/existing-file",
+ content: "The page you're looking for could not be found.",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(fmt.Sprintf("%s/%s", test.host, test.path), func(t *testing.T) {
+ for _, spec := range listeners {
+ rsp, err := GetPageFromListener(t, spec, test.host, test.path)
+
+ require.NoError(t, err)
+ defer rsp.Body.Close()
+ require.Equal(t, http.StatusNotFound, rsp.StatusCode)
+
+ page, err := ioutil.ReadAll(rsp.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(page), test.content)
+ }
+ })
+ }
+}
+
func TestCORSWhenDisabled(t *testing.T) {
skipUnlessEnabled(t)
teardown := RunPagesProcess(t, *pagesBinary, listeners, "", "-disable-cross-origin-requests")
@@ -1143,6 +1206,113 @@ func TestAccessControlUnderCustomDomain(t *testing.T) {
require.Equal(t, http.StatusOK, authrsp.StatusCode)
}
+func TestCustomErrorPageWithAuth(t *testing.T) {
+ skipUnlessEnabled(t, "not-inplace-chroot")
+ testServer := makeGitLabPagesAccessStub(t)
+ testServer.Start()
+ defer testServer.Close()
+
+ teardown := RunPagesProcessWithAuthServer(t, *pagesBinary, listeners, "", testServer.URL)
+ defer teardown()
+
+ tests := []struct {
+ name string
+ domain string
+ path string
+ expectedErrorPage string
+ }{
+ {
+ name: "private_project_authorized",
+ domain: "group.404.gitlab-example.com",
+ path: "/private_project/unknown",
+ expectedErrorPage: "Private custom 404 error page",
+ },
+ {
+ name: "public_namespace_with_private_unauthorized_project",
+ domain: "group.404.gitlab-example.com",
+ // /private_unauthorized/config.json resolves project ID to 2000 which will cause a 401 from the mock GitLab testServer
+ path: "/private_unauthorized/unknown",
+ expectedErrorPage: "Custom 404 group page",
+ },
+ {
+ name: "private_namespace_authorized",
+ domain: "group.auth.gitlab-example.com",
+ path: "/unknown",
+ expectedErrorPage: "group.auth.gitlab-example.com namespace custom 404",
+ },
+ {
+ name: "private_namespace_with_private_project_auth_failed",
+ domain: "group.auth.gitlab-example.com",
+ // project ID is 2000
+ path: "/private.project.1/unknown",
+ expectedErrorPage: "The page you're looking for could not be found.",
+ },
+ }
+ for _, tt := range tests {
+ 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()
+
+ cookie := rsp.Header.Get("Set-Cookie")
+
+ url, err := url.Parse(rsp.Header.Get("Location"))
+ require.NoError(t, err)
+
+ state := url.Query().Get("state")
+ require.Equal(t, "http://"+tt.domain, url.Query().Get("domain"))
+
+ pagesrsp, err := GetRedirectPage(t, httpListener, url.Host, url.Path+"?"+url.RawQuery)
+ require.NoError(t, err)
+ defer pagesrsp.Body.Close()
+
+ pagescookie := pagesrsp.Header.Get("Set-Cookie")
+
+ // Go to auth page with correct state will cause fetching the token
+ authrsp, err := GetRedirectPageWithCookie(t, httpListener, "projects.gitlab-example.com", "/auth?code=1&state="+
+ state, pagescookie)
+
+ require.NoError(t, err)
+ defer authrsp.Body.Close()
+
+ url, err = url.Parse(authrsp.Header.Get("Location"))
+ require.NoError(t, err)
+
+ // Will redirect to custom domain
+ require.Equal(t, tt.domain, url.Host)
+ require.Equal(t, "1", url.Query().Get("code"))
+ require.Equal(t, state, url.Query().Get("state"))
+
+ // Run auth callback in custom domain
+ authrsp, err = GetRedirectPageWithCookie(t, httpListener, tt.domain, "/auth?code=1&state="+
+ state, cookie)
+
+ require.NoError(t, err)
+ defer authrsp.Body.Close()
+
+ // Will redirect to the page
+ groupCookie := authrsp.Header.Get("Set-Cookie")
+ require.Equal(t, http.StatusFound, authrsp.StatusCode)
+
+ url, err = url.Parse(authrsp.Header.Get("Location"))
+ require.NoError(t, err)
+
+ // Will redirect to custom domain error page
+ require.Equal(t, "http://"+tt.domain+tt.path, url.String())
+
+ // Fetch page in custom domain
+ anotherResp, err := GetRedirectPageWithCookie(t, httpListener, tt.domain, tt.path, groupCookie)
+ require.NoError(t, err)
+
+ require.Equal(t, http.StatusNotFound, anotherResp.StatusCode)
+
+ page, err := ioutil.ReadAll(anotherResp.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(page), tt.expectedErrorPage)
+ })
+ }
+}
+
func TestAccessControlUnderCustomDomainWithHTTPSProxy(t *testing.T) {
skipUnlessEnabled(t, "not-inplace-chroot")
diff --git a/app.go b/app.go
index 7b1749a6..5a195396 100644
--- a/app.go
+++ b/app.go
@@ -94,28 +94,18 @@ func (a *theApp) domain(host string) (*domain.Domain, error) {
return a.domains.GetDomain(host)
}
-func (a *theApp) checkAuthenticationIfNotExists(domain *domain.Domain, w http.ResponseWriter, r *http.Request) bool {
- if domain == nil || !domain.HasLookupPath(r) {
- // Only if auth is supported
- if a.Auth.IsAuthSupported() {
- // To avoid user knowing if pages exist, we will force user to login and authorize pages
- if a.Auth.CheckAuthenticationWithoutProject(w, r) {
- return true
- }
-
- // User is authenticated, show the 404
- httperrors.Serve404(w)
- return true
- }
- }
-
- // Without auth, fall back to 404
- if domain == nil {
- httperrors.Serve404(w)
+// checkAuthAndServeNotFound performs the auth process if domain can't be found
+// the main purpose of this process is to avoid leaking the project existence/not-existence
+// by behaving the same if user has no access to the project or if project simply does not exists
+func (a *theApp) checkAuthAndServeNotFound(domain *domain.Domain, w http.ResponseWriter, r *http.Request) bool {
+ // To avoid user knowing if pages exist, we will force user to login and authorize pages
+ if a.Auth.CheckAuthenticationWithoutProject(w, r, domain) {
return true
}
- return false
+ // auth succeeded try to serve the correct 404 page
+ domain.ServeNotFoundAuthFailed(w, r)
+ return true
}
func (a *theApp) tryAuxiliaryHandlers(w http.ResponseWriter, r *http.Request, https bool, host string, domain *domain.Domain) bool {
@@ -134,8 +124,11 @@ func (a *theApp) tryAuxiliaryHandlers(w http.ResponseWriter, r *http.Request, ht
return true
}
- if a.checkAuthenticationIfNotExists(domain, w, r) {
- return true
+ if !domain.HasLookupPath(r) {
+ // redirect to auth and serve not found
+ if a.checkAuthAndServeNotFound(domain, w, r) {
+ return true
+ }
}
if !https && domain.IsHTTPSOnly(r) {
@@ -245,7 +238,7 @@ func (a *theApp) accessControlMiddleware(handler http.Handler) http.Handler {
// Only for projects that have access control enabled
if domain.IsAccessControlEnabled(r) {
// accessControlMiddleware
- if a.Auth.CheckAuthentication(w, r, domain.GetProjectID(r)) {
+ if a.Auth.CheckAuthentication(w, r, domain) {
return
}
}
@@ -267,16 +260,14 @@ func (a *theApp) serveFileOrNotFoundHandler() http.Handler {
if !fileServed {
// We need to trigger authentication flow here if file does not exist to prevent exposing possibly private project existence,
// because the projects override the paths of the namespace project and they might be private even though
- // namespace project is public.
+ // namespace project is public
if domain.IsNamespaceProject(r) {
- if a.Auth.CheckAuthenticationWithoutProject(w, r) {
+ if a.Auth.CheckAuthenticationWithoutProject(w, r, domain) {
return
}
-
- httperrors.Serve404(w)
- return
}
+ // domain found and authentication succeeds
domain.ServeNotFoundHTTP(w, r)
}
})
diff --git a/internal/auth/auth.go b/internal/auth/auth.go
index 6dce1ab8..eaf3c25d 100644
--- a/internal/auth/auth.go
+++ b/internal/auth/auth.go
@@ -71,6 +71,10 @@ type errorResponse struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
}
+type domain interface {
+ GetProjectID(r *http.Request) uint64
+ ServeNotFoundAuthFailed(w http.ResponseWriter, r *http.Request)
+}
func (a *Auth) getSessionFromStore(r *http.Request) (*sessions.Session, error) {
session, err := a.store.Get(r, "gitlab-pages")
@@ -436,12 +440,13 @@ func (a *Auth) IsAuthSupported() bool {
return a != nil
}
-func (a *Auth) checkAuthentication(w http.ResponseWriter, r *http.Request, projectID uint64) bool {
+func (a *Auth) checkAuthentication(w http.ResponseWriter, r *http.Request, domain domain) bool {
session := a.checkSessionIsValid(w, r)
if session == nil {
return true
}
+ projectID := domain.GetProjectID(r)
// Access token exists, authorize request
var url string
if projectID > 0 {
@@ -471,8 +476,8 @@ func (a *Auth) checkAuthentication(w http.ResponseWriter, r *http.Request, proje
logRequest(r).WithError(err).Error("Failed to retrieve info with token")
}
- // We return 404 if for some reason token is not valid to avoid (not) existence leak
- httperrors.Serve404(w)
+ // call serve404 handler when auth fails
+ domain.ServeNotFoundAuthFailed(w, r)
return true
}
@@ -480,13 +485,13 @@ func (a *Auth) checkAuthentication(w http.ResponseWriter, r *http.Request, proje
}
// CheckAuthenticationWithoutProject checks if user is authenticated and has a valid token
-func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http.Request) bool {
+func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http.Request, domain domain) bool {
if a == nil {
// No auth supported
return false
}
- return a.checkAuthentication(w, r, 0)
+ return a.checkAuthentication(w, r, domain)
}
// GetTokenIfExists returns the token if it exists
@@ -513,7 +518,8 @@ func (a *Auth) RequireAuth(w http.ResponseWriter, r *http.Request) bool {
}
// CheckAuthentication checks if user is authenticated and has access to the project
-func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, projectID uint64) bool {
+// will return contentServed = false when authFailed = true
+func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, domain domain) bool {
logRequest(r).Debug("Authenticate request")
if a == nil {
@@ -524,7 +530,7 @@ func (a *Auth) CheckAuthentication(w http.ResponseWriter, r *http.Request, proje
return true
}
- return a.checkAuthentication(w, r, projectID)
+ return a.checkAuthentication(w, r, domain)
}
// CheckResponseForInvalidToken checks response for invalid token and destroys session if it was invalid
diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go
index fc8ddb44..39a533b3 100644
--- a/internal/auth/auth_test.go
+++ b/internal/auth/auth_test.go
@@ -29,6 +29,20 @@ func defaultCookieStore() sessions.Store {
return createCookieStore("something-very-secret")
}
+type domainMock struct {
+ projectID uint64
+ notFoundContent string
+}
+
+func (dm *domainMock) GetProjectID(r *http.Request) uint64 {
+ return dm.projectID
+}
+
+func (dm *domainMock) ServeNotFoundAuthFailed(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+ w.Write([]byte(dm.notFoundContent))
+}
+
// Gorilla's sessions use request context to save session
// Which makes session sharable between test code and actually manipulating session
// Which leads to negative side effects: we can't test encryption, and cookie params
@@ -180,8 +194,10 @@ func TestCheckAuthenticationWhenAccess(t *testing.T) {
session, _ := store.Get(r, "gitlab-pages")
session.Values["access_token"] = "abc"
session.Save(r, result)
+ contentServed := auth.CheckAuthentication(result, r, &domainMock{projectID: 1000})
+ require.False(t, contentServed)
- require.Equal(t, false, auth.CheckAuthentication(result, r, 1000))
+ // notFoundContent wasn't served so the default response from CheckAuthentication should be 200
require.Equal(t, 200, result.Code)
}
@@ -209,7 +225,8 @@ func TestCheckAuthenticationWhenNoAccess(t *testing.T) {
"http://pages.gitlab-example.com/auth",
apiServer.URL)
- result := httptest.NewRecorder()
+ w := httptest.NewRecorder()
+
reqURL, err := url.Parse("/auth?code=1&state=state")
require.NoError(t, err)
reqURL.Scheme = request.SchemeHTTPS
@@ -217,10 +234,18 @@ func TestCheckAuthenticationWhenNoAccess(t *testing.T) {
session, _ := store.Get(r, "gitlab-pages")
session.Values["access_token"] = "abc"
- session.Save(r, result)
+ session.Save(r, w)
- require.Equal(t, true, auth.CheckAuthentication(result, r, 1000))
- require.Equal(t, 404, result.Code)
+ contentServed := auth.CheckAuthentication(w, r, &domainMock{projectID: 1000, notFoundContent: "Generic 404"})
+ require.True(t, contentServed)
+ res := w.Result()
+ defer res.Body.Close()
+
+ require.Equal(t, 404, res.StatusCode)
+
+ body, err := ioutil.ReadAll(res.Body)
+ require.NoError(t, err)
+ require.Equal(t, string(body), "Generic 404")
}
func TestCheckAuthenticationWhenInvalidToken(t *testing.T) {
@@ -257,7 +282,8 @@ func TestCheckAuthenticationWhenInvalidToken(t *testing.T) {
session.Values["access_token"] = "abc"
session.Save(r, result)
- require.Equal(t, true, auth.CheckAuthentication(result, r, 1000))
+ contentServed := auth.CheckAuthentication(result, r, &domainMock{projectID: 1000})
+ require.True(t, contentServed)
require.Equal(t, 302, result.Code)
}
@@ -295,7 +321,8 @@ func TestCheckAuthenticationWithoutProject(t *testing.T) {
session.Values["access_token"] = "abc"
session.Save(r, result)
- require.Equal(t, false, auth.CheckAuthenticationWithoutProject(result, r))
+ contentServed := auth.CheckAuthenticationWithoutProject(result, r, &domainMock{projectID: 0})
+ require.False(t, contentServed)
require.Equal(t, 200, result.Code)
}
@@ -332,7 +359,8 @@ func TestCheckAuthenticationWithoutProjectWhenInvalidToken(t *testing.T) {
session.Values["access_token"] = "abc"
session.Save(r, result)
- require.Equal(t, true, auth.CheckAuthenticationWithoutProject(result, r))
+ contentServed := auth.CheckAuthenticationWithoutProject(result, r, &domainMock{projectID: 0})
+ require.True(t, contentServed)
require.Equal(t, 302, result.Code)
}
diff --git a/internal/domain/domain.go b/internal/domain/domain.go
index 17a0e1d3..7c1639a3 100644
--- a/internal/domain/domain.go
+++ b/internal/domain/domain.go
@@ -1,6 +1,7 @@
package domain
import (
+ "context"
"crypto/tls"
"errors"
"net/http"
@@ -168,3 +169,41 @@ func (d *Domain) ServeNotFoundHTTP(w http.ResponseWriter, r *http.Request) {
request.ServeNotFoundHTTP(w, r)
}
+
+// serveNamespaceNotFound will try to find a parent namespace domain for a request
+// that failed authentication so that we serve the custom namespace error page for
+// public namespace domains
+func (d *Domain) serveNamespaceNotFound(w http.ResponseWriter, r *http.Request) {
+ // clone r and override the path and try to resolve the domain name
+ clonedReq := r.Clone(context.Background())
+ clonedReq.URL.Path = "/"
+
+ namespaceDomain, err := d.Resolver.Resolve(clonedReq)
+ if err != nil || namespaceDomain.LookupPath == nil {
+ httperrors.Serve404(w)
+ return
+ }
+
+ // for namespace domains that have no access control enabled
+ if !namespaceDomain.LookupPath.HasAccessControl {
+ namespaceDomain.ServeNotFoundHTTP(w, r)
+ return
+ }
+
+ httperrors.Serve404(w)
+}
+
+// ServeNotFoundAuthFailed handler to be called when auth failed so the correct custom
+// 404 page is served.
+func (d *Domain) ServeNotFoundAuthFailed(w http.ResponseWriter, r *http.Request) {
+ if d.isUnconfigured() || !d.HasLookupPath(r) {
+ httperrors.Serve404(w)
+ return
+ }
+ if d.IsNamespaceProject(r) && !d.GetLookupPath(r).HasAccessControl {
+ d.ServeNotFoundHTTP(w, r)
+ return
+ }
+
+ d.serveNamespaceNotFound(w, r)
+}
diff --git a/internal/domain/domain_test.go b/internal/domain/domain_test.go
index 49d46cb3..fc5611ba 100644
--- a/internal/domain/domain_test.go
+++ b/internal/domain/domain_test.go
@@ -1,7 +1,10 @@
package domain
import (
+ "fmt"
+ "io/ioutil"
"net/http"
+ "net/http/httptest"
"os"
"testing"
@@ -153,3 +156,85 @@ func chdirInPath(t require.TestingT, path string) func() {
chdirSet = false
}
}
+
+func TestServeNamespaceNotFound(t *testing.T) {
+ tests := []struct {
+ name string
+ domain string
+ path string
+ resolver *stubbedResolver
+ expectedResponse string
+ }{
+ {
+ name: "public_namespace_domain",
+ domain: "group.404.gitlab-example.com",
+ path: "/unknown",
+ resolver: &stubbedResolver{
+ project: &serving.LookupPath{
+ Path: "../../shared/pages/group.404/group.404.gitlab-example.com/public",
+ IsNamespaceProject: true,
+ },
+ subpath: "/unknown",
+ },
+ expectedResponse: "Custom 404 group page",
+ },
+ {
+ name: "private_project_under_public_namespace_domain",
+ domain: "group.404.gitlab-example.com",
+ path: "/private_project/unknown",
+ resolver: &stubbedResolver{
+ project: &serving.LookupPath{
+ Path: "../../shared/pages/group.404/group.404.gitlab-example.com/public",
+ IsNamespaceProject: true,
+ HasAccessControl: false,
+ },
+ subpath: "/",
+ },
+ expectedResponse: "Custom 404 group page",
+ },
+ {
+ name: "private_namespace_domain",
+ domain: "group.404.gitlab-example.com",
+ path: "/unknown",
+ resolver: &stubbedResolver{
+ project: &serving.LookupPath{
+ Path: "../../shared/pages/group.404/group.404.gitlab-example.com/public",
+ IsNamespaceProject: true,
+ HasAccessControl: true,
+ },
+ subpath: "/",
+ },
+ expectedResponse: "The page you're looking for could not be found.",
+ },
+ {
+ name: "no_parent_namespace_domain",
+ domain: "group.404.gitlab-example.com",
+ path: "/unknown",
+ resolver: &stubbedResolver{
+ project: nil,
+ subpath: "/",
+ },
+ expectedResponse: "The page you're looking for could not be found.",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ d := &Domain{
+ Name: tt.domain,
+ Resolver: tt.resolver,
+ }
+ w := httptest.NewRecorder()
+ r := httptest.NewRequest("GET", fmt.Sprintf("http://%s%s", tt.domain, tt.path), nil)
+ d.serveNamespaceNotFound(w, r)
+
+ resp := w.Result()
+ defer resp.Body.Close()
+
+ require.Equal(t, http.StatusNotFound, resp.StatusCode)
+ body, err := ioutil.ReadAll(resp.Body)
+ require.NoError(t, err)
+
+ require.Contains(t, string(body), tt.expectedResponse)
+ })
+ }
+}
diff --git a/internal/source/disk/domain_test.go b/internal/source/disk/domain_test.go
index d114ff8a..56297fd1 100644
--- a/internal/source/disk/domain_test.go
+++ b/internal/source/disk/domain_test.go
@@ -308,8 +308,8 @@ func TestDomain404ServeHTTP(t *testing.T) {
},
}
- testhelpers.AssertHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/not-existing-file", nil, "Custom 404 group page")
- testhelpers.AssertHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/", nil, "Custom 404 group page")
+ testhelpers.AssertHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/not-existing-file", nil, "Custom domain.404 page")
+ testhelpers.AssertHTTP404(t, serveFileOrNotFound(testDomain), "GET", "http://group.404.test.io/", nil, "Custom domain.404 page")
}
func TestPredefined404ServeHTTP(t *testing.T) {
diff --git a/internal/source/disk/map_test.go b/internal/source/disk/map_test.go
index d904e6aa..d2162883 100644
--- a/internal/source/disk/map_test.go
+++ b/internal/source/disk/map_test.go
@@ -49,6 +49,7 @@ func TestReadProjects(t *testing.T) {
"group.acme.test.io",
"withacmechallenge.domain.com",
"capitalgroup.test.io",
+ "group.404.gitlab-example.com",
}
for _, expected := range domains {
diff --git a/shared/pages/group.404/domain.404/public/404.html b/shared/pages/group.404/domain.404/public/404.html
index 454a3d50..ad0ed073 100644
--- a/shared/pages/group.404/domain.404/public/404.html
+++ b/shared/pages/group.404/domain.404/public/404.html
@@ -1 +1 @@
-Custom 404 group page
+Custom domain.404 page
diff --git a/shared/pages/group.404/group.404.gitlab-example.com/public/404.html b/shared/pages/group.404/group.404.gitlab-example.com/public/404.html
new file mode 100644
index 00000000..454a3d50
--- /dev/null
+++ b/shared/pages/group.404/group.404.gitlab-example.com/public/404.html
@@ -0,0 +1 @@
+Custom 404 group page
diff --git a/shared/pages/group.404/private_project/config.json b/shared/pages/group.404/private_project/config.json
new file mode 100644
index 00000000..5c0ebb50
--- /dev/null
+++ b/shared/pages/group.404/private_project/config.json
@@ -0,0 +1,5 @@
+{ "domains": [
+ {
+ "Domain": "group.404.gitlab-example.com"
+ }
+], "id": 1000, "access_control": true }
diff --git a/shared/pages/group.404/private_project/public/404.html b/shared/pages/group.404/private_project/public/404.html
new file mode 100644
index 00000000..6993ae1a
--- /dev/null
+++ b/shared/pages/group.404/private_project/public/404.html
@@ -0,0 +1 @@
+Private custom 404 error page
diff --git a/shared/pages/group.404/private_unauthorized/config.json b/shared/pages/group.404/private_unauthorized/config.json
new file mode 100644
index 00000000..79349565
--- /dev/null
+++ b/shared/pages/group.404/private_unauthorized/config.json
@@ -0,0 +1,5 @@
+{ "domains": [
+ {
+ "Domain": "group.404.gitlab-example.com"
+ }
+], "id": 2000, "access_control": true }
diff --git a/shared/pages/group.404/private_unauthorized/public/404.html b/shared/pages/group.404/private_unauthorized/public/404.html
new file mode 100644
index 00000000..6993ae1a
--- /dev/null
+++ b/shared/pages/group.404/private_unauthorized/public/404.html
@@ -0,0 +1 @@
+Private custom 404 error page
diff --git a/shared/pages/group.auth/group.auth.gitlab-example.com/config.json b/shared/pages/group.auth/group.auth.gitlab-example.com/config.json
new file mode 100644
index 00000000..292ba673
--- /dev/null
+++ b/shared/pages/group.auth/group.auth.gitlab-example.com/config.json
@@ -0,0 +1 @@
+{ "domains": [], "id": 1000, "access_control": true }
diff --git a/shared/pages/group.auth/group.auth.gitlab-example.com/public/404.html b/shared/pages/group.auth/group.auth.gitlab-example.com/public/404.html
new file mode 100644
index 00000000..f345e8bc
--- /dev/null
+++ b/shared/pages/group.auth/group.auth.gitlab-example.com/public/404.html
@@ -0,0 +1 @@
+group.auth.gitlab-example.com namespace custom 404
diff --git a/shared/pages/group.auth/private.project.1/public/404.html b/shared/pages/group.auth/private.project.1/public/404.html
new file mode 100644
index 00000000..3b751385
--- /dev/null
+++ b/shared/pages/group.auth/private.project.1/public/404.html
@@ -0,0 +1 @@
+group.auth.gitlab-example.com/private.project.1 custom 404