@@ -0,0 +1,417 @@
1+package upstream
2+
3+import (
4+ "bytes"
5+ "crypto"
6+ "crypto/tls"
7+ "encoding/json"
8+ "fmt"
9+ "net/http"
10+ "net/http/httptest"
11+ "net/http/httputil"
12+ "net/url"
13+ "strings"
14+ "time"
15+
16+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
17+ . "github.com/onsi/ginkgo"
18+ . "github.com/onsi/ginkgo/extensions/table"
19+ . "github.com/onsi/gomega"
20+ "golang.org/x/net/websocket"
21+)
22+
23+var _ = Describe("HTTP Upstream Suite", func() {
24+
25+ const flushInterval5s = 5 * time.Second
26+ const flushInterval1s = 1 * time.Second
27+
28+ type httpUpstreamTableInput struct {
29+ id string
30+ serverAddr *string
31+ target string
32+ method string
33+ body []byte
34+ signatureData *options.SignatureData
35+ existingHeaders map[string]string
36+ expectedResponse testHTTPResponse
37+ errorHandler ProxyErrorHandler
38+ }
39+
40+ DescribeTable("HTTP Upstream ServeHTTP",
41+ func(in *httpUpstreamTableInput) {
42+ buf := bytes.NewBuffer(in.body)
43+ req := httptest.NewRequest(in.method, in.target, buf)
44+ // Don't mock the remote Address
45+ req.RemoteAddr = ""
46+
47+ for key, value := range in.existingHeaders {
48+ req.Header.Add(key, value)
49+ }
50+
51+ rw := httptest.NewRecorder()
52+
53+ flush := 1 * time.Second
54+ upstream := options.Upstream{
55+ ID: in.id,
56+ PassHostHeader: true,
57+ ProxyWebSockets: false,
58+ InsecureSkipTLSVerify: false,
59+ FlushInterval: &flush,
60+ }
61+
62+ Expect(in.serverAddr).ToNot(BeNil())
63+ u, err := url.Parse(*in.serverAddr)
64+ Expect(err).ToNot(HaveOccurred())
65+
66+ handler := newHTTPUpstreamProxy(upstream, u, in.signatureData, in.errorHandler)
67+ handler.ServeHTTP(rw, req)
68+
69+ Expect(rw.Code).To(Equal(in.expectedResponse.code))
70+
71+ // Delete extra headers that aren't relevant to tests
72+ testSanitizeResponseHeader(rw.Header())
73+ Expect(rw.Header()).To(Equal(in.expectedResponse.header))
74+
75+ body := rw.Body.Bytes()
76+ if in.expectedResponse.raw != "" || rw.Code != http.StatusOK {
77+ Expect(string(body)).To(Equal(in.expectedResponse.raw))
78+ return
79+ }
80+
81+ // Compare the reflected request to the upstream
82+ request := testHTTPRequest{}
83+ Expect(json.Unmarshal(body, &request)).To(Succeed())
84+ testSanitizeRequestHeader(request.Header)
85+ Expect(request).To(Equal(in.expectedResponse.request))
86+ },
87+ Entry("request a path on the server", &httpUpstreamTableInput{
88+ id: "default",
89+ serverAddr: &serverAddr,
90+ target: "http://example.localhost/foo",
91+ method: "GET",
92+ body: []byte{},
93+ errorHandler: nil,
94+ expectedResponse: testHTTPResponse{
95+ code: 200,
96+ header: map[string][]string{
97+ gapUpstream: {"default"},
98+ contentType: {applicationJSON},
99+ },
100+ request: testHTTPRequest{
101+ Method: "GET",
102+ URL: "http://example.localhost/foo",
103+ Header: map[string][]string{},
104+ Body: []byte{},
105+ Host: "example.localhost",
106+ RequestURI: "http://example.localhost/foo",
107+ },
108+ },
109+ }),
110+ Entry("request a path with encoded slashes", &httpUpstreamTableInput{
111+ id: "encodedSlashes",
112+ serverAddr: &serverAddr,
113+ target: "http://example.localhost/foo%2fbar/?baz=1",
114+ method: "GET",
115+ body: []byte{},
116+ errorHandler: nil,
117+ expectedResponse: testHTTPResponse{
118+ code: 200,
119+ header: map[string][]string{
120+ gapUpstream: {"encodedSlashes"},
121+ contentType: {applicationJSON},
122+ },
123+ request: testHTTPRequest{
124+ Method: "GET",
125+ URL: "http://example.localhost/foo%2fbar/?baz=1",
126+ Header: map[string][]string{},
127+ Body: []byte{},
128+ Host: "example.localhost",
129+ RequestURI: "http://example.localhost/foo%2fbar/?baz=1",
130+ },
131+ },
132+ }),
133+ Entry("when the request has a body", &httpUpstreamTableInput{
134+ id: "requestWithBody",
135+ serverAddr: &serverAddr,
136+ target: "http://example.localhost/withBody",
137+ method: "POST",
138+ body: []byte("body"),
139+ errorHandler: nil,
140+ expectedResponse: testHTTPResponse{
141+ code: 200,
142+ header: map[string][]string{
143+ gapUpstream: {"requestWithBody"},
144+ contentType: {applicationJSON},
145+ },
146+ request: testHTTPRequest{
147+ Method: "POST",
148+ URL: "http://example.localhost/withBody",
149+ Header: map[string][]string{
150+ contentLength: {"4"},
151+ },
152+ Body: []byte("body"),
153+ Host: "example.localhost",
154+ RequestURI: "http://example.localhost/withBody",
155+ },
156+ },
157+ }),
158+ Entry("when the upstream is unavailable", &httpUpstreamTableInput{
159+ id: "unavailableUpstream",
160+ serverAddr: &invalidServer,
161+ target: "http://example.localhost/unavailableUpstream",
162+ method: "GET",
163+ body: []byte{},
164+ errorHandler: nil,
165+ expectedResponse: testHTTPResponse{
166+ code: 502,
167+ header: map[string][]string{
168+ gapUpstream: {"unavailableUpstream"},
169+ },
170+ request: testHTTPRequest{},
171+ },
172+ }),
173+ Entry("when the upstream is unavailable and an error handler is set", &httpUpstreamTableInput{
174+ id: "withErrorHandler",
175+ serverAddr: &invalidServer,
176+ target: "http://example.localhost/withErrorHandler",
177+ method: "GET",
178+ body: []byte{},
179+ errorHandler: func(rw http.ResponseWriter, _ *http.Request, _ error) {
180+ rw.WriteHeader(502)
181+ rw.Write([]byte("error"))
182+ },
183+ expectedResponse: testHTTPResponse{
184+ code: 502,
185+ header: map[string][]string{
186+ gapUpstream: {"withErrorHandler"},
187+ },
188+ raw: "error",
189+ request: testHTTPRequest{},
190+ },
191+ }),
192+ Entry("with a signature", &httpUpstreamTableInput{
193+ id: "withSignature",
194+ serverAddr: &serverAddr,
195+ target: "http://example.localhost/withSignature",
196+ method: "GET",
197+ body: []byte{},
198+ signatureData: &options.SignatureData{
199+ Hash: crypto.SHA256,
200+ Key: "key",
201+ },
202+ errorHandler: nil,
203+ expectedResponse: testHTTPResponse{
204+ code: 200,
205+ header: map[string][]string{
206+ contentType: {applicationJSON},
207+ gapUpstream: {"withSignature"},
208+ },
209+ request: testHTTPRequest{
210+ Method: "GET",
211+ URL: "http://example.localhost/withSignature",
212+ Header: map[string][]string{
213+ gapAuth: {""},
214+ gapSignature: {"sha256 osMWI8Rr0Zr5HgNq6wakrgJITVJQMmFN1fXCesrqrmM="},
215+ },
216+ Body: []byte{},
217+ Host: "example.localhost",
218+ RequestURI: "http://example.localhost/withSignature",
219+ },
220+ },
221+ }),
222+ Entry("with existing headers", &httpUpstreamTableInput{
223+ id: "existingHeaders",
224+ serverAddr: &serverAddr,
225+ target: "http://example.localhost/existingHeaders",
226+ method: "GET",
227+ body: []byte{},
228+ errorHandler: nil,
229+ existingHeaders: map[string]string{
230+ "Header1": "value1",
231+ "Header2": "value2",
232+ },
233+ expectedResponse: testHTTPResponse{
234+ code: 200,
235+ header: map[string][]string{
236+ gapUpstream: {"existingHeaders"},
237+ contentType: {applicationJSON},
238+ },
239+ request: testHTTPRequest{
240+ Method: "GET",
241+ URL: "http://example.localhost/existingHeaders",
242+ Header: map[string][]string{
243+ "Header1": {"value1"},
244+ "Header2": {"value2"},
245+ },
246+ Body: []byte{},
247+ Host: "example.localhost",
248+ RequestURI: "http://example.localhost/existingHeaders",
249+ },
250+ },
251+ }),
252+ )
253+
254+ It("ServeHTTP, when not passing a host header", func() {
255+ req := httptest.NewRequest("", "http://example.localhost/foo", nil)
256+ rw := httptest.NewRecorder()
257+
258+ flush := 1 * time.Second
259+ upstream := options.Upstream{
260+ ID: "noPassHost",
261+ PassHostHeader: false,
262+ ProxyWebSockets: false,
263+ InsecureSkipTLSVerify: false,
264+ FlushInterval: &flush,
265+ }
266+
267+ u, err := url.Parse(serverAddr)
268+ Expect(err).ToNot(HaveOccurred())
269+
270+ handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
271+ httpUpstream, ok := handler.(*httpUpstreamProxy)
272+ Expect(ok).To(BeTrue())
273+
274+ // Override the handler to just run the director and not actually send the request
275+ requestInterceptor := func(h http.Handler) http.Handler {
276+ return http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
277+ proxy, ok := h.(*httputil.ReverseProxy)
278+ Expect(ok).To(BeTrue())
279+ proxy.Director(req)
280+ })
281+ }
282+ httpUpstream.handler = requestInterceptor(httpUpstream.handler)
283+
284+ httpUpstream.ServeHTTP(rw, req)
285+ Expect(req.Host).To(Equal(strings.TrimPrefix(serverAddr, "http://")))
286+ })
287+
288+ type newUpstreamTableInput struct {
289+ proxyWebSockets bool
290+ flushInterval time.Duration
291+ skipVerify bool
292+ sigData *options.SignatureData
293+ errorHandler func(http.ResponseWriter, *http.Request, error)
294+ }
295+
296+ DescribeTable("newHTTPUpstreamProxy",
297+ func(in *newUpstreamTableInput) {
298+ u, err := url.Parse("http://upstream:1234")
299+ Expect(err).ToNot(HaveOccurred())
300+
301+ upstream := options.Upstream{
302+ ID: "foo123",
303+ FlushInterval: &in.flushInterval,
304+ InsecureSkipTLSVerify: in.skipVerify,
305+ ProxyWebSockets: in.proxyWebSockets,
306+ }
307+
308+ handler := newHTTPUpstreamProxy(upstream, u, in.sigData, in.errorHandler)
309+ upstreamProxy, ok := handler.(*httpUpstreamProxy)
310+ Expect(ok).To(BeTrue())
311+
312+ Expect(upstreamProxy.auth != nil).To(Equal(in.sigData != nil))
313+ Expect(upstreamProxy.wsHandler != nil).To(Equal(in.proxyWebSockets))
314+ Expect(upstreamProxy.upstream).To(Equal(upstream.ID))
315+ Expect(upstreamProxy.handler).ToNot(BeNil())
316+
317+ proxy, ok := upstreamProxy.handler.(*httputil.ReverseProxy)
318+ Expect(ok).To(BeTrue())
319+ Expect(proxy.FlushInterval).To(Equal(in.flushInterval))
320+ Expect(proxy.ErrorHandler != nil).To(Equal(in.errorHandler != nil))
321+ if in.skipVerify {
322+ Expect(proxy.Transport).To(Equal(&http.Transport{
323+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
324+ }))
325+ }
326+ },
327+ Entry("with proxy websockets", &newUpstreamTableInput{
328+ proxyWebSockets: true,
329+ flushInterval: flushInterval1s,
330+ skipVerify: false,
331+ sigData: nil,
332+ errorHandler: nil,
333+ }),
334+ Entry("with a non standard flush interval", &newUpstreamTableInput{
335+ proxyWebSockets: false,
336+ flushInterval: flushInterval5s,
337+ skipVerify: false,
338+ sigData: nil,
339+ errorHandler: nil,
340+ }),
341+ Entry("with a InsecureSkipTLSVerify", &newUpstreamTableInput{
342+ proxyWebSockets: false,
343+ flushInterval: flushInterval1s,
344+ skipVerify: true,
345+ sigData: nil,
346+ errorHandler: nil,
347+ }),
348+ Entry("with a SignatureData", &newUpstreamTableInput{
349+ proxyWebSockets: false,
350+ flushInterval: flushInterval1s,
351+ skipVerify: false,
352+ sigData: &options.SignatureData{Hash: crypto.SHA256, Key: "secret"},
353+ errorHandler: nil,
354+ }),
355+ Entry("with an error handler", &newUpstreamTableInput{
356+ proxyWebSockets: false,
357+ flushInterval: flushInterval1s,
358+ skipVerify: false,
359+ sigData: nil,
360+ errorHandler: func(rw http.ResponseWriter, req *http.Request, arg3 error) {
361+ rw.WriteHeader(502)
362+ },
363+ }),
364+ )
365+
366+ Context("with a websocket proxy", func() {
367+ var proxyServer *httptest.Server
368+
369+ BeforeEach(func() {
370+ flush := 1 * time.Second
371+ upstream := options.Upstream{
372+ ID: "websocketProxy",
373+ PassHostHeader: true,
374+ ProxyWebSockets: true,
375+ InsecureSkipTLSVerify: false,
376+ FlushInterval: &flush,
377+ }
378+
379+ u, err := url.Parse(serverAddr)
380+ Expect(err).ToNot(HaveOccurred())
381+
382+ handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
383+ proxyServer = httptest.NewServer(handler)
384+ })
385+
386+ AfterEach(func() {
387+ proxyServer.Close()
388+ })
389+
390+ It("will proxy websockets", func() {
391+ origin := "http://example.localhost"
392+ message := "Hello, world!"
393+
394+ proxyURL, err := url.Parse(fmt.Sprintf("http://%s", proxyServer.Listener.Addr().String()))
395+ Expect(err).ToNot(HaveOccurred())
396+
397+ wsAddr := fmt.Sprintf("ws://%s/", proxyURL.Host)
398+ ws, err := websocket.Dial(wsAddr, "", origin)
399+ Expect(err).ToNot(HaveOccurred())
400+
401+ Expect(websocket.Message.Send(ws, []byte(message))).To(Succeed())
402+ var response testWebSocketResponse
403+ Expect(websocket.JSON.Receive(ws, &response)).To(Succeed())
404+ Expect(response).To(Equal(testWebSocketResponse{
405+ Message: message,
406+ Origin: origin,
407+ }))
408+ })
409+
410+ It("will proxy HTTP requests", func() {
411+ response, err := http.Get(fmt.Sprintf("http://%s", proxyServer.Listener.Addr().String()))
412+ Expect(err).ToNot(HaveOccurred())
413+ Expect(response.StatusCode).To(Equal(200))
414+ Expect(response.Header.Get(gapUpstream)).To(Equal("websocketProxy"))
415+ })
416+ })
417+})