Skip to content
This repository has been archived by the owner on Dec 7, 2020. It is now read-only.

[KEYCLOAK-10864] - Add support for Looser path normalization #483

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions config_sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down
38 changes: 37 additions & 1 deletion middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
19 changes: 19 additions & 0 deletions middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
8 changes: 7 additions & 1 deletion server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down