@@ -18,174 +18,369 @@ import (
1818var _ = Describe("Proxy Suite", func() {
1919 var upstreamServer http.Handler
2020
21− BeforeEach(func() {
22− sigData := &options.SignatureData{Hash: crypto.SHA256, Key: "secret"}
21+ Context("multiUpstreamProxy", func() {
22+ BeforeEach(func() {
23+ sigData := &options.SignatureData{Hash: crypto.SHA256, Key: "secret"}
2324
24− writer := &pagewriter.WriterFuncs{
25− ProxyErrorFunc: func(rw http.ResponseWriter, _ *http.Request, _ error) {
26− rw.WriteHeader(502)
27− rw.Write([]byte("Proxy Error"))
28− },
29− }
25+ writer := &pagewriter.WriterFuncs{
26+ ProxyErrorFunc: func(rw http.ResponseWriter, _ *http.Request, _ error) {
27+ rw.WriteHeader(502)
28+ rw.Write([]byte("Proxy Error"))
29+ },
30+ }
3031
31− ok := http.StatusOK
32+ ok := http.StatusOK
33+ accepted := http.StatusAccepted
3234
33− upstreams := options.Upstreams{
34− {
35− ID: "http-backend",
36− Path: "/http/",
37− URI: serverAddr,
38− },
39− {
40− ID: "file-backend",
41− Path: "/files/",
42− URI: fmt.Sprintf("file:///%s", filesDir),
43− },
44− {
45− ID: "static-backend",
46− Path: "/static/",
47− Static: true,
48− StaticCode: &ok,
49− },
50− {
51− ID: "bad-http-backend",
52− Path: "/bad-http/",
53− URI: "http://::1",
54− },
55− {
56− ID: "single-path-backend",
57− Path: "/single-path",
58− Static: true,
59− StaticCode: &ok,
60− },
35+ upstreams := options.Upstreams{
36+ {
37+ ID: "http-backend",
38+ Path: "/http/",
39+ URI: serverAddr,
40+ },
41+ {
42+ ID: "file-backend",
43+ Path: "/files/",
44+ URI: fmt.Sprintf("file:///%s", filesDir),
45+ },
46+ {
47+ ID: "static-backend",
48+ Path: "/static/",
49+ Static: true,
50+ StaticCode: &ok,
51+ },
52+ {
53+ ID: "static-backend-no-trailing-slash",
54+ Path: "/static",
55+ Static: true,
56+ StaticCode: &accepted,
57+ },
58+ {
59+ ID: "static-backend-long",
60+ Path: "/static/long",
61+ Static: true,
62+ StaticCode: &accepted,
63+ },
64+ {
65+ ID: "bad-http-backend",
66+ Path: "/bad-http/",
67+ URI: "http://::1",
68+ },
69+ {
70+ ID: "single-path-backend",
71+ Path: "/single-path",
72+ Static: true,
73+ StaticCode: &ok,
74+ },
75+ {
76+ ID: "backend-with-rewrite-prefix",
77+ Path: "^/rewrite-prefix/(.*)",
78+ RewriteTarget: "/different/backend/path/$1",
79+ URI: serverAddr,
80+ },
81+ {
82+ ID: "double-match-plain",
83+ Path: "/double-match/",
84+ URI: serverAddr,
85+ },
86+ {
87+ ID: "double-match-rewrite",
88+ Path: "^/double-match/(.*)",
89+ RewriteTarget: "/double-match/rewrite/$1",
90+ URI: serverAddr,
91+ },
92+ }
93+
94+ var err error
95+ upstreamServer, err = NewProxy(upstreams, sigData, writer)
96+ Expect(err).ToNot(HaveOccurred())
97+ })
98+
99+ type proxyTableInput struct {
100+ target string
101+ response testHTTPResponse
102+ upstream string
61103 }
62104
63− var err error
64− upstreamServer, err = NewProxy(upstreams, sigData, writer)
65− Expect(err).ToNot(HaveOccurred())
66− })
105+ DescribeTable("Proxy ServeHTTP",
106+ func(in *proxyTableInput) {
107+ req := middlewareapi.AddRequestScope(
108+ httptest.NewRequest("", in.target, nil),
109+ &middlewareapi.RequestScope{},
110+ )
111+ rw := httptest.NewRecorder()
112+ // Don't mock the remote Address
113+ req.RemoteAddr = ""
67114
68− type proxyTableInput struct {
69− target string
70− response testHTTPResponse
71− upstream string
72− }
73−
74− DescribeTable("Proxy ServeHTTP",
75− func(in *proxyTableInput) {
76− req := middlewareapi.AddRequestScope(
77− httptest.NewRequest("", in.target, nil),
78− &middlewareapi.RequestScope{},
79− )
80− rw := httptest.NewRecorder()
81− // Don't mock the remote Address
82− req.RemoteAddr = ""
83−
84− upstreamServer.ServeHTTP(rw, req)
85−
86− scope := middlewareapi.GetRequestScope(req)
87− Expect(scope.Upstream).To(Equal(in.upstream))
88−
89− Expect(rw.Code).To(Equal(in.response.code))
90−
91− // Delete extra headers that aren't relevant to tests
92− testSanitizeResponseHeader(rw.Header())
93− Expect(rw.Header()).To(Equal(in.response.header))
94−
95− body := rw.Body.Bytes()
96− // If the raw body is set, check that, else check the Request object
97− if in.response.raw != "" {
98− Expect(string(body)).To(Equal(in.response.raw))
99− return
100− }
115+ upstreamServer.ServeHTTP(rw, req)
116+
117+ scope := middlewareapi.GetRequestScope(req)
118+ Expect(scope.Upstream).To(Equal(in.upstream))
119+
120+ Expect(rw.Code).To(Equal(in.response.code))
121+
122+ // Delete extra headers that aren't relevant to tests
123+ testSanitizeResponseHeader(rw.Header())
124+ Expect(rw.Header()).To(Equal(in.response.header))
125+
126+ body := rw.Body.Bytes()
127+ // If the raw body is set, check that, else check the Request object
128+ if in.response.raw != "" {
129+ Expect(string(body)).To(Equal(in.response.raw))
130+ return
131+ }
101132
102− // Compare the reflected request to the upstream
103− request := testHTTPRequest{}
104− Expect(json.Unmarshal(body, &request)).To(Succeed())
105− testSanitizeRequestHeader(request.Header)
106− Expect(request).To(Equal(in.response.request))
107− },
108− Entry("with a request to the HTTP service", &proxyTableInput{
109− target: "http://example.localhost/http/1234",
110− response: testHTTPResponse{
111− code: 200,
112− header: map[string][]string{
113− contentType: {applicationJSON},
114− },
115− request: testHTTPRequest{
116− Method: "GET",
117− URL: "http://example.localhost/http/1234",
118− Header: map[string][]string{
119− "Gap-Auth": {""},
120− "Gap-Signature": {"sha256 ofB1u6+FhEUbFLc3/uGbJVkl7GaN4egFqVvyO3+2I1w="},
133+ // Compare the reflected request to the upstream
134+ request := testHTTPRequest{}
135+ Expect(json.Unmarshal(body, &request)).To(Succeed())
136+ testSanitizeRequestHeader(request.Header)
137+ Expect(request).To(Equal(in.response.request))
138+ },
139+ Entry("with a request to the HTTP service", &proxyTableInput{
140+ target: "http://example.localhost/http/1234",
141+ response: testHTTPResponse{
142+ code: 200,
143+ header: map[string][]string{
144+ contentType: {applicationJSON},
145+ },
146+ request: testHTTPRequest{
147+ Method: "GET",
148+ URL: "http://example.localhost/http/1234",
149+ Header: map[string][]string{
150+ "Gap-Auth": {""},
151+ "Gap-Signature": {"sha256 ofB1u6+FhEUbFLc3/uGbJVkl7GaN4egFqVvyO3+2I1w="},
152+ },
153+ Body: []byte{},
154+ Host: "example.localhost",
155+ RequestURI: "http://example.localhost/http/1234",
121156 },
122− Body: []byte{},
123− Host: "example.localhost",
124− RequestURI: "http://example.localhost/http/1234",
125157 },
158+ upstream: "http-backend",
159+ }),
160+ Entry("with a request to the File backend", &proxyTableInput{
161+ target: "http://example.localhost/files/foo",
162+ response: testHTTPResponse{
163+ code: 200,
164+ header: map[string][]string{
165+ contentType: {textPlainUTF8},
166+ },
167+ raw: "foo",
168+ },
169+ upstream: "file-backend",
170+ }),
171+ Entry("with a request to the Static backend", &proxyTableInput{
172+ target: "http://example.localhost/static/bar",
173+ response: testHTTPResponse{
174+ code: 200,
175+ header: map[string][]string{},
176+ raw: "Authenticated",
177+ },
178+ upstream: "static-backend",
179+ }),
180+ Entry("with a request to the bad HTTP backend", &proxyTableInput{
181+ target: "http://example.localhost/bad-http/bad",
182+ response: testHTTPResponse{
183+ code: 502,
184+ header: map[string][]string{},
185+ // This tests the error handler
186+ raw: "Proxy Error",
187+ },
188+ upstream: "bad-http-backend",
189+ }),
190+ Entry("with a request to the to an unregistered path", &proxyTableInput{
191+ target: "http://example.localhost/unregistered",
192+ response: testHTTPResponse{
193+ code: 404,
194+ header: map[string][]string{
195+ "X-Content-Type-Options": {"nosniff"},
196+ contentType: {textPlainUTF8},
197+ },
198+ raw: "404 page not found\n",
199+ },
200+ }),
201+ Entry("with a request to the to backend registered to a single path", &proxyTableInput{
202+ target: "http://example.localhost/single-path",
203+ response: testHTTPResponse{
204+ code: 200,
205+ header: map[string][]string{},
206+ raw: "Authenticated",
207+ },
208+ upstream: "single-path-backend",
209+ }),
210+ Entry("with a request to the to a subpath of a backend registered to a single path", &proxyTableInput{
211+ target: "http://example.localhost/single-path/unregistered",
212+ response: testHTTPResponse{
213+ code: 404,
214+ header: map[string][]string{
215+ "X-Content-Type-Options": {"nosniff"},
216+ contentType: {textPlainUTF8},
217+ },
218+ raw: "404 page not found\n",
219+ },
220+ }),
221+ Entry("with a request to the rewrite prefix server", &proxyTableInput{
222+ target: "http://example.localhost/rewrite-prefix/1234",
223+ response: testHTTPResponse{
224+ code: 200,
225+ header: map[string][]string{
226+ contentType: {applicationJSON},
227+ },
228+ request: testHTTPRequest{
229+ Method: "GET",
230+ URL: "http://example.localhost/different/backend/path/1234",
231+ Header: map[string][]string{
232+ "Gap-Auth": {""},
233+ "Gap-Signature": {"sha256 jeAeM7wHSj2ab/l9YPvtTJ9l/8q1tpY2V/iwXF48bgw="},
234+ },
235+ Body: []byte{},
236+ Host: "example.localhost",
237+ RequestURI: "http://example.localhost/different/backend/path/1234",
238+ },
239+ },
240+ upstream: "backend-with-rewrite-prefix",
241+ }),
242+ Entry("with a request to a subpath of the rewrite prefix server", &proxyTableInput{
243+ target: "http://example.localhost/rewrite-prefix/1234/abc",
244+ response: testHTTPResponse{
245+ code: 200,
246+ header: map[string][]string{
247+ contentType: {applicationJSON},
248+ },
249+ request: testHTTPRequest{
250+ Method: "GET",
251+ URL: "http://example.localhost/different/backend/path/1234/abc",
252+ Header: map[string][]string{
253+ "Gap-Auth": {""},
254+ "Gap-Signature": {"sha256 rAkAc9gp7EndoOppJuvbuPnYuBcqrTkBnQx6iPS8xTA="},
255+ },
256+ Body: []byte{},
257+ Host: "example.localhost",
258+ RequestURI: "http://example.localhost/different/backend/path/1234/abc",
259+ },
260+ },
261+ upstream: "backend-with-rewrite-prefix",
262+ }),
263+ Entry("with a request to a path, missing the trailing slash", &proxyTableInput{
264+ target: "http://example.localhost/http",
265+ response: testHTTPResponse{
266+ code: 301,
267+ header: map[string][]string{
268+ contentType: {textHTMLUTF8},
269+ "Location": {"http://example.localhost/http/"},
270+ },
271+ raw: "<a href=\"http://example.localhost/http/\">Moved Permanently</a>.\n\n",
272+ },
273+ }),
274+ Entry("with a request to a path, missing the trailing slash, but registered separately", &proxyTableInput{
275+ target: "http://example.localhost/static",
276+ response: testHTTPResponse{
277+ code: 202,
278+ header: map[string][]string{},
279+ raw: "Authenticated",
280+ },
281+ upstream: "static-backend-no-trailing-slash",
282+ }),
283+ Entry("should match longest path first", &proxyTableInput{
284+ target: "http://example.localhost/static/long",
285+ response: testHTTPResponse{
286+ code: 202,
287+ header: map[string][]string{},
288+ raw: "Authenticated",
289+ },
290+ upstream: "static-backend-long",
291+ }),
292+ Entry("should match rewrite path first", &proxyTableInput{
293+ target: "http://example.localhost/double-match/foo",
294+ response: testHTTPResponse{
295+ code: 200,
296+ header: map[string][]string{
297+ contentType: {applicationJSON},
298+ },
299+ request: testHTTPRequest{
300+ Method: "GET",
301+ URL: "http://example.localhost/double-match/rewrite/foo",
302+ Header: map[string][]string{
303+ "Gap-Auth": {""},
304+ "Gap-Signature": {"sha256 eYyUNdsrTmnvFpavpP8AdHGUGzqJ39QEjqn0/3fQPHA="},
305+ },
306+ Body: []byte{},
307+ Host: "example.localhost",
308+ RequestURI: "http://example.localhost/double-match/rewrite/foo",
309+ },
310+ },
311+ upstream: "double-match-rewrite",
312+ }),
313+ )
314+ })
315+
316+ Context("sortByPathLongest", func() {
317+ type sortByPathLongestTableInput struct {
318+ input options.Upstreams
319+ expectedOutput options.Upstreams
320+ }
321+
322+ var httpPath = options.Upstream{
323+ Path: "/http/",
324+ }
325+
326+ var httpSubPath = options.Upstream{
327+ Path: "/http/subpath/",
328+ }
329+
330+ var longerPath = options.Upstream{
331+ Path: "/longer-than-http",
332+ }
333+
334+ var shortPathWithRewrite = options.Upstream{
335+ Path: "^/h/(.*)",
336+ RewriteTarget: "/$1",
337+ }
338+
339+ var shortSubPathWithRewrite = options.Upstream{
340+ Path: "^/h/bar/(.*)",
341+ RewriteTarget: "/$1",
342+ }
343+
344+ DescribeTable("short sort into the correct order",
345+ func(in sortByPathLongestTableInput) {
346+ Expect(sortByPathLongest(in.input)).To(Equal(in.expectedOutput))
126347 },
127− upstream: "http-backend",
128− }),
129− Entry("with a request to the File backend", &proxyTableInput{
130− target: "http://example.localhost/files/foo",
131− response: testHTTPResponse{
132− code: 200,
133− header: map[string][]string{
134− contentType: {textPlainUTF8},
135− },
136− raw: "foo",
137− },
138− upstream: "file-backend",
139− }),
140− Entry("with a request to the Static backend", &proxyTableInput{
141− target: "http://example.localhost/static/bar",
142− response: testHTTPResponse{
143− code: 200,
144− header: map[string][]string{},
145− raw: "Authenticated",
146− },
147− upstream: "static-backend",
148− }),
149− Entry("with a request to the bad HTTP backend", &proxyTableInput{
150− target: "http://example.localhost/bad-http/bad",
151− response: testHTTPResponse{
152− code: 502,
153− header: map[string][]string{},
154− // This tests the error handler
155− raw: "Proxy Error",
156− },
157− upstream: "bad-http-backend",
158− }),
159− Entry("with a request to the to an unregistered path", &proxyTableInput{
160− target: "http://example.localhost/unregistered",
161− response: testHTTPResponse{
162− code: 404,
163− header: map[string][]string{
164− "X-Content-Type-Options": {"nosniff"},
165− contentType: {textPlainUTF8},
166− },
167− raw: "404 page not found\n",
168− },
169− }),
170− Entry("with a request to the to backend registered to a single path", &proxyTableInput{
171− target: "http://example.localhost/single-path",
172− response: testHTTPResponse{
173− code: 200,
174− header: map[string][]string{},
175− raw: "Authenticated",
176− },
177− upstream: "single-path-backend",
178− }),
179− Entry("with a request to the to a subpath of a backend registered to a single path", &proxyTableInput{
180− target: "http://example.localhost/single-path/unregistered",
181− response: testHTTPResponse{
182− code: 404,
183− header: map[string][]string{
184− "X-Content-Type-Options": {"nosniff"},
185− contentType: {textPlainUTF8},
186− },
187− raw: "404 page not found\n",
188− },
189− }),
190− )
348+ Entry("with a mix of paths registered", sortByPathLongestTableInput{
349+ input: options.Upstreams{httpPath, httpSubPath, shortSubPathWithRewrite, longerPath, shortPathWithRewrite},
350+ expectedOutput: options.Upstreams{shortSubPathWithRewrite, shortPathWithRewrite, longerPath, httpSubPath, httpPath},
351+ }),
352+ Entry("when a subpath is registered (in order)", sortByPathLongestTableInput{
353+ input: options.Upstreams{httpSubPath, httpPath},
354+ expectedOutput: options.Upstreams{httpSubPath, httpPath},
355+ }),
356+ Entry("when a subpath is registered (out of order)", sortByPathLongestTableInput{
357+ input: options.Upstreams{httpPath, httpSubPath},
358+ expectedOutput: options.Upstreams{httpSubPath, httpPath},
359+ }),
360+ Entry("when longer paths are registered (in order)", sortByPathLongestTableInput{
361+ input: options.Upstreams{longerPath, httpPath},
362+ expectedOutput: options.Upstreams{longerPath, httpPath},
363+ }),
364+ Entry("when longer paths are registered (out of order)", sortByPathLongestTableInput{
365+ input: options.Upstreams{httpPath, longerPath},
366+ expectedOutput: options.Upstreams{longerPath, httpPath},
367+ }),
368+ Entry("when a rewrite target is registered (in order)", sortByPathLongestTableInput{
369+ input: options.Upstreams{shortPathWithRewrite, longerPath},
370+ expectedOutput: options.Upstreams{shortPathWithRewrite, longerPath},
371+ }),
372+ Entry("when a rewrite target is registered (out of order)", sortByPathLongestTableInput{
373+ input: options.Upstreams{longerPath, shortPathWithRewrite},
374+ expectedOutput: options.Upstreams{shortPathWithRewrite, longerPath},
375+ }),
376+ Entry("with multiple rewrite targets registered (in order)", sortByPathLongestTableInput{
377+ input: options.Upstreams{shortSubPathWithRewrite, shortPathWithRewrite},
378+ expectedOutput: options.Upstreams{shortSubPathWithRewrite, shortPathWithRewrite},
379+ }),
380+ Entry("with multiple rewrite targets registered (out of order)", sortByPathLongestTableInput{
381+ input: options.Upstreams{shortPathWithRewrite, shortSubPathWithRewrite},
382+ expectedOutput: options.Upstreams{shortSubPathWithRewrite, shortPathWithRewrite},
383+ }),
384+ )
385+ })
191386})