(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. 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 ### Policy
- Environmental Variable: `POLICY` - Environmental Variable: `POLICY`

View file

@ -129,6 +129,10 @@ type Options struct {
// Sub-routes // Sub-routes
Routes map[string]string `mapstructure:"routes"` Routes map[string]string `mapstructure:"routes"`
DefaultUpstreamTimeout time.Duration `mapstructure:"default_upstream_timeout"` 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 // NewOptions returns a new options struct with default values
@ -160,6 +164,7 @@ func NewOptions() *Options {
AuthenticateInternalAddr: new(url.URL), AuthenticateInternalAddr: new(url.URL),
AuthorizeURL: new(url.URL), AuthorizeURL: new(url.URL),
RefreshCooldown: time.Duration(5 * time.Minute), RefreshCooldown: time.Duration(5 * time.Minute),
AllowWebsockets: false,
} }
return o 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 func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now() start := time.Now()
lw := NewWrapResponseWriter(w, 2) lw := NewWrapResponseWriter(w, r.ProtoMajor)
next.ServeHTTP(lw, r) next.ServeHTTP(lw, r)
f(r, lw.Status(), lw.BytesWritten(), time.Since(start)) f(r, lw.Status(), lw.BytesWritten(), time.Since(start))
}) })

View file

@ -3,6 +3,7 @@ package proxy // import "github.com/pomerium/pomerium/proxy"
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"github.com/pomerium/pomerium/internal/config"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -493,3 +494,27 @@ func (p *Proxy) GetSignOutURL(authenticateURL, redirectURL *url.URL) *url.URL {
func extendDeadline(ttl time.Duration) time.Time { func extendDeadline(ttl time.Duration) time.Time {
return time.Now().Add(ttl).Truncate(time.Second) 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() defer ts.Close()
opts := testOptionsTestServer(ts.URL) opts, optsWs := testOptionsTestServer(ts.URL), testOptionsTestServer(ts.URL)
optsCORS := testOptionsWithCORS(ts.URL) optsCORS := testOptionsWithCORS(ts.URL)
optsPublic := testOptionsWithPublicAccess(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("origin", "anything")
goodCORSHeaders.Set("access-control-request-method", "anything") goodCORSHeaders.Set("access-control-request-method", "anything")
// missing "Origin" // missing "Origin"
badCORSHeaders.Set("access-control-request-method", "anything") badCORSHeaders.Set("access-control-request-method", "anything")
headersWs.Set("Connection", "Upgrade")
headersWs.Set("Upgrade", "websocket")
tests := []struct { tests := []struct {
name string 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}, {"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 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}, {"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 { for _, tt := range tests {

View file

@ -273,7 +273,8 @@ func NewReverseProxyHandler(o *config.Options, proxy *httputil.ReverseProxy, rou
timeout = route.UpstreamTimeout timeout = route.UpstreamTimeout
} }
timeoutMsg := fmt.Sprintf("%s failed to respond within the %s timeout period", route.Destination.Host, timeout) 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. // urlParse wraps url.Parse to add a scheme if none-exists.