diff --git a/docs/docs/configuration/alpha_config.md b/docs/docs/configuration/alpha_config.md index ca490b34..cd8f0f3f 100644 --- a/docs/docs/configuration/alpha_config.md +++ b/docs/docs/configuration/alpha_config.md @@ -371,7 +371,8 @@ Requests will be proxied to this upstream if the path matches the request path. | Field | Type | Description | | ----- | ---- | ----------- | | `id` | _string_ | ID should be a unique identifier for the upstream.
This value is required for all upstreams. | -| `path` | _string_ | Path is used to map requests to the upstream server.
The closest match will take precedence and all Paths must be unique. | +| `path` | _string_ | Path is used to map requests to the upstream server.
The closest match will take precedence and all Paths must be unique.
Path can also take a pattern when used with RewriteTarget.
Path segments can be captured and matched using regular experessions.
Eg:
- `^/foo$`: Match only the explicit path `/foo`
- `^/bar/$`: Match any path prefixed with `/bar/`
- `^/baz/(.*)$`: Match any path prefixed with `/baz` and capture the remaining path for use with RewriteTarget | +| `rewriteTarget` | _string_ | RewriteTarget allows users to rewrite the request path before it is sent to
the upstream server.
Use the Path to capture segments for reuse within the rewrite target.
Eg: With a Path of `^/baz/(.*)`, a RewriteTarget of `/foo/$1` would rewrite
the request `/baz/abc/123` to `/foo/abc/123` before proxying to the
upstream server. | | `uri` | _string_ | The URI of the upstream server. This may be an HTTP(S) server of a File
based URL. It may include a path, in which case all requests will be served
under that path.
Eg:
- http://localhost:8080
- https://service.localhost
- https://service.localhost/path
- file://host/path
If the URI's path is "/base" and the incoming request was for "/dir",
the upstream request will be for "/base/dir". | | `insecureSkipTLSVerify` | _bool_ | InsecureSkipTLSVerify will skip TLS verification of upstream HTTPS hosts.
This option is insecure and will allow potential Man-In-The-Middle attacks
betweem OAuth2 Proxy and the usptream server.
Defaults to false. | | `static` | _bool_ | Static will make all requests to this upstream have a static response.
The response will have a body of "Authenticated" and a response code
matching StaticCode.
If StaticCode is not set, the response will return a 200 response. | diff --git a/go.mod b/go.mod index c5da2c7d..b3f328e0 100644 --- a/go.mod +++ b/go.mod @@ -31,8 +31,8 @@ require ( github.com/stretchr/testify v1.6.1 github.com/vmihailenco/msgpack/v4 v4.3.11 github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997 - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 - golang.org/x/net v0.0.0-20200707034311-ab3426394381 + golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a + golang.org/x/net v0.0.0-20200822124328-c89045814202 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sync v0.0.0-20201207232520-09787c993a3a google.golang.org/api v0.20.0 diff --git a/go.sum b/go.sum index 5344792d..2b425081 100644 --- a/go.sum +++ b/go.sum @@ -460,8 +460,9 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.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 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/pkg/apis/options/upstreams.go b/pkg/apis/options/upstreams.go index 6ae87487..1977c8da 100644 --- a/pkg/apis/options/upstreams.go +++ b/pkg/apis/options/upstreams.go @@ -19,8 +19,22 @@ type Upstream struct { // Path is used to map requests to the upstream server. // The closest match will take precedence and all Paths must be unique. + // Path can also take a pattern when used with RewriteTarget. + // Path segments can be captured and matched using regular experessions. + // Eg: + // - `^/foo$`: Match only the explicit path `/foo` + // - `^/bar/$`: Match any path prefixed with `/bar/` + // - `^/baz/(.*)$`: Match any path prefixed with `/baz` and capture the remaining path for use with RewriteTarget Path string `json:"path,omitempty"` + // RewriteTarget allows users to rewrite the request path before it is sent to + // the upstream server. + // Use the Path to capture segments for reuse within the rewrite target. + // Eg: With a Path of `^/baz/(.*)`, a RewriteTarget of `/foo/$1` would rewrite + // the request `/baz/abc/123` to `/foo/abc/123` before proxying to the + // upstream server. + RewriteTarget string `json:"rewriteTarget,omitempty"` + // The URI of the upstream server. This may be an HTTP(S) server of a File // based URL. It may include a path, in which case all requests will be served // under that path. diff --git a/pkg/upstream/proxy.go b/pkg/upstream/proxy.go index e4b22bff..9b6246c8 100644 --- a/pkg/upstream/proxy.go +++ b/pkg/upstream/proxy.go @@ -4,9 +4,11 @@ import ( "fmt" "net/http" "net/url" + "regexp" "strings" "github.com/gorilla/mux" + "github.com/justinas/alice" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" @@ -25,7 +27,9 @@ func NewProxy(upstreams options.Upstreams, sigData *options.SignatureData, write for _, upstream := range upstreams { if upstream.Static { - m.registerStaticResponseHandler(upstream) + if err := m.registerStaticResponseHandler(upstream, writer); err != nil { + return nil, fmt.Errorf("could not register static upstream %q: %v", upstream.ID, err) + } continue } @@ -35,9 +39,13 @@ func NewProxy(upstreams options.Upstreams, sigData *options.SignatureData, write } switch u.Scheme { case fileScheme: - m.registerFileServer(upstream, u) + if err := m.registerFileServer(upstream, u, writer); err != nil { + return nil, fmt.Errorf("could not register file upstream %q: %v", upstream.ID, err) + } case httpScheme, httpsScheme: - m.registerHTTPUpstreamProxy(upstream, u, sigData, writer) + if err := m.registerHTTPUpstreamProxy(upstream, u, sigData, writer); err != nil { + return nil, fmt.Errorf("could not register HTTP upstream %q: %v", upstream.ID, err) + } default: return nil, fmt.Errorf("unknown scheme for upstream %q: %q", upstream.ID, u.Scheme) } @@ -57,21 +65,31 @@ func (m *multiUpstreamProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request } // registerStaticResponseHandler registers a static response handler with at the given path. -func (m *multiUpstreamProxy) registerStaticResponseHandler(upstream options.Upstream) { +func (m *multiUpstreamProxy) registerStaticResponseHandler(upstream options.Upstream, writer pagewriter.Writer) error { logger.Printf("mapping path %q => static response %d", upstream.Path, derefStaticCode(upstream.StaticCode)) - m.registerSimpleHandler(upstream.Path, newStaticResponseHandler(upstream.ID, upstream.StaticCode)) + return m.registerHandler(upstream, newStaticResponseHandler(upstream.ID, upstream.StaticCode), writer) } // registerFileServer registers a new fileServer based on the configuration given. -func (m *multiUpstreamProxy) registerFileServer(upstream options.Upstream, u *url.URL) { +func (m *multiUpstreamProxy) registerFileServer(upstream options.Upstream, u *url.URL, writer pagewriter.Writer) error { logger.Printf("mapping path %q => file system %q", upstream.Path, u.Path) - m.registerSimpleHandler(upstream.Path, newFileServer(upstream.ID, upstream.Path, u.Path)) + return m.registerHandler(upstream, newFileServer(upstream.ID, upstream.Path, u.Path), writer) } // registerHTTPUpstreamProxy registers a new httpUpstreamProxy based on the configuration given. -func (m *multiUpstreamProxy) registerHTTPUpstreamProxy(upstream options.Upstream, u *url.URL, sigData *options.SignatureData, writer pagewriter.Writer) { +func (m *multiUpstreamProxy) registerHTTPUpstreamProxy(upstream options.Upstream, u *url.URL, sigData *options.SignatureData, writer pagewriter.Writer) error { logger.Printf("mapping path %q => upstream %q", upstream.Path, upstream.URI) - m.registerSimpleHandler(upstream.Path, newHTTPUpstreamProxy(upstream, u, sigData, writer.ProxyErrorHandler)) + return m.registerHandler(upstream, newHTTPUpstreamProxy(upstream, u, sigData, writer.ProxyErrorHandler), writer) +} + +// registerHandler ensures the given handler is regiestered with the serveMux. +func (m *multiUpstreamProxy) registerHandler(upstream options.Upstream, handler http.Handler, writer pagewriter.Writer) error { + if upstream.RewriteTarget == "" { + m.registerSimpleHandler(upstream.Path, handler) + return nil + } + + return m.registerRewriteHandler(upstream, handler, writer) } // registerSimpleHandler maintains the behaviour of the go standard serveMux @@ -83,3 +101,22 @@ func (m *multiUpstreamProxy) registerSimpleHandler(path string, handler http.Han m.serveMux.Path(path).Handler(handler) } } + +// registerRewriteHandler ensures the handler is registered for all paths +// which match the regex defined in the Path. +// Requests to the handler will have the request path rewritten before the +// request is made to the next handler. +func (m *multiUpstreamProxy) registerRewriteHandler(upstream options.Upstream, handler http.Handler, writer pagewriter.Writer) error { + rewriteRegExp, err := regexp.Compile(upstream.Path) + if err != nil { + return fmt.Errorf("invalid path %q for upstream: %v", upstream.Path, err) + } + + rewrite := newRewritePath(rewriteRegExp, upstream.RewriteTarget, writer) + h := alice.New(rewrite).Then(handler) + m.serveMux.MatcherFunc(func(req *http.Request, match *mux.RouteMatch) bool { + return rewriteRegExp.MatchString(req.URL.Path) + }).Handler(h) + + return nil +} diff --git a/pkg/upstream/proxy_test.go b/pkg/upstream/proxy_test.go index 96b7a0dd..56570b14 100644 --- a/pkg/upstream/proxy_test.go +++ b/pkg/upstream/proxy_test.go @@ -58,6 +58,12 @@ var _ = Describe("Proxy Suite", func() { Static: true, StaticCode: &ok, }, + { + ID: "backend-with-rewrite-prefix", + Path: "^/rewrite-prefix/(.*)", + RewriteTarget: "/different/backend/path/$1", + URI: serverAddr, + }, } var err error @@ -187,5 +193,47 @@ var _ = Describe("Proxy Suite", func() { raw: "404 page not found\n", }, }), + Entry("with a request to the rewrite prefix server", &proxyTableInput{ + target: "http://example.localhost/rewrite-prefix/1234", + response: testHTTPResponse{ + code: 200, + header: map[string][]string{ + contentType: {applicationJSON}, + }, + request: testHTTPRequest{ + Method: "GET", + URL: "http://example.localhost/different/backend/path/1234", + Header: map[string][]string{ + "Gap-Auth": {""}, + "Gap-Signature": {"sha256 jeAeM7wHSj2ab/l9YPvtTJ9l/8q1tpY2V/iwXF48bgw="}, + }, + Body: []byte{}, + Host: "example.localhost", + RequestURI: "http://example.localhost/different/backend/path/1234", + }, + }, + upstream: "backend-with-rewrite-prefix", + }), + Entry("with a request to a subpath of the rewrite prefix server", &proxyTableInput{ + target: "http://example.localhost/rewrite-prefix/1234/abc", + response: testHTTPResponse{ + code: 200, + header: map[string][]string{ + contentType: {applicationJSON}, + }, + request: testHTTPRequest{ + Method: "GET", + URL: "http://example.localhost/different/backend/path/1234/abc", + Header: map[string][]string{ + "Gap-Auth": {""}, + "Gap-Signature": {"sha256 rAkAc9gp7EndoOppJuvbuPnYuBcqrTkBnQx6iPS8xTA="}, + }, + Body: []byte{}, + Host: "example.localhost", + RequestURI: "http://example.localhost/different/backend/path/1234/abc", + }, + }, + upstream: "backend-with-rewrite-prefix", + }), ) }) diff --git a/pkg/upstream/rewrite.go b/pkg/upstream/rewrite.go new file mode 100644 index 00000000..a84e18c0 --- /dev/null +++ b/pkg/upstream/rewrite.go @@ -0,0 +1,80 @@ +package upstream + +import ( + "fmt" + "net/http" + "net/url" + "regexp" + "strings" + + "github.com/justinas/alice" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" +) + +// newRewritePath creates a new middleware that will rewrite the request URI +// path before handing the request to the next server. +func newRewritePath(rewriteRegExp *regexp.Regexp, rewriteTarget string, writer pagewriter.Writer) alice.Constructor { + return func(next http.Handler) http.Handler { + return rewritePath(rewriteRegExp, rewriteTarget, writer, next) + } +} + +// rewritePath uses the regexp to rewrite the request URI based on the provided +// rewriteTarget. +func rewritePath(rewriteRegExp *regexp.Regexp, rewriteTarget string, writer pagewriter.Writer, next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + reqURL, err := url.ParseRequestURI(req.RequestURI) + if err != nil { + logger.Errorf("could not parse request URI: %v", err) + writer.WriteErrorPage(rw, pagewriter.ErrorPageOpts{ + Status: http.StatusInternalServerError, + RequestID: middleware.GetRequestScope(req).RequestID, + AppError: fmt.Sprintf("Could not parse request URI: %v", err), + }) + return + } + + // Use the regex to rewrite the request path before proxying to the upstream. + newURI := rewriteRegExp.ReplaceAllString(reqURL.Path, rewriteTarget) + reqURL.Path, reqURL.RawQuery, err = splitPathAndQuery(reqURL.Query(), newURI) + if err != nil { + logger.Errorf("could not parse rewrite URI: %v", err) + writer.WriteErrorPage(rw, pagewriter.ErrorPageOpts{ + Status: http.StatusInternalServerError, + RequestID: middleware.GetRequestScope(req).RequestID, + AppError: fmt.Sprintf("Could not parse rewrite URI: %v", err), + }) + return + } + + req.RequestURI = reqURL.String() + next.ServeHTTP(rw, req) + }) +} + +// splitPathAndQuery splits the rewritten path into the URL Path and the URL +// raw query. Any rewritten query values are appended to the original query +// values. +// This relies on the underlying URL library to encode the query string. +// For duplicate values it appends each as a separate value, e.g. ?foo=bar&foo=baz. +func splitPathAndQuery(originalQuery url.Values, raw string) (string, string, error) { + s := strings.SplitN(raw, "?", 2) + if len(s) == 1 { + return s[0], originalQuery.Encode(), nil + } + + queryValues, err := url.ParseQuery(s[1]) + if err != nil { + return "", "", nil + } + + for key, values := range queryValues { + for _, value := range values { + originalQuery.Add(key, value) + } + } + + return s[0], originalQuery.Encode(), nil +} diff --git a/pkg/upstream/rewrite_test.go b/pkg/upstream/rewrite_test.go new file mode 100644 index 00000000..57a0f244 --- /dev/null +++ b/pkg/upstream/rewrite_test.go @@ -0,0 +1,60 @@ +package upstream + +import ( + "net/http" + "net/http/httptest" + "regexp" + + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter" + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("Rewrite", func() { + type rewritePathTableInput struct { + rewriteRegex *regexp.Regexp + rewriteTarget string + requestTarget string + expectedRequestURI string + } + + DescribeTable("should rewrite the request path", + func(in rewritePathTableInput) { + req := httptest.NewRequest("", in.requestTarget, nil) + rw := httptest.NewRecorder() + + var gotRequestURI string + handler := newRewritePath(in.rewriteRegex, in.rewriteTarget, &pagewriter.WriterFuncs{})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotRequestURI = r.RequestURI + })) + handler.ServeHTTP(rw, req) + + Expect(gotRequestURI).To(Equal(in.expectedRequestURI)) + }, + Entry("when the path matches the regexp", rewritePathTableInput{ + rewriteRegex: regexp.MustCompile("^/http/(.*)"), + rewriteTarget: "/$1", + requestTarget: "http://example.com/http/foo/bar", + expectedRequestURI: "http://example.com/foo/bar", + }), + Entry("when the path does not match the regexp", rewritePathTableInput{ + rewriteRegex: regexp.MustCompile("^/http/(.*)"), + rewriteTarget: "/$1", + requestTarget: "https://example.com/https/foo/bar", + expectedRequestURI: "https://example.com/https/foo/bar", + }), + Entry("when the regexp is not anchored", rewritePathTableInput{ + rewriteRegex: regexp.MustCompile("/http/(.*)"), + rewriteTarget: "/$1", + requestTarget: "http://example.com/bar/http/foo/bar", + expectedRequestURI: "http://example.com/bar/foo/bar", + }), + Entry("when the regexp is rewriting to a query", rewritePathTableInput{ + rewriteRegex: regexp.MustCompile(`/articles/([a-z0-9\-]*)`), + rewriteTarget: "/article?id=$1", + requestTarget: "http://example.com/articles/blog-2021-01-01", + expectedRequestURI: "http://example.com/article?id=blog-2021-01-01", + }), + ) +})