@@ -0,0 +1,417 @@
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"
23+var _ = Describe("HTTP Upstream Suite", func() {
25+ const flushInterval5s = 5 * time.Second
26+ const flushInterval1s = 1 * time.Second
28+ type httpUpstreamTableInput struct {
34+ signatureData *options.SignatureData
35+ existingHeaders map[string]string
36+ expectedResponse testHTTPResponse
37+ errorHandler ProxyErrorHandler
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
47+ for key, value := range in.existingHeaders {
48+ req.Header.Add(key, value)
51+ rw := httptest.NewRecorder()
53+ flush := 1 * time.Second
54+ upstream := options.Upstream{
56+ PassHostHeader: true,
57+ ProxyWebSockets: false,
58+ InsecureSkipTLSVerify: false,
59+ FlushInterval: &flush,
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))
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))
87+ Entry("request a path on the server", &httpUpstreamTableInput{
89+ serverAddr: &serverAddr,
90+ target: "http://example.localhost/foo",
94+ expectedResponse: testHTTPResponse{
96+ header: map[string][]string{
97+ gapUpstream: {"default"},
98+ contentType: {applicationJSON},
100+ request: testHTTPRequest{
102+ URL: "http://example.localhost/foo",
103+ Header: map[string][]string{},
105+ Host: "example.localhost",
106+ RequestURI: "http://example.localhost/foo",
110+ Entry("request a path with encoded slashes", &httpUpstreamTableInput{
111+ id: "encodedSlashes",
112+ serverAddr: &serverAddr,
113+ target: "http://example.localhost/foo%2fbar/?baz=1",
117+ expectedResponse: testHTTPResponse{
119+ header: map[string][]string{
120+ gapUpstream: {"encodedSlashes"},
121+ contentType: {applicationJSON},
123+ request: testHTTPRequest{
125+ URL: "http://example.localhost/foo%2fbar/?baz=1",
126+ Header: map[string][]string{},
128+ Host: "example.localhost",
129+ RequestURI: "http://example.localhost/foo%2fbar/?baz=1",
133+ Entry("when the request has a body", &httpUpstreamTableInput{
134+ id: "requestWithBody",
135+ serverAddr: &serverAddr,
136+ target: "http://example.localhost/withBody",
138+ body: []byte("body"),
140+ expectedResponse: testHTTPResponse{
142+ header: map[string][]string{
143+ gapUpstream: {"requestWithBody"},
144+ contentType: {applicationJSON},
146+ request: testHTTPRequest{
148+ URL: "http://example.localhost/withBody",
149+ Header: map[string][]string{
150+ contentLength: {"4"},
152+ Body: []byte("body"),
153+ Host: "example.localhost",
154+ RequestURI: "http://example.localhost/withBody",
158+ Entry("when the upstream is unavailable", &httpUpstreamTableInput{
159+ id: "unavailableUpstream",
160+ serverAddr: &invalidServer,
161+ target: "http://example.localhost/unavailableUpstream",
165+ expectedResponse: testHTTPResponse{
167+ header: map[string][]string{
168+ gapUpstream: {"unavailableUpstream"},
170+ request: testHTTPRequest{},
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",
179+ errorHandler: func(rw http.ResponseWriter, _ *http.Request, _ error) {
181+ rw.Write([]byte("error"))
183+ expectedResponse: testHTTPResponse{
185+ header: map[string][]string{
186+ gapUpstream: {"withErrorHandler"},
189+ request: testHTTPRequest{},
192+ Entry("with a signature", &httpUpstreamTableInput{
193+ id: "withSignature",
194+ serverAddr: &serverAddr,
195+ target: "http://example.localhost/withSignature",
198+ signatureData: &options.SignatureData{
199+ Hash: crypto.SHA256,
203+ expectedResponse: testHTTPResponse{
205+ header: map[string][]string{
206+ contentType: {applicationJSON},
207+ gapUpstream: {"withSignature"},
209+ request: testHTTPRequest{
211+ URL: "http://example.localhost/withSignature",
212+ Header: map[string][]string{
214+ gapSignature: {"sha256 osMWI8Rr0Zr5HgNq6wakrgJITVJQMmFN1fXCesrqrmM="},
217+ Host: "example.localhost",
218+ RequestURI: "http://example.localhost/withSignature",
222+ Entry("with existing headers", &httpUpstreamTableInput{
223+ id: "existingHeaders",
224+ serverAddr: &serverAddr,
225+ target: "http://example.localhost/existingHeaders",
229+ existingHeaders: map[string]string{
230+ "Header1": "value1",
231+ "Header2": "value2",
233+ expectedResponse: testHTTPResponse{
235+ header: map[string][]string{
236+ gapUpstream: {"existingHeaders"},
237+ contentType: {applicationJSON},
239+ request: testHTTPRequest{
241+ URL: "http://example.localhost/existingHeaders",
242+ Header: map[string][]string{
243+ "Header1": {"value1"},
244+ "Header2": {"value2"},
247+ Host: "example.localhost",
248+ RequestURI: "http://example.localhost/existingHeaders",
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{
261+ PassHostHeader: false,
262+ ProxyWebSockets: false,
263+ InsecureSkipTLSVerify: false,
264+ FlushInterval: &flush,
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())
282+ httpUpstream.handler = requestInterceptor(httpUpstream.handler)
284+ httpUpstream.ServeHTTP(rw, req)
285+ Expect(req.Host).To(Equal(strings.TrimPrefix(serverAddr, "http://")))
288+ type newUpstreamTableInput struct {
289+ proxyWebSockets bool
290+ flushInterval time.Duration
292+ sigData *options.SignatureData
293+ errorHandler func(http.ResponseWriter, *http.Request, error)
296+ DescribeTable("newHTTPUpstreamProxy",
297+ func(in *newUpstreamTableInput) {
298+ u, err := url.Parse("http://upstream:1234")
299+ Expect(err).ToNot(HaveOccurred())
301+ upstream := options.Upstream{
303+ FlushInterval: &in.flushInterval,
304+ InsecureSkipTLSVerify: in.skipVerify,
305+ ProxyWebSockets: in.proxyWebSockets,
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))
322+ Expect(proxy.Transport).To(Equal(&http.Transport{
323+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
327+ Entry("with proxy websockets", &newUpstreamTableInput{
328+ proxyWebSockets: true,
329+ flushInterval: flushInterval1s,
334+ Entry("with a non standard flush interval", &newUpstreamTableInput{
335+ proxyWebSockets: false,
336+ flushInterval: flushInterval5s,
341+ Entry("with a InsecureSkipTLSVerify", &newUpstreamTableInput{
342+ proxyWebSockets: false,
343+ flushInterval: flushInterval1s,
348+ Entry("with a SignatureData", &newUpstreamTableInput{
349+ proxyWebSockets: false,
350+ flushInterval: flushInterval1s,
352+ sigData: &options.SignatureData{Hash: crypto.SHA256, Key: "secret"},
355+ Entry("with an error handler", &newUpstreamTableInput{
356+ proxyWebSockets: false,
357+ flushInterval: flushInterval1s,
360+ errorHandler: func(rw http.ResponseWriter, req *http.Request, arg3 error) {
366+ Context("with a websocket proxy", func() {
367+ var proxyServer *httptest.Server
370+ flush := 1 * time.Second
371+ upstream := options.Upstream{
372+ ID: "websocketProxy",
373+ PassHostHeader: true,
374+ ProxyWebSockets: true,
375+ InsecureSkipTLSVerify: false,
376+ FlushInterval: &flush,
379+ u, err := url.Parse(serverAddr)
380+ Expect(err).ToNot(HaveOccurred())
382+ handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
383+ proxyServer = httptest.NewServer(handler)
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{
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"))