mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-29 10:26:29 +02:00
(proxy, internal/config, internal/log, docs): opt-in websocket support
This commit is contained in:
parent
cf61c6be3d
commit
f966e5ab19
6 changed files with 55 additions and 4 deletions
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Reference in a new issue