diff --git a/CHANGELOG.md b/CHANGELOG.md index bf6e7c96..31e5b55e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - [#2274](https://github.com/oauth2-proxy/oauth2-proxy/pull/2274) Upgrade golang.org/x/net to v0.17.0 (@pierluigilenoci) - [#2282](https://github.com/oauth2-proxy/oauth2-proxy/pull/2282) Fixed checking Google Groups membership using Google Application Credentials (@kvanzuijlen) - [#2183](https://github.com/oauth2-proxy/oauth2-proxy/pull/2183) Allowing relative redirect url though an option +- [#1866](https://github.com/oauth2-proxy/oauth2-proxy/pull/1866) Add support for unix socker as upstream (@babs) - # V7.5.1 diff --git a/docs/docs/configuration/overview.md b/docs/docs/configuration/overview.md index 36848ca6..e5613c61 100644 --- a/docs/docs/configuration/overview.md +++ b/docs/docs/configuration/overview.md @@ -223,7 +223,11 @@ See below for provider specific options ### Upstreams Configuration -`oauth2-proxy` supports having multiple upstreams, and has the option to pass requests on to HTTP(S) servers or serve static files from the file system. HTTP and HTTPS upstreams are configured by providing a URL such as `http://127.0.0.1:8080/` for the upstream parameter. This will forward all authenticated requests to the upstream server. If you instead provide `http://127.0.0.1:8080/some/path/` then it will only be requests that start with `/some/path/` which are forwarded to the upstream. +`oauth2-proxy` supports having multiple upstreams, and has the option to pass requests on to HTTP(S) servers, unix socket or serve static files from the file system. + +HTTP and HTTPS upstreams are configured by providing a URL such as `http://127.0.0.1:8080/` for the upstream parameter. . This will forward all authenticated requests to the upstream server. If you instead provide `http://127.0.0.1:8080/some/path/` then it will only be requests that start with `/some/path/` which are forwarded to the upstream. + +Unix socket upstreams are configured as `unix:///path/to/unix.sock`. Static file paths are configured as a file:// URL. `file:///var/www/static/` will serve the files from that directory at `http://[oauth2-proxy url]/var/www/static/`, which may not be what you want. You can provide the path to where the files should be available by adding a fragment to the configured URL. The value of the fragment will then be used to specify which path the files are available at, e.g. `file:///var/www/static/#/static/` will make `/var/www/static/` available at `http://[oauth2-proxy url]/static/`. diff --git a/pkg/apis/options/legacy_options.go b/pkg/apis/options/legacy_options.go index 1bb06104..fcd1f3d3 100644 --- a/pkg/apis/options/legacy_options.go +++ b/pkg/apis/options/legacy_options.go @@ -176,6 +176,8 @@ func (l *LegacyUpstreams) convert() (UpstreamConfig, error) { upstream.ProxyWebSockets = nil upstream.FlushInterval = nil upstream.Timeout = nil + case "unix": + upstream.Path = "/" } upstreams.Upstreams = append(upstreams.Upstreams, upstream) diff --git a/pkg/upstream/http.go b/pkg/upstream/http.go index 031368e5..56fcc23e 100644 --- a/pkg/upstream/http.go +++ b/pkg/upstream/http.go @@ -1,6 +1,8 @@ package upstream import ( + "context" + "net" "net/http" "net/http/httputil" "net/url" @@ -18,6 +20,7 @@ const ( httpScheme = "http" httpsScheme = "https" + unixScheme = "unix" ) // SignatureHeaders contains the headers to be signed by the hmac algorithm @@ -40,7 +43,10 @@ var SignatureHeaders = []string{ // to a single upstream host. func newHTTPUpstreamProxy(upstream options.Upstream, u *url.URL, sigData *options.SignatureData, errorHandler ProxyErrorHandler) http.Handler { // Set path to empty so that request paths start at the server root - u.Path = "" + // Unix scheme need the path to find the socket + if u.Scheme != "unix" { + u.Path = "" + } // Create a ReverseProxy proxy := newReverseProxy(u, upstream, errorHandler) @@ -92,6 +98,25 @@ func (h *httpUpstreamProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) } } +// Unix implementation of http.RoundTripper, required to register unix protocol in reverse proxy +type unixRoundTripper struct { + Transport *http.Transport +} + +// Implementation of https://pkg.go.dev/net/http#RoundTripper interface to support http protocol over unix socket +func (t *unixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + // Inspired by https://github.com/tv42/httpunix + // Not having a Host, even if not used, makes the reverseproxy fail with a "no Host in request URL" + if req.Host == "" { + req.Host = "localhost" + } + req.URL.Host = req.Host + tt := t.Transport + req = req.Clone(req.Context()) + req.URL.Scheme = "http" + return tt.RoundTrip(req) +} + // newReverseProxy creates a new reverse proxy for proxying requests to upstream // servers based on the upstream configuration provided. // The proxy should render an error page if there are failures connecting to the @@ -102,6 +127,14 @@ func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler Pr // Inherit default transport options from Go's stdlib transport := http.DefaultTransport.(*http.Transport).Clone() + if target.Scheme == "unix" { + transport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) { + dialer := net.Dialer{} + return dialer.DialContext(ctx, target.Scheme, target.Path) + } + transport.RegisterProtocol(target.Scheme, &unixRoundTripper{Transport: transport}) + } + // Change default duration for waiting for an upstream response if upstream.Timeout != nil { transport.ResponseHeaderTimeout = upstream.Timeout.Duration() diff --git a/pkg/upstream/http_test.go b/pkg/upstream/http_test.go index 23753086..b18815a2 100644 --- a/pkg/upstream/http_test.go +++ b/pkg/upstream/http_test.go @@ -312,6 +312,29 @@ var _ = Describe("HTTP Upstream Suite", func() { }, expectedUpstream: "passExistingHostHeader", }), + Entry("request using UNIX socket upstream", &httpUpstreamTableInput{ + id: "unix-upstream", + serverAddr: &unixServerAddr, + target: "http://example.localhost/file", + method: "GET", + body: []byte{}, + errorHandler: nil, + expectedResponse: testHTTPResponse{ + code: 200, + header: map[string][]string{ + contentType: {applicationJSON}, + }, + request: testHTTPRequest{ + Method: "GET", + URL: "http://example.localhost/file", + Header: map[string][]string{}, + Body: []byte{}, + Host: "example.localhost", + RequestURI: "http://example.localhost/file", + }, + }, + expectedUpstream: "unix-upstream", + }), ) It("ServeHTTP, when not passing a host header", func() { diff --git a/pkg/upstream/proxy.go b/pkg/upstream/proxy.go index eabb47ff..0f3f5740 100644 --- a/pkg/upstream/proxy.go +++ b/pkg/upstream/proxy.go @@ -48,9 +48,9 @@ func NewProxy(upstreams options.UpstreamConfig, sigData *options.SignatureData, 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, unixScheme: if err := m.registerHTTPUpstreamProxy(upstream, u, sigData, writer); err != nil { - return nil, fmt.Errorf("could not register HTTP upstream %q: %v", upstream.ID, err) + return nil, fmt.Errorf("could not register %s upstream %q: %v", u.Scheme, upstream.ID, err) } default: return nil, fmt.Errorf("unknown scheme for upstream %q: %q", upstream.ID, u.Scheme) diff --git a/pkg/upstream/proxy_test.go b/pkg/upstream/proxy_test.go index 87c27e4b..374c59c5 100644 --- a/pkg/upstream/proxy_test.go +++ b/pkg/upstream/proxy_test.go @@ -98,6 +98,11 @@ var _ = Describe("Proxy Suite", func() { RewriteTarget: "/double-match/rewrite/$1", URI: serverAddr, }, + { + ID: "unix-upstream", + Path: "/unix/", + URI: unixServerAddr, + }, } } @@ -337,6 +342,27 @@ var _ = Describe("Proxy Suite", func() { }, upstream: "", }), + Entry("with a request to the UNIX socket backend", &proxyTableInput{ + target: "http://example.localhost/unix/file", + response: testHTTPResponse{ + code: 200, + header: map[string][]string{ + contentType: {applicationJSON}, + }, + request: testHTTPRequest{ + Method: "GET", + URL: "http://example.localhost/unix/file", + Header: map[string][]string{ + "Gap-Auth": {""}, + "Gap-Signature": {"sha256 4ux8esLj2fw9sTWZwgFhb00bGbw0Fnhed5Fm9jz5Blw="}, + }, + Body: []byte{}, + Host: "example.localhost", + RequestURI: "http://example.localhost/unix/file", + }, + }, + upstream: "unix-upstream", + }), ) }) diff --git a/pkg/upstream/upstream_suite_test.go b/pkg/upstream/upstream_suite_test.go index 95eb6811..ecb8d0b7 100644 --- a/pkg/upstream/upstream_suite_test.go +++ b/pkg/upstream/upstream_suite_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "log" + "net" "net/http" "net/http/httptest" "os" @@ -18,10 +19,12 @@ import ( ) var ( - filesDir string - server *httptest.Server - serverAddr string - invalidServer = "http://::1" + filesDir string + server *httptest.Server + serverAddr string + unixServer *httptest.Server + unixServerAddr string + invalidServer = "http://::1" ) func TestUpstreamSuite(t *testing.T) { @@ -46,10 +49,17 @@ var _ = BeforeSuite(func() { // Set up a webserver that reflects requests server = httptest.NewServer(&testHTTPUpstream{}) serverAddr = fmt.Sprintf("http://%s", server.Listener.Addr().String()) + + unixServer = httptest.NewUnstartedServer(&testHTTPUpstream{}) + unixListener, _ := net.Listen("unix", path.Join(filesDir, "test.sock")) + unixServer.Listener = unixListener + unixServer.Start() + unixServerAddr = fmt.Sprintf("unix://%s", path.Join(filesDir, "test.sock")) }) var _ = AfterSuite(func() { server.Close() + unixServer.Close() Expect(os.RemoveAll(filesDir)).To(Succeed()) }) diff --git a/pkg/validation/upstreams.go b/pkg/validation/upstreams.go index ba1e216a..bafed628 100644 --- a/pkg/validation/upstreams.go +++ b/pkg/validation/upstreams.go @@ -102,7 +102,7 @@ func validateUpstreamURI(upstream options.Upstream) []string { } switch u.Scheme { - case "http", "https", "file": + case "http", "https", "file", "unix": // Valid, do nothing default: msgs = append(msgs, fmt.Sprintf("upstream %q has invalid scheme: %q", upstream.ID, u.Scheme))