package upstream import ( "crypto" "encoding/json" "fmt" "net/http" "net/http/httptest" middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" "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("Proxy Suite", func() { type proxyTableInput struct { target string response testHTTPResponse upstream string upstreams options.UpstreamConfig } Context("multiUpstreamProxy", func() { DescribeTable("Proxy ServeHTTP", func(in *proxyTableInput) { sigData := &options.SignatureData{Hash: crypto.SHA256, Key: "secret"} writer := &pagewriter.WriterFuncs{ ProxyErrorFunc: func(rw http.ResponseWriter, _ *http.Request, _ error) { rw.WriteHeader(502) rw.Write([]byte("Proxy Error")) }, } ok := http.StatusOK accepted := http.StatusAccepted // Allows for specifying settings and even individual upstreams for specific tests and uses the default upstreams/configs otherwise upstreams := in.upstreams if len(in.upstreams.Upstreams) == 0 { upstreams.Upstreams = []options.Upstream{ { 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: "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, }, } } upstreamServer, err := NewProxy(upstreams, sigData, writer) Expect(err).ToNot(HaveOccurred()) 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 if body != nil { 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", }, }, upstream: "http-backend", }), Entry("with a request to the File backend", &proxyTableInput{ target: "http://example.localhost/files/foo", response: testHTTPResponse{ code: 200, header: map[string][]string{ contentType: {textPlainUTF8}, }, raw: "foo", }, upstream: "file-backend", }), Entry("with a request to the Static backend", &proxyTableInput{ target: "http://example.localhost/static/bar", response: testHTTPResponse{ code: 200, 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", }, }), 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: "Moved Permanently.\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", }), Entry("containing an escaped '/' without ProxyRawPath", &proxyTableInput{ target: "http://example.localhost/%2F/test1/%2F/test2", response: testHTTPResponse{ code: 301, header: map[string][]string{ "Location": { "http://example.localhost/test1/test2", }, }, }, upstream: "", }), Entry("containing an escaped '/' with ProxyRawPath", &proxyTableInput{ upstreams: options.UpstreamConfig{ProxyRawPath: true}, target: "http://example.localhost/%2F/test1/%2F/test2", response: testHTTPResponse{ code: 404, header: map[string][]string{ "X-Content-Type-Options": {"nosniff"}, contentType: {textPlainUTF8}, }, raw: "404 page not found\n", }, upstream: "", }), ) }) Context("sortByPathLongest", func() { type sortByPathLongestTableInput struct { input []options.Upstream expectedOutput []options.Upstream } httpPath := options.Upstream{ Path: "/http/", } httpSubPath := options.Upstream{ Path: "/http/subpath/", } longerPath := options.Upstream{ Path: "/longer-than-http", } shortPathWithRewrite := options.Upstream{ Path: "^/h/(.*)", RewriteTarget: "/$1", } 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.Upstream{httpPath, httpSubPath, shortSubPathWithRewrite, longerPath, shortPathWithRewrite}, expectedOutput: []options.Upstream{shortSubPathWithRewrite, shortPathWithRewrite, longerPath, httpSubPath, httpPath}, }), Entry("when a subpath is registered (in order)", sortByPathLongestTableInput{ input: []options.Upstream{httpSubPath, httpPath}, expectedOutput: []options.Upstream{httpSubPath, httpPath}, }), Entry("when a subpath is registered (out of order)", sortByPathLongestTableInput{ input: []options.Upstream{httpPath, httpSubPath}, expectedOutput: []options.Upstream{httpSubPath, httpPath}, }), Entry("when longer paths are registered (in order)", sortByPathLongestTableInput{ input: []options.Upstream{longerPath, httpPath}, expectedOutput: []options.Upstream{longerPath, httpPath}, }), Entry("when longer paths are registered (out of order)", sortByPathLongestTableInput{ input: []options.Upstream{httpPath, longerPath}, expectedOutput: []options.Upstream{longerPath, httpPath}, }), Entry("when a rewrite target is registered (in order)", sortByPathLongestTableInput{ input: []options.Upstream{shortPathWithRewrite, longerPath}, expectedOutput: []options.Upstream{shortPathWithRewrite, longerPath}, }), Entry("when a rewrite target is registered (out of order)", sortByPathLongestTableInput{ input: []options.Upstream{longerPath, shortPathWithRewrite}, expectedOutput: []options.Upstream{shortPathWithRewrite, longerPath}, }), Entry("with multiple rewrite targets registered (in order)", sortByPathLongestTableInput{ input: []options.Upstream{shortSubPathWithRewrite, shortPathWithRewrite}, expectedOutput: []options.Upstream{shortSubPathWithRewrite, shortPathWithRewrite}, }), Entry("with multiple rewrite targets registered (out of order)", sortByPathLongestTableInput{ input: []options.Upstream{shortPathWithRewrite, shortSubPathWithRewrite}, expectedOutput: []options.Upstream{shortSubPathWithRewrite, shortPathWithRewrite}, }), ) }) })