From 8f0b61870a4e837b7e4e5fd40d5e086f1ae8db21 Mon Sep 17 00:00:00 2001 From: Phil Pennock Date: Fri, 23 Oct 2020 20:44:34 -0400 Subject: [PATCH] Add websocket browser_redirect_url config option If an HTTP(S) request is received without the Upgrade: header, then if this new option is set then we'll assume it's a browser and issue a redirect. There's no flexibility based upon the passed in URL; this should help with redirecting people to dashboards or other tooling. --- server/opts.go | 8 ++++++++ server/websocket.go | 24 +++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/server/opts.go b/server/opts.go index bd67267b..4aab525e 100644 --- a/server/opts.go +++ b/server/opts.go @@ -263,6 +263,12 @@ type WebsocketOpts struct { // The host:port to advertise to websocket clients in the cluster. Advertise string + // If someone visits the NATS server with a browser, they will send a GET + // without the Upgrade header. + // For convenience, you can supply a URL to be used as an HTTP redirect, + // instead of erroring out. + BrowserRedirectURL string + // If no user name is provided when a client connects, will default to the // matching user from the global list of users in `Options.Users`. NoAuthUser string @@ -3453,6 +3459,8 @@ func parseWebsocket(v interface{}, o *Options, errors *[]error, warnings *[]erro o.Websocket.JWTCookie = mv.(string) case "no_auth_user": o.Websocket.NoAuthUser = mv.(string) + case "browser_redirect_url": + o.Websocket.BrowserRedirectURL = mv.(string) default: if !tk.IsUsedVariable() { err := &unknownConfigFieldErr{ diff --git a/server/websocket.go b/server/websocket.go index 7b10877d..661e5226 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -529,7 +529,13 @@ func (s *Server) wsUpgrade(w http.ResponseWriter, r *http.Request) (*wsUpgradeRe } // Point 3. if !wsHeaderContains(r.Header, "Upgrade", "websocket") { - return nil, wsReturnHTTPError(w, http.StatusBadRequest, "invalid value for header 'Upgrade'") + _, haveUpgrade := r.Header["Upgrade"] + if s.opts.Websocket.BrowserRedirectURL != "" && !haveUpgrade { + // We still have to return an error, to prevent the connection upgrade. + return nil, wsHTTPRedirectAndReturnError(w, s.opts.Websocket.BrowserRedirectURL) + } else { + return nil, wsReturnHTTPError(w, http.StatusBadRequest, "invalid value for header 'Upgrade'") + } } // Point 4. if !wsHeaderContains(r.Header, "Connection", "Upgrade") { @@ -645,6 +651,15 @@ func wsReturnHTTPError(w http.ResponseWriter, status int, reason string) error { return err } +// Send a redirect to a browser and return an error +func wsHTTPRedirectAndReturnError(w http.ResponseWriter, redirectURL string) error { + err := fmt.Errorf("not a websocket request, issued redirect") + w.Header().Set("Sec-Websocket-Version", "13") + w.Header().Set("Location", redirectURL) + http.Error(w, http.StatusText(http.StatusFound), http.StatusFound) + return err +} + // If the server is configured to accept any origin, then this function returns // `nil` without checking if the Origin is present and valid. // Otherwise, this will check that the Origin matches the same origine or @@ -761,6 +776,13 @@ func validateWebsocketOptions(o *Options) error { return fmt.Errorf("trusted operators or trusted keys configuration is required for JWT authentication via cookie %q", wo.JWTCookie) } } + // Ensure that we're not given random junk which might have us do + // non-sensical things later. + if wo.BrowserRedirectURL != "" { + if _, err := url.Parse(wo.BrowserRedirectURL); err != nil { + return fmt.Errorf("unable to parse get redirect URL: %v", err) + } + } return nil }