From f966e5ab19d82b2351716743c5b1f1b0a6c2e26d Mon Sep 17 00:00:00 2001 From: Tejasvi Nareddy Date: Thu, 30 May 2019 22:20:22 -0400 Subject: [PATCH] (proxy, internal/config, internal/log, docs): opt-in websocket support --- docs/reference/readme.md | 13 +++++++++++++ internal/config/options.go | 5 +++++ internal/log/middleware.go | 2 +- proxy/handlers.go | 25 +++++++++++++++++++++++++ proxy/handlers_test.go | 11 +++++++++-- proxy/proxy.go | 3 ++- 6 files changed, 55 insertions(+), 4 deletions(-) diff --git a/docs/reference/readme.md b/docs/reference/readme.md index b2f1152a5..b1a7cd060 100644 --- a/docs/reference/readme.md +++ b/docs/reference/readme.md @@ -143,6 +143,19 @@ Timeouts set the global server timeouts. For route-specific timeouts, see [polic If set, the HTTP Redirect Address specifies the host and port to redirect http to https traffic on. If unset, no redirect server is started. +### Websocket Connections + +- Environmental Variable: `ALLOW_WEBSOCKETS` +- Config File Key: `allow_websockets` +- Type: `bool` +- Default: `false` + +If set, enables proxying of websocket connections. +Otherwise the proxy responds with `400 Bad Request` to all websocket connections. + +**Use with caution:** By definition, websockets are long-lived connections, so [global timeouts](#global-timeouts) are not enforced. +Allowing websocket connections to the proxy could result in abuse via DOS attacks. + ### Policy - Environmental Variable: `POLICY` diff --git a/internal/config/options.go b/internal/config/options.go index ef5decbff..3c9db9983 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -129,6 +129,10 @@ type Options struct { // Sub-routes Routes map[string]string `mapstructure:"routes"` DefaultUpstreamTimeout time.Duration `mapstructure:"default_upstream_timeout"` + + // Enable proxying of websocket connections. Defaults to "false". + // Caution: Enabling this feature could result in abuse via DOS attacks. + AllowWebsockets bool `mapstructure:"allow_websockets"` } // NewOptions returns a new options struct with default values @@ -160,6 +164,7 @@ func NewOptions() *Options { AuthenticateInternalAddr: new(url.URL), AuthorizeURL: new(url.URL), RefreshCooldown: time.Duration(5 * time.Minute), + AllowWebsockets: false, } return o } diff --git a/internal/log/middleware.go b/internal/log/middleware.go index 4d47c42de..ac6df227d 100644 --- a/internal/log/middleware.go +++ b/internal/log/middleware.go @@ -164,7 +164,7 @@ func AccessHandler(f func(r *http.Request, status, size int, duration time.Durat return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() - lw := NewWrapResponseWriter(w, 2) + lw := NewWrapResponseWriter(w, r.ProtoMajor) next.ServeHTTP(lw, r) f(r, lw.Status(), lw.BytesWritten(), time.Since(start)) }) diff --git a/proxy/handlers.go b/proxy/handlers.go index 016d18980..8beb31928 100644 --- a/proxy/handlers.go +++ b/proxy/handlers.go @@ -3,6 +3,7 @@ package proxy // import "github.com/pomerium/pomerium/proxy" import ( "encoding/base64" "fmt" + "github.com/pomerium/pomerium/internal/config" "net/http" "net/url" "strings" @@ -493,3 +494,27 @@ func (p *Proxy) GetSignOutURL(authenticateURL, redirectURL *url.URL) *url.URL { func extendDeadline(ttl time.Duration) time.Time { return time.Now().Add(ttl).Truncate(time.Second) } + +// websocketHandlerFunc splits request serving with timeouts depending on the protocol +func websocketHandlerFunc(baseHandler http.Handler, timeoutHandler http.Handler, o *config.Options) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // Do not use timeouts for websockets because they are long-lived connections. + if r.ProtoMajor == 1 && + strings.EqualFold(r.Header.Get("Connection"), "upgrade") && + strings.EqualFold(r.Header.Get("Upgrade"), "websocket") { + + if o.AllowWebsockets { + baseHandler.ServeHTTP(w, r) + return + } + + log.FromRequest(r).Warn().Msg("proxy: attempt to proxy a websocket connection, but websocket support is disabled in the configuration") + httputil.ErrorResponse(w, r, "websockets not supported by proxy", http.StatusBadRequest) + return + } + + // All other non-websocket requests are served with timeouts to prevent abuse + timeoutHandler.ServeHTTP(w, r) + }) +} diff --git a/proxy/handlers_test.go b/proxy/handlers_test.go index 65d549769..2fbca421b 100644 --- a/proxy/handlers_test.go +++ b/proxy/handlers_test.go @@ -275,15 +275,18 @@ func TestProxy_Proxy(t *testing.T) { })) defer ts.Close() - opts := testOptionsTestServer(ts.URL) + opts, optsWs := testOptionsTestServer(ts.URL), testOptionsTestServer(ts.URL) optsCORS := testOptionsWithCORS(ts.URL) optsPublic := testOptionsWithPublicAccess(ts.URL) + optsWs.AllowWebsockets = true - defaultHeaders, goodCORSHeaders, badCORSHeaders := http.Header{}, http.Header{}, http.Header{} + defaultHeaders, goodCORSHeaders, badCORSHeaders, headersWs := http.Header{}, http.Header{}, http.Header{}, http.Header{} goodCORSHeaders.Set("origin", "anything") goodCORSHeaders.Set("access-control-request-method", "anything") // missing "Origin" badCORSHeaders.Set("access-control-request-method", "anything") + headersWs.Set("Connection", "Upgrade") + headersWs.Set("Upgrade", "websocket") tests := []struct { name string @@ -315,6 +318,10 @@ func TestProxy_Proxy(t *testing.T) { {"public access, but unknown host", optsPublic, http.MethodGet, defaultHeaders, "https://nothttpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: false}, http.StatusUnauthorized}, // no session, redirect to login {"no http found (no session)", opts, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{LoadError: http.ErrNoCookie}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest}, + // Should be expecting a 101 Switching Protocols, but expect a 200 OK because we don't have a websocket backend to respond + {"ws supported, ws connection", optsWs, http.MethodGet, headersWs, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK}, + {"ws supported, http connection", optsWs, http.MethodGet, defaultHeaders, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusOK}, + {"ws unsupported, ws connection", opts, http.MethodGet, headersWs, "https://httpbin.corp.example", &sessions.MockSessionStore{Session: goodSession}, clients.MockAuthenticate{ValidateResponse: true}, clients.MockAuthorize{AuthorizeResponse: true}, http.StatusBadRequest}, } for _, tt := range tests { diff --git a/proxy/proxy.go b/proxy/proxy.go index 59f372435..bc2afe1fe 100755 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -273,7 +273,8 @@ func NewReverseProxyHandler(o *config.Options, proxy *httputil.ReverseProxy, rou timeout = route.UpstreamTimeout } timeoutMsg := fmt.Sprintf("%s failed to respond within the %s timeout period", route.Destination.Host, timeout) - return http.TimeoutHandler(up, timeout, timeoutMsg), nil + timeoutHandler := http.TimeoutHandler(up, timeout, timeoutMsg) + return websocketHandlerFunc(up, timeoutHandler, o), nil } // urlParse wraps url.Parse to add a scheme if none-exists.