git.iko.soy/typeable/oauth2-proxy
on master help sign in
Allow request paths to be rewritten before proxying to upstream server
commit6c62b25bf13f245fb42db465ae1cd872e8f8cb38 authorJoel Speed <joel.speed@hotmail.co.uk> dateMon 22 Feb 2021, 15:56 (6 years ago) parentd2d62bb
8 files changed+256−14
docs/docs/configuration/alpha_config.md+2−1 go.mod+2−2 go.sum+4−2 pkg/apis/options/upstreams.go+14 pkg/upstream/proxy.go+46−9 pkg/upstream/proxy_test.go+48 pkg/upstream/rewrite.go+80 pkg/upstream/rewrite_test.go+60
docs/docs/configuration/alpha_config.md+2−1
@@ -371,7 +371,8 @@ Requests will be proxied to this upstream if the path matches the request path.
371371| Field | Type | Description |
372372| ----- | ---- | ----------- |
373373| `id` | _string_ | ID should be a unique identifier for the upstream.<br/>This value is required for all upstreams. |
374−| `path` | _string_ | Path is used to map requests to the upstream server.<br/>The closest match will take precedence and all Paths must be unique. |
374+| `path` | _string_ | Path is used to map requests to the upstream server.<br/>The closest match will take precedence and all Paths must be unique.<br/>Path can also take a pattern when used with RewriteTarget.<br/>Path segments can be captured and matched using regular experessions.<br/>Eg:<br/>- `^/foo$`: Match only the explicit path `/foo`<br/>- `^/bar/$`: Match any path prefixed with `/bar/`<br/>- `^/baz/(.*)$`: Match any path prefixed with `/baz` and capture the remaining path for use with RewriteTarget |
375+| `rewriteTarget` | _string_ | RewriteTarget allows users to rewrite the request path before it is sent to<br/>the upstream server.<br/>Use the Path to capture segments for reuse within the rewrite target.<br/>Eg: With a Path of `^/baz/(.*)`, a RewriteTarget of `/foo/$1` would rewrite<br/>the request `/baz/abc/123` to `/foo/abc/123` before proxying to the<br/>upstream server. |
375376| `uri` | _string_ | The URI of the upstream server. This may be an HTTP(S) server of a File<br/>based URL. It may include a path, in which case all requests will be served<br/>under that path.<br/>Eg:<br/>- http://localhost:8080<br/>- https://service.localhost<br/>- https://service.localhost/path<br/>- file://host/path<br/>If the URI's path is "/base" and the incoming request was for "/dir",<br/>the upstream request will be for "/base/dir". |
376377| `insecureSkipTLSVerify` | _bool_ | InsecureSkipTLSVerify will skip TLS verification of upstream HTTPS hosts.<br/>This option is insecure and will allow potential Man-In-The-Middle attacks<br/>betweem OAuth2 Proxy and the usptream server.<br/>Defaults to false. |
377378| `static` | _bool_ | Static will make all requests to this upstream have a static response.<br/>The response will have a body of "Authenticated" and a response code<br/>matching StaticCode.<br/>If StaticCode is not set, the response will return a 200 response. |
go.mod+2−2
@@ -31,8 +31,8 @@ require (
3131 github.com/stretchr/testify v1.6.1
3232 github.com/vmihailenco/msgpack/v4 v4.3.11
3333 github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997
34− golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
35− golang.org/x/net v0.0.0-20200707034311-ab3426394381
34+ golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
35+ golang.org/x/net v0.0.0-20200822124328-c89045814202
3636 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
3737 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
3838 google.golang.org/api v0.20.0
go.sum+4−2
@@ -460,8 +460,9 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
460460golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
461461golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
462462golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
463−golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
464463golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
464+golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
465+golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
465466golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
466467golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
467468golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw=
@@ -503,8 +504,9 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLL
503504golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
504505golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
505506golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
506−golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
507507golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
508+golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
509+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
508510golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
509511golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
510512golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
pkg/apis/options/upstreams.go+14
@@ -19,8 +19,22 @@ type Upstream struct {
1919
2020 // Path is used to map requests to the upstream server.
2121 // The closest match will take precedence and all Paths must be unique.
22+ // Path can also take a pattern when used with RewriteTarget.
23+ // Path segments can be captured and matched using regular experessions.
24+ // Eg:
25+ // - `^/foo$`: Match only the explicit path `/foo`
26+ // - `^/bar/$`: Match any path prefixed with `/bar/`
27+ // - `^/baz/(.*)$`: Match any path prefixed with `/baz` and capture the remaining path for use with RewriteTarget
2228 Path string `json:"path,omitempty"`
2329
30+ // RewriteTarget allows users to rewrite the request path before it is sent to
31+ // the upstream server.
32+ // Use the Path to capture segments for reuse within the rewrite target.
33+ // Eg: With a Path of `^/baz/(.*)`, a RewriteTarget of `/foo/$1` would rewrite
34+ // the request `/baz/abc/123` to `/foo/abc/123` before proxying to the
35+ // upstream server.
36+ RewriteTarget string `json:"rewriteTarget,omitempty"`
37+
2438 // The URI of the upstream server. This may be an HTTP(S) server of a File
2539 // based URL. It may include a path, in which case all requests will be served
2640 // under that path.
pkg/upstream/proxy.go+46−9
@@ -4,9 +4,11 @@ import (
44 "fmt"
55 "net/http"
66 "net/url"
7+ "regexp"
78 "strings"
89
910 "github.com/gorilla/mux"
11+ "github.com/justinas/alice"
1012 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1113 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
1214 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
@@ -25,7 +27,9 @@ func NewProxy(upstreams options.Upstreams, sigData *options.SignatureData, write
2527
2628 for _, upstream := range upstreams {
2729 if upstream.Static {
28− m.registerStaticResponseHandler(upstream)
30+ if err := m.registerStaticResponseHandler(upstream, writer); err != nil {
31+ return nil, fmt.Errorf("could not register static upstream %q: %v", upstream.ID, err)
32+ }
2933 continue
3034 }
3135
@@ -35,9 +39,13 @@ func NewProxy(upstreams options.Upstreams, sigData *options.SignatureData, write
3539 }
3640 switch u.Scheme {
3741 case fileScheme:
38− m.registerFileServer(upstream, u)
42+ if err := m.registerFileServer(upstream, u, writer); err != nil {
43+ return nil, fmt.Errorf("could not register file upstream %q: %v", upstream.ID, err)
44+ }
3945 case httpScheme, httpsScheme:
40− m.registerHTTPUpstreamProxy(upstream, u, sigData, writer)
46+ if err := m.registerHTTPUpstreamProxy(upstream, u, sigData, writer); err != nil {
47+ return nil, fmt.Errorf("could not register HTTP upstream %q: %v", upstream.ID, err)
48+ }
4149 default:
4250 return nil, fmt.Errorf("unknown scheme for upstream %q: %q", upstream.ID, u.Scheme)
4351 }
@@ -57,21 +65,31 @@ func (m *multiUpstreamProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request
5765}
5866
5967// registerStaticResponseHandler registers a static response handler with at the given path.
60−func (m *multiUpstreamProxy) registerStaticResponseHandler(upstream options.Upstream) {
68+func (m *multiUpstreamProxy) registerStaticResponseHandler(upstream options.Upstream, writer pagewriter.Writer) error {
6169 logger.Printf("mapping path %q => static response %d", upstream.Path, derefStaticCode(upstream.StaticCode))
62− m.registerSimpleHandler(upstream.Path, newStaticResponseHandler(upstream.ID, upstream.StaticCode))
70+ return m.registerHandler(upstream, newStaticResponseHandler(upstream.ID, upstream.StaticCode), writer)
6371}
6472
6573// registerFileServer registers a new fileServer based on the configuration given.
66−func (m *multiUpstreamProxy) registerFileServer(upstream options.Upstream, u *url.URL) {
74+func (m *multiUpstreamProxy) registerFileServer(upstream options.Upstream, u *url.URL, writer pagewriter.Writer) error {
6775 logger.Printf("mapping path %q => file system %q", upstream.Path, u.Path)
68− m.registerSimpleHandler(upstream.Path, newFileServer(upstream.ID, upstream.Path, u.Path))
76+ return m.registerHandler(upstream, newFileServer(upstream.ID, upstream.Path, u.Path), writer)
6977}
7078
7179// registerHTTPUpstreamProxy registers a new httpUpstreamProxy based on the configuration given.
72−func (m *multiUpstreamProxy) registerHTTPUpstreamProxy(upstream options.Upstream, u *url.URL, sigData *options.SignatureData, writer pagewriter.Writer) {
80+func (m *multiUpstreamProxy) registerHTTPUpstreamProxy(upstream options.Upstream, u *url.URL, sigData *options.SignatureData, writer pagewriter.Writer) error {
7381 logger.Printf("mapping path %q => upstream %q", upstream.Path, upstream.URI)
74− m.registerSimpleHandler(upstream.Path, newHTTPUpstreamProxy(upstream, u, sigData, writer.ProxyErrorHandler))
82+ return m.registerHandler(upstream, newHTTPUpstreamProxy(upstream, u, sigData, writer.ProxyErrorHandler), writer)
83+}
84+
85+// registerHandler ensures the given handler is regiestered with the serveMux.
86+func (m *multiUpstreamProxy) registerHandler(upstream options.Upstream, handler http.Handler, writer pagewriter.Writer) error {
87+ if upstream.RewriteTarget == "" {
88+ m.registerSimpleHandler(upstream.Path, handler)
89+ return nil
90+ }
91+
92+ return m.registerRewriteHandler(upstream, handler, writer)
7593}
7694
7795// registerSimpleHandler maintains the behaviour of the go standard serveMux
@@ -83,3 +101,22 @@ func (m *multiUpstreamProxy) registerSimpleHandler(path string, handler http.Han
83101 m.serveMux.Path(path).Handler(handler)
84102 }
85103}
104+
105+// registerRewriteHandler ensures the handler is registered for all paths
106+// which match the regex defined in the Path.
107+// Requests to the handler will have the request path rewritten before the
108+// request is made to the next handler.
109+func (m *multiUpstreamProxy) registerRewriteHandler(upstream options.Upstream, handler http.Handler, writer pagewriter.Writer) error {
110+ rewriteRegExp, err := regexp.Compile(upstream.Path)
111+ if err != nil {
112+ return fmt.Errorf("invalid path %q for upstream: %v", upstream.Path, err)
113+ }
114+
115+ rewrite := newRewritePath(rewriteRegExp, upstream.RewriteTarget, writer)
116+ h := alice.New(rewrite).Then(handler)
117+ m.serveMux.MatcherFunc(func(req *http.Request, match *mux.RouteMatch) bool {
118+ return rewriteRegExp.MatchString(req.URL.Path)
119+ }).Handler(h)
120+
121+ return nil
122+}
pkg/upstream/proxy_test.go+48
@@ -58,6 +58,12 @@ var _ = Describe("Proxy Suite", func() {
5858 Static: true,
5959 StaticCode: &ok,
6060 },
61+ {
62+ ID: "backend-with-rewrite-prefix",
63+ Path: "^/rewrite-prefix/(.*)",
64+ RewriteTarget: "/different/backend/path/$1",
65+ URI: serverAddr,
66+ },
6167 }
6268
6369 var err error
@@ -187,5 +193,47 @@ var _ = Describe("Proxy Suite", func() {
187193 raw: "404 page not found\n",
188194 },
189195 }),
196+ Entry("with a request to the rewrite prefix server", &proxyTableInput{
197+ target: "http://example.localhost/rewrite-prefix/1234",
198+ response: testHTTPResponse{
199+ code: 200,
200+ header: map[string][]string{
201+ contentType: {applicationJSON},
202+ },
203+ request: testHTTPRequest{
204+ Method: "GET",
205+ URL: "http://example.localhost/different/backend/path/1234",
206+ Header: map[string][]string{
207+ "Gap-Auth": {""},
208+ "Gap-Signature": {"sha256 jeAeM7wHSj2ab/l9YPvtTJ9l/8q1tpY2V/iwXF48bgw="},
209+ },
210+ Body: []byte{},
211+ Host: "example.localhost",
212+ RequestURI: "http://example.localhost/different/backend/path/1234",
213+ },
214+ },
215+ upstream: "backend-with-rewrite-prefix",
216+ }),
217+ Entry("with a request to a subpath of the rewrite prefix server", &proxyTableInput{
218+ target: "http://example.localhost/rewrite-prefix/1234/abc",
219+ response: testHTTPResponse{
220+ code: 200,
221+ header: map[string][]string{
222+ contentType: {applicationJSON},
223+ },
224+ request: testHTTPRequest{
225+ Method: "GET",
226+ URL: "http://example.localhost/different/backend/path/1234/abc",
227+ Header: map[string][]string{
228+ "Gap-Auth": {""},
229+ "Gap-Signature": {"sha256 rAkAc9gp7EndoOppJuvbuPnYuBcqrTkBnQx6iPS8xTA="},
230+ },
231+ Body: []byte{},
232+ Host: "example.localhost",
233+ RequestURI: "http://example.localhost/different/backend/path/1234/abc",
234+ },
235+ },
236+ upstream: "backend-with-rewrite-prefix",
237+ }),
190238 )
191239})
pkg/upstream/rewrite.gonew file+80
@@ -0,0 +1,80 @@
1+package upstream
2+
3+import (
4+ "fmt"
5+ "net/http"
6+ "net/url"
7+ "regexp"
8+ "strings"
9+
10+ "github.com/justinas/alice"
11+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
12+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
13+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
14+)
15+
16+// newRewritePath creates a new middleware that will rewrite the request URI
17+// path before handing the request to the next server.
18+func newRewritePath(rewriteRegExp *regexp.Regexp, rewriteTarget string, writer pagewriter.Writer) alice.Constructor {
19+ return func(next http.Handler) http.Handler {
20+ return rewritePath(rewriteRegExp, rewriteTarget, writer, next)
21+ }
22+}
23+
24+// rewritePath uses the regexp to rewrite the request URI based on the provided
25+// rewriteTarget.
26+func rewritePath(rewriteRegExp *regexp.Regexp, rewriteTarget string, writer pagewriter.Writer, next http.Handler) http.Handler {
27+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
28+ reqURL, err := url.ParseRequestURI(req.RequestURI)
29+ if err != nil {
30+ logger.Errorf("could not parse request URI: %v", err)
31+ writer.WriteErrorPage(rw, pagewriter.ErrorPageOpts{
32+ Status: http.StatusInternalServerError,
33+ RequestID: middleware.GetRequestScope(req).RequestID,
34+ AppError: fmt.Sprintf("Could not parse request URI: %v", err),
35+ })
36+ return
37+ }
38+
39+ // Use the regex to rewrite the request path before proxying to the upstream.
40+ newURI := rewriteRegExp.ReplaceAllString(reqURL.Path, rewriteTarget)
41+ reqURL.Path, reqURL.RawQuery, err = splitPathAndQuery(reqURL.Query(), newURI)
42+ if err != nil {
43+ logger.Errorf("could not parse rewrite URI: %v", err)
44+ writer.WriteErrorPage(rw, pagewriter.ErrorPageOpts{
45+ Status: http.StatusInternalServerError,
46+ RequestID: middleware.GetRequestScope(req).RequestID,
47+ AppError: fmt.Sprintf("Could not parse rewrite URI: %v", err),
48+ })
49+ return
50+ }
51+
52+ req.RequestURI = reqURL.String()
53+ next.ServeHTTP(rw, req)
54+ })
55+}
56+
57+// splitPathAndQuery splits the rewritten path into the URL Path and the URL
58+// raw query. Any rewritten query values are appended to the original query
59+// values.
60+// This relies on the underlying URL library to encode the query string.
61+// For duplicate values it appends each as a separate value, e.g. ?foo=bar&foo=baz.
62+func splitPathAndQuery(originalQuery url.Values, raw string) (string, string, error) {
63+ s := strings.SplitN(raw, "?", 2)
64+ if len(s) == 1 {
65+ return s[0], originalQuery.Encode(), nil
66+ }
67+
68+ queryValues, err := url.ParseQuery(s[1])
69+ if err != nil {
70+ return "", "", nil
71+ }
72+
73+ for key, values := range queryValues {
74+ for _, value := range values {
75+ originalQuery.Add(key, value)
76+ }
77+ }
78+
79+ return s[0], originalQuery.Encode(), nil
80+}
pkg/upstream/rewrite_test.gonew file+60
@@ -0,0 +1,60 @@
1+package upstream
2+
3+import (
4+ "net/http"
5+ "net/http/httptest"
6+ "regexp"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
9+ . "github.com/onsi/ginkgo"
10+ . "github.com/onsi/ginkgo/extensions/table"
11+ . "github.com/onsi/gomega"
12+)
13+
14+var _ = Describe("Rewrite", func() {
15+ type rewritePathTableInput struct {
16+ rewriteRegex *regexp.Regexp
17+ rewriteTarget string
18+ requestTarget string
19+ expectedRequestURI string
20+ }
21+
22+ DescribeTable("should rewrite the request path",
23+ func(in rewritePathTableInput) {
24+ req := httptest.NewRequest("", in.requestTarget, nil)
25+ rw := httptest.NewRecorder()
26+
27+ var gotRequestURI string
28+ handler := newRewritePath(in.rewriteRegex, in.rewriteTarget, &pagewriter.WriterFuncs{})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
29+ gotRequestURI = r.RequestURI
30+ }))
31+ handler.ServeHTTP(rw, req)
32+
33+ Expect(gotRequestURI).To(Equal(in.expectedRequestURI))
34+ },
35+ Entry("when the path matches the regexp", rewritePathTableInput{
36+ rewriteRegex: regexp.MustCompile("^/http/(.*)"),
37+ rewriteTarget: "/$1",
38+ requestTarget: "http://example.com/http/foo/bar",
39+ expectedRequestURI: "http://example.com/foo/bar",
40+ }),
41+ Entry("when the path does not match the regexp", rewritePathTableInput{
42+ rewriteRegex: regexp.MustCompile("^/http/(.*)"),
43+ rewriteTarget: "/$1",
44+ requestTarget: "https://example.com/https/foo/bar",
45+ expectedRequestURI: "https://example.com/https/foo/bar",
46+ }),
47+ Entry("when the regexp is not anchored", rewritePathTableInput{
48+ rewriteRegex: regexp.MustCompile("/http/(.*)"),
49+ rewriteTarget: "/$1",
50+ requestTarget: "http://example.com/bar/http/foo/bar",
51+ expectedRequestURI: "http://example.com/bar/foo/bar",
52+ }),
53+ Entry("when the regexp is rewriting to a query", rewritePathTableInput{
54+ rewriteRegex: regexp.MustCompile(`/articles/([a-z0-9\-]*)`),
55+ rewriteTarget: "/article?id=$1",
56+ requestTarget: "http://example.com/articles/blog-2021-01-01",
57+ expectedRequestURI: "http://example.com/article?id=blog-2021-01-01",
58+ }),
59+ )
60+})