git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #591 from oauth2-proxy/upstream-proxy

Introduce upstream package with new reverse proxy implementation

commit99481b3a391599a59de4c15e326468c767b96202 authorJoel Speed <Joel.speed@hotmail.co.uk> dateSun 5 Jul 2020, 20:36 (6 years ago) parentfb1bef237c76b6
14 files changed+1628
CHANGELOG.md+1 pkg/apis/options/upstreams.go+60 pkg/upstream/file.go+42 pkg/upstream/file_test.go+58 pkg/upstream/http.go+163 pkg/upstream/http_test.go+417 pkg/upstream/proxy.go+90 pkg/upstream/proxy_test.go+182 pkg/upstream/static.go+34 pkg/upstream/static_test.go+81 pkg/upstream/upstream_suite_test.go+180 pkg/validation/upstreams.go+113 pkg/validation/upstreams_test.go+191 pkg/validation/validation_suite_test.go+16
CHANGELOG.md+1
@@ -8,6 +8,7 @@
88
99## Changes since v6.0.0
1010
11+- [#591](https://github.com/oauth2-proxy/oauth2-proxy/pull/591) Introduce upstream package with new reverse proxy implementation (@JoelSpeed)
1112- [#576](https://github.com/oauth2-proxy/oauth2-proxy/pull/576) Separate Cookie validation out of main options validation (@JoelSpeed)
1213- [#656](https://github.com/oauth2-proxy/oauth2-proxy/pull/656) Split long session cookies more precisely (@NickMeves)
1314- [#619](https://github.com/oauth2-proxy/oauth2-proxy/pull/619) Improve Redirect to HTTPs behaviour (@JoelSpeed)
pkg/apis/options/upstreams.gonew file+60
@@ -0,0 +1,60 @@
1+package options
2+
3+import "time"
4+
5+// Upstreams is a collection of definitions for upstream servers.
6+type Upstreams []Upstream
7+
8+// Upstream represents the configuration for an upstream server.
9+// Requests will be proxied to this upstream if the path matches the request path.
10+type Upstream struct {
11+ // ID should be a unique identifier for the upstream.
12+ // This value is required for all upstreams.
13+ ID string `json:"id"`
14+
15+ // Path is used to map requests to the upstream server.
16+ // The closest match will take precedence and all Paths must be unique.
17+ Path string `json:"path"`
18+
19+ // The URI of the upstream server. This may be an HTTP(S) server of a File
20+ // based URL. It may include a path, in which case all requests will be served
21+ // under that path.
22+ // Eg:
23+ // - http://localhost:8080
24+ // - https://service.localhost
25+ // - https://service.localhost/path
26+ // - file://host/path
27+ // If the URI's path is "/base" and the incoming request was for "/dir",
28+ // the upstream request will be for "/base/dir".
29+ URI string `json:"uri"`
30+
31+ // InsecureSkipTLSVerify will skip TLS verification of upstream HTTPS hosts.
32+ // This option is insecure and will allow potential Man-In-The-Middle attacks
33+ // betweem OAuth2 Proxy and the usptream server.
34+ // Defaults to false.
35+ InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify"`
36+
37+ // Static will make all requests to this upstream have a static response.
38+ // The response will have a body of "Authenticated" and a response code
39+ // matching StaticCode.
40+ // If StaticCode is not set, the response will return a 200 response.
41+ Static bool `json:"static"`
42+
43+ // StaticCode determines the response code for the Static response.
44+ // This option can only be used with Static enabled.
45+ StaticCode *int `json:"staticCode,omitempty"`
46+
47+ // FlushInterval is the period between flushing the response buffer when
48+ // streaming response from the upstream.
49+ // Defaults to 1 second.
50+ FlushInterval *time.Duration `json:"flushInterval,omitempty"`
51+
52+ // PassHostHeader determines whether the request host header should be proxied
53+ // to the upstream server.
54+ // Defaults to true.
55+ PassHostHeader bool `json:"passHostHeader"`
56+
57+ // ProxyWebSockets enables proxying of websockets to upstream servers
58+ // Defaults to true.
59+ ProxyWebSockets bool `json:"proxyWebSockets"`
60+}
pkg/upstream/file.gonew file+42
@@ -0,0 +1,42 @@
1+package upstream
2+
3+import (
4+ "net/http"
5+ "runtime"
6+ "strings"
7+)
8+
9+const fileScheme = "file"
10+
11+// newFileServer creates a new fileServer that can serve requests
12+// to a file system location.
13+func newFileServer(id, path, fileSystemPath string) http.Handler {
14+ return &fileServer{
15+ upstream: id,
16+ handler: newFileServerForPath(path, fileSystemPath),
17+ }
18+}
19+
20+// newFileServerForPath creates a http.Handler to serve files from the filesystem
21+func newFileServerForPath(path string, filesystemPath string) http.Handler {
22+ // Windows fileSSystemPath will be be prefixed with `/`, eg`/C:/...,
23+ // if they were parsed by url.Parse`
24+ if runtime.GOOS == "windows" {
25+ filesystemPath = strings.TrimPrefix(filesystemPath, "/")
26+ }
27+
28+ return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath)))
29+}
30+
31+// fileServer represents a single filesystem upstream proxy
32+type fileServer struct {
33+ upstream string
34+ handler http.Handler
35+}
36+
37+// ServeHTTP proxies requests to the upstream provider while signing the
38+// request headers
39+func (u *fileServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
40+ rw.Header().Set("GAP-Upstream-Address", u.upstream)
41+ u.handler.ServeHTTP(rw, req)
42+}
pkg/upstream/file_test.gonew file+58
@@ -0,0 +1,58 @@
1+package upstream
2+
3+import (
4+ "crypto/rand"
5+ "io"
6+ "net/http"
7+ "net/http/httptest"
8+ "os"
9+
10+ . "github.com/onsi/ginkgo"
11+ . "github.com/onsi/ginkgo/extensions/table"
12+ . "github.com/onsi/gomega"
13+)
14+
15+var _ = Describe("File Server Suite", func() {
16+ var dir string
17+ var handler http.Handler
18+ var id string
19+
20+ const (
21+ foo = "foo"
22+ bar = "bar"
23+ baz = "baz"
24+ pageNotFound = "404 page not found\n"
25+ )
26+
27+ BeforeEach(func() {
28+ // Generate a random id before each test to check the GAP-Upstream-Address
29+ // is being set correctly
30+ idBytes := make([]byte, 16)
31+ _, err := io.ReadFull(rand.Reader, idBytes)
32+ Expect(err).ToNot(HaveOccurred())
33+ id = string(idBytes)
34+
35+ handler = newFileServer(id, "/files", filesDir)
36+ })
37+
38+ AfterEach(func() {
39+ Expect(os.RemoveAll(dir)).To(Succeed())
40+ })
41+
42+ DescribeTable("fileServer ServeHTTP",
43+ func(requestPath string, expectedResponseCode int, expectedBody string) {
44+ req := httptest.NewRequest("", requestPath, nil)
45+ rw := httptest.NewRecorder()
46+ handler.ServeHTTP(rw, req)
47+
48+ Expect(rw.Header().Get("GAP-Upstream-Address")).To(Equal(id))
49+ Expect(rw.Code).To(Equal(expectedResponseCode))
50+ Expect(rw.Body.String()).To(Equal(expectedBody))
51+ },
52+ Entry("for file foo", "/files/foo", 200, foo),
53+ Entry("for file bar", "/files/bar", 200, bar),
54+ Entry("for file foo/baz", "/files/subdir/baz", 200, baz),
55+ Entry("for a non-existent file inside the path", "/files/baz", 404, pageNotFound),
56+ Entry("for a non-existent file oustide the path", "/baz", 404, pageNotFound),
57+ )
58+})
pkg/upstream/http.gonew file+163
@@ -0,0 +1,163 @@
1+package upstream
2+
3+import (
4+ "crypto/tls"
5+ "net/http"
6+ "net/http/httputil"
7+ "net/url"
8+ "strings"
9+ "time"
10+
11+ "github.com/mbland/hmacauth"
12+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
13+ "github.com/yhat/wsutil"
14+)
15+
16+const (
17+ // SignatureHeader is the name of the request header containing the GAP Signature
18+ // Part of hmacauth
19+ SignatureHeader = "GAP-Signature"
20+
21+ httpScheme = "http"
22+ httpsScheme = "https"
23+)
24+
25+// SignatureHeaders contains the headers to be signed by the hmac algorithm
26+// Part of hmacauth
27+var SignatureHeaders = []string{
28+ "Content-Length",
29+ "Content-Md5",
30+ "Content-Type",
31+ "Date",
32+ "Authorization",
33+ "X-Forwarded-User",
34+ "X-Forwarded-Email",
35+ "X-Forwarded-Preferred-User",
36+ "X-Forwarded-Access-Token",
37+ "Cookie",
38+ "Gap-Auth",
39+}
40+
41+// newHTTPUpstreamProxy creates a new httpUpstreamProxy that can serve requests
42+// to a single upstream host.
43+func newHTTPUpstreamProxy(upstream options.Upstream, u *url.URL, sigData *options.SignatureData, errorHandler ProxyErrorHandler) http.Handler {
44+ // Set path to empty so that request paths start at the server root
45+ u.Path = ""
46+
47+ // Create a ReverseProxy
48+ proxy := newReverseProxy(u, upstream, errorHandler)
49+
50+ // Set up a WebSocket proxy if required
51+ var wsProxy http.Handler
52+ if upstream.ProxyWebSockets {
53+ wsProxy = newWebSocketReverseProxy(u, upstream.InsecureSkipTLSVerify)
54+ }
55+
56+ var auth hmacauth.HmacAuth
57+ if sigData != nil {
58+ auth = hmacauth.NewHmacAuth(sigData.Hash, []byte(sigData.Key), SignatureHeader, SignatureHeaders)
59+ }
60+
61+ return &httpUpstreamProxy{
62+ upstream: upstream.ID,
63+ handler: proxy,
64+ wsHandler: wsProxy,
65+ auth: auth,
66+ }
67+}
68+
69+// httpUpstreamProxy represents a single HTTP(S) upstream proxy
70+type httpUpstreamProxy struct {
71+ upstream string
72+ handler http.Handler
73+ wsHandler http.Handler
74+ auth hmacauth.HmacAuth
75+}
76+
77+// ServeHTTP proxies requests to the upstream provider while signing the
78+// request headers
79+func (h *httpUpstreamProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
80+ rw.Header().Set("GAP-Upstream-Address", h.upstream)
81+ if h.auth != nil {
82+ req.Header.Set("GAP-Auth", rw.Header().Get("GAP-Auth"))
83+ h.auth.SignRequest(req)
84+ }
85+ if h.wsHandler != nil && strings.EqualFold(req.Header.Get("Connection"), "upgrade") && req.Header.Get("Upgrade") == "websocket" {
86+ h.wsHandler.ServeHTTP(rw, req)
87+ } else {
88+ h.handler.ServeHTTP(rw, req)
89+ }
90+}
91+
92+// newReverseProxy creates a new reverse proxy for proxying requests to upstream
93+// servers based on the upstream configuration provided.
94+// The proxy should render an error page if there are failures connecting to the
95+// upstream server.
96+func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler ProxyErrorHandler) http.Handler {
97+ proxy := httputil.NewSingleHostReverseProxy(target)
98+
99+ // Configure options on the SingleHostReverseProxy
100+ if upstream.FlushInterval != nil {
101+ proxy.FlushInterval = *upstream.FlushInterval
102+ } else {
103+ proxy.FlushInterval = 1 * time.Second
104+ }
105+
106+ if upstream.InsecureSkipTLSVerify {
107+ proxy.Transport = &http.Transport{
108+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
109+ }
110+ }
111+
112+ // Set the request director based on the PassHostHeader option
113+ if !upstream.PassHostHeader {
114+ setProxyUpstreamHostHeader(proxy, target)
115+ } else {
116+ setProxyDirector(proxy)
117+ }
118+
119+ // Set the error handler so that upstream connection failures render the
120+ // error page instead of sending a empty response
121+ if errorHandler != nil {
122+ proxy.ErrorHandler = errorHandler
123+ }
124+ return proxy
125+}
126+
127+// setProxyUpstreamHostHeader sets the proxy.Director so that upstream requests
128+// receive a host header matching the target URL.
129+func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
130+ director := proxy.Director
131+ proxy.Director = func(req *http.Request) {
132+ director(req)
133+ // use RequestURI so that we aren't unescaping encoded slashes in the request path
134+ req.Host = target.Host
135+ req.URL.Opaque = req.RequestURI
136+ req.URL.RawQuery = ""
137+ }
138+}
139+
140+// setProxyDirector sets the proxy.Director so that request URIs are escaped
141+// when proxying to usptream servers.
142+func setProxyDirector(proxy *httputil.ReverseProxy) {
143+ director := proxy.Director
144+ proxy.Director = func(req *http.Request) {
145+ director(req)
146+ // use RequestURI so that we aren't unescaping encoded slashes in the request path
147+ req.URL.Opaque = req.RequestURI
148+ req.URL.RawQuery = ""
149+ }
150+}
151+
152+// newWebSocketReverseProxy creates a new reverse proxy for proxying websocket connections.
153+func newWebSocketReverseProxy(u *url.URL, skipTLSVerify bool) http.Handler {
154+ // This should create the correct scheme for insecure vs secure connections
155+ wsScheme := "ws" + strings.TrimPrefix(u.Scheme, "http")
156+ wsURL := &url.URL{Scheme: wsScheme, Host: u.Host}
157+
158+ wsProxy := wsutil.NewSingleHostReverseProxy(wsURL)
159+ if skipTLSVerify {
160+ wsProxy.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
161+ }
162+ return wsProxy
163+}
pkg/upstream/http_test.gonew file+417
@@ -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+})
pkg/upstream/proxy.gonew file+90
@@ -0,0 +1,90 @@
1+package upstream
2+
3+import (
4+ "fmt"
5+ "html/template"
6+ "net/http"
7+ "net/url"
8+
9+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
10+ "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
11+)
12+
13+// ProxyErrorHandler is a function that will be used to render error pages when
14+// HTTP proxies fail to connect to upstream servers.
15+type ProxyErrorHandler func(http.ResponseWriter, *http.Request, error)
16+
17+// NewProxy creates a new multiUpstreamProxy that can serve requests directed to
18+// multiple upstreams.
19+func NewProxy(upstreams options.Upstreams, sigData *options.SignatureData, errorHandler ProxyErrorHandler) (http.Handler, error) {
20+ m := &multiUpstreamProxy{
21+ serveMux: http.NewServeMux(),
22+ }
23+
24+ for _, upstream := range upstreams {
25+ if upstream.Static {
26+ m.registerStaticResponseHandler(upstream)
27+ continue
28+ }
29+
30+ u, err := url.Parse(upstream.URI)
31+ if err != nil {
32+ return nil, fmt.Errorf("error parsing URI for upstream %q: %w", upstream.ID, err)
33+ }
34+ switch u.Scheme {
35+ case fileScheme:
36+ m.registerFileServer(upstream, u)
37+ case httpScheme, httpsScheme:
38+ m.registerHTTPUpstreamProxy(upstream, u, sigData, errorHandler)
39+ default:
40+ return nil, fmt.Errorf("unknown scheme for upstream %q: %q", upstream.ID, u.Scheme)
41+ }
42+ }
43+ return m, nil
44+}
45+
46+// multiUpstreamProxy will serve requests directed to multiple upstream servers
47+// registered in the serverMux.
48+type multiUpstreamProxy struct {
49+ serveMux *http.ServeMux
50+}
51+
52+// ServerHTTP handles HTTP requests.
53+func (m *multiUpstreamProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
54+ m.serveMux.ServeHTTP(rw, req)
55+}
56+
57+// registerStaticResponseHandler registers a static response handler with at the given path.
58+func (m *multiUpstreamProxy) registerStaticResponseHandler(upstream options.Upstream) {
59+ m.serveMux.Handle(upstream.Path, newStaticResponseHandler(upstream.ID, upstream.StaticCode))
60+}
61+
62+// registerFileServer registers a new fileServer based on the configuration given.
63+func (m *multiUpstreamProxy) registerFileServer(upstream options.Upstream, u *url.URL) {
64+ logger.Printf("mapping path %q => file system %q", upstream.Path, u.Path)
65+ m.serveMux.Handle(upstream.Path, newFileServer(upstream.ID, upstream.Path, u.Path))
66+}
67+
68+// registerHTTPUpstreamProxy registers a new httpUpstreamProxy based on the configuration given.
69+func (m *multiUpstreamProxy) registerHTTPUpstreamProxy(upstream options.Upstream, u *url.URL, sigData *options.SignatureData, errorHandler ProxyErrorHandler) {
70+ logger.Printf("mapping path %q => upstream %q", upstream.Path, upstream.URI)
71+ m.serveMux.Handle(upstream.Path, newHTTPUpstreamProxy(upstream, u, sigData, errorHandler))
72+}
73+
74+// NewProxyErrorHandler creates a ProxyErrorHandler using the template given.
75+func NewProxyErrorHandler(errorTemplate *template.Template, proxyPrefix string) ProxyErrorHandler {
76+ return func(rw http.ResponseWriter, req *http.Request, proxyErr error) {
77+ logger.Printf("Error proxying to upstream server: %v", proxyErr)
78+ rw.WriteHeader(http.StatusBadGateway)
79+ data := struct {
80+ Title string
81+ Message string
82+ ProxyPrefix string
83+ }{
84+ Title: "Bad Gateway",
85+ Message: "Error proxying to upstream server",
86+ ProxyPrefix: proxyPrefix,
87+ }
88+ errorTemplate.Execute(rw, data)
89+ }
90+}
pkg/upstream/proxy_test.gonew file+182
@@ -0,0 +1,182 @@
1+package upstream
2+
3+import (
4+ "crypto"
5+ "encoding/json"
6+ "fmt"
7+ "html/template"
8+ "net/http"
9+ "net/http/httptest"
10+
11+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
12+ . "github.com/onsi/ginkgo"
13+ . "github.com/onsi/ginkgo/extensions/table"
14+ . "github.com/onsi/gomega"
15+)
16+
17+var _ = Describe("Proxy Suite", func() {
18+ var upstreamServer http.Handler
19+
20+ BeforeEach(func() {
21+ sigData := &options.SignatureData{Hash: crypto.SHA256, Key: "secret"}
22+
23+ tmpl, err := template.New("").Parse("{{ .Title }}\n{{ .Message }}\n{{ .ProxyPrefix }}")
24+ Expect(err).ToNot(HaveOccurred())
25+ errorHandler := NewProxyErrorHandler(tmpl, "prefix")
26+
27+ ok := http.StatusOK
28+
29+ upstreams := options.Upstreams{
30+ {
31+ ID: "http-backend",
32+ Path: "/http/",
33+ URI: serverAddr,
34+ },
35+ {
36+ ID: "file-backend",
37+ Path: "/files/",
38+ URI: fmt.Sprintf("file:///%s", filesDir),
39+ },
40+ {
41+ ID: "static-backend",
42+ Path: "/static/",
43+ Static: true,
44+ StaticCode: &ok,
45+ },
46+ {
47+ ID: "bad-http-backend",
48+ Path: "/bad-http/",
49+ URI: "http://::1",
50+ },
51+ {
52+ ID: "single-path-backend",
53+ Path: "/single-path",
54+ Static: true,
55+ StaticCode: &ok,
56+ },
57+ }
58+
59+ upstreamServer, err = NewProxy(upstreams, sigData, errorHandler)
60+ Expect(err).ToNot(HaveOccurred())
61+ })
62+
63+ type proxyTableInput struct {
64+ target string
65+ response testHTTPResponse
66+ }
67+
68+ DescribeTable("Proxy ServerHTTP",
69+ func(in *proxyTableInput) {
70+ req := httptest.NewRequest("", in.target, nil)
71+ rw := httptest.NewRecorder()
72+ // Don't mock the remote Address
73+ req.RemoteAddr = ""
74+
75+ upstreamServer.ServeHTTP(rw, req)
76+
77+ Expect(rw.Code).To(Equal(in.response.code))
78+
79+ // Delete extra headers that aren't relevant to tests
80+ testSanitizeResponseHeader(rw.Header())
81+ Expect(rw.Header()).To(Equal(in.response.header))
82+
83+ body := rw.Body.Bytes()
84+ // If the raw body is set, check that, else check the Request object
85+ if in.response.raw != "" {
86+ Expect(string(body)).To(Equal(in.response.raw))
87+ return
88+ }
89+
90+ // Compare the reflected request to the upstream
91+ request := testHTTPRequest{}
92+ Expect(json.Unmarshal(body, &request)).To(Succeed())
93+ testSanitizeRequestHeader(request.Header)
94+ Expect(request).To(Equal(in.response.request))
95+ },
96+ Entry("with a request to the HTTP service", &proxyTableInput{
97+ target: "http://example.localhost/http/1234",
98+ response: testHTTPResponse{
99+ code: 200,
100+ header: map[string][]string{
101+ gapUpstream: {"http-backend"},
102+ contentType: {applicationJSON},
103+ },
104+ request: testHTTPRequest{
105+ Method: "GET",
106+ URL: "http://example.localhost/http/1234",
107+ Header: map[string][]string{
108+ "Gap-Auth": {""},
109+ "Gap-Signature": {"sha256 ofB1u6+FhEUbFLc3/uGbJVkl7GaN4egFqVvyO3+2I1w="},
110+ },
111+ Body: []byte{},
112+ Host: "example.localhost",
113+ RequestURI: "http://example.localhost/http/1234",
114+ },
115+ },
116+ }),
117+ Entry("with a request to the File backend", &proxyTableInput{
118+ target: "http://example.localhost/files/foo",
119+ response: testHTTPResponse{
120+ code: 200,
121+ header: map[string][]string{
122+ contentType: {textPlainUTF8},
123+ gapUpstream: {"file-backend"},
124+ },
125+ raw: "foo",
126+ },
127+ }),
128+ Entry("with a request to the Static backend", &proxyTableInput{
129+ target: "http://example.localhost/static/bar",
130+ response: testHTTPResponse{
131+ code: 200,
132+ header: map[string][]string{
133+ gapUpstream: {"static-backend"},
134+ },
135+ raw: "Authenticated",
136+ },
137+ }),
138+ Entry("with a request to the bad HTTP backend", &proxyTableInput{
139+ target: "http://example.localhost/bad-http/bad",
140+ response: testHTTPResponse{
141+ code: 502,
142+ header: map[string][]string{
143+ gapUpstream: {"bad-http-backend"},
144+ },
145+ // This tests the error handler
146+ raw: "Bad Gateway\nError proxying to upstream server\nprefix",
147+ },
148+ }),
149+ Entry("with a request to the to an unregistered path", &proxyTableInput{
150+ target: "http://example.localhost/unregistered",
151+ response: testHTTPResponse{
152+ code: 404,
153+ header: map[string][]string{
154+ "X-Content-Type-Options": {"nosniff"},
155+ contentType: {textPlainUTF8},
156+ },
157+ raw: "404 page not found\n",
158+ },
159+ }),
160+ Entry("with a request to the to backend registered to a single path", &proxyTableInput{
161+ target: "http://example.localhost/single-path",
162+ response: testHTTPResponse{
163+ code: 200,
164+ header: map[string][]string{
165+ gapUpstream: {"single-path-backend"},
166+ },
167+ raw: "Authenticated",
168+ },
169+ }),
170+ Entry("with a request to the to a subpath of a backend registered to a single path", &proxyTableInput{
171+ target: "http://example.localhost/single-path/unregistered",
172+ response: testHTTPResponse{
173+ code: 404,
174+ header: map[string][]string{
175+ "X-Content-Type-Options": {"nosniff"},
176+ contentType: {textPlainUTF8},
177+ },
178+ raw: "404 page not found\n",
179+ },
180+ }),
181+ )
182+})
pkg/upstream/static.gonew file+34
@@ -0,0 +1,34 @@
1+package upstream
2+
3+import (
4+ "fmt"
5+ "net/http"
6+)
7+
8+const defaultStaticResponseCode = 200
9+
10+// newStaticResponseHandler creates a new staticResponseHandler that serves a
11+// a static response code.
12+func newStaticResponseHandler(upstream string, code *int) http.Handler {
13+ if code == nil {
14+ c := defaultStaticResponseCode
15+ code = &c
16+ }
17+ return &staticResponseHandler{
18+ code: *code,
19+ upstream: upstream,
20+ }
21+}
22+
23+// staticResponseHandler responds with a static response with the given response code.
24+type staticResponseHandler struct {
25+ code int
26+ upstream string
27+}
28+
29+// ServeHTTP serves a static response.
30+func (s *staticResponseHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
31+ rw.Header().Set("GAP-Upstream-Address", s.upstream)
32+ rw.WriteHeader(s.code)
33+ fmt.Fprintf(rw, "Authenticated")
34+}
pkg/upstream/static_test.gonew file+81
@@ -0,0 +1,81 @@
1+package upstream
2+
3+import (
4+ "crypto/rand"
5+ "io"
6+ "net/http"
7+ "net/http/httptest"
8+
9+ . "github.com/onsi/ginkgo"
10+ . "github.com/onsi/ginkgo/extensions/table"
11+ . "github.com/onsi/gomega"
12+)
13+
14+var _ = Describe("Static Response Suite", func() {
15+ const authenticated = "Authenticated"
16+ var id string
17+
18+ BeforeEach(func() {
19+ // Generate a random id before each test to check the GAP-Upstream-Address
20+ // is being set correctly
21+ idBytes := make([]byte, 16)
22+ _, err := io.ReadFull(rand.Reader, idBytes)
23+ Expect(err).ToNot(HaveOccurred())
24+ id = string(idBytes)
25+ })
26+
27+ type serveHTTPTableInput struct {
28+ requestPath string
29+ staticCode int
30+ expectedBody string
31+ expectedCode int
32+ }
33+
34+ DescribeTable("staticResponse ServeHTTP",
35+ func(in *serveHTTPTableInput) {
36+ var code *int
37+ if in.staticCode != 0 {
38+ code = &in.staticCode
39+ }
40+ handler := newStaticResponseHandler(id, code)
41+
42+ req := httptest.NewRequest("", in.requestPath, nil)
43+ rw := httptest.NewRecorder()
44+ handler.ServeHTTP(rw, req)
45+
46+ Expect(rw.Header().Get("GAP-Upstream-Address")).To(Equal(id))
47+ Expect(rw.Code).To(Equal(in.expectedCode))
48+ Expect(rw.Body.String()).To(Equal(in.expectedBody))
49+ },
50+ Entry("with no given code", &serveHTTPTableInput{
51+ requestPath: "/",
52+ staticCode: 0, // Placeholder for nil
53+ expectedBody: authenticated,
54+ expectedCode: http.StatusOK,
55+ }),
56+ Entry("with status OK", &serveHTTPTableInput{
57+ requestPath: "/abc",
58+ staticCode: http.StatusOK,
59+ expectedBody: authenticated,
60+ expectedCode: http.StatusOK,
61+ }),
62+ Entry("with status NoContent", &serveHTTPTableInput{
63+ requestPath: "/def",
64+ staticCode: http.StatusNoContent,
65+ expectedBody: authenticated,
66+ expectedCode: http.StatusNoContent,
67+ }),
68+ Entry("with status NotFound", &serveHTTPTableInput{
69+ requestPath: "/ghi",
70+ staticCode: http.StatusNotFound,
71+ expectedBody: authenticated,
72+ expectedCode: http.StatusNotFound,
73+ }),
74+ Entry("with status Teapot", &serveHTTPTableInput{
75+ requestPath: "/jkl",
76+ staticCode: http.StatusTeapot,
77+ expectedBody: authenticated,
78+ expectedCode: http.StatusTeapot,
79+ }),
80+ )
81+})
pkg/upstream/upstream_suite_test.gonew file+180
@@ -0,0 +1,180 @@
1+package upstream
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+ "io/ioutil"
7+ "log"
8+ "net/http"
9+ "net/http/httptest"
10+ "os"
11+ "path"
12+ "testing"
13+
14+ "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
15+ . "github.com/onsi/ginkgo"
16+ . "github.com/onsi/gomega"
17+ "golang.org/x/net/websocket"
18+)
19+
20+var (
21+ filesDir string
22+ server *httptest.Server
23+ serverAddr string
24+ invalidServer = "http://::1"
25+)
26+
27+func TestUpstreamSuite(t *testing.T) {
28+ logger.SetOutput(GinkgoWriter)
29+ log.SetOutput(GinkgoWriter)
30+
31+ RegisterFailHandler(Fail)
32+ RunSpecs(t, "Upstream Suite")
33+}
34+
35+var _ = BeforeSuite(func() {
36+ // Set up files for serving via file servers
37+ dir, err := ioutil.TempDir("", "oauth2-proxy-upstream-suite")
38+ Expect(err).ToNot(HaveOccurred())
39+ Expect(ioutil.WriteFile(path.Join(dir, "foo"), []byte("foo"), 0644)).To(Succeed())
40+ Expect(ioutil.WriteFile(path.Join(dir, "bar"), []byte("bar"), 0644)).To(Succeed())
41+ Expect(os.Mkdir(path.Join(dir, "subdir"), os.ModePerm)).To(Succeed())
42+ Expect(ioutil.WriteFile(path.Join(dir, "subdir", "baz"), []byte("baz"), 0644)).To(Succeed())
43+ filesDir = dir
44+
45+ // Set up a webserver that reflects requests
46+ server = httptest.NewServer(&testHTTPUpstream{})
47+ serverAddr = fmt.Sprintf("http://%s", server.Listener.Addr().String())
48+})
49+
50+var _ = AfterSuite(func() {
51+ server.Close()
52+ Expect(os.RemoveAll(filesDir)).To(Succeed())
53+})
54+
55+const (
56+ contentType = "Content-Type"
57+ contentLength = "Content-Length"
58+ acceptEncoding = "Accept-Encoding"
59+ applicationJSON = "application/json"
60+ textPlainUTF8 = "text/plain; charset=utf-8"
61+ gapUpstream = "Gap-Upstream-Address"
62+ gapAuth = "Gap-Auth"
63+ gapSignature = "Gap-Signature"
64+)
65+
66+// testHTTPResponse is a struct used for checking responses in table tests
67+type testHTTPResponse struct {
68+ code int
69+ header http.Header
70+ raw string
71+ request testHTTPRequest
72+}
73+
74+// testHTTPRequest is a struct used to capture the state of a request made to
75+// an upstream during a test
76+type testHTTPRequest struct {
77+ Method string
78+ URL string
79+ Header http.Header
80+ Body []byte
81+ Host string
82+ RequestURI string
83+}
84+
85+type testWebSocketResponse struct {
86+ Message string
87+ Origin string
88+}
89+
90+type testHTTPUpstream struct{}
91+
92+func (t *testHTTPUpstream) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
93+ if req.Header.Get("Upgrade") == "websocket" {
94+ t.websocketHandler().ServeHTTP(rw, req)
95+ } else {
96+ t.serveHTTP(rw, req)
97+ }
98+}
99+
100+func (t *testHTTPUpstream) serveHTTP(rw http.ResponseWriter, req *http.Request) {
101+ request, err := toTestHTTPRequest(req)
102+ if err != nil {
103+ t.writeError(rw, err)
104+ return
105+ }
106+
107+ data, err := json.Marshal(request)
108+ if err != nil {
109+ t.writeError(rw, err)
110+ return
111+ }
112+
113+ rw.Header().Set("Content-Type", "application/json")
114+ rw.Write(data)
115+}
116+
117+func (t *testHTTPUpstream) websocketHandler() http.Handler {
118+ return websocket.Handler(func(ws *websocket.Conn) {
119+ defer ws.Close()
120+ var data []byte
121+ err := websocket.Message.Receive(ws, &data)
122+ if err != nil {
123+ websocket.Message.Send(ws, []byte(err.Error()))
124+ return
125+ }
126+
127+ wsResponse := testWebSocketResponse{
128+ Message: string(data),
129+ Origin: ws.Request().Header.Get("Origin"),
130+ }
131+ err = websocket.JSON.Send(ws, wsResponse)
132+ if err != nil {
133+ websocket.Message.Send(ws, []byte(err.Error()))
134+ return
135+ }
136+ })
137+}
138+
139+func (t *testHTTPUpstream) writeError(rw http.ResponseWriter, err error) {
140+ rw.WriteHeader(500)
141+ if err != nil {
142+ rw.Write([]byte(err.Error()))
143+ }
144+}
145+
146+func toTestHTTPRequest(req *http.Request) (testHTTPRequest, error) {
147+ requestBody := []byte{}
148+ if req.Body != http.NoBody {
149+ var err error
150+ requestBody, err = ioutil.ReadAll(req.Body)
151+ if err != nil {
152+ return testHTTPRequest{}, err
153+ }
154+ }
155+
156+ return testHTTPRequest{
157+ Method: req.Method,
158+ URL: req.URL.String(),
159+ Header: req.Header,
160+ Body: requestBody,
161+ Host: req.Host,
162+ RequestURI: req.RequestURI,
163+ }, nil
164+}
165+
166+// String headers added to the response that we do not want to test
167+func testSanitizeResponseHeader(h http.Header) {
168+ // From HTTP responses
169+ h.Del("Date")
170+ h.Del(contentLength)
171+
172+ // From File responses
173+ h.Del("Accept-Ranges")
174+ h.Del("Last-Modified")
175+}
176+
177+// Strip the accept header that is added by the HTTP Transport
178+func testSanitizeRequestHeader(h http.Header) {
179+ h.Del(acceptEncoding)
180+}
pkg/validation/upstreams.gonew file+113
@@ -0,0 +1,113 @@
1+package validation
2+
3+import (
4+ "fmt"
5+ "net/url"
6+ "time"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
9+)
10+
11+func validateUpstreams(upstreams options.Upstreams) []string {
12+ msgs := []string{}
13+ ids := make(map[string]struct{})
14+ paths := make(map[string]struct{})
15+
16+ for _, upstream := range upstreams {
17+ msgs = append(msgs, validateUpstream(upstream, ids, paths)...)
18+ }
19+
20+ return msgs
21+}
22+
23+// validateUpstream validates that the upstream has valid options and that
24+// the ids and paths are unique across all options
25+func validateUpstream(upstream options.Upstream, ids, paths map[string]struct{}) []string {
26+ msgs := []string{}
27+
28+ if upstream.ID == "" {
29+ msgs = append(msgs, "upstream has empty id: ids are required for all upstreams")
30+ }
31+ if upstream.Path == "" {
32+ msgs = append(msgs, fmt.Sprintf("upstream %q has empty path: paths are required for all upstreams", upstream.ID))
33+ }
34+
35+ // Ensure upstream IDs are unique
36+ if _, ok := ids[upstream.ID]; ok {
37+ msgs = append(msgs, fmt.Sprintf("multiple upstreams found with id %q: upstream ids must be unique", upstream.ID))
38+ }
39+ ids[upstream.ID] = struct{}{}
40+
41+ // Ensure upstream Paths are unique
42+ if _, ok := paths[upstream.Path]; ok {
43+ msgs = append(msgs, fmt.Sprintf("multiple upstreams found with path %q: upstream paths must be unique", upstream.Path))
44+ }
45+ paths[upstream.Path] = struct{}{}
46+
47+ msgs = append(msgs, validateUpstreamURI(upstream)...)
48+ msgs = append(msgs, validateStaticUpstream(upstream)...)
49+ return msgs
50+}
51+
52+// validateStaticUpstream checks that the StaticCode is only set when Static
53+// is set, and that any options that do not make sense for a static upstream
54+// are not set.
55+func validateStaticUpstream(upstream options.Upstream) []string {
56+ msgs := []string{}
57+
58+ if !upstream.Static && upstream.StaticCode != nil {
59+ msgs = append(msgs, fmt.Sprintf("upstream %q has staticCode (%d), but is not a static upstream, set 'static' for a static response", upstream.ID, *upstream.StaticCode))
60+ }
61+
62+ // Checks after this only make sense when the upstream is static
63+ if !upstream.Static {
64+ return msgs
65+ }
66+
67+ if upstream.URI != "" {
68+ msgs = append(msgs, fmt.Sprintf("upstream %q has uri, but is a static upstream, this will have no effect.", upstream.ID))
69+ }
70+ if upstream.InsecureSkipTLSVerify {
71+ msgs = append(msgs, fmt.Sprintf("upstream %q has insecureSkipTLSVerify, but is a static upstream, this will have no effect.", upstream.ID))
72+ }
73+ if upstream.FlushInterval != nil && *upstream.FlushInterval != time.Second {
74+ msgs = append(msgs, fmt.Sprintf("upstream %q has flushInterval, but is a static upstream, this will have no effect.", upstream.ID))
75+ }
76+ if !upstream.PassHostHeader {
77+ msgs = append(msgs, fmt.Sprintf("upstream %q has passHostHeader, but is a static upstream, this will have no effect.", upstream.ID))
78+ }
79+ if !upstream.ProxyWebSockets {
80+ msgs = append(msgs, fmt.Sprintf("upstream %q has proxyWebSockets, but is a static upstream, this will have no effect.", upstream.ID))
81+ }
82+
83+ return msgs
84+}
85+
86+func validateUpstreamURI(upstream options.Upstream) []string {
87+ msgs := []string{}
88+
89+ if !upstream.Static && upstream.URI == "" {
90+ msgs = append(msgs, fmt.Sprintf("upstream %q has empty uri: uris are required for all non-static upstreams", upstream.ID))
91+ return msgs
92+ }
93+
94+ // Checks after this only make sense the upstream is not static
95+ if upstream.Static {
96+ return msgs
97+ }
98+
99+ u, err := url.Parse(upstream.URI)
100+ if err != nil {
101+ msgs = append(msgs, fmt.Sprintf("upstream %q has invalid uri: %v", upstream.ID, err))
102+ return msgs
103+ }
104+
105+ switch u.Scheme {
106+ case "http", "https", "file":
107+ // Valid, do nothing
108+ default:
109+ msgs = append(msgs, fmt.Sprintf("upstream %q has invalid scheme: %q", upstream.ID, u.Scheme))
110+ }
111+
112+ return msgs
113+}
pkg/validation/upstreams_test.gonew file+191
@@ -0,0 +1,191 @@
1+package validation
2+
3+import (
4+ "time"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/ginkgo/extensions/table"
9+ . "github.com/onsi/gomega"
10+)
11+
12+var _ = Describe("Upstreams", func() {
13+ type validateUpstreamTableInput struct {
14+ upstreams options.Upstreams
15+ errStrings []string
16+ }
17+
18+ flushInterval := 5 * time.Second
19+ staticCode200 := 200
20+
21+ validHTTPUpstream := options.Upstream{
22+ ID: "validHTTPUpstream",
23+ Path: "/validHTTPUpstream",
24+ URI: "http://localhost:8080",
25+ }
26+ validStaticUpstream := options.Upstream{
27+ ID: "validStaticUpstream",
28+ Path: "/validStaticUpstream",
29+ Static: true,
30+ PassHostHeader: true, // This would normally be defaulted
31+ ProxyWebSockets: true, // this would normally be defaulted
32+ }
33+ validFileUpstream := options.Upstream{
34+ ID: "validFileUpstream",
35+ Path: "/validFileUpstream",
36+ URI: "file://var/lib/foo",
37+ }
38+
39+ emptyIDMsg := "upstream has empty id: ids are required for all upstreams"
40+ emptyPathMsg := "upstream \"foo\" has empty path: paths are required for all upstreams"
41+ emptyURIMsg := "upstream \"foo\" has empty uri: uris are required for all non-static upstreams"
42+ invalidURIMsg := "upstream \"foo\" has invalid uri: parse \":\": missing protocol scheme"
43+ invalidURISchemeMsg := "upstream \"foo\" has invalid scheme: \"ftp\""
44+ staticWithURIMsg := "upstream \"foo\" has uri, but is a static upstream, this will have no effect."
45+ staticWithInsecureMsg := "upstream \"foo\" has insecureSkipTLSVerify, but is a static upstream, this will have no effect."
46+ staticWithFlushIntervalMsg := "upstream \"foo\" has flushInterval, but is a static upstream, this will have no effect."
47+ staticWithPassHostHeaderMsg := "upstream \"foo\" has passHostHeader, but is a static upstream, this will have no effect."
48+ staticWithProxyWebSocketsMsg := "upstream \"foo\" has proxyWebSockets, but is a static upstream, this will have no effect."
49+ multipleIDsMsg := "multiple upstreams found with id \"foo\": upstream ids must be unique"
50+ multiplePathsMsg := "multiple upstreams found with path \"/foo\": upstream paths must be unique"
51+ staticCodeMsg := "upstream \"foo\" has staticCode (200), but is not a static upstream, set 'static' for a static response"
52+
53+ DescribeTable("validateUpstreams",
54+ func(o *validateUpstreamTableInput) {
55+ Expect(validateUpstreams(o.upstreams)).To(ConsistOf(o.errStrings))
56+ },
57+ Entry("with no upstreams", &validateUpstreamTableInput{
58+ upstreams: options.Upstreams{},
59+ errStrings: []string{},
60+ }),
61+ Entry("with valid upstreams", &validateUpstreamTableInput{
62+ upstreams: options.Upstreams{
63+ validHTTPUpstream,
64+ validStaticUpstream,
65+ validFileUpstream,
66+ },
67+ errStrings: []string{},
68+ }),
69+ Entry("with an empty ID", &validateUpstreamTableInput{
70+ upstreams: options.Upstreams{
71+ {
72+ ID: "",
73+ Path: "/foo",
74+ URI: "http://localhost:8080",
75+ },
76+ },
77+ errStrings: []string{emptyIDMsg},
78+ }),
79+ Entry("with an empty Path", &validateUpstreamTableInput{
80+ upstreams: options.Upstreams{
81+ {
82+ ID: "foo",
83+ Path: "",
84+ URI: "http://localhost:8080",
85+ },
86+ },
87+ errStrings: []string{emptyPathMsg},
88+ }),
89+ Entry("with an empty Path", &validateUpstreamTableInput{
90+ upstreams: options.Upstreams{
91+ {
92+ ID: "foo",
93+ Path: "",
94+ URI: "http://localhost:8080",
95+ },
96+ },
97+ errStrings: []string{emptyPathMsg},
98+ }),
99+ Entry("with an empty URI", &validateUpstreamTableInput{
100+ upstreams: options.Upstreams{
101+ {
102+ ID: "foo",
103+ Path: "/foo",
104+ URI: "",
105+ },
106+ },
107+ errStrings: []string{emptyURIMsg},
108+ }),
109+ Entry("with an invalid URI", &validateUpstreamTableInput{
110+ upstreams: options.Upstreams{
111+ {
112+ ID: "foo",
113+ Path: "/foo",
114+ URI: ":",
115+ },
116+ },
117+ errStrings: []string{invalidURIMsg},
118+ }),
119+ Entry("with an invalid URI scheme", &validateUpstreamTableInput{
120+ upstreams: options.Upstreams{
121+ {
122+ ID: "foo",
123+ Path: "/foo",
124+ URI: "ftp://foo",
125+ },
126+ },
127+ errStrings: []string{invalidURISchemeMsg},
128+ }),
129+ Entry("with a static upstream and invalid optons", &validateUpstreamTableInput{
130+ upstreams: options.Upstreams{
131+ {
132+ ID: "foo",
133+ Path: "/foo",
134+ URI: "ftp://foo",
135+ Static: true,
136+ FlushInterval: &flushInterval,
137+ PassHostHeader: false,
138+ ProxyWebSockets: false,
139+ InsecureSkipTLSVerify: true,
140+ },
141+ },
142+ errStrings: []string{
143+ staticWithURIMsg,
144+ staticWithInsecureMsg,
145+ staticWithFlushIntervalMsg,
146+ staticWithPassHostHeaderMsg,
147+ staticWithProxyWebSocketsMsg,
148+ },
149+ }),
150+ Entry("with duplicate IDs", &validateUpstreamTableInput{
151+ upstreams: options.Upstreams{
152+ {
153+ ID: "foo",
154+ Path: "/foo1",
155+ URI: "http://foo",
156+ },
157+ {
158+ ID: "foo",
159+ Path: "/foo2",
160+ URI: "http://foo",
161+ },
162+ },
163+ errStrings: []string{multipleIDsMsg},
164+ }),
165+ Entry("with duplicate Paths", &validateUpstreamTableInput{
166+ upstreams: options.Upstreams{
167+ {
168+ ID: "foo1",
169+ Path: "/foo",
170+ URI: "http://foo",
171+ },
172+ {
173+ ID: "foo2",
174+ Path: "/foo",
175+ URI: "http://foo",
176+ },
177+ },
178+ errStrings: []string{multiplePathsMsg},
179+ }),
180+ Entry("when a static code is supplied without static", &validateUpstreamTableInput{
181+ upstreams: options.Upstreams{
182+ {
183+ ID: "foo",
184+ Path: "/foo",
185+ StaticCode: &staticCode200,
186+ },
187+ },
188+ errStrings: []string{emptyURIMsg, staticCodeMsg},
189+ }),
190+ )
191+})
pkg/validation/validation_suite_test.gonew file+16
@@ -0,0 +1,16 @@
1+package validation
2+
3+import (
4+ "testing"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/gomega"
9+)
10+
11+func TestValidationSuite(t *testing.T) {
12+ logger.SetOutput(GinkgoWriter)
13+
14+ RegisterFailHandler(Fail)
15+ RunSpecs(t, "Validation Suite")
16+}