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

Commit f623dee

Browse files
NuruBruno Oliveira da Silva
authored andcommitted
Pass URI path upstream verbatim
Closes #528 Closes #483 by superseding it as a bug fix, not an option
1 parent a0cad52 commit f623dee

File tree

6 files changed

+164
-31
lines changed

6 files changed

+164
-31
lines changed

doc.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,11 @@ type RequestScope struct {
385385
AccessDenied bool
386386
// Identity is the user Identity of the request
387387
Identity *userContext
388+
// The parsed (unescaped) value of the request path
389+
Path string
390+
// Preserve the original request path: KEYCLOAK-10864, KEYCLOAK-11276, KEYCLOAK-13315
391+
// The exact path received in the request, if different than Path
392+
RawPath string
388393
}
389394

390395
// storage is used to hold the offline refresh token, assuming you don't want to use

forwarding.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ func (r *oauthProxy) proxyMiddleware(next http.Handler) http.Handler {
3232

3333
// @step: retrieve the request scope
3434
scope := req.Context().Value(contextScopeName)
35+
var sc *RequestScope
3536
if scope != nil {
36-
sc := scope.(*RequestScope)
37+
sc = scope.(*RequestScope)
3738
if sc.AccessDenied {
3839
return
3940
}
@@ -56,6 +57,12 @@ func (r *oauthProxy) proxyMiddleware(next http.Handler) http.Handler {
5657
// @note: by default goproxy only provides a forwarding proxy, thus all requests have to be absolute and we must update the host headers
5758
req.URL.Host = r.endpoint.Host
5859
req.URL.Scheme = r.endpoint.Scheme
60+
// Restore the unprocessed original path, so that we pass upstream exactly what we received
61+
// as the resource request.
62+
if sc != nil {
63+
req.URL.Path = sc.Path
64+
req.URL.RawPath = sc.RawPath
65+
}
5966
if v := req.Header.Get("Host"); v != "" {
6067
req.Host = v
6168
req.Header.Del("Host")

middleware.go

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,30 +41,35 @@ const (
4141
// entrypointMiddleware is custom filtering for incoming requests
4242
func entrypointMiddleware(next http.Handler) http.Handler {
4343
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
44-
keep := req.URL.Path
44+
// @step: create a context for the request
45+
scope := &RequestScope{}
46+
// Save the exact formatting of the incoming request so we can use it later
47+
scope.Path = req.URL.Path
48+
scope.RawPath = req.URL.RawPath
49+
50+
// We want to Normalize the URL so that we can more easily and accurately
51+
// parse it to apply resource protection rules.
4552
purell.NormalizeURL(req.URL, normalizeFlags)
4653

4754
// ensure we have a slash in the url
4855
if !strings.HasPrefix(req.URL.Path, "/") {
4956
req.URL.Path = "/" + req.URL.Path
5057
}
51-
req.RequestURI = req.URL.RawPath
52-
req.URL.RawPath = req.URL.Path
58+
req.URL.RawPath = req.URL.EscapedPath()
5359

54-
// @step: create a context for the request
55-
scope := &RequestScope{}
5660
resp := middleware.NewWrapResponseWriter(w, 1)
5761
start := time.Now()
62+
// All the processing, including forwarding the request upstream and getting the response,
63+
// happens here in this chain.
5864
next.ServeHTTP(resp, req.WithContext(context.WithValue(req.Context(), contextScopeName, scope)))
5965

6066
// @metric record the time taken then response code
6167
latencyMetric.Observe(time.Since(start).Seconds())
6268
statusMetric.WithLabelValues(fmt.Sprintf("%d", resp.Status()), req.Method).Inc()
6369

64-
// place back the original uri for proxying request
65-
req.URL.Path = keep
66-
req.URL.RawPath = keep
67-
req.RequestURI = keep
70+
// place back the original uri for any later consumers
71+
req.URL.Path = scope.Path
72+
req.URL.RawPath = scope.RawPath
6873
})
6974
}
7075

@@ -92,13 +97,24 @@ func (r *oauthProxy) loggingMiddleware(next http.Handler) http.Handler {
9297
resp := w.(middleware.WrapResponseWriter)
9398
next.ServeHTTP(resp, req)
9499
addr := req.RemoteAddr
95-
r.log.Info("client request",
96-
zap.Duration("latency", time.Since(start)),
97-
zap.Int("status", resp.Status()),
98-
zap.Int("bytes", resp.BytesWritten()),
99-
zap.String("client_ip", addr),
100-
zap.String("method", req.Method),
101-
zap.String("path", req.URL.Path))
100+
if req.URL.Path == req.URL.RawPath || req.URL.RawPath == "" {
101+
r.log.Info("client request",
102+
zap.Duration("latency", time.Since(start)),
103+
zap.Int("status", resp.Status()),
104+
zap.Int("bytes", resp.BytesWritten()),
105+
zap.String("client_ip", addr),
106+
zap.String("method", req.Method),
107+
zap.String("path", req.URL.Path))
108+
} else {
109+
r.log.Info("client request",
110+
zap.Duration("latency", time.Since(start)),
111+
zap.Int("status", resp.Status()),
112+
zap.Int("bytes", resp.BytesWritten()),
113+
zap.String("client_ip", addr),
114+
zap.String("method", req.Method),
115+
zap.String("path", req.URL.Path),
116+
zap.String("raw path", req.URL.RawPath))
117+
}
102118
})
103119
}
104120

middleware_test.go

Lines changed: 107 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -333,18 +333,19 @@ func TestMetricsMiddleware(t *testing.T) {
333333
cfg.EnableMetrics = true
334334
cfg.LocalhostMetrics = true
335335
requests := []fakeRequest{
336-
{
337-
URI: cfg.WithOAuthURI(metricsURL),
338-
ExpectedCode: http.StatusOK,
339-
ExpectedContentContains: "proxy_request_status_total",
340-
},
341336
{
342337
URI: cfg.WithOAuthURI(metricsURL),
343338
Headers: map[string]string{
344339
"X-Forwarded-For": "10.0.0.1",
345340
},
346341
ExpectedCode: http.StatusForbidden,
347342
},
343+
// Some request must run before this one to generate request status numbers
344+
{
345+
URI: cfg.WithOAuthURI(metricsURL),
346+
ExpectedCode: http.StatusOK,
347+
ExpectedContentContains: "proxy_request_status_total",
348+
},
348349
}
349350
newFakeProxy(cfg).RunTests(t, requests)
350351
}
@@ -433,6 +434,100 @@ func TestMethodExclusions(t *testing.T) {
433434
newFakeProxy(cfg).RunTests(t, requests)
434435
}
435436

437+
func TestPreserveURLEncoding(t *testing.T) {
438+
cfg := newFakeKeycloakConfig()
439+
cfg.EnableLogging = true
440+
cfg.Resources = []*Resource{
441+
{
442+
URL: "/api/v2/*",
443+
Methods: allHTTPMethods,
444+
Roles: []string{"dev"},
445+
},
446+
{
447+
URL: "/api/v1/auth*",
448+
Methods: allHTTPMethods,
449+
Roles: []string{"admin"},
450+
},
451+
{
452+
URL: "/api/v1/*",
453+
Methods: allHTTPMethods,
454+
WhiteListed: true,
455+
},
456+
{
457+
URL: "/*",
458+
Methods: allHTTPMethods,
459+
Roles: []string{"user"},
460+
},
461+
}
462+
requests := []fakeRequest{
463+
{
464+
URI: "/test",
465+
HasToken: true,
466+
Roles: []string{"nothing"},
467+
ExpectedCode: http.StatusForbidden,
468+
},
469+
{
470+
URI: "/",
471+
ExpectedCode: http.StatusUnauthorized,
472+
},
473+
{ // See KEYCLOAK-10864
474+
URI: "/administrativeMonitor/hudson.diagnosis.ReverseProxySetupMonitor/testForReverseProxySetup/https%3A%2F%2Flocalhost%3A6001%2Fmanage/",
475+
ExpectedContentContains: `"uri":"/administrativeMonitor/hudson.diagnosis.ReverseProxySetupMonitor/testForReverseProxySetup/https%3A%2F%2Flocalhost%3A6001%2Fmanage/"`,
476+
HasToken: true,
477+
Roles: []string{"user"},
478+
ExpectedProxy: true,
479+
ExpectedCode: http.StatusOK,
480+
},
481+
{ // See KEYCLOAK-11276
482+
URI: "/iiif/2/edepot_local:ST%2F00001%2FST00005_00001.jpg/full/1000,/0/default.png",
483+
ExpectedContentContains: `"uri":"/iiif/2/edepot_local:ST%2F00001%2FST00005_00001.jpg/full/1000,/0/default.png"`,
484+
HasToken: true,
485+
Roles: []string{"user"},
486+
ExpectedProxy: true,
487+
ExpectedCode: http.StatusOK,
488+
},
489+
{ // See KEYCLOAK-13315
490+
URI: "/rabbitmqui/%2F/replicate-to-central",
491+
ExpectedContentContains: `"uri":"/rabbitmqui/%2F/replicate-to-central"`,
492+
HasToken: true,
493+
Roles: []string{"user"},
494+
ExpectedProxy: true,
495+
ExpectedCode: http.StatusOK,
496+
},
497+
{ // should work
498+
URI: "/api/v1/auth",
499+
HasToken: true,
500+
Roles: []string{"admin"},
501+
ExpectedProxy: true,
502+
ExpectedCode: http.StatusOK,
503+
},
504+
{ // should work
505+
URI: "/api/v1/auth?referer=https%3A%2F%2Fwww.example.com%2Fauth",
506+
ExpectedContentContains: `"uri":"/api/v1/auth?referer=https%3A%2F%2Fwww.example.com%2Fauth"`,
507+
HasToken: true,
508+
Roles: []string{"admin"},
509+
ExpectedProxy: true,
510+
ExpectedCode: http.StatusOK,
511+
},
512+
{
513+
URI: "/api/v1/auth?referer=https%3A%2F%2Fwww.example.com%2Fauth",
514+
HasToken: true,
515+
Roles: []string{"user"},
516+
ExpectedCode: http.StatusForbidden,
517+
},
518+
{ // should work
519+
URI: "/api/v3/auth?referer=https%3A%2F%2Fwww.example.com%2Fauth",
520+
ExpectedContentContains: `"uri":"/api/v3/auth?referer=https%3A%2F%2Fwww.example.com%2Fauth"`,
521+
HasToken: true,
522+
Roles: []string{"user"},
523+
ExpectedProxy: true,
524+
ExpectedCode: http.StatusOK,
525+
},
526+
}
527+
528+
newFakeProxy(cfg).RunTests(t, requests)
529+
}
530+
436531
func TestStrangeRoutingError(t *testing.T) {
437532
cfg := newFakeKeycloakConfig()
438533
cfg.Resources = []*Resource{
@@ -459,12 +554,13 @@ func TestStrangeRoutingError(t *testing.T) {
459554
}
460555
requests := []fakeRequest{
461556
{ // should work
462-
URI: "/api/v1/events/123456789",
463-
HasToken: true,
464-
Redirects: true,
465-
Roles: []string{"user"},
466-
ExpectedProxy: true,
467-
ExpectedCode: http.StatusOK,
557+
URI: "/api/v1/events/123456789",
558+
HasToken: true,
559+
Redirects: true,
560+
Roles: []string{"user"},
561+
ExpectedProxy: true,
562+
ExpectedCode: http.StatusOK,
563+
ExpectedContentContains: `"uri":"/api/v1/events/123456789"`,
468564
},
469565
{ // should break with bad role
470566
URI: "/api/v1/events/123456789",

server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import (
3232
"strings"
3333
"time"
3434

35+
"go.uber.org/zap/zapcore"
36+
3537
"golang.org/x/crypto/acme/autocert"
3638

3739
httplog "log"
@@ -137,6 +139,8 @@ func createLogger(config *Config) (*zap.Logger, error) {
137139
c := zap.NewProductionConfig()
138140
c.DisableStacktrace = true
139141
c.DisableCaller = true
142+
// Use human-readable timestamps in the logs until KEYCLOAK-12100 is fixed
143+
c.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
140144
// are we enabling json logging?
141145
if !config.EnableJSONLogging {
142146
c.Encoding = "console"

server_test.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,10 @@ func TestReverseProxyHeaders(t *testing.T) {
8686
token := newTestToken(p.idp.getLocation())
8787
token.addRealmRoles([]string{fakeAdminRole})
8888
signed, _ := p.idp.signToken(token.claims)
89+
uri := "/auth_all/test"
8990
requests := []fakeRequest{
9091
{
91-
URI: "/auth_all/test",
92+
URI: uri,
9293
RawToken: signed.Encode(),
9394
ExpectedProxy: true,
9495
ExpectedProxyHeaders: map[string]string{
@@ -99,7 +100,8 @@ func TestReverseProxyHeaders(t *testing.T) {
99100
"X-Auth-Userid": "rjayawardene",
100101
"X-Auth-Username": "rjayawardene",
101102
},
102-
ExpectedCode: http.StatusOK,
103+
ExpectedCode: http.StatusOK,
104+
ExpectedContentContains: `"uri":"` + uri + `"`,
103105
},
104106
}
105107
p.RunTests(t, requests)
@@ -553,7 +555,10 @@ func (f *fakeUpstreamService) ServeHTTP(w http.ResponseWriter, r *http.Request)
553555
w.Header().Set("Content-Type", "application/json")
554556
w.WriteHeader(http.StatusOK)
555557
content, _ := json.Marshal(&fakeUpstreamResponse{
556-
URI: r.RequestURI,
558+
// r.RequestURI is what was received by the proxy.
559+
// r.URL.String() is what is actually sent to the upstream service.
560+
// KEYCLOAK-10864, KEYCLOAK-11276, KEYCLOAK-13315
561+
URI: r.URL.String(),
557562
Method: r.Method,
558563
Address: r.RemoteAddr,
559564
Headers: r.Header,

0 commit comments

Comments
 (0)