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:
commit
ac188b129f
@ -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)
|
||||||
|
@ -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
6
go.mod
@ -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
6
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-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=
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
80
pkg/upstream/rewrite.go
Normal 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
|
||||||
|
}
|
60
pkg/upstream/rewrite_test.go
Normal file
60
pkg/upstream/rewrite_test.go
Normal 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",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
@ -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"
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user