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",
+ }),
+ )
+})