git.iko.soy/typeable/oauth2-proxy
on request-rules help sign in
417 lines 12 kB history raw
1package upstream
3import (
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"
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"
23var _ = Describe("HTTP Upstream Suite", func() {
25 const flushInterval5s = 5 * time.Second
26 const flushInterval1s = 1 * time.Second
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 }
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 = ""
47 for key, value := range in.existingHeaders {
48 req.Header.Add(key, value)
49 }
51 rw := httptest.NewRecorder()
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 }
62 Expect(in.serverAddr).ToNot(BeNil())
63 u, err := url.Parse(*in.serverAddr)
64 Expect(err).ToNot(HaveOccurred())
66 handler := newHTTPUpstreamProxy(upstream, u, in.signatureData, in.errorHandler)
67 handler.ServeHTTP(rw, req)
69 Expect(rw.Code).To(Equal(in.expectedResponse.code))
71 // Delete extra headers that aren't relevant to tests
72 testSanitizeResponseHeader(rw.Header())
73 Expect(rw.Header()).To(Equal(in.expectedResponse.header))
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 }
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 )
254 It("ServeHTTP, when not passing a host header", func() {
255 req := httptest.NewRequest("", "http://example.localhost/foo", nil)
256 rw := httptest.NewRecorder()
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 }
267 u, err := url.Parse(serverAddr)
268 Expect(err).ToNot(HaveOccurred())
270 handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
271 httpUpstream, ok := handler.(*httpUpstreamProxy)
272 Expect(ok).To(BeTrue())
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)
284 httpUpstream.ServeHTTP(rw, req)
285 Expect(req.Host).To(Equal(strings.TrimPrefix(serverAddr, "http://")))
286 })
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 }
296 DescribeTable("newHTTPUpstreamProxy",
297 func(in *newUpstreamTableInput) {
298 u, err := url.Parse("http://upstream:1234")
299 Expect(err).ToNot(HaveOccurred())
301 upstream := options.Upstream{
302 ID: "foo123",
303 FlushInterval: &in.flushInterval,
304 InsecureSkipTLSVerify: in.skipVerify,
305 ProxyWebSockets: in.proxyWebSockets,
306 }
308 handler := newHTTPUpstreamProxy(upstream, u, in.sigData, in.errorHandler)
309 upstreamProxy, ok := handler.(*httpUpstreamProxy)
310 Expect(ok).To(BeTrue())
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())
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 )
366 Context("with a websocket proxy", func() {
367 var proxyServer *httptest.Server
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 }
379 u, err := url.Parse(serverAddr)
380 Expect(err).ToNot(HaveOccurred())
382 handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
383 proxyServer = httptest.NewServer(handler)
384 })
386 AfterEach(func() {
387 proxyServer.Close()
388 })
390 It("will proxy websockets", func() {
391 origin := "http://example.localhost"
392 message := "Hello, world!"
394 proxyURL, err := url.Parse(fmt.Sprintf("http://%s", proxyServer.Listener.Addr().String()))
395 Expect(err).ToNot(HaveOccurred())
397 wsAddr := fmt.Sprintf("ws://%s/", proxyURL.Host)
398 ws, err := websocket.Dial(wsAddr, "", origin)
399 Expect(err).ToNot(HaveOccurred())
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 })
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})