internal/namespaceinpath/middleware.go (82 lines of code) (raw):
package namespaceinpath
import (
"net"
"net/http"
"net/url"
"strings"
"gitlab.com/gitlab-org/labkit/log"
"gitlab.com/gitlab-org/gitlab-pages/internal/httperrors"
"gitlab.com/gitlab-org/gitlab-pages/internal/utils"
)
const authPath = "/auth"
type middleware struct {
next http.Handler
pagesDomain string
authRedirectURI string
skipAuthRewrite bool
}
// NewMiddleware creates a new middleware
func NewMiddleware(h http.Handler, pagesDomain string, authRedirectURI string) http.Handler {
return &middleware{
next: h,
pagesDomain: pagesDomain,
authRedirectURI: authRedirectURI,
// Based on the documentation: https://gitlab.com/gitlab-org/gitlab-pages#gitlab-access-control
// When auth redirect URI is without reserved namespace, we skip url rewrite.
skipAuthRewrite: isAuthRedirectURLWithoutNamespace(authRedirectURI),
}
}
// ServeHTTP handles the HTTP request and rewrites the URL from namespace in path to namespace in host.
// Example: example.com/namespace/a-path rewrites to namespace.example.com/a-path
func (m *middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Host == "" {
r.URL.Host = r.Host
}
// When namespace in path is enabled, do not serve namespace in host urls.
if m.isNamespaceInHost(r.URL) {
httperrors.Serve404(w)
return
}
m.extractNamespaceFromPath(r)
crw := newResponseWriter(w, m.pagesDomain, m.authRedirectURI)
m.next.ServeHTTP(crw, r)
}
// extractNamespaceFromPath extracts the namespace from the request URL path
// and updates the request object accordingly.
func (m *middleware) extractNamespaceFromPath(r *http.Request) {
if m.skipAuthRewrite && isAuthRedirectURL(r.URL, m.authRedirectURI) {
return
}
oldHost := r.Host
newURL := &customURL{URL: r.URL, pagesDomain: m.pagesDomain}
if err := newURL.convertFromNamespaceInPath(); err != nil {
log.WithFields(log.Fields{
"orig_host": r.Host,
"orig_path": r.URL.Path,
"pages_domain": m.pagesDomain,
}).WithError(err).Error("can't convert URL")
return
}
log.WithFields(log.Fields{
"new_host": newURL.Host,
"new_path": newURL.Path,
}).Debug("Rewrite namespace host")
r.Host = newURL.Host
r.URL = newURL.URL
namespace := strings.TrimSuffix(newURL.Host, "."+oldHost)
r.Header.Set("X-Gitlab-Namespace-In-Path", namespace)
r.RequestURI = strings.TrimPrefix(r.RequestURI, "/"+namespace)
}
// isNamespaceInHost checks if the request is for a namespace in the host
// rather than a namespace in the path.
func (m *middleware) isNamespaceInHost(reqURL *url.URL) bool {
reqHost, _, err := net.SplitHostPort(reqURL.Host)
if err != nil {
reqHost = reqURL.Host
}
return len(reqHost) > len(m.pagesDomain) && strings.HasSuffix(reqHost, m.pagesDomain)
}
// isAuthRedirectURL checks if the request URL matches the configured auth redirect URL.
func isAuthRedirectURL(reqURL *url.URL, authRedirect string) bool {
authURL, valid := utils.ParseURL(authRedirect)
if !valid {
return false
}
// Compare the host and path of the request URL with the auth redirect URL
return reqURL.Host == authURL.Host && strings.HasPrefix(reqURL.Path, authURL.Path)
}
// isAuthRedirectURLWithoutNamespace checks if the auth redirect URL has only "/auth" as the path.
func isAuthRedirectURLWithoutNamespace(authRedirect string) bool {
authURL, valid := utils.ParseURL(authRedirect)
if !valid {
return false
}
// Compare the path of the auth redirect URL with "/auth" as the path.
return authURL.Path == authPath
}