1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2024-11-24 08:52:25 +02:00

Allow request paths to be rewritten before proxying to upstream server

This commit is contained in:
Joel Speed 2021-02-22 15:56:37 +00:00
parent d2d62bb452
commit 6c62b25bf1
No known key found for this signature in database
GPG Key ID: 6E80578D6751DEFB
8 changed files with 256 additions and 14 deletions

View File

@ -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.<br/>This value is required for all upstreams. |
| `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. |
| `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 |
| `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. |
| `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". |
| `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. |
| `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. |

4
go.mod
View File

@ -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

6
go.sum
View File

@ -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=

View File

@ -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.

View File

@ -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
}

View File

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

80
pkg/upstream/rewrite.go Normal file
View File

@ -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
}

View File

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