1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-01-10 04:18:14 +02:00

Merge pull request #1060 from oauth2-proxy/upstream-rewrite

Implement RewriteTarget to allow requests to be rewritten before proxying to upstream servers
This commit is contained in:
Joel Speed 2021-06-23 13:36:03 +01:00 committed by GitHub
commit ac188b129f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 637 additions and 178 deletions

View File

@ -13,6 +13,7 @@
## Changes since v7.1.3 ## Changes since v7.1.3
- [#1060](https://github.com/oauth2-proxy/oauth2-proxy/pull/1060) Implement RewriteTarget to allow requests to be rewritten before proxying to upstream servers (@JoelSpeed)
- [#1086](https://github.com/oauth2-proxy/oauth2-proxy/pull/1086) Refresh sessions before token expiration if configured (@NickMeves) - [#1086](https://github.com/oauth2-proxy/oauth2-proxy/pull/1086) Refresh sessions before token expiration if configured (@NickMeves)
- [#1226](https://github.com/oauth2-proxy/oauth2-proxy/pull/1226) Move app redirection logic to its own package (@JoelSpeed) - [#1226](https://github.com/oauth2-proxy/oauth2-proxy/pull/1226) Move app redirection logic to its own package (@JoelSpeed)
- [#1128](https://github.com/oauth2-proxy/oauth2-proxy/pull/1128) Use gorilla mux for OAuth Proxy routing (@JoelSpeed) - [#1128](https://github.com/oauth2-proxy/oauth2-proxy/pull/1128) Use gorilla mux for OAuth Proxy routing (@JoelSpeed)

View File

@ -371,7 +371,8 @@ Requests will be proxied to this upstream if the path matches the request path.
| Field | Type | Description | | Field | Type | Description |
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `id` | _string_ | ID should be a unique identifier for the upstream.<br/>This value is required for all upstreams. | | `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". | | `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. | | `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. | | `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. |

6
go.mod
View File

@ -16,7 +16,7 @@ require (
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
github.com/go-redis/redis/v8 v8.2.3 github.com/go-redis/redis/v8 v8.2.3
github.com/google/uuid v1.2.0 github.com/google/uuid v1.2.0
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0
github.com/justinas/alice v1.2.0 github.com/justinas/alice v1.2.0
github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa
github.com/mitchellh/mapstructure v1.1.2 github.com/mitchellh/mapstructure v1.1.2
@ -31,8 +31,8 @@ require (
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
github.com/vmihailenco/msgpack/v4 v4.3.11 github.com/vmihailenco/msgpack/v4 v4.3.11
github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997 github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
golang.org/x/net v0.0.0-20200707034311-ab3426394381 golang.org/x/net v0.0.0-20200822124328-c89045814202
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
google.golang.org/api v0.20.0 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-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-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-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-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-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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw= 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-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-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-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-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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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. // Path is used to map requests to the upstream server.
// The closest match will take precedence and all Paths must be unique. // 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"` 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 // 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 // based URL. It may include a path, in which case all requests will be served
// under that path. // under that path.

View File

@ -1,10 +1,16 @@
package upstream package upstream
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"regexp"
"sort"
"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/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
@ -18,12 +24,14 @@ type ProxyErrorHandler func(http.ResponseWriter, *http.Request, error)
// multiple upstreams. // multiple upstreams.
func NewProxy(upstreams options.Upstreams, sigData *options.SignatureData, writer pagewriter.Writer) (http.Handler, error) { func NewProxy(upstreams options.Upstreams, sigData *options.SignatureData, writer pagewriter.Writer) (http.Handler, error) {
m := &multiUpstreamProxy{ m := &multiUpstreamProxy{
serveMux: http.NewServeMux(), serveMux: mux.NewRouter(),
} }
for _, upstream := range upstreams { for _, upstream := range sortByPathLongest(upstreams) {
if upstream.Static { 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 continue
} }
@ -33,20 +41,26 @@ func NewProxy(upstreams options.Upstreams, sigData *options.SignatureData, write
} }
switch u.Scheme { switch u.Scheme {
case fileScheme: 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: 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: default:
return nil, fmt.Errorf("unknown scheme for upstream %q: %q", upstream.ID, u.Scheme) return nil, fmt.Errorf("unknown scheme for upstream %q: %q", upstream.ID, u.Scheme)
} }
} }
registerTrailingSlashHandler(m.serveMux)
return m, nil return m, nil
} }
// multiUpstreamProxy will serve requests directed to multiple upstream servers // multiUpstreamProxy will serve requests directed to multiple upstream servers
// registered in the serverMux. // registered in the serverMux.
type multiUpstreamProxy struct { type multiUpstreamProxy struct {
serveMux *http.ServeMux serveMux *mux.Router
} }
// ServerHTTP handles HTTP requests. // ServerHTTP handles HTTP requests.
@ -55,19 +69,110 @@ func (m *multiUpstreamProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request
} }
// registerStaticResponseHandler registers a static response handler with at the given path. // 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)) logger.Printf("mapping path %q => static response %d", upstream.Path, derefStaticCode(upstream.StaticCode))
m.serveMux.Handle(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. // 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) logger.Printf("mapping path %q => file system %q", upstream.Path, u.Path)
m.serveMux.Handle(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. // 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) logger.Printf("mapping path %q => upstream %q", upstream.Path, upstream.URI)
m.serveMux.Handle(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
// by ensuring any path with a trailing `/` matches all paths under that prefix.
func (m *multiUpstreamProxy) registerSimpleHandler(path string, handler http.Handler) {
if strings.HasSuffix(path, "/") {
m.serveMux.PathPrefix(path).Handler(handler)
} else {
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
}
// registerTrailingSlashHandler creates a new matcher that will check if the
// requested path would match if it had a trailing slash appended.
// If the path matches with a trailing slash, we send back a redirect.
// This allows us to be consistent with the built in go servemux implementation.
func registerTrailingSlashHandler(serveMux *mux.Router) {
serveMux.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
if strings.HasSuffix(req.URL.Path, "/") {
return false
}
// Use a separate RouteMatch so that we can redirect to the path + /.
// If we pass through the match then the matched backed will be served
// instead of the redirect handler.
m := &mux.RouteMatch{}
slashReq := req.Clone(context.Background())
slashReq.URL.Path += "/"
return serveMux.Match(slashReq, m)
}).Handler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
http.Redirect(rw, req, req.URL.String()+"/", http.StatusMovedPermanently)
}))
}
// sortByPathLongest ensures that the upstreams are sorted by longest path.
// If rewrites are involved, a rewrite takes precedence over a non-rewrite.
// When two upstreams define rewrites, whichever has the longest path will take
// precedence (note this is the input to the rewrite logic).
// This does not account for when a rewrite would actually make the path shorter.
// This should maintain the sorting behaviour of the standard go serve mux.
func sortByPathLongest(in options.Upstreams) options.Upstreams {
sort.Slice(in, func(i, j int) bool {
iRW := in[i].RewriteTarget
jRW := in[j].RewriteTarget
switch {
case iRW != "" && jRW != "":
// If both have a rewrite target, whichever has the longest pattern
// should go first
return len(in[i].Path) > len(in[j].Path)
case iRW != "" && jRW == "":
// Only one has rewrite, it goes first
return true
case iRW == "" && jRW != "":
// Only one has rewrite, it goes first
return false
default:
// Default to longest Path wins
return len(in[i].Path) > len(in[j].Path)
}
})
return in
} }

View File

@ -18,174 +18,369 @@ import (
var _ = Describe("Proxy Suite", func() { var _ = Describe("Proxy Suite", func() {
var upstreamServer http.Handler var upstreamServer http.Handler
BeforeEach(func() { Context("multiUpstreamProxy", func() {
sigData := &options.SignatureData{Hash: crypto.SHA256, Key: "secret"} BeforeEach(func() {
sigData := &options.SignatureData{Hash: crypto.SHA256, Key: "secret"}
writer := &pagewriter.WriterFuncs{ writer := &pagewriter.WriterFuncs{
ProxyErrorFunc: func(rw http.ResponseWriter, _ *http.Request, _ error) { ProxyErrorFunc: func(rw http.ResponseWriter, _ *http.Request, _ error) {
rw.WriteHeader(502) rw.WriteHeader(502)
rw.Write([]byte("Proxy Error")) rw.Write([]byte("Proxy Error"))
}, },
}
ok := http.StatusOK
upstreams := options.Upstreams{
{
ID: "http-backend",
Path: "/http/",
URI: serverAddr,
},
{
ID: "file-backend",
Path: "/files/",
URI: fmt.Sprintf("file:///%s", filesDir),
},
{
ID: "static-backend",
Path: "/static/",
Static: true,
StaticCode: &ok,
},
{
ID: "bad-http-backend",
Path: "/bad-http/",
URI: "http://::1",
},
{
ID: "single-path-backend",
Path: "/single-path",
Static: true,
StaticCode: &ok,
},
}
var err error
upstreamServer, err = NewProxy(upstreams, sigData, writer)
Expect(err).ToNot(HaveOccurred())
})
type proxyTableInput struct {
target string
response testHTTPResponse
upstream string
}
DescribeTable("Proxy ServeHTTP",
func(in *proxyTableInput) {
req := middlewareapi.AddRequestScope(
httptest.NewRequest("", in.target, nil),
&middlewareapi.RequestScope{},
)
rw := httptest.NewRecorder()
// Don't mock the remote Address
req.RemoteAddr = ""
upstreamServer.ServeHTTP(rw, req)
scope := middlewareapi.GetRequestScope(req)
Expect(scope.Upstream).To(Equal(in.upstream))
Expect(rw.Code).To(Equal(in.response.code))
// Delete extra headers that aren't relevant to tests
testSanitizeResponseHeader(rw.Header())
Expect(rw.Header()).To(Equal(in.response.header))
body := rw.Body.Bytes()
// If the raw body is set, check that, else check the Request object
if in.response.raw != "" {
Expect(string(body)).To(Equal(in.response.raw))
return
} }
// Compare the reflected request to the upstream ok := http.StatusOK
request := testHTTPRequest{} accepted := http.StatusAccepted
Expect(json.Unmarshal(body, &request)).To(Succeed())
testSanitizeRequestHeader(request.Header) upstreams := options.Upstreams{
Expect(request).To(Equal(in.response.request)) {
}, ID: "http-backend",
Entry("with a request to the HTTP service", &proxyTableInput{ Path: "/http/",
target: "http://example.localhost/http/1234", URI: serverAddr,
response: testHTTPResponse{
code: 200,
header: map[string][]string{
contentType: {applicationJSON},
}, },
request: testHTTPRequest{ {
Method: "GET", ID: "file-backend",
URL: "http://example.localhost/http/1234", Path: "/files/",
Header: map[string][]string{ URI: fmt.Sprintf("file:///%s", filesDir),
"Gap-Auth": {""}, },
"Gap-Signature": {"sha256 ofB1u6+FhEUbFLc3/uGbJVkl7GaN4egFqVvyO3+2I1w="}, {
ID: "static-backend",
Path: "/static/",
Static: true,
StaticCode: &ok,
},
{
ID: "static-backend-no-trailing-slash",
Path: "/static",
Static: true,
StaticCode: &accepted,
},
{
ID: "static-backend-long",
Path: "/static/long",
Static: true,
StaticCode: &accepted,
},
{
ID: "bad-http-backend",
Path: "/bad-http/",
URI: "http://::1",
},
{
ID: "single-path-backend",
Path: "/single-path",
Static: true,
StaticCode: &ok,
},
{
ID: "backend-with-rewrite-prefix",
Path: "^/rewrite-prefix/(.*)",
RewriteTarget: "/different/backend/path/$1",
URI: serverAddr,
},
{
ID: "double-match-plain",
Path: "/double-match/",
URI: serverAddr,
},
{
ID: "double-match-rewrite",
Path: "^/double-match/(.*)",
RewriteTarget: "/double-match/rewrite/$1",
URI: serverAddr,
},
}
var err error
upstreamServer, err = NewProxy(upstreams, sigData, writer)
Expect(err).ToNot(HaveOccurred())
})
type proxyTableInput struct {
target string
response testHTTPResponse
upstream string
}
DescribeTable("Proxy ServeHTTP",
func(in *proxyTableInput) {
req := middlewareapi.AddRequestScope(
httptest.NewRequest("", in.target, nil),
&middlewareapi.RequestScope{},
)
rw := httptest.NewRecorder()
// Don't mock the remote Address
req.RemoteAddr = ""
upstreamServer.ServeHTTP(rw, req)
scope := middlewareapi.GetRequestScope(req)
Expect(scope.Upstream).To(Equal(in.upstream))
Expect(rw.Code).To(Equal(in.response.code))
// Delete extra headers that aren't relevant to tests
testSanitizeResponseHeader(rw.Header())
Expect(rw.Header()).To(Equal(in.response.header))
body := rw.Body.Bytes()
// If the raw body is set, check that, else check the Request object
if in.response.raw != "" {
Expect(string(body)).To(Equal(in.response.raw))
return
}
// Compare the reflected request to the upstream
request := testHTTPRequest{}
Expect(json.Unmarshal(body, &request)).To(Succeed())
testSanitizeRequestHeader(request.Header)
Expect(request).To(Equal(in.response.request))
},
Entry("with a request to the HTTP service", &proxyTableInput{
target: "http://example.localhost/http/1234",
response: testHTTPResponse{
code: 200,
header: map[string][]string{
contentType: {applicationJSON},
},
request: testHTTPRequest{
Method: "GET",
URL: "http://example.localhost/http/1234",
Header: map[string][]string{
"Gap-Auth": {""},
"Gap-Signature": {"sha256 ofB1u6+FhEUbFLc3/uGbJVkl7GaN4egFqVvyO3+2I1w="},
},
Body: []byte{},
Host: "example.localhost",
RequestURI: "http://example.localhost/http/1234",
}, },
Body: []byte{},
Host: "example.localhost",
RequestURI: "http://example.localhost/http/1234",
}, },
}, upstream: "http-backend",
upstream: "http-backend", }),
}), Entry("with a request to the File backend", &proxyTableInput{
Entry("with a request to the File backend", &proxyTableInput{ target: "http://example.localhost/files/foo",
target: "http://example.localhost/files/foo", response: testHTTPResponse{
response: testHTTPResponse{ code: 200,
code: 200, header: map[string][]string{
header: map[string][]string{ contentType: {textPlainUTF8},
contentType: {textPlainUTF8}, },
raw: "foo",
}, },
raw: "foo", upstream: "file-backend",
}, }),
upstream: "file-backend", Entry("with a request to the Static backend", &proxyTableInput{
}), target: "http://example.localhost/static/bar",
Entry("with a request to the Static backend", &proxyTableInput{ response: testHTTPResponse{
target: "http://example.localhost/static/bar", code: 200,
response: testHTTPResponse{ header: map[string][]string{},
code: 200, raw: "Authenticated",
header: map[string][]string{},
raw: "Authenticated",
},
upstream: "static-backend",
}),
Entry("with a request to the bad HTTP backend", &proxyTableInput{
target: "http://example.localhost/bad-http/bad",
response: testHTTPResponse{
code: 502,
header: map[string][]string{},
// This tests the error handler
raw: "Proxy Error",
},
upstream: "bad-http-backend",
}),
Entry("with a request to the to an unregistered path", &proxyTableInput{
target: "http://example.localhost/unregistered",
response: testHTTPResponse{
code: 404,
header: map[string][]string{
"X-Content-Type-Options": {"nosniff"},
contentType: {textPlainUTF8},
}, },
raw: "404 page not found\n", upstream: "static-backend",
}, }),
}), Entry("with a request to the bad HTTP backend", &proxyTableInput{
Entry("with a request to the to backend registered to a single path", &proxyTableInput{ target: "http://example.localhost/bad-http/bad",
target: "http://example.localhost/single-path", response: testHTTPResponse{
response: testHTTPResponse{ code: 502,
code: 200, header: map[string][]string{},
header: map[string][]string{}, // This tests the error handler
raw: "Authenticated", raw: "Proxy Error",
},
upstream: "single-path-backend",
}),
Entry("with a request to the to a subpath of a backend registered to a single path", &proxyTableInput{
target: "http://example.localhost/single-path/unregistered",
response: testHTTPResponse{
code: 404,
header: map[string][]string{
"X-Content-Type-Options": {"nosniff"},
contentType: {textPlainUTF8},
}, },
raw: "404 page not found\n", upstream: "bad-http-backend",
}),
Entry("with a request to the to an unregistered path", &proxyTableInput{
target: "http://example.localhost/unregistered",
response: testHTTPResponse{
code: 404,
header: map[string][]string{
"X-Content-Type-Options": {"nosniff"},
contentType: {textPlainUTF8},
},
raw: "404 page not found\n",
},
}),
Entry("with a request to the to backend registered to a single path", &proxyTableInput{
target: "http://example.localhost/single-path",
response: testHTTPResponse{
code: 200,
header: map[string][]string{},
raw: "Authenticated",
},
upstream: "single-path-backend",
}),
Entry("with a request to the to a subpath of a backend registered to a single path", &proxyTableInput{
target: "http://example.localhost/single-path/unregistered",
response: testHTTPResponse{
code: 404,
header: map[string][]string{
"X-Content-Type-Options": {"nosniff"},
contentType: {textPlainUTF8},
},
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",
}),
Entry("with a request to a path, missing the trailing slash", &proxyTableInput{
target: "http://example.localhost/http",
response: testHTTPResponse{
code: 301,
header: map[string][]string{
contentType: {textHTMLUTF8},
"Location": {"http://example.localhost/http/"},
},
raw: "<a href=\"http://example.localhost/http/\">Moved Permanently</a>.\n\n",
},
}),
Entry("with a request to a path, missing the trailing slash, but registered separately", &proxyTableInput{
target: "http://example.localhost/static",
response: testHTTPResponse{
code: 202,
header: map[string][]string{},
raw: "Authenticated",
},
upstream: "static-backend-no-trailing-slash",
}),
Entry("should match longest path first", &proxyTableInput{
target: "http://example.localhost/static/long",
response: testHTTPResponse{
code: 202,
header: map[string][]string{},
raw: "Authenticated",
},
upstream: "static-backend-long",
}),
Entry("should match rewrite path first", &proxyTableInput{
target: "http://example.localhost/double-match/foo",
response: testHTTPResponse{
code: 200,
header: map[string][]string{
contentType: {applicationJSON},
},
request: testHTTPRequest{
Method: "GET",
URL: "http://example.localhost/double-match/rewrite/foo",
Header: map[string][]string{
"Gap-Auth": {""},
"Gap-Signature": {"sha256 eYyUNdsrTmnvFpavpP8AdHGUGzqJ39QEjqn0/3fQPHA="},
},
Body: []byte{},
Host: "example.localhost",
RequestURI: "http://example.localhost/double-match/rewrite/foo",
},
},
upstream: "double-match-rewrite",
}),
)
})
Context("sortByPathLongest", func() {
type sortByPathLongestTableInput struct {
input options.Upstreams
expectedOutput options.Upstreams
}
var httpPath = options.Upstream{
Path: "/http/",
}
var httpSubPath = options.Upstream{
Path: "/http/subpath/",
}
var longerPath = options.Upstream{
Path: "/longer-than-http",
}
var shortPathWithRewrite = options.Upstream{
Path: "^/h/(.*)",
RewriteTarget: "/$1",
}
var shortSubPathWithRewrite = options.Upstream{
Path: "^/h/bar/(.*)",
RewriteTarget: "/$1",
}
DescribeTable("short sort into the correct order",
func(in sortByPathLongestTableInput) {
Expect(sortByPathLongest(in.input)).To(Equal(in.expectedOutput))
}, },
}), Entry("with a mix of paths registered", sortByPathLongestTableInput{
) input: options.Upstreams{httpPath, httpSubPath, shortSubPathWithRewrite, longerPath, shortPathWithRewrite},
expectedOutput: options.Upstreams{shortSubPathWithRewrite, shortPathWithRewrite, longerPath, httpSubPath, httpPath},
}),
Entry("when a subpath is registered (in order)", sortByPathLongestTableInput{
input: options.Upstreams{httpSubPath, httpPath},
expectedOutput: options.Upstreams{httpSubPath, httpPath},
}),
Entry("when a subpath is registered (out of order)", sortByPathLongestTableInput{
input: options.Upstreams{httpPath, httpSubPath},
expectedOutput: options.Upstreams{httpSubPath, httpPath},
}),
Entry("when longer paths are registered (in order)", sortByPathLongestTableInput{
input: options.Upstreams{longerPath, httpPath},
expectedOutput: options.Upstreams{longerPath, httpPath},
}),
Entry("when longer paths are registered (out of order)", sortByPathLongestTableInput{
input: options.Upstreams{httpPath, longerPath},
expectedOutput: options.Upstreams{longerPath, httpPath},
}),
Entry("when a rewrite target is registered (in order)", sortByPathLongestTableInput{
input: options.Upstreams{shortPathWithRewrite, longerPath},
expectedOutput: options.Upstreams{shortPathWithRewrite, longerPath},
}),
Entry("when a rewrite target is registered (out of order)", sortByPathLongestTableInput{
input: options.Upstreams{longerPath, shortPathWithRewrite},
expectedOutput: options.Upstreams{shortPathWithRewrite, longerPath},
}),
Entry("with multiple rewrite targets registered (in order)", sortByPathLongestTableInput{
input: options.Upstreams{shortSubPathWithRewrite, shortPathWithRewrite},
expectedOutput: options.Upstreams{shortSubPathWithRewrite, shortPathWithRewrite},
}),
Entry("with multiple rewrite targets registered (out of order)", sortByPathLongestTableInput{
input: options.Upstreams{shortPathWithRewrite, shortSubPathWithRewrite},
expectedOutput: options.Upstreams{shortSubPathWithRewrite, shortPathWithRewrite},
}),
)
})
}) })

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

View File

@ -59,6 +59,7 @@ const (
acceptEncoding = "Accept-Encoding" acceptEncoding = "Accept-Encoding"
applicationJSON = "application/json" applicationJSON = "application/json"
textPlainUTF8 = "text/plain; charset=utf-8" textPlainUTF8 = "text/plain; charset=utf-8"
textHTMLUTF8 = "text/html; charset=utf-8"
gapAuth = "Gap-Auth" gapAuth = "Gap-Auth"
gapSignature = "Gap-Signature" gapSignature = "Gap-Signature"
) )