diff --git a/app.go b/app.go index 12a65b1fddbeadaff6fd7b072e03ee167f55c894..a36a74449fc23175e8727a5807324a2c7149917d 100644 --- a/app.go +++ b/app.go @@ -31,6 +31,7 @@ import ( health "gitlab.com/gitlab-org/gitlab-pages/internal/healthcheck" "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" "gitlab.com/gitlab-org/gitlab-pages/internal/logging" + "gitlab.com/gitlab-org/gitlab-pages/internal/namespaceinpath" "gitlab.com/gitlab-org/gitlab-pages/internal/netutil" "gitlab.com/gitlab-org/gitlab-pages/internal/redirects" "gitlab.com/gitlab-org/gitlab-pages/internal/rejectmethods" @@ -224,6 +225,17 @@ func (a *theApp) Run() error { return fmt.Errorf("unable to configure pipeline: %w", err) } + // Initialize the proxy handler + commonHandlerPipeline, err = namespaceinpath.NewProxyHandler( + commonHandlerPipeline, + a.config.General.Domain, + a.config.Authentication.RedirectURI, + a.config.General.NamespaceInPath, + ) + if err != nil { + return fmt.Errorf("unable to configure pipeline: %w", err) + } + proxyHandler := ghandlers.ProxyHeaders(commonHandlerPipeline) httpHandler := a.httpInitialMiddleware(commonHandlerPipeline) diff --git a/internal/namespaceinpath/path_proxy.go b/internal/namespaceinpath/path_proxy.go new file mode 100644 index 0000000000000000000000000000000000000000..f602b10616dbff4b150b90b9ef967ebf68f30f93 --- /dev/null +++ b/internal/namespaceinpath/path_proxy.go @@ -0,0 +1,115 @@ +package namespaceinpath + +import ( + "fmt" + "net" + "net/http" + "net/http/httputil" + "net/url" + "strings" +) + +// ProxyHandler is a custom proxy handler that rewrites URLs +type ProxyHandler struct { + handlerPipeline http.Handler + pagesDomain string + authRedirectURI string + namespaceInPath bool + proxy *httputil.ReverseProxy +} + +// NewProxyHandler creates a new ProxyHandler +func NewProxyHandler(h http.Handler, pagesDomain string, authRedirectURI string, namespaceInPath bool) (*ProxyHandler, error) { + // Pending + return &ProxyHandler{ + handlerPipeline: h, + pagesDomain: pagesDomain, + authRedirectURI: authRedirectURI, + namespaceInPath: namespaceInPath, + proxy: &httputil.ReverseProxy{}, + }, nil +} + +// ServeHTTP handles the HTTP request and rewrites the URL +func (p *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Rewrite the URL + host, port, err := net.SplitHostPort(r.Host) + if err != nil { + host = r.Host + } + + if p.namespaceInPath && p.pagesDomain == host { + // Use a custom Director to modify the request before it's sent to the target + p.proxy.Director = func(req *http.Request) { + namespace := p.extractNamespaceFromPath(req.URL.Path) + req.Header.Set("X-Gitlab-Namespace-In-Path", namespace) + + hostWithNamespace := p.prepareHostWithNamespace(host, port, namespace) + req.URL.Host = hostWithNamespace + req.Host = hostWithNamespace + + req.URL.Path = strings.TrimPrefix(req.URL.Path, "/"+namespace) + req.RequestURI = strings.TrimPrefix(req.RequestURI, "/"+namespace) + } + + // Use a custom ModifyResponse to modify the Location header in the response + p.proxy.ModifyResponse = func(resp *http.Response) error { + location := resp.Header.Get("Location") + if location != "" && !strings.HasPrefix(location, p.authRedirectURI) { + parsedURL, err := url.Parse(location) + if err != nil { + return err + } + + pagesDomainWithPort := p.pagesDomain + if port != "" { + pagesDomainWithPort = p.pagesDomain + ":" + port + } + + if strings.HasSuffix(parsedURL.Host, pagesDomainWithPort) { + namespace := p.extractNamespaceFromHost(parsedURL.Host, pagesDomainWithPort) + if len(namespace) > 0 { + parsedURL.Host = pagesDomainWithPort + parsedURL.Path = "/" + namespace + "/" + strings.TrimPrefix(parsedURL.Path, "/") + resp.Header.Set("Location", parsedURL.String()) + return nil + } + } + } + return nil + } + + // Forward the request + p.proxy.ServeHTTP(w, r) + } else { + p.handlerPipeline.ServeHTTP(w, r) + } +} + +func (p *ProxyHandler) prepareHostWithNamespace(host string, port string, namespace string) string { + if port != "" { + host = host + ":" + port + } + return namespace + "." + host +} + +func (p *ProxyHandler) extractNamespaceFromPath(path string) string { + return strings.Split(strings.TrimPrefix(path, "/"), "/")[0] +} + +func (p *ProxyHandler) extractNamespaceFromHost(host string, pagesDomainWithPort string) string { + return strings.Trim(strings.TrimSuffix(host, pagesDomainWithPort), ".") +} + +func (p *ProxyHandler) isAuthRequest(r *http.Request) bool { + // Parse the auth redirect URL + authURL, err := url.Parse(p.authRedirectURI) + if err != nil { + // If there is an error parsing the auth URL, return false + fmt.Println("Error parsing auth redirect URL:", err) + return false + } + + // Compare the host and path of the request URL with the auth redirect URL + return r.Host == authURL.Host && strings.HasPrefix(r.RequestURI, authURL.Path) +}