diff --git a/config.go b/config.go index 5c4509f81..7713a2c4a 100644 --- a/config.go +++ b/config.go @@ -45,6 +45,7 @@ func newDefaultConfig() *Config { EnableTokenHeader: true, HTTPOnlyCookie: true, Headers: make(map[string]string), + LoosePathNormalization: false, LetsEncryptCacheDir: "./cache/", MatchClaims: make(map[string]string), MaxIdleConns: 100, diff --git a/config_sample.yml b/config_sample.yml index 1626d2c3c..6c332b4db 100644 --- a/config_sample.yml +++ b/config_sample.yml @@ -86,3 +86,6 @@ cors-methods: [] cors-credentials: true|false # the max age (Access-Control-Max-Age) cors-max-age: 1h + +# enable looser path normalization. allow for double slashes in url and does not decode url. Enable only if you know what you are doing ( default: false) +loose-path-normalization: false|true diff --git a/doc.go b/doc.go index f0686fdf2..358b7d816 100644 --- a/doc.go +++ b/doc.go @@ -360,6 +360,10 @@ type Config struct { // ForwardingDomains is a collection of domains to signs ForwardingDomains []string `json:"forwarding-domains" yaml:"forwarding-domains" usage:"list of domains which should be signed; everything else is relayed unsigned"` + // LoosePathNormalization uses a looser filter to the proxied request. + // This can be useful to proxy to services that require a not normalized path like Jenkins. + LoosePathNormalization bool `json:"loose-path-normalization" yaml:"loose-path-normalization" usage:"looser path normalization. allow for double slashes in url and does not decode url. Enable only if you know what you are doing."` + // DisableAllLogging indicates no logging at all DisableAllLogging bool `json:"disable-all-logging" yaml:"disable-all-logging" usage:"disables all logging to stdout and stderr"` } diff --git a/middleware.go b/middleware.go index fff6864e9..eaebb6280 100644 --- a/middleware.go +++ b/middleware.go @@ -34,7 +34,8 @@ import ( const ( // normalizeFlags is the options to purell - normalizeFlags purell.NormalizationFlags = purell.FlagRemoveDotSegments | purell.FlagRemoveDuplicateSlashes + normalizeFlags purell.NormalizationFlags = purell.FlagRemoveDotSegments | purell.FlagRemoveDuplicateSlashes + looseNormalizeFlags purell.NormalizationFlags = purell.FlagRemoveDotSegments ) // entrypointMiddleware is custom filtering for incoming requests @@ -67,6 +68,41 @@ func entrypointMiddleware(next http.Handler) http.Handler { }) } +// looseEntrypointMiddleware is custom filtering for incoming requests with looser settings +// Allow for double slashes in URL +// Do not force to use decoded URL if normalized PATH and RAWPATH match +func looseEntrypointMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + keep := req.URL.Path + keepRaw := req.URL.RawPath + keepURI := req.RequestURI + + purell.NormalizeURL(req.URL, looseNormalizeFlags) + + // ensure we have a slash in all url + if !strings.HasPrefix(req.URL.Path, "/") { + req.URL.Path = "/" + req.URL.Path + req.URL.RawPath = "/" + req.URL.RawPath + req.RequestURI = "/" + req.RequestURI + } + + // @step: create a context for the request + scope := &RequestScope{} + resp := middleware.NewWrapResponseWriter(w, 1) + start := time.Now() + next.ServeHTTP(resp, req.WithContext(context.WithValue(req.Context(), contextScopeName, scope))) + + // @metric record the time taken then response code + latencyMetric.Observe(time.Since(start).Seconds()) + statusMetric.WithLabelValues(fmt.Sprintf("%d", resp.Status()), req.Method).Inc() + + // place back the original uri for proxying request + req.URL.Path = keep + req.URL.RawPath = keepRaw + req.RequestURI = keepURI + }) +} + // requestIDMiddleware is responsible for adding a request id if none found func (r *oauthProxy) requestIDMiddleware(header string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { diff --git a/middleware_test.go b/middleware_test.go index 94e8f9a53..6697f1f16 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -538,6 +538,25 @@ func TestNoProxyingRequests(t *testing.T) { newFakeProxy(c).RunTests(t, requests) } +func TestLooseNormalizationProxyingRequests(t *testing.T) { + c := newFakeKeycloakConfig() + c.LoosePathNormalization = true + c.Resources = []*Resource{ + { + URL: "/*", + Methods: allHTTPMethods, + }, + } + requests := []fakeRequest{ + { // check for loose path normalization + URI: "/testForReverseProxySetup/https%3A%2F%2Flocalhost%3A6001%2Fmanage/", + Redirects: true, + ExpectedCode: http.StatusTemporaryRedirect, + }, + } + newFakeProxy(c).RunTests(t, requests) +} + const testAdminURI = "/admin/test" func TestStrangeAdminRequests(t *testing.T) { diff --git a/server.go b/server.go index 761967a70..c30c78d05 100644 --- a/server.go +++ b/server.go @@ -168,7 +168,13 @@ func (r *oauthProxy) createReverseProxy() error { engine.Use(r.requestIDMiddleware(r.config.RequestIDHeader)) } // @step: enable the entrypoint middleware - engine.Use(entrypointMiddleware) + if r.config.LoosePathNormalization { + r.log.Info("Path Normalization is Loose.") + engine.Use(looseEntrypointMiddleware) + } else { + r.log.Info("Path Normalization is Strict.") + engine.Use(entrypointMiddleware) + } if r.config.EnableLogging { engine.Use(r.loggingMiddleware)