(proxy, internal/config, internal/log, docs): opt-in websocket support

This commit is contained in:
Tejasvi Nareddy 2019-05-30 22:20:22 -04:00 committed by Teju Nareddy
parent cf61c6be3d
commit f966e5ab19
6 changed files with 55 additions and 4 deletions

View file

@ -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`

View file

@ -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
}

View file

@ -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))
})

View file

@ -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)
})
}

View file

@ -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 {

View file

@ -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.