Skip to content

Commit 49b1042

Browse files
authored
Fixed query parameters causing route matching problems (#37)
Fixed query parameters causing route matching problems in original request headers case.
1 parent 0c9d6bc commit 49b1042

File tree

5 files changed

+146
-11
lines changed

5 files changed

+146
-11
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77
## [Unreleased]
88
There are currently no unreleased changes.
99

10+
## [v0.0.2] - 2021-03-25
11+
### Fixed
12+
- Query parameters in request causing route matching failures when request paths are received through headers.
13+
1014
## [v0.0.1] - 2020-06-12
1115
### Added
1216
- Support for original request path and method specification through headers (see [nginx docs](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/)).
@@ -24,5 +28,6 @@ This is the first version that includes the following functionality:
2428
- Claims-based authorization
2529
- Pure authorization server and reverse proxy modes
2630

27-
[Unreleased]: https://github.com/kaancfidan/bouncer/compare/v0.0.1...master
31+
[Unreleased]: https://github.com/kaancfidan/bouncer/compare/v0.0.2...master
32+
[v0.0.2]: https://github.com/kaancfidan/bouncer/compare/v0.0.1...v0.0.2
2833
[v0.0.1]: https://github.com/kaancfidan/bouncer/compare/v0.0.0...v0.0.1

services/route_matcher.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package services
22

33
import (
4+
"fmt"
5+
"net/url"
46
"strings"
57

68
"github.com/gobwas/glob"
@@ -29,12 +31,17 @@ func NewRouteMatcher(routePolicies []models.RoutePolicy) *RouteMatcherImpl {
2931
func (g RouteMatcherImpl) MatchRoutePolicies(path string, method string) ([]models.RoutePolicy, error) {
3032
matches := make([]models.RoutePolicy, 0)
3133
for _, rp := range g.routePolicies {
32-
normalizedPath := "/" + strings.Trim(path, " \t\n/") + "/"
34+
parsed, err := url.Parse(path)
35+
if err != nil {
36+
return nil, fmt.Errorf("could not parse path: %v", err)
37+
}
38+
39+
normalizedPath := "/" + strings.Trim(parsed.Path, " \t\n/") + "/"
3340
normalizedPolicyPath := "/" + strings.Trim(rp.Path, " \t\n/") + "/"
3441

3542
g, err := glob.Compile(normalizedPolicyPath, '/')
3643
if err != nil {
37-
return nil, err
44+
return nil, fmt.Errorf("could not compile policy glob: %v", err)
3845
}
3946

4047
// check if route matches

services/route_matcher_test.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ func TestRouteMatcherImpl_MatchRoutePolicies(t *testing.T) {
3131
want: nil,
3232
wantErr: true,
3333
},
34+
{
35+
name: "path error",
36+
routePolicies: []models.RoutePolicy{
37+
{Path: "/test", Methods: []string{"GET"}},
38+
},
39+
path: ".::this is not a valid path::.",
40+
want: nil,
41+
wantErr: true,
42+
},
3443
{
3544
name: "exact matched route",
3645
routePolicies: []models.RoutePolicy{
@@ -44,7 +53,19 @@ func TestRouteMatcherImpl_MatchRoutePolicies(t *testing.T) {
4453
wantErr: false,
4554
},
4655
{
47-
name: "exact matched route with trailing separator",
56+
name: "exact matched route with trailing separator in request path",
57+
routePolicies: []models.RoutePolicy{
58+
{Path: "/test", Methods: []string{"GET"}},
59+
},
60+
path: "/test/",
61+
method: "GET",
62+
want: []models.RoutePolicy{
63+
{Path: "/test", Methods: []string{"GET"}},
64+
},
65+
wantErr: false,
66+
},
67+
{
68+
name: "exact matched route with trailing separator in route policy",
4869
routePolicies: []models.RoutePolicy{
4970
{Path: "/test/", Methods: []string{"GET"}},
5071
},
@@ -55,6 +76,18 @@ func TestRouteMatcherImpl_MatchRoutePolicies(t *testing.T) {
5576
},
5677
wantErr: false,
5778
},
79+
{
80+
name: "exact matched route with query parameters",
81+
routePolicies: []models.RoutePolicy{
82+
{Path: "/test/", Methods: []string{"GET"}},
83+
},
84+
path: "/test?someBool=true&someString=test",
85+
method: "GET",
86+
want: []models.RoutePolicy{
87+
{Path: "/test/", Methods: []string{"GET"}},
88+
},
89+
wantErr: false,
90+
},
5891
{
5992
name: "exact matched route with spaces all around",
6093
routePolicies: []models.RoutePolicy{

services/server.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package services
33
import (
44
"log"
55
"net/http"
6+
"net/url"
67
"reflect"
78

89
"github.com/google/uuid"
@@ -50,7 +51,14 @@ func (s Server) Handle(writer http.ResponseWriter, request *http.Request) {
5051
path = request.URL.Path
5152
method = request.Method
5253
} else {
53-
path = request.Header.Get(s.config.OriginalRequestHeaders.Path)
54+
parsed, err := url.Parse(request.Header.Get(s.config.OriginalRequestHeaders.Path))
55+
if err != nil {
56+
log.Printf("[%v] Request path read from header could not be parsed: %v", requestID, err)
57+
writer.WriteHeader(http.StatusBadRequest)
58+
return
59+
}
60+
61+
path = parsed.Path
5462
method = request.Header.Get(s.config.OriginalRequestHeaders.Method)
5563
}
5664

services/server_test.go

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ import (
1818
func TestServer_Handle(t *testing.T) {
1919
tests := []struct {
2020
name string
21+
requestPath string
22+
config models.ServerConfig
2123
proxyEnabled bool
2224
expectations func(*http.Request, *mocks.RouteMatcher, *mocks.Authenticator, *mocks.Authorizer)
2325
wantUpstreamCalled bool
2426
wantStatusCode int
2527
}{
2628
{
2729
name: "route matching failed",
30+
requestPath: "/some/path",
2831
proxyEnabled: false,
2932
expectations: func(
3033
request *http.Request,
@@ -38,8 +41,28 @@ func TestServer_Handle(t *testing.T) {
3841
wantUpstreamCalled: false,
3942
wantStatusCode: http.StatusInternalServerError,
4043
},
44+
{
45+
name: "invalid path in original request headers",
46+
requestPath: "**-.::invalid path::.-**",
47+
config: models.ServerConfig{
48+
OriginalRequestHeaders: &models.OriginalRequestHeaders{
49+
Method: "X-Original-Method",
50+
Path: "X-Original-URI",
51+
},
52+
},
53+
proxyEnabled: false,
54+
expectations: func(
55+
request *http.Request,
56+
routeMatcher *mocks.RouteMatcher,
57+
authenticator *mocks.Authenticator,
58+
authorizer *mocks.Authorizer) {
59+
},
60+
wantUpstreamCalled: false,
61+
wantStatusCode: http.StatusBadRequest,
62+
},
4163
{
4264
name: "allow anonymous",
65+
requestPath: "/some/path",
4366
proxyEnabled: false,
4467
expectations: func(
4568
request *http.Request,
@@ -62,8 +85,43 @@ func TestServer_Handle(t *testing.T) {
6285
wantUpstreamCalled: false,
6386
wantStatusCode: http.StatusOK,
6487
},
88+
{
89+
name: "allow anonymous through original request headers",
90+
requestPath: "/some/path",
91+
config: models.ServerConfig{
92+
OriginalRequestHeaders: &models.OriginalRequestHeaders{
93+
Method: "X-Original-Method",
94+
Path: "X-Original-URI",
95+
},
96+
},
97+
proxyEnabled: false,
98+
expectations: func(
99+
request *http.Request,
100+
routeMatcher *mocks.RouteMatcher,
101+
authenticator *mocks.Authenticator,
102+
authorizer *mocks.Authorizer) {
103+
104+
matchedRoutes := []models.RoutePolicy{
105+
{
106+
Path: "/",
107+
AllowAnonymous: true,
108+
},
109+
}
110+
111+
routeMatcher.On("MatchRoutePolicies",
112+
request.Header.Get("X-Original-URI"),
113+
request.Header.Get("X-Original-Method")).Return(matchedRoutes, nil)
114+
115+
authorizer.On("IsAnonymousAllowed",
116+
matchedRoutes,
117+
request.Header.Get("X-Original-Method")).Return(true)
118+
},
119+
wantUpstreamCalled: false,
120+
wantStatusCode: http.StatusOK,
121+
},
65122
{
66123
name: "proxy enabled, allow anonymous",
124+
requestPath: "/some/path",
67125
proxyEnabled: true,
68126
expectations: func(
69127
request *http.Request,
@@ -88,6 +146,7 @@ func TestServer_Handle(t *testing.T) {
88146
},
89147
{
90148
name: "authentication - success, authorization - success",
149+
requestPath: "/some/path",
91150
proxyEnabled: false,
92151
expectations: func(
93152
request *http.Request,
@@ -122,6 +181,7 @@ func TestServer_Handle(t *testing.T) {
122181
},
123182
{
124183
name: "proxy enabled, authentication - success, authorization - success",
184+
requestPath: "/some/path",
125185
proxyEnabled: true,
126186
expectations: func(
127187
request *http.Request,
@@ -156,6 +216,7 @@ func TestServer_Handle(t *testing.T) {
156216
},
157217
{
158218
name: "authentication - failed",
219+
requestPath: "/some/path",
159220
proxyEnabled: false,
160221
expectations: func(
161222
request *http.Request,
@@ -179,6 +240,7 @@ func TestServer_Handle(t *testing.T) {
179240
},
180241
{
181242
name: "authentication - success, authorization - failed",
243+
requestPath: "/some/path",
182244
proxyEnabled: false,
183245
expectations: func(
184246
request *http.Request,
@@ -218,6 +280,7 @@ func TestServer_Handle(t *testing.T) {
218280
},
219281
{
220282
name: "error while authorization",
283+
requestPath: "/some/path",
221284
proxyEnabled: false,
222285
expectations: func(
223286
request *http.Request,
@@ -260,10 +323,24 @@ func TestServer_Handle(t *testing.T) {
260323
t.Run(tt.name, func(t *testing.T) {
261324
header := http.Header{}
262325

263-
request, err := http.NewRequest("GET", "/", nil)
264-
if err != nil {
265-
t.Errorf("could not be create request: %v", err)
266-
return
326+
var request *http.Request
327+
var err error
328+
329+
if tt.config.OriginalRequestHeaders != nil {
330+
request, err = http.NewRequest("GET", "/", nil)
331+
if err != nil {
332+
t.Errorf("could not be create request: %v", err)
333+
return
334+
}
335+
336+
request.Header.Set("X-Original-URI", tt.requestPath)
337+
request.Header.Set("X-Original-Method", "POST")
338+
} else {
339+
request, err = http.NewRequest("POST", tt.requestPath, nil)
340+
if err != nil {
341+
t.Errorf("could not be create request: %v", err)
342+
return
343+
}
267344
}
268345

269346
var upstream *mocks.Handler
@@ -282,11 +359,16 @@ func TestServer_Handle(t *testing.T) {
282359
routeMatcher,
283360
authorizer,
284361
authenticator,
285-
models.ServerConfig{})
362+
tt.config)
286363

287364
tt.expectations(request, routeMatcher, authenticator, authorizer)
288365

289366
if tt.wantUpstreamCalled {
367+
if upstream == nil {
368+
t.Errorf("invalid test case: proxy must be enabled to be called")
369+
return
370+
}
371+
290372
upstream.On("ServeHTTP", responseWriter, request).Return()
291373
} else {
292374
responseWriter.On("WriteHeader", tt.wantStatusCode).Return()
@@ -302,7 +384,7 @@ func TestServer_Handle(t *testing.T) {
302384
assert.Equal(t, "Bearer", header["Www-Authenticate"][0])
303385
}
304386

305-
if tt.proxyEnabled {
387+
if upstream != nil {
306388
upstream.AssertExpectations(t)
307389
}
308390

0 commit comments

Comments
 (0)