Merge pull request #2589 from ianroberts/file-upstream-rewrite
Add support for rewriteTarget in file: upstreams
This commit is contained in:
		
						commit
						e293dddef4
					
				|  | @ -15,6 +15,7 @@ | ||||||
| - [#2459](https://github.com/oauth2-proxy/oauth2-proxy/pull/2459) chore(deps): Updated to ginkgo v2 (@kvanzuijlen, @tuunit) | - [#2459](https://github.com/oauth2-proxy/oauth2-proxy/pull/2459) chore(deps): Updated to ginkgo v2 (@kvanzuijlen, @tuunit) | ||||||
| - [#2112](https://github.com/oauth2-proxy/oauth2-proxy/pull/2112) docs: update list of providers which support refresh tokens (@mikefab-msf) | - [#2112](https://github.com/oauth2-proxy/oauth2-proxy/pull/2112) docs: update list of providers which support refresh tokens (@mikefab-msf) | ||||||
| - [#2734](https://github.com/oauth2-proxy/oauth2-proxy/pull/2734) Added s390x architecture option support (@priby05) | - [#2734](https://github.com/oauth2-proxy/oauth2-proxy/pull/2734) Added s390x architecture option support (@priby05) | ||||||
|  | - [#2589](https://github.com/oauth2-proxy/oauth2-proxy/pull/2589) Added support for regex path matching and rewriting when using a static `file:` upstream (@ianroberts) | ||||||
| 
 | 
 | ||||||
| # V7.6.0 | # V7.6.0 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -524,7 +524,7 @@ Requests will be proxied to this upstream if the path matches the request path. | ||||||
| | ----- | ---- | ----------- | | | ----- | ---- | ----------- | | ||||||
| | `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.<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 | | | `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. | | | `rewriteTarget` | _string_ | RewriteTarget allows users to rewrite the request path before it is sent to<br/>the upstream server (for an HTTP/HTTPS upstream) or mapped to the filesystem<br/>(for a `file:` upstream).<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.  Or if the upstream were `file:///app`, a request for<br/>`/baz/info.html` would return the contents of the file `/app/foo/info.html`. | | ||||||
| | `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/>between OAuth2 Proxy and the upstream 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/>between OAuth2 Proxy and the upstream 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. | | ||||||
|  |  | ||||||
|  | @ -39,11 +39,13 @@ type Upstream struct { | ||||||
| 	Path string `json:"path,omitempty"` | 	Path string `json:"path,omitempty"` | ||||||
| 
 | 
 | ||||||
| 	// RewriteTarget allows users to rewrite the request path before it is sent to
 | 	// RewriteTarget allows users to rewrite the request path before it is sent to
 | ||||||
| 	// the upstream server.
 | 	// the upstream server (for an HTTP/HTTPS upstream) or mapped to the filesystem
 | ||||||
|  | 	// (for a `file:` upstream).
 | ||||||
| 	// Use the Path to capture segments for reuse within the rewrite target.
 | 	// 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
 | 	// 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
 | 	// the request `/baz/abc/123` to `/foo/abc/123` before proxying to the
 | ||||||
| 	// upstream server.
 | 	// upstream server.  Or if the upstream were `file:///app`, a request for
 | ||||||
|  | 	// `/baz/info.html` would return the contents of the file `/app/foo/info.html`.
 | ||||||
| 	RewriteTarget string `json:"rewriteTarget,omitempty"` | 	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
 | ||||||
|  |  | ||||||
|  | @ -2,9 +2,12 @@ package upstream | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
| 	"runtime" | 	"runtime" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" | ||||||
|  | 
 | ||||||
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -12,22 +15,53 @@ const fileScheme = "file" | ||||||
| 
 | 
 | ||||||
| // newFileServer creates a new fileServer that can serve requests
 | // newFileServer creates a new fileServer that can serve requests
 | ||||||
| // to a file system location.
 | // to a file system location.
 | ||||||
| func newFileServer(id, path, fileSystemPath string) http.Handler { | func newFileServer(upstream options.Upstream, fileSystemPath string) http.Handler { | ||||||
|  | 	handler := newFileServerForPath(fileSystemPath) | ||||||
|  | 
 | ||||||
|  | 	if upstream.RewriteTarget == "" { | ||||||
|  | 		// if the upstream does not have a rewrite target, strip off the Path prefix
 | ||||||
|  | 		// (so e.g. a request for /static/some-file.html looks for some-file.html
 | ||||||
|  | 		// relative to the fileSystemPath rather than static/some-file.html).
 | ||||||
|  | 		handler = http.StripPrefix(upstream.Path, handler) | ||||||
|  | 	} else { | ||||||
|  | 		// if the upstream *does* have a rewrite target then that means the target
 | ||||||
|  | 		// path relative to the fileSystemPath will be the one in the (rewritten)
 | ||||||
|  | 		// RequestURI.
 | ||||||
|  | 		handler = requestURIToURL(handler) | ||||||
|  | 	} | ||||||
| 	return &fileServer{ | 	return &fileServer{ | ||||||
| 		upstream: id, | 		upstream: upstream.ID, | ||||||
| 		handler:  newFileServerForPath(path, fileSystemPath), | 		handler:  handler, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // newFileServerForPath creates a http.Handler to serve files from the filesystem
 | // newFileServerForPath creates a http.Handler to serve files from the filesystem
 | ||||||
| func newFileServerForPath(path string, filesystemPath string) http.Handler { | func newFileServerForPath(filesystemPath string) http.Handler { | ||||||
| 	// Windows fileSSystemPath will be be prefixed with `/`, eg`/C:/...,
 | 	// Windows fileSSystemPath will be be prefixed with `/`, eg`/C:/...,
 | ||||||
| 	// if they were parsed by url.Parse`
 | 	// if they were parsed by url.Parse`
 | ||||||
| 	if runtime.GOOS == "windows" { | 	if runtime.GOOS == "windows" { | ||||||
| 		filesystemPath = strings.TrimPrefix(filesystemPath, "/") | 		filesystemPath = strings.TrimPrefix(filesystemPath, "/") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath))) | 	return http.FileServer(http.Dir(filesystemPath)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // requestURIToURL returns a Handler that replaces the URL in its request with
 | ||||||
|  | // the result of parsing req.RequestURI.  This is necessary for file handlers
 | ||||||
|  | // that have a rewrite target, since http.FileServer uses req.URL.Path when
 | ||||||
|  | // looking for the target file, but the rewrite handler only updates the
 | ||||||
|  | // RequestURI, leaving the original path in the URL.
 | ||||||
|  | func requestURIToURL(handler http.Handler) http.Handler { | ||||||
|  | 	return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { | ||||||
|  | 		reqURL, err := url.ParseRequestURI(req.RequestURI) | ||||||
|  | 		if err != nil { | ||||||
|  | 			http.Error(rw, "500 Internal Server Error", http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		req.URL = reqURL | ||||||
|  | 		handler.ServeHTTP(rw, req) | ||||||
|  | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // fileServer represents a single filesystem upstream proxy
 | // fileServer represents a single filesystem upstream proxy
 | ||||||
|  |  | ||||||
|  | @ -7,6 +7,8 @@ import ( | ||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
| 	"os" | 	"os" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" | ||||||
|  | 
 | ||||||
| 	middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" | 	middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" | ||||||
| 	. "github.com/onsi/ginkgo/v2" | 	. "github.com/onsi/ginkgo/v2" | ||||||
| 	. "github.com/onsi/gomega" | 	. "github.com/onsi/gomega" | ||||||
|  | @ -31,8 +33,12 @@ var _ = Describe("File Server Suite", func() { | ||||||
| 		_, err := io.ReadFull(rand.Reader, idBytes) | 		_, err := io.ReadFull(rand.Reader, idBytes) | ||||||
| 		Expect(err).ToNot(HaveOccurred()) | 		Expect(err).ToNot(HaveOccurred()) | ||||||
| 		id = string(idBytes) | 		id = string(idBytes) | ||||||
|  | 		upstream := options.Upstream{ | ||||||
|  | 			ID:   id, | ||||||
|  | 			Path: "/files", | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 		handler = newFileServer(id, "/files", filesDir) | 		handler = newFileServer(upstream, filesDir) | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	AfterEach(func() { | 	AfterEach(func() { | ||||||
|  |  | ||||||
|  | @ -81,7 +81,7 @@ func (m *multiUpstreamProxy) registerStaticResponseHandler(upstream options.Upst | ||||||
| // 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, writer pagewriter.Writer) error { | 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) | ||||||
| 	return m.registerHandler(upstream, newFileServer(upstream.ID, upstream.Path, u.Path), writer) | 	return m.registerHandler(upstream, newFileServer(upstream, u.Path), writer) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // registerHTTPUpstreamProxy registers a new httpUpstreamProxy based on the configuration given.
 | // registerHTTPUpstreamProxy registers a new httpUpstreamProxy based on the configuration given.
 | ||||||
|  |  | ||||||
|  | @ -51,6 +51,12 @@ var _ = Describe("Proxy Suite", func() { | ||||||
| 							Path: "/files/", | 							Path: "/files/", | ||||||
| 							URI:  fmt.Sprintf("file:///%s", filesDir), | 							URI:  fmt.Sprintf("file:///%s", filesDir), | ||||||
| 						}, | 						}, | ||||||
|  | 						{ | ||||||
|  | 							ID:            "rewrite-file-backend", | ||||||
|  | 							Path:          "^/rewrite-files/.*/(.*)$", | ||||||
|  | 							RewriteTarget: "/$1", | ||||||
|  | 							URI:           fmt.Sprintf("file:///%s", filesDir), | ||||||
|  | 						}, | ||||||
| 						{ | 						{ | ||||||
| 							ID:         "static-backend", | 							ID:         "static-backend", | ||||||
| 							Path:       "/static/", | 							Path:       "/static/", | ||||||
|  | @ -174,6 +180,17 @@ var _ = Describe("Proxy Suite", func() { | ||||||
| 				}, | 				}, | ||||||
| 				upstream: "file-backend", | 				upstream: "file-backend", | ||||||
| 			}), | 			}), | ||||||
|  | 			Entry("with a request to the File backend with rewrite", &proxyTableInput{ | ||||||
|  | 				target: "http://example.localhost/rewrite-files/anything-at-all/foo", | ||||||
|  | 				response: testHTTPResponse{ | ||||||
|  | 					code: 200, | ||||||
|  | 					header: map[string][]string{ | ||||||
|  | 						contentType: {textPlainUTF8}, | ||||||
|  | 					}, | ||||||
|  | 					raw: "foo", | ||||||
|  | 				}, | ||||||
|  | 				upstream: "rewrite-file-backend", | ||||||
|  | 			}), | ||||||
| 			Entry("with a request to the Static backend", &proxyTableInput{ | 			Entry("with a request to the Static backend", &proxyTableInput{ | ||||||
| 				target: "http://example.localhost/static/bar", | 				target: "http://example.localhost/static/bar", | ||||||
| 				response: testHTTPResponse{ | 				response: testHTTPResponse{ | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue