From 7946cb0dc92ac6a306b55af1ec3c17dc936d742c Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Tue, 4 Apr 2023 09:32:18 +0000 Subject: Redirect to unique domain --- app.go | 2 + internal/serving/lookup_path.go | 1 + internal/source/gitlab/api/lookup_path.go | 1 + internal/source/gitlab/factory.go | 1 + internal/uniqueDomain/middleware.go | 74 +++++++++++++++++++++++ test/acceptance/uniqueDomain_test.go | 97 +++++++++++++++++++++++++++++++ test/gitlabstub/api_responses.go | 87 +++++++++++++++++++++++++-- test/gitlabstub/handlers.go | 2 +- 8 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 internal/uniqueDomain/middleware.go create mode 100644 test/acceptance/uniqueDomain_test.go diff --git a/app.go b/app.go index f35df26c..96aa084b 100644 --- a/app.go +++ b/app.go @@ -40,6 +40,7 @@ import ( "gitlab.com/gitlab-org/gitlab-pages/internal/source" "gitlab.com/gitlab-org/gitlab-pages/internal/source/gitlab" "gitlab.com/gitlab-org/gitlab-pages/internal/tls" + "gitlab.com/gitlab-org/gitlab-pages/internal/uniqueDomain" "gitlab.com/gitlab-org/gitlab-pages/internal/urilimiter" "gitlab.com/gitlab-org/gitlab-pages/metrics" ) @@ -133,6 +134,7 @@ func setRequestScheme(r *http.Request) *http.Request { func (a *theApp) buildHandlerPipeline() (http.Handler, error) { // Handlers should be applied in a reverse order handler := a.serveFileOrNotFoundHandler() + handler = uniqueDomain.NewMiddleware(handler) handler = a.Auth.AuthorizationMiddleware(handler) handler = routing.NewMiddleware(handler, a.source) diff --git a/internal/serving/lookup_path.go b/internal/serving/lookup_path.go index 421e2a51..6a283b6e 100644 --- a/internal/serving/lookup_path.go +++ b/internal/serving/lookup_path.go @@ -10,4 +10,5 @@ type LookupPath struct { IsHTTPSOnly bool HasAccessControl bool ProjectID uint64 + UniqueHost string } diff --git a/internal/source/gitlab/api/lookup_path.go b/internal/source/gitlab/api/lookup_path.go index 834767bd..a61d2f7c 100644 --- a/internal/source/gitlab/api/lookup_path.go +++ b/internal/source/gitlab/api/lookup_path.go @@ -7,6 +7,7 @@ type LookupPath struct { HTTPSOnly bool `json:"https_only,omitempty"` Prefix string `json:"prefix,omitempty"` Source Source `json:"source,omitempty"` + UniqueHost string `json:"unique_host,omitempty"` } // Source describes GitLab Page serving variant diff --git a/internal/source/gitlab/factory.go b/internal/source/gitlab/factory.go index 312d09bf..1c5d15f5 100644 --- a/internal/source/gitlab/factory.go +++ b/internal/source/gitlab/factory.go @@ -31,6 +31,7 @@ func fabricateLookupPath(size int, lookup api.LookupPath) *serving.LookupPath { IsHTTPSOnly: lookup.HTTPSOnly, HasAccessControl: lookup.AccessControl, ProjectID: uint64(lookup.ProjectID), + UniqueHost: lookup.UniqueHost, } } diff --git a/internal/uniqueDomain/middleware.go b/internal/uniqueDomain/middleware.go new file mode 100644 index 00000000..b0af761a --- /dev/null +++ b/internal/uniqueDomain/middleware.go @@ -0,0 +1,74 @@ +package uniqueDomain + +import ( + "net" + "net/http" + "strings" + + "gitlab.com/gitlab-org/gitlab-pages/internal/domain" + "gitlab.com/gitlab-org/gitlab-pages/internal/logging" +) + +func NewMiddleware(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + uniqueURL := getUniqueURL(r) + if uniqueURL == "" { + logging. + LogRequest(r). + WithField("uniqueURL", uniqueURL). + Debug("unique domain: doing nothing") + + handler.ServeHTTP(w, r) + return + } + + logging. + LogRequest(r). + WithField("uniqueURL", uniqueURL). + Info("redirecting to unique domain") + + http.Redirect(w, r, uniqueURL, http.StatusPermanentRedirect) + }) +} + +func getUniqueURL(r *http.Request) string { + domain := domain.FromRequest(r) + lookupPath, err := domain.GetLookupPath(r) + if err != nil { + logging. + LogRequest(r). + WithError(err). + Error("uniqueDomain: failed to get lookupPath") + return "" + } + + // No uniqueHost to redirect + if lookupPath.UniqueHost == "" { + return "" + } + + requestHost, port, err := net.SplitHostPort(r.Host) + if err != nil { + requestHost = r.Host + } + + // Already serving the uniqueHost + if lookupPath.UniqueHost == requestHost { + return "" + } + + uniqueURL := *r.URL + if port == "" { + uniqueURL.Host = lookupPath.UniqueHost + } else { + uniqueURL.Host = net.JoinHostPort(lookupPath.UniqueHost, port) + } + + // Ensure to redirect to the same path requested + uniqueURL.Path = strings.TrimPrefix( + r.URL.Path, + strings.TrimSuffix(lookupPath.Prefix, "/"), + ) + + return uniqueURL.String() +} diff --git a/test/acceptance/uniqueDomain_test.go b/test/acceptance/uniqueDomain_test.go new file mode 100644 index 00000000..1f423170 --- /dev/null +++ b/test/acceptance/uniqueDomain_test.go @@ -0,0 +1,97 @@ +package acceptance_test + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers" +) + +func TestRedirectToUniqueDomain(t *testing.T) { + RunPagesProcess(t) + + tests := []struct { + name string + requestDomain string + requestPath string + redirectURL string + httpStatus int + }{ + { + name: "when project has unique domain", + requestDomain: "group.unique-url.gitlab-example.com", + requestPath: "with-unique-url/", + redirectURL: "https://unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com/", + httpStatus: http.StatusPermanentRedirect, + }, + { + name: "when requesting implicit index.html", + requestDomain: "group.unique-url.gitlab-example.com", + requestPath: "with-unique-url", + redirectURL: "https://unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com", + httpStatus: http.StatusPermanentRedirect, + }, + { + name: "when project is nested", + requestDomain: "group.unique-url.gitlab-example.com", + requestPath: "subgroup1/subgroup2/with-unique-url/subdir/index.html", + redirectURL: "https://unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com/subdir/index.html", + httpStatus: http.StatusPermanentRedirect, + }, + { + name: "when serving with a port", + requestDomain: "group.unique-url.gitlab-example.com:8080", + requestPath: "with-unique-url/", + redirectURL: "https://unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com:8080/", + httpStatus: http.StatusPermanentRedirect, + }, + { + name: "when serving a path with a port", + requestDomain: "group.unique-url.gitlab-example.com:8080", + requestPath: "with-unique-url/subdir/index.html", + redirectURL: "https://unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com:8080/subdir/index.html", + httpStatus: http.StatusPermanentRedirect, + }, + { + name: "when already serving the unique domain", + requestDomain: "unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com", + httpStatus: http.StatusOK, + }, + { + name: "when already serving the unique domain with a port", + requestDomain: "unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com:8080", + httpStatus: http.StatusOK, + }, + { + name: "when already serving the unique domain with path", + requestDomain: "unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com", + requestPath: "subdir/index.html", + httpStatus: http.StatusOK, + }, + { + name: "when project does not have unique domain with a path", + requestDomain: "group.unique-url.gitlab-example.com", + requestPath: "without-unique-url/subdir/index.html", + httpStatus: http.StatusOK, + }, + { + name: "when project does not exist", + requestDomain: "group.unique-url.gitlab-example.com", + requestPath: "inexisting-project/", + httpStatus: http.StatusNotFound, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + rsp, err := GetRedirectPage(t, httpsListener, test.requestDomain, test.requestPath) + require.NoError(t, err) + testhelpers.Close(t, rsp.Body) + + require.Equal(t, test.httpStatus, rsp.StatusCode) + require.Equal(t, test.redirectURL, rsp.Header.Get("Location")) + }) + } +} diff --git a/test/gitlabstub/api_responses.go b/test/gitlabstub/api_responses.go index 93bc8995..b9459160 100644 --- a/test/gitlabstub/api_responses.go +++ b/test/gitlabstub/api_responses.go @@ -14,6 +14,15 @@ import ( type responseFn func(string) api.VirtualDomain +type projectConfig struct { + // refer to makeGitLabPagesAccessStub for custom HTTP responses per projectID + projectID int + accessControl bool + https bool + pathOnDisk string + uniqueHost string +} + // domainResponses holds the predefined API responses for certain domains // that can be used with the GitLab API stub in acceptance tests // Assume the working dir is inside shared/pages/ @@ -114,6 +123,43 @@ var domainResponses = map[string]responseFn{ projectID: 1008, pathOnDisk: "group.acme/with.redirects", }), + "group.unique-url.gitlab-example.com": generateVirtualDomain(map[string]projectConfig{ + "/with-unique-url": { + uniqueHost: "unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com", + pathOnDisk: "group/project", + }, + "/subgroup1/subgroup2/with-unique-url": { + uniqueHost: "unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com", + pathOnDisk: "group/project", + }, + "/with-unique-url-with-port": { + uniqueHost: "unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com", + pathOnDisk: "group/project", + }, + "/with-malformed-unique-url": { + uniqueHost: "unique-url@gitlab-example.com:", + pathOnDisk: "group/project", + }, + "/with-different-protocol": { + uniqueHost: "unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com", + pathOnDisk: "group/project", + }, + "/without-unique-url": { + pathOnDisk: "group/project", + }, + }), + "unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com": generateVirtualDomain(map[string]projectConfig{ + "/": { + uniqueHost: "unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com", + pathOnDisk: "group/project", + }, + }), + "unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com:8080": generateVirtualDomain(map[string]projectConfig{ + "/": { + uniqueHost: "unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com", + pathOnDisk: "group/project", + }, + }), // NOTE: before adding more domains here, generate the zip archive by running (per project) // make zip PROJECT_SUBDIR=group/serving // make zip PROJECT_SUBDIR=group/project2 @@ -187,6 +233,7 @@ func generateVirtualDomainFromDir(dir, rootDomain string, perPrefixConfig map[st Path: sourcePath, SHA256: sha, }, + UniqueHost: cfg.uniqueHost, } lookupPaths = append(lookupPaths, lookupPath) @@ -198,12 +245,39 @@ func generateVirtualDomainFromDir(dir, rootDomain string, perPrefixConfig map[st } } -type projectConfig struct { - // refer to makeGitLabPagesAccessStub for custom HTTP responses per projectID - projectID int - accessControl bool - https bool - pathOnDisk string +func generateVirtualDomain(projectConfigs map[string]projectConfig) responseFn { + return func(wd string) api.VirtualDomain { + nextID := 1000 + lookupPaths := make([]api.LookupPath, 0, len(projectConfigs)) + + for project, config := range projectConfigs { + if config.projectID == 0 { + config.projectID = nextID + nextID++ + } + + sourcePath := fmt.Sprintf("file://%s/%s/public.zip", wd, config.pathOnDisk) + sum := sha256.Sum256([]byte(sourcePath)) + sha := hex.EncodeToString(sum[:]) + + lookupPaths = append(lookupPaths, api.LookupPath{ + ProjectID: config.projectID, + AccessControl: config.accessControl, + HTTPSOnly: config.https, + Prefix: ensureEndingSlash(project), + UniqueHost: config.uniqueHost, + Source: api.Source{ + Type: "zip", + Path: sourcePath, + SHA256: sha, + }, + }) + } + + return api.VirtualDomain{ + LookupPaths: lookupPaths, + } + } } // customDomain with per project config @@ -230,6 +304,7 @@ func customDomain(config projectConfig) responseFn { SHA256: sha, Path: sourcePath, }, + UniqueHost: config.uniqueHost, }, }, } diff --git a/test/gitlabstub/handlers.go b/test/gitlabstub/handlers.go index df09d914..8089277b 100644 --- a/test/gitlabstub/handlers.go +++ b/test/gitlabstub/handlers.go @@ -29,7 +29,7 @@ func defaultAPIHandler(delay time.Duration, pagesRoot string) http.HandlerFunc { // check if predefined response exists if responseFn, ok := domainResponses[domain]; ok { if err := json.NewEncoder(w).Encode(responseFn(pagesRoot)); err != nil { - log.Fatal(err) + log.Fatalf("fail to encode response for domain %q: %v", domain, err) } return } -- cgit v1.2.3