git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #997 from FStelzer/issue844

Use the raw url path when proxying upstream requests

commitee7c405bd839d060a5f90822b40db7cff0b7c378 authorJoel Speed <Joel.speed@hotmail.co.uk> dateFri 17 Sep 2021, 13:45 (5 years ago) parentccbb98a88f32ae
16 files changed+410−309
CHANGELOG.md+1 docs/docs/configuration/alpha_config.md+8−6 main_test.go+17−11 oauthproxy.go+3−1 oauthproxy_test.go+66−41 pkg/apis/options/alpha_options.go+4−5 pkg/apis/options/legacy_options.go+4−4 pkg/apis/options/legacy_options_test.go+42−40 pkg/apis/options/load_test.go+14−11 pkg/apis/options/options.go+1−1 pkg/apis/options/upstreams.go+10−2 pkg/upstream/proxy.go+7−3 pkg/upstream/proxy_test.go+137−110 pkg/validation/options_test.go+1−1 pkg/validation/upstreams.go+2−2 pkg/validation/upstreams_test.go+93−71
CHANGELOG.md+1
@@ -35,6 +35,7 @@
3535- [#1317](https://github.com/oauth2-proxy/oauth2-proxy/pull/1317) Fix incorrect `</form>` tag on the sing_in page when *not* using a custom template (@jord1e)
3636- [#1330](https://github.com/oauth2-proxy/oauth2-proxy/pull/1330) Allow specifying URL as input for custom sign in logo (@MaikuMori)
3737- [#1357](https://github.com/oauth2-proxy/oauth2-proxy/pull/1357) Fix unsafe access to session variable (@harzallah)
38+- [#997](https://github.com/oauth2-proxy/oauth2-proxy/pull/997) Allow passing the raw url path when proxying upstream requests - e.g. /%2F/ (@FStelzer)
3839
3940# V7.1.3
4041
docs/docs/configuration/alpha_config.md+8−6
@@ -124,7 +124,7 @@ They may change between releases without notice.
124124
125125| Field | Type | Description |
126126| ----- | ---- | ----------- |
127−| `upstreams` | _[Upstreams](#upstreams)_ | Upstreams is used to configure upstream servers.<br/>Once a user is authenticated, requests to the server will be proxied to<br/>these upstream servers based on the path mappings defined in this list. |
127+| `upstreamConfig` | _[UpstreamConfig](#upstreamconfig)_ | UpstreamConfig is used to configure upstream servers.<br/>Once a user is authenticated, requests to the server will be proxied to<br/>these upstream servers based on the path mappings defined in this list. |
128128| `injectRequestHeaders` | _[[]Header](#header)_ | InjectRequestHeaders is used to configure headers that should be added<br/>to requests to upstream servers.<br/>Headers may source values from either the authenticated user's session<br/>or from a static secret value. |
129129| `injectResponseHeaders` | _[[]Header](#header)_ | InjectResponseHeaders is used to configure headers that should be added<br/>to responses from the proxy.<br/>This is typically used when using the proxy as an external authentication<br/>provider in conjunction with another proxy such as NGINX and its<br/>auth_request module.<br/>Headers may source values from either the authenticated user's session<br/>or from a static secret value. |
130130| `server` | _[Server](#server)_ | Server is used to configure the HTTP(S) server for the proxy application.<br/>You may choose to run both HTTP and HTTPS servers simultaneously.<br/>This can be done by setting the BindAddress and the SecureBindAddress simultaneously.<br/>To use the secure server you must configure a TLS certificate and key. |
@@ -364,7 +364,7 @@ TLS contains the information for loading a TLS certifcate and key.
364364
365365### Upstream
366366
367−(**Appears on:** [Upstreams](#upstreams))
367+(**Appears on:** [UpstreamConfig](#upstreamconfig))
368368
369369Upstream represents the configuration for an upstream server.
370370Requests will be proxied to this upstream if the path matches the request path.
@@ -382,11 +382,13 @@ Requests will be proxied to this upstream if the path matches the request path.
382382| `passHostHeader` | _bool_ | PassHostHeader determines whether the request host header should be proxied<br/>to the upstream server.<br/>Defaults to true. |
383383| `proxyWebSockets` | _bool_ | ProxyWebSockets enables proxying of websockets to upstream servers<br/>Defaults to true. |
384384
385−### Upstreams
386−
387−#### ([[]Upstream](#upstream) alias)
385+### UpstreamConfig
388386
389387(**Appears on:** [AlphaOptions](#alphaoptions))
390388
391−Upstreams is a collection of definitions for upstream servers.
389+UpstreamConfig is a collection of definitions for upstream servers.
392390
391+| Field | Type | Description |
392+| ----- | ---- | ----------- |
393+| `proxyRawPath` | _bool_ | ProxyRawPath will pass the raw url path to upstream allowing for url's<br/>like: "/%2F/" which would otherwise be redirected to "/" |
394+| `upstreams` | _[[]Upstream](#upstream)_ | Upstreams represents the configuration for the upstream servers.<br/>Requests will be proxied to this upstream if the path matches the request path. |
main_test.go+17−11
@@ -2,8 +2,10 @@ package main
22
33import (
44 "errors"
5+ "fmt"
56 "io/ioutil"
67 "os"
8+ "strings"
79 "time"
810
911 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
@@ -24,7 +26,9 @@ client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
2426`
2527
2628 const testAlphaConfig = `
27−upstreams:
29+upstreamConfig:
30+ proxyrawpath: false
31+ upstreams:
2832 - id: /
2933 path: /
3034 uri: http://httpbin
@@ -100,14 +104,16 @@ redirect_url="http://localhost:4180/oauth2/callback"
100104 opts.Cookie.Secure = false
101105 opts.RawRedirectURL = "http://localhost:4180/oauth2/callback"
102106
103− opts.UpstreamServers = options.Upstreams{
104− {
105− ID: "/",
106− Path: "/",
107− URI: "http://httpbin",
108− FlushInterval: durationPtr(options.DefaultUpstreamFlushInterval),
109− PassHostHeader: boolPtr(true),
110− ProxyWebSockets: boolPtr(true),
107+ opts.UpstreamServers = options.UpstreamConfig{
108+ Upstreams: []options.Upstream{
109+ {
110+ ID: "/",
111+ Path: "/",
112+ URI: "http://httpbin",
113+ FlushInterval: durationPtr(options.DefaultUpstreamFlushInterval),
114+ PassHostHeader: boolPtr(true),
115+ ProxyWebSockets: boolPtr(true),
116+ },
111117 },
112118 }
113119
@@ -130,7 +136,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
130136 opts.InjectResponseHeaders = append(opts.InjectResponseHeaders, authHeader)
131137
132138 opts.Providers = options.Providers{
133− {
139+ options.Provider{
134140 ID: "google=oauth2-proxy",
135141 Type: "google",
136142 ClientSecret: "b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK",
@@ -230,7 +236,7 @@ redirect_url="http://localhost:4180/oauth2/callback"
230236 configContent: testCoreConfig,
231237 alphaConfigContent: testAlphaConfig + ":",
232238 expectedOptions: func() *options.Options { return nil },
233− expectedErr: errors.New("failed to load alpha options: error unmarshalling config: error converting YAML to JSON: yaml: line 49: did not find expected key"),
239+ expectedErr: fmt.Errorf("failed to load alpha options: error unmarshalling config: error converting YAML to JSON: yaml: line %d: did not find expected key", strings.Count(testAlphaConfig, "\n")),
234240 }),
235241 Entry("with alpha configuration and bad core configuration", loadConfigurationTableInput{
236242 configContent: testCoreConfig + "unknown_field=\"something\"",
oauthproxy.go+3−1
@@ -265,7 +265,9 @@ func (p *OAuthProxy) setupServer(opts *options.Options) error {
265265}
266266
267267func (p *OAuthProxy) buildServeMux(proxyPrefix string) {
268− r := mux.NewRouter()
268+ // Use the encoded path here so we can have the option to pass it on in the upstream mux.
269+ // Otherwise something like /%2F/ would be redirected to / here already.
270+ r := mux.NewRouter().UseEncodedPath()
269271 // Everything served by the router must go through the preAuthChain first.
270272 r.Use(p.preAuthChain.Then)
271273
oauthproxy_test.go+66−41
@@ -197,11 +197,13 @@ func TestBasicAuthPassword(t *testing.T) {
197197
198198 basicAuthPassword := "This is a secure password"
199199 opts := baseTestOptions()
200− opts.UpstreamServers = options.Upstreams{
201− {
202− ID: providerServer.URL,
203− Path: "/",
204− URI: providerServer.URL,
200+ opts.UpstreamServers = options.UpstreamConfig{
201+ Upstreams: []options.Upstream{
202+ {
203+ ID: providerServer.URL,
204+ Path: "/",
205+ URI: providerServer.URL,
206+ },
205207 },
206208 }
207209
@@ -346,15 +348,17 @@ func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) (*PassAccessTokenTe
346348 }))
347349
348350 patt.opts = baseTestOptions()
349− patt.opts.UpstreamServers = options.Upstreams{
350− {
351− ID: patt.providerServer.URL,
352− Path: "/",
353− URI: patt.providerServer.URL,
351+ patt.opts.UpstreamServers = options.UpstreamConfig{
352+ Upstreams: []options.Upstream{
353+ {
354+ ID: patt.providerServer.URL,
355+ Path: "/",
356+ URI: patt.providerServer.URL,
357+ },
354358 },
355359 }
356360 if opts.ProxyUpstream.ID != "" {
357− patt.opts.UpstreamServers = append(patt.opts.UpstreamServers, opts.ProxyUpstream)
361+ patt.opts.UpstreamServers.Upstreams = append(patt.opts.UpstreamServers.Upstreams, opts.ProxyUpstream)
358362 }
359363
360364 patt.opts.Cookie.Secure = false
@@ -915,6 +919,15 @@ func TestUserInfoEndpointUnauthorizedOnNoCookieSetError(t *testing.T) {
915919 assert.Equal(t, http.StatusUnauthorized, test.rw.Code)
916920}
917921
922+func TestEncodedUrlsStayEncoded(t *testing.T) {
923+ encodeTest, err := NewSignInPageTest(false)
924+ if err != nil {
925+ t.Fatal(err)
926+ }
927+ code, _ := encodeTest.GetEndpoint("/%2F/test1/%2F/test2")
928+ assert.Equal(t, 403, code)
929+}
930+
918931func NewAuthOnlyEndpointTest(querystring string, modifiers ...OptionsModifier) (*ProcessCookieTest, error) {
919932 pcTest, err := NewProcessCookieTestWithOptionsModifiers(modifiers...)
920933 if err != nil {
@@ -1260,11 +1273,13 @@ func TestAuthSkippedForPreflightRequests(t *testing.T) {
12601273 t.Cleanup(upstreamServer.Close)
12611274
12621275 opts := baseTestOptions()
1263− opts.UpstreamServers = options.Upstreams{
1264− {
1265− ID: upstreamServer.URL,
1266− Path: "/",
1267− URI: upstreamServer.URL,
1276+ opts.UpstreamServers = options.UpstreamConfig{
1277+ Upstreams: []options.Upstream{
1278+ {
1279+ ID: upstreamServer.URL,
1280+ Path: "/",
1281+ URI: upstreamServer.URL,
1282+ },
12681283 },
12691284 }
12701285 opts.SkipAuthPreflight = true
@@ -1335,11 +1350,13 @@ func NewSignatureTest() (*SignatureTest, error) {
13351350 if err != nil {
13361351 return nil, err
13371352 }
1338− opts.UpstreamServers = options.Upstreams{
1339− {
1340− ID: upstreamServer.URL,
1341− Path: "/",
1342− URI: upstreamServer.URL,
1353+ opts.UpstreamServers = options.UpstreamConfig{
1354+ Upstreams: []options.Upstream{
1355+ {
1356+ ID: upstreamServer.URL,
1357+ Path: "/",
1358+ URI: upstreamServer.URL,
1359+ },
13431360 },
13441361 }
13451362
@@ -1771,11 +1788,13 @@ func Test_noCacheHeaders(t *testing.T) {
17711788 t.Cleanup(upstreamServer.Close)
17721789
17731790 opts := baseTestOptions()
1774− opts.UpstreamServers = options.Upstreams{
1775− {
1776− ID: upstreamServer.URL,
1777− Path: "/",
1778− URI: upstreamServer.URL,
1791+ opts.UpstreamServers = options.UpstreamConfig{
1792+ Upstreams: []options.Upstream{
1793+ {
1794+ ID: upstreamServer.URL,
1795+ Path: "/",
1796+ URI: upstreamServer.URL,
1797+ },
17791798 },
17801799 }
17811800 opts.SkipAuthRegex = []string{".*"}
@@ -2041,11 +2060,13 @@ func TestTrustedIPs(t *testing.T) {
20412060 for _, tt := range tests {
20422061 t.Run(tt.name, func(t *testing.T) {
20432062 opts := baseTestOptions()
2044− opts.UpstreamServers = options.Upstreams{
2045− {
2046− ID: "static",
2047− Path: "/",
2048− Static: true,
2063+ opts.UpstreamServers = options.UpstreamConfig{
2064+ Upstreams: []options.Upstream{
2065+ {
2066+ ID: "static",
2067+ Path: "/",
2068+ Static: true,
2069+ },
20492070 },
20502071 }
20512072 opts.TrustedIPs = tt.trustedIPs
@@ -2234,11 +2255,13 @@ func TestAllowedRequest(t *testing.T) {
22342255 t.Cleanup(upstreamServer.Close)
22352256
22362257 opts := baseTestOptions()
2237− opts.UpstreamServers = options.Upstreams{
2238− {
2239− ID: upstreamServer.URL,
2240− Path: "/",
2241− URI: upstreamServer.URL,
2258+ opts.UpstreamServers = options.UpstreamConfig{
2259+ Upstreams: []options.Upstream{
2260+ {
2261+ ID: upstreamServer.URL,
2262+ Path: "/",
2263+ URI: upstreamServer.URL,
2264+ },
22422265 },
22432266 }
22442267 opts.SkipAuthRegex = []string{
@@ -2349,11 +2372,13 @@ func TestProxyAllowedGroups(t *testing.T) {
23492372
23502373 test, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) {
23512374 opts.Providers[0].AllowedGroups = tt.allowedGroups
2352− opts.UpstreamServers = options.Upstreams{
2353− {
2354− ID: upstreamServer.URL,
2355− Path: "/",
2356− URI: upstreamServer.URL,
2375+ opts.UpstreamServers = options.UpstreamConfig{
2376+ Upstreams: []options.Upstream{
2377+ {
2378+ ID: upstreamServer.URL,
2379+ Path: "/",
2380+ URI: upstreamServer.URL,
2381+ },
23572382 },
23582383 }
23592384 })
pkg/apis/options/alpha_options.go+4−5
@@ -9,10 +9,10 @@ package options
99// They may change between releases without notice.
1010// :::
1111type AlphaOptions struct {
12− // Upstreams is used to configure upstream servers.
12+ // UpstreamConfig is used to configure upstream servers.
1313 // Once a user is authenticated, requests to the server will be proxied to
1414 // these upstream servers based on the path mappings defined in this list.
15− Upstreams Upstreams `json:"upstreams,omitempty"`
15+ UpstreamConfig UpstreamConfig `json:"upstreamConfig,omitempty"`
1616
1717 // InjectRequestHeaders is used to configure headers that should be added
1818 // to requests to upstream servers.
@@ -48,19 +48,18 @@ type AlphaOptions struct {
4848// MergeInto replaces alpha options in the Options struct with the values
4949// from the AlphaOptions
5050func (a *AlphaOptions) MergeInto(opts *Options) {
51− opts.UpstreamServers = a.Upstreams
51+ opts.UpstreamServers = a.UpstreamConfig
5252 opts.InjectRequestHeaders = a.InjectRequestHeaders
5353 opts.InjectResponseHeaders = a.InjectResponseHeaders
5454 opts.Server = a.Server
5555 opts.MetricsServer = a.MetricsServer
5656 opts.Providers = a.Providers
57−
5857}
5958
6059// ExtractFrom populates the fields in the AlphaOptions with the values from
6160// the Options
6261func (a *AlphaOptions) ExtractFrom(opts *Options) {
63− a.Upstreams = opts.UpstreamServers
62+ a.UpstreamConfig = opts.UpstreamServers
6463 a.InjectRequestHeaders = opts.InjectRequestHeaders
6564 a.InjectResponseHeaders = opts.InjectResponseHeaders
6665 a.Server = opts.Server
pkg/apis/options/legacy_options.go+4−4
@@ -114,13 +114,13 @@ func legacyUpstreamsFlagSet() *pflag.FlagSet {
114114 return flagSet
115115}
116116
117−func (l *LegacyUpstreams) convert() (Upstreams, error) {
118− upstreams := Upstreams{}
117+func (l *LegacyUpstreams) convert() (UpstreamConfig, error) {
118+ upstreams := UpstreamConfig{}
119119
120120 for _, upstreamString := range l.Upstreams {
121121 u, err := url.Parse(upstreamString)
122122 if err != nil {
123− return nil, fmt.Errorf("could not parse upstream %q: %v", upstreamString, err)
123+ return UpstreamConfig{}, fmt.Errorf("could not parse upstream %q: %v", upstreamString, err)
124124 }
125125
126126 if u.Path == "" {
@@ -169,7 +169,7 @@ func (l *LegacyUpstreams) convert() (Upstreams, error) {
169169 upstream.FlushInterval = nil
170170 }
171171
172− upstreams = append(upstreams, upstream)
172+ upstreams.Upstreams = append(upstreams.Upstreams, upstream)
173173 }
174174
175175 return upstreams, nil
pkg/apis/options/legacy_options_test.go+42−40
@@ -26,35 +26,37 @@ var _ = Describe("Legacy Options", func() {
2626
2727 truth := true
2828 staticCode := 204
29− opts.UpstreamServers = Upstreams{
30− {
31− ID: "/baz",
32− Path: "/baz",
33− URI: "http://foo.bar/baz",
34− FlushInterval: &flushInterval,
35− InsecureSkipTLSVerify: true,
36− PassHostHeader: &truth,
37− ProxyWebSockets: &truth,
38− },
39− {
40− ID: "/bar",
41− Path: "/bar",
42− URI: "file:///var/lib/website",
43− FlushInterval: &flushInterval,
44− InsecureSkipTLSVerify: true,
45− PassHostHeader: &truth,
46− ProxyWebSockets: &truth,
47− },
48− {
49− ID: "static://204",
50− Path: "/",
51− URI: "",
52− Static: true,
53− StaticCode: &staticCode,
54− FlushInterval: nil,
55− InsecureSkipTLSVerify: false,
56− PassHostHeader: nil,
57− ProxyWebSockets: nil,
29+ opts.UpstreamServers = UpstreamConfig{
30+ Upstreams: []Upstream{
31+ {
32+ ID: "/baz",
33+ Path: "/baz",
34+ URI: "http://foo.bar/baz",
35+ FlushInterval: &flushInterval,
36+ InsecureSkipTLSVerify: true,
37+ PassHostHeader: &truth,
38+ ProxyWebSockets: &truth,
39+ },
40+ {
41+ ID: "/bar",
42+ Path: "/bar",
43+ URI: "file:///var/lib/website",
44+ FlushInterval: &flushInterval,
45+ InsecureSkipTLSVerify: true,
46+ PassHostHeader: &truth,
47+ ProxyWebSockets: &truth,
48+ },
49+ {
50+ ID: "static://204",
51+ Path: "/",
52+ URI: "",
53+ Static: true,
54+ StaticCode: &staticCode,
55+ FlushInterval: nil,
56+ InsecureSkipTLSVerify: false,
57+ PassHostHeader: nil,
58+ ProxyWebSockets: nil,
59+ },
5860 },
5961 }
6062
@@ -124,7 +126,7 @@ var _ = Describe("Legacy Options", func() {
124126 Context("Legacy Upstreams", func() {
125127 type convertUpstreamsTableInput struct {
126128 upstreamStrings []string
127− expectedUpstreams Upstreams
129+ expectedUpstreams []Upstream
128130 errMsg string
129131 }
130132
@@ -219,51 +221,51 @@ var _ = Describe("Legacy Options", func() {
219221 Expect(err).ToNot(HaveOccurred())
220222 }
221223
222− Expect(upstreams).To(ConsistOf(in.expectedUpstreams))
224+ Expect(upstreams.Upstreams).To(ConsistOf(in.expectedUpstreams))
223225 },
224226 Entry("with no upstreams", &convertUpstreamsTableInput{
225227 upstreamStrings: []string{},
226− expectedUpstreams: Upstreams{},
228+ expectedUpstreams: []Upstream{},
227229 errMsg: "",
228230 }),
229231 Entry("with a valid HTTP upstream", &convertUpstreamsTableInput{
230232 upstreamStrings: []string{validHTTP},
231− expectedUpstreams: Upstreams{validHTTPUpstream},
233+ expectedUpstreams: []Upstream{validHTTPUpstream},
232234 errMsg: "",
233235 }),
234236 Entry("with a HTTP upstream with an empty path", &convertUpstreamsTableInput{
235237 upstreamStrings: []string{emptyPathHTTP},
236− expectedUpstreams: Upstreams{emptyPathHTTPUpstream},
238+ expectedUpstreams: []Upstream{emptyPathHTTPUpstream},
237239 errMsg: "",
238240 }),
239241 Entry("with a valid File upstream with a fragment", &convertUpstreamsTableInput{
240242 upstreamStrings: []string{validFileWithFragment},
241− expectedUpstreams: Upstreams{validFileWithFragmentUpstream},
243+ expectedUpstreams: []Upstream{validFileWithFragmentUpstream},
242244 errMsg: "",
243245 }),
244246 Entry("with a valid static upstream", &convertUpstreamsTableInput{
245247 upstreamStrings: []string{validStatic},
246− expectedUpstreams: Upstreams{validStaticUpstream},
248+ expectedUpstreams: []Upstream{validStaticUpstream},
247249 errMsg: "",
248250 }),
249251 Entry("with an invalid static upstream, code is 200", &convertUpstreamsTableInput{
250252 upstreamStrings: []string{invalidStatic},
251− expectedUpstreams: Upstreams{invalidStaticUpstream},
253+ expectedUpstreams: []Upstream{invalidStaticUpstream},
252254 errMsg: "",
253255 }),
254256 Entry("with an invalid HTTP upstream", &convertUpstreamsTableInput{
255257 upstreamStrings: []string{invalidHTTP},
256− expectedUpstreams: Upstreams{},
258+ expectedUpstreams: []Upstream{},
257259 errMsg: invalidHTTPErrMsg,
258260 }),
259261 Entry("with an invalid HTTP upstream and other upstreams", &convertUpstreamsTableInput{
260262 upstreamStrings: []string{validHTTP, invalidHTTP},
261− expectedUpstreams: Upstreams{},
263+ expectedUpstreams: []Upstream{},
262264 errMsg: invalidHTTPErrMsg,
263265 }),
264266 Entry("with multiple valid upstreams", &convertUpstreamsTableInput{
265267 upstreamStrings: []string{validHTTP, validFileWithFragment, validStatic},
266− expectedUpstreams: Upstreams{validHTTPUpstream, validFileWithFragmentUpstream, validStaticUpstream},
268+ expectedUpstreams: []Upstream{validHTTPUpstream, validFileWithFragmentUpstream, validStaticUpstream},
267269 errMsg: "",
268270 }),
269271 )
pkg/apis/options/load_test.go+14−11
@@ -469,11 +469,12 @@ sub:
469469
470470 It("should load a full example AlphaOptions", func() {
471471 config := []byte(`
472−upstreams:
473−- id: httpbin
474− path: /
475− uri: http://httpbin
476− flushInterval: 500ms
472+upstreamConfig:
473+ upstreams:
474+ - id: httpbin
475+ path: /
476+ uri: http://httpbin
477+ flushInterval: 500ms
477478injectRequestHeaders:
478479- name: X-Forwarded-User
479480 values:
@@ -502,12 +503,14 @@ injectResponseHeaders:
502503 flushInterval := Duration(500 * time.Millisecond)
503504
504505 Expect(into).To(Equal(&AlphaOptions{
505− Upstreams: []Upstream{
506− {
507− ID: "httpbin",
508− Path: "/",
509− URI: "http://httpbin",
510− FlushInterval: &flushInterval,
506+ UpstreamConfig: UpstreamConfig{
507+ Upstreams: []Upstream{
508+ {
509+ ID: "httpbin",
510+ Path: "/",
511+ URI: "http://httpbin",
512+ FlushInterval: &flushInterval,
513+ },
511514 },
512515 },
513516 InjectRequestHeaders: []Header{
pkg/apis/options/options.go+1−1
@@ -41,7 +41,7 @@ type Options struct {
4141
4242 // Not used in the legacy config, name not allowed to match an external key (upstreams)
4343 // TODO(JoelSpeed): Rename when legacy config is removed
44− UpstreamServers Upstreams `cfg:",internal"`
44+ UpstreamServers UpstreamConfig `cfg:",internal"`
4545
4646 InjectRequestHeaders []Header `cfg:",internal"`
4747 InjectResponseHeaders []Header `cfg:",internal"`
pkg/apis/options/upstreams.go+10−2
@@ -7,8 +7,16 @@ const (
77 DefaultUpstreamFlushInterval = 1 * time.Second
88)
99
10−// Upstreams is a collection of definitions for upstream servers.
11−type Upstreams []Upstream
10+// UpstreamConfig is a collection of definitions for upstream servers.
11+type UpstreamConfig struct {
12+ // ProxyRawPath will pass the raw url path to upstream allowing for url's
13+ // like: "/%2F/" which would otherwise be redirected to "/"
14+ ProxyRawPath bool `json:"proxyRawPath,omitempty"`
15+
16+ // Upstreams represents the configuration for the upstream servers.
17+ // Requests will be proxied to this upstream if the path matches the request path.
18+ Upstreams []Upstream `json:"upstreams,omitempty"`
19+}
1220
1321// Upstream represents the configuration for an upstream server.
1422// Requests will be proxied to this upstream if the path matches the request path.
pkg/upstream/proxy.go+7−3
@@ -22,12 +22,16 @@ type ProxyErrorHandler func(http.ResponseWriter, *http.Request, error)
2222
2323// NewProxy creates a new multiUpstreamProxy that can serve requests directed to
2424// multiple upstreams.
25−func NewProxy(upstreams options.Upstreams, sigData *options.SignatureData, writer pagewriter.Writer) (http.Handler, error) {
25+func NewProxy(upstreams options.UpstreamConfig, sigData *options.SignatureData, writer pagewriter.Writer) (http.Handler, error) {
2626 m := &multiUpstreamProxy{
2727 serveMux: mux.NewRouter(),
2828 }
2929
30− for _, upstream := range sortByPathLongest(upstreams) {
30+ if upstreams.ProxyRawPath {
31+ m.serveMux.UseEncodedPath()
32+ }
33+
34+ for _, upstream := range sortByPathLongest(upstreams.Upstreams) {
3135 if upstream.Static {
3236 if err := m.registerStaticResponseHandler(upstream, writer); err != nil {
3337 return nil, fmt.Errorf("could not register static upstream %q: %v", upstream.ID, err)
@@ -153,7 +157,7 @@ func registerTrailingSlashHandler(serveMux *mux.Router) {
153157// precedence (note this is the input to the rewrite logic).
154158// This does not account for when a rewrite would actually make the path shorter.
155159// This should maintain the sorting behaviour of the standard go serve mux.
156−func sortByPathLongest(in options.Upstreams) options.Upstreams {
160+func sortByPathLongest(in []options.Upstream) []options.Upstream {
157161 sort.Slice(in, func(i, j int) bool {
158162 iRW := in[i].RewriteTarget
159163 jRW := in[j].RewriteTarget
pkg/upstream/proxy_test.go+137−110
@@ -16,94 +16,94 @@ import (
1616)
1717
1818var _ = Describe("Proxy Suite", func() {
19− var upstreamServer http.Handler
19+ type proxyTableInput struct {
20+ target string
21+ response testHTTPResponse
22+ upstream string
23+ upstreams options.UpstreamConfig
24+ }
2025
2126 Context("multiUpstreamProxy", func() {
22− BeforeEach(func() {
23− sigData := &options.SignatureData{Hash: crypto.SHA256, Key: "secret"}
24−
25− writer := &pagewriter.WriterFuncs{
26− ProxyErrorFunc: func(rw http.ResponseWriter, _ *http.Request, _ error) {
27− rw.WriteHeader(502)
28− rw.Write([]byte("Proxy Error"))
29− },
30− }
27+ DescribeTable("Proxy ServeHTTP",
28+ func(in *proxyTableInput) {
29+ sigData := &options.SignatureData{Hash: crypto.SHA256, Key: "secret"}
3130
32− ok := http.StatusOK
33− accepted := http.StatusAccepted
31+ writer := &pagewriter.WriterFuncs{
32+ ProxyErrorFunc: func(rw http.ResponseWriter, _ *http.Request, _ error) {
33+ rw.WriteHeader(502)
34+ rw.Write([]byte("Proxy Error"))
35+ },
36+ }
3437
35− upstreams := options.Upstreams{
36− {
37− ID: "http-backend",
38− Path: "/http/",
39− URI: serverAddr,
40− },
41− {
42− ID: "file-backend",
43− Path: "/files/",
44− URI: fmt.Sprintf("file:///%s", filesDir),
45− },
46− {
47− ID: "static-backend",
48− Path: "/static/",
49− Static: true,
50− StaticCode: &ok,
51− },
52− {
53− ID: "static-backend-no-trailing-slash",
54− Path: "/static",
55− Static: true,
56− StaticCode: &accepted,
57− },
58− {
59− ID: "static-backend-long",
60− Path: "/static/long",
61− Static: true,
62− StaticCode: &accepted,
63− },
64− {
65− ID: "bad-http-backend",
66− Path: "/bad-http/",
67− URI: "http://::1",
68− },
69− {
70− ID: "single-path-backend",
71− Path: "/single-path",
72− Static: true,
73− StaticCode: &ok,
74− },
75− {
76− ID: "backend-with-rewrite-prefix",
77− Path: "^/rewrite-prefix/(.*)",
78− RewriteTarget: "/different/backend/path/$1",
79− URI: serverAddr,
80− },
81− {
82− ID: "double-match-plain",
83− Path: "/double-match/",
84− URI: serverAddr,
85− },
86− {
87− ID: "double-match-rewrite",
88− Path: "^/double-match/(.*)",
89− RewriteTarget: "/double-match/rewrite/$1",
90− URI: serverAddr,
91− },
92− }
38+ ok := http.StatusOK
39+ accepted := http.StatusAccepted
9340
94− var err error
95− upstreamServer, err = NewProxy(upstreams, sigData, writer)
96− Expect(err).ToNot(HaveOccurred())
97− })
41+ // Allows for specifying settings and even individual upstreams for specific tests and uses the default upstreams/configs otherwise
42+ upstreams := in.upstreams
43+ if len(in.upstreams.Upstreams) == 0 {
44+ upstreams.Upstreams = []options.Upstream{
45+ {
46+ ID: "http-backend",
47+ Path: "/http/",
48+ URI: serverAddr,
49+ },
50+ {
51+ ID: "file-backend",
52+ Path: "/files/",
53+ URI: fmt.Sprintf("file:///%s", filesDir),
54+ },
55+ {
56+ ID: "static-backend",
57+ Path: "/static/",
58+ Static: true,
59+ StaticCode: &ok,
60+ },
61+ {
62+ ID: "static-backend-no-trailing-slash",
63+ Path: "/static",
64+ Static: true,
65+ StaticCode: &accepted,
66+ },
67+ {
68+ ID: "static-backend-long",
69+ Path: "/static/long",
70+ Static: true,
71+ StaticCode: &accepted,
72+ },
73+ {
74+ ID: "bad-http-backend",
75+ Path: "/bad-http/",
76+ URI: "http://::1",
77+ },
78+ {
79+ ID: "single-path-backend",
80+ Path: "/single-path",
81+ Static: true,
82+ StaticCode: &ok,
83+ },
84+ {
85+ ID: "backend-with-rewrite-prefix",
86+ Path: "^/rewrite-prefix/(.*)",
87+ RewriteTarget: "/different/backend/path/$1",
88+ URI: serverAddr,
89+ },
90+ {
91+ ID: "double-match-plain",
92+ Path: "/double-match/",
93+ URI: serverAddr,
94+ },
95+ {
96+ ID: "double-match-rewrite",
97+ Path: "^/double-match/(.*)",
98+ RewriteTarget: "/double-match/rewrite/$1",
99+ URI: serverAddr,
100+ },
101+ }
102+ }
98103
99− type proxyTableInput struct {
100− target string
101− response testHTTPResponse
102− upstream string
103− }
104+ upstreamServer, err := NewProxy(upstreams, sigData, writer)
105+ Expect(err).ToNot(HaveOccurred())
104106
105− DescribeTable("Proxy ServeHTTP",
106− func(in *proxyTableInput) {
107107 req := middlewareapi.AddRequestScope(
108108 httptest.NewRequest("", in.target, nil),
109109 &middlewareapi.RequestScope{},
@@ -131,10 +131,12 @@ var _ = Describe("Proxy Suite", func() {
131131 }
132132
133133 // Compare the reflected request to the upstream
134− request := testHTTPRequest{}
135− Expect(json.Unmarshal(body, &request)).To(Succeed())
136− testSanitizeRequestHeader(request.Header)
137− Expect(request).To(Equal(in.response.request))
134+ if body != nil {
135+ request := testHTTPRequest{}
136+ Expect(json.Unmarshal(body, &request)).To(Succeed())
137+ testSanitizeRequestHeader(request.Header)
138+ Expect(request).To(Equal(in.response.request))
139+ }
138140 },
139141 Entry("with a request to the HTTP service", &proxyTableInput{
140142 target: "http://example.localhost/http/1234",
@@ -310,33 +312,58 @@ var _ = Describe("Proxy Suite", func() {
310312 },
311313 upstream: "double-match-rewrite",
312314 }),
315+ Entry("containing an escaped '/' without ProxyRawPath", &proxyTableInput{
316+ target: "http://example.localhost/%2F/test1/%2F/test2",
317+ response: testHTTPResponse{
318+ code: 301,
319+ header: map[string][]string{
320+ "Location": {
321+ "http://example.localhost/test1/test2",
322+ },
323+ },
324+ },
325+ upstream: "",
326+ }),
327+ Entry("containing an escaped '/' with ProxyRawPath", &proxyTableInput{
328+ upstreams: options.UpstreamConfig{ProxyRawPath: true},
329+ target: "http://example.localhost/%2F/test1/%2F/test2",
330+ response: testHTTPResponse{
331+ code: 404,
332+ header: map[string][]string{
333+ "X-Content-Type-Options": {"nosniff"},
334+ contentType: {textPlainUTF8},
335+ },
336+ raw: "404 page not found\n",
337+ },
338+ upstream: "",
339+ }),
313340 )
314341 })
315342
316343 Context("sortByPathLongest", func() {
317344 type sortByPathLongestTableInput struct {
318− input options.Upstreams
319− expectedOutput options.Upstreams
345+ input []options.Upstream
346+ expectedOutput []options.Upstream
320347 }
321348
322− var httpPath = options.Upstream{
349+ httpPath := options.Upstream{
323350 Path: "/http/",
324351 }
325352
326− var httpSubPath = options.Upstream{
353+ httpSubPath := options.Upstream{
327354 Path: "/http/subpath/",
328355 }
329356
330− var longerPath = options.Upstream{
357+ longerPath := options.Upstream{
331358 Path: "/longer-than-http",
332359 }
333360
334− var shortPathWithRewrite = options.Upstream{
361+ shortPathWithRewrite := options.Upstream{
335362 Path: "^/h/(.*)",
336363 RewriteTarget: "/$1",
337364 }
338365
339− var shortSubPathWithRewrite = options.Upstream{
366+ shortSubPathWithRewrite := options.Upstream{
340367 Path: "^/h/bar/(.*)",
341368 RewriteTarget: "/$1",
342369 }
@@ -346,40 +373,40 @@ var _ = Describe("Proxy Suite", func() {
346373 Expect(sortByPathLongest(in.input)).To(Equal(in.expectedOutput))
347374 },
348375 Entry("with a mix of paths registered", sortByPathLongestTableInput{
349− input: options.Upstreams{httpPath, httpSubPath, shortSubPathWithRewrite, longerPath, shortPathWithRewrite},
350− expectedOutput: options.Upstreams{shortSubPathWithRewrite, shortPathWithRewrite, longerPath, httpSubPath, httpPath},
376+ input: []options.Upstream{httpPath, httpSubPath, shortSubPathWithRewrite, longerPath, shortPathWithRewrite},
377+ expectedOutput: []options.Upstream{shortSubPathWithRewrite, shortPathWithRewrite, longerPath, httpSubPath, httpPath},
351378 }),
352379 Entry("when a subpath is registered (in order)", sortByPathLongestTableInput{
353− input: options.Upstreams{httpSubPath, httpPath},
354− expectedOutput: options.Upstreams{httpSubPath, httpPath},
380+ input: []options.Upstream{httpSubPath, httpPath},
381+ expectedOutput: []options.Upstream{httpSubPath, httpPath},
355382 }),
356383 Entry("when a subpath is registered (out of order)", sortByPathLongestTableInput{
357− input: options.Upstreams{httpPath, httpSubPath},
358− expectedOutput: options.Upstreams{httpSubPath, httpPath},
384+ input: []options.Upstream{httpPath, httpSubPath},
385+ expectedOutput: []options.Upstream{httpSubPath, httpPath},
359386 }),
360387 Entry("when longer paths are registered (in order)", sortByPathLongestTableInput{
361− input: options.Upstreams{longerPath, httpPath},
362− expectedOutput: options.Upstreams{longerPath, httpPath},
388+ input: []options.Upstream{longerPath, httpPath},
389+ expectedOutput: []options.Upstream{longerPath, httpPath},
363390 }),
364391 Entry("when longer paths are registered (out of order)", sortByPathLongestTableInput{
365− input: options.Upstreams{httpPath, longerPath},
366− expectedOutput: options.Upstreams{longerPath, httpPath},
392+ input: []options.Upstream{httpPath, longerPath},
393+ expectedOutput: []options.Upstream{longerPath, httpPath},
367394 }),
368395 Entry("when a rewrite target is registered (in order)", sortByPathLongestTableInput{
369− input: options.Upstreams{shortPathWithRewrite, longerPath},
370− expectedOutput: options.Upstreams{shortPathWithRewrite, longerPath},
396+ input: []options.Upstream{shortPathWithRewrite, longerPath},
397+ expectedOutput: []options.Upstream{shortPathWithRewrite, longerPath},
371398 }),
372399 Entry("when a rewrite target is registered (out of order)", sortByPathLongestTableInput{
373− input: options.Upstreams{longerPath, shortPathWithRewrite},
374− expectedOutput: options.Upstreams{shortPathWithRewrite, longerPath},
400+ input: []options.Upstream{longerPath, shortPathWithRewrite},
401+ expectedOutput: []options.Upstream{shortPathWithRewrite, longerPath},
375402 }),
376403 Entry("with multiple rewrite targets registered (in order)", sortByPathLongestTableInput{
377− input: options.Upstreams{shortSubPathWithRewrite, shortPathWithRewrite},
378− expectedOutput: options.Upstreams{shortSubPathWithRewrite, shortPathWithRewrite},
404+ input: []options.Upstream{shortSubPathWithRewrite, shortPathWithRewrite},
405+ expectedOutput: []options.Upstream{shortSubPathWithRewrite, shortPathWithRewrite},
379406 }),
380407 Entry("with multiple rewrite targets registered (out of order)", sortByPathLongestTableInput{
381− input: options.Upstreams{shortPathWithRewrite, shortSubPathWithRewrite},
382− expectedOutput: options.Upstreams{shortSubPathWithRewrite, shortPathWithRewrite},
408+ input: []options.Upstream{shortPathWithRewrite, shortSubPathWithRewrite},
409+ expectedOutput: []options.Upstream{shortSubPathWithRewrite, shortPathWithRewrite},
383410 }),
384411 )
385412 })
pkg/validation/options_test.go+1−1
@@ -22,7 +22,7 @@ const (
2222
2323func testOptions() *options.Options {
2424 o := options.NewOptions()
25− o.UpstreamServers = append(o.UpstreamServers, options.Upstream{
25+ o.UpstreamServers.Upstreams = append(o.UpstreamServers.Upstreams, options.Upstream{
2626 ID: "upstream",
2727 Path: "/",
2828 URI: "http://127.0.0.1:8080/",
pkg/validation/upstreams.go+2−2
@@ -7,12 +7,12 @@ import (
77 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
88)
99
10−func validateUpstreams(upstreams options.Upstreams) []string {
10+func validateUpstreams(upstreams options.UpstreamConfig) []string {
1111 msgs := []string{}
1212 ids := make(map[string]struct{})
1313 paths := make(map[string]struct{})
1414
15− for _, upstream := range upstreams {
15+ for _, upstream := range upstreams.Upstreams {
1616 msgs = append(msgs, validateUpstream(upstream, ids, paths)...)
1717 }
1818
pkg/validation/upstreams_test.go+93−71
@@ -11,7 +11,7 @@ import (
1111
1212var _ = Describe("Upstreams", func() {
1313 type validateUpstreamTableInput struct {
14− upstreams options.Upstreams
14+ upstreams options.UpstreamConfig
1515 errStrings []string
1616 }
1717
@@ -54,88 +54,104 @@ var _ = Describe("Upstreams", func() {
5454 Expect(validateUpstreams(o.upstreams)).To(ConsistOf(o.errStrings))
5555 },
5656 Entry("with no upstreams", &validateUpstreamTableInput{
57− upstreams: options.Upstreams{},
57+ upstreams: options.UpstreamConfig{},
5858 errStrings: []string{},
5959 }),
6060 Entry("with valid upstreams", &validateUpstreamTableInput{
61− upstreams: options.Upstreams{
62− validHTTPUpstream,
63− validStaticUpstream,
64− validFileUpstream,
61+ upstreams: options.UpstreamConfig{
62+ Upstreams: []options.Upstream{
63+ validHTTPUpstream,
64+ validStaticUpstream,
65+ validFileUpstream,
66+ },
6567 },
6668 errStrings: []string{},
6769 }),
6870 Entry("with an empty ID", &validateUpstreamTableInput{
69− upstreams: options.Upstreams{
70− {
71− ID: "",
72− Path: "/foo",
73− URI: "http://localhost:8080",
71+ upstreams: options.UpstreamConfig{
72+ Upstreams: []options.Upstream{
73+ {
74+ ID: "",
75+ Path: "/foo",
76+ URI: "http://localhost:8080",
77+ },
7478 },
7579 },
7680 errStrings: []string{emptyIDMsg},
7781 }),
7882 Entry("with an empty Path", &validateUpstreamTableInput{
79− upstreams: options.Upstreams{
80− {
81− ID: "foo",
82− Path: "",
83− URI: "http://localhost:8080",
83+ upstreams: options.UpstreamConfig{
84+ Upstreams: []options.Upstream{
85+ {
86+ ID: "foo",
87+ Path: "",
88+ URI: "http://localhost:8080",
89+ },
8490 },
8591 },
8692 errStrings: []string{emptyPathMsg},
8793 }),
8894 Entry("with an empty Path", &validateUpstreamTableInput{
89− upstreams: options.Upstreams{
90− {
91− ID: "foo",
92− Path: "",
93− URI: "http://localhost:8080",
95+ upstreams: options.UpstreamConfig{
96+ Upstreams: []options.Upstream{
97+ {
98+ ID: "foo",
99+ Path: "",
100+ URI: "http://localhost:8080",
101+ },
94102 },
95103 },
96104 errStrings: []string{emptyPathMsg},
97105 }),
98106 Entry("with an empty URI", &validateUpstreamTableInput{
99− upstreams: options.Upstreams{
100− {
101− ID: "foo",
102− Path: "/foo",
103− URI: "",
107+ upstreams: options.UpstreamConfig{
108+ Upstreams: []options.Upstream{
109+ {
110+ ID: "foo",
111+ Path: "/foo",
112+ URI: "",
113+ },
104114 },
105115 },
106116 errStrings: []string{emptyURIMsg},
107117 }),
108118 Entry("with an invalid URI", &validateUpstreamTableInput{
109− upstreams: options.Upstreams{
110− {
111− ID: "foo",
112− Path: "/foo",
113− URI: ":",
119+ upstreams: options.UpstreamConfig{
120+ Upstreams: []options.Upstream{
121+ {
122+ ID: "foo",
123+ Path: "/foo",
124+ URI: ":",
125+ },
114126 },
115127 },
116128 errStrings: []string{invalidURIMsg},
117129 }),
118130 Entry("with an invalid URI scheme", &validateUpstreamTableInput{
119− upstreams: options.Upstreams{
120− {
121− ID: "foo",
122− Path: "/foo",
123− URI: "ftp://foo",
131+ upstreams: options.UpstreamConfig{
132+ Upstreams: []options.Upstream{
133+ {
134+ ID: "foo",
135+ Path: "/foo",
136+ URI: "ftp://foo",
137+ },
124138 },
125139 },
126140 errStrings: []string{invalidURISchemeMsg},
127141 }),
128142 Entry("with a static upstream and invalid optons", &validateUpstreamTableInput{
129− upstreams: options.Upstreams{
130− {
131− ID: "foo",
132− Path: "/foo",
133− URI: "ftp://foo",
134− Static: true,
135− FlushInterval: &flushInterval,
136− PassHostHeader: &truth,
137− ProxyWebSockets: &truth,
138− InsecureSkipTLSVerify: true,
143+ upstreams: options.UpstreamConfig{
144+ Upstreams: []options.Upstream{
145+ {
146+ ID: "foo",
147+ Path: "/foo",
148+ URI: "ftp://foo",
149+ Static: true,
150+ FlushInterval: &flushInterval,
151+ PassHostHeader: &truth,
152+ ProxyWebSockets: &truth,
153+ InsecureSkipTLSVerify: true,
154+ },
139155 },
140156 },
141157 errStrings: []string{
@@ -147,41 +163,47 @@ var _ = Describe("Upstreams", func() {
147163 },
148164 }),
149165 Entry("with duplicate IDs", &validateUpstreamTableInput{
150− upstreams: options.Upstreams{
151− {
152− ID: "foo",
153− Path: "/foo1",
154− URI: "http://foo",
155− },
156− {
157− ID: "foo",
158− Path: "/foo2",
159− URI: "http://foo",
166+ upstreams: options.UpstreamConfig{
167+ Upstreams: []options.Upstream{
168+ {
169+ ID: "foo",
170+ Path: "/foo1",
171+ URI: "http://foo",
172+ },
173+ {
174+ ID: "foo",
175+ Path: "/foo2",
176+ URI: "http://foo",
177+ },
160178 },
161179 },
162180 errStrings: []string{multipleIDsMsg},
163181 }),
164182 Entry("with duplicate Paths", &validateUpstreamTableInput{
165− upstreams: options.Upstreams{
166− {
167− ID: "foo1",
168− Path: "/foo",
169− URI: "http://foo",
170− },
171− {
172− ID: "foo2",
173− Path: "/foo",
174− URI: "http://foo",
183+ upstreams: options.UpstreamConfig{
184+ Upstreams: []options.Upstream{
185+ {
186+ ID: "foo1",
187+ Path: "/foo",
188+ URI: "http://foo",
189+ },
190+ {
191+ ID: "foo2",
192+ Path: "/foo",
193+ URI: "http://foo",
194+ },
175195 },
176196 },
177197 errStrings: []string{multiplePathsMsg},
178198 }),
179199 Entry("when a static code is supplied without static", &validateUpstreamTableInput{
180− upstreams: options.Upstreams{
181− {
182− ID: "foo",
183− Path: "/foo",
184− StaticCode: &staticCode200,
200+ upstreams: options.UpstreamConfig{
201+ Upstreams: []options.Upstream{
202+ {
203+ ID: "foo",
204+ Path: "/foo",
205+ StaticCode: &staticCode200,
206+ },
185207 },
186208 },
187209 errStrings: []string{emptyURIMsg, staticCodeMsg},