From edf9609d34ec13e9ea132a4c221aa272de14ba90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Tur=C3=B3ci?= <64769322+mturoci@users.noreply.github.com> Date: Tue, 5 Mar 2024 15:49:41 +0100 Subject: [PATCH] feat: Allow specifying websocket origins. #2230 (#2279) --- client.go | 4 ---- cmd/wave/main.go | 9 +++++++ conf.go | 2 ++ server.go | 2 +- socket.go | 18 +++++++++++--- website/docs/configuration.md | 45 +++++++++++++++++++---------------- 6 files changed, 51 insertions(+), 29 deletions(-) diff --git a/client.go b/client.go index bec2679ba6..df88541228 100644 --- a/client.go +++ b/client.go @@ -37,10 +37,6 @@ var ( notFoundMsg = []byte(`{"e":"not_found"}`) disconnectMsg = []byte(`{"data": {"":{"@system":{"client_disconnect":true}}}}`) clearStateMsg = []byte(`{"c":1}`) - upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, // TODO review - WriteBufferSize: 1024, // TODO review - } ) // BootMsg represents the initial message sent to an app when a client first connects to it. diff --git a/cmd/wave/main.go b/cmd/wave/main.go index 9b70b537c5..f357d2d92f 100644 --- a/cmd/wave/main.go +++ b/cmd/wave/main.go @@ -194,6 +194,15 @@ func main() { panic(err) } + if conf.AllowedOrigins != "" { + origins := strings.Split(conf.AllowedOrigins, ",") + allowedOrigins := make(map[string]bool, len(origins)) + for _, origin := range origins { + allowedOrigins[strings.TrimSpace(origin)] = true + } + serverConf.AllowedOrigins = allowedOrigins + } + serverConf.WebDir, _ = filepath.Abs(conf.WebDir) serverConf.DataDir, _ = filepath.Abs(conf.DataDir) serverConf.Version = Version diff --git a/conf.go b/conf.go index 78f368c46a..0b5e2485dc 100644 --- a/conf.go +++ b/conf.go @@ -53,6 +53,7 @@ type ServerConf struct { KeepAppLive bool PingInterval time.Duration ReconnectTimeout time.Duration + AllowedOrigins map[string]bool } type AuthConf struct { @@ -114,4 +115,5 @@ type Conf struct { KeepAppLive bool `cfg:"keep-app-live" env:"H2O_WAVE_KEEP_APP_LIVE" cfgDefault:"false" cfgHelper:"do not unregister unresponsive apps"` Conf string `cfg:"conf" env:"H2O_WAVE_CONF" cfgDefault:".env" cfgHelper:"path to configuration file"` ReconnectTimeout string `cfg:"reconnect-timeout" env:"H2O_WAVE_RECONNECT_TIMEOUT" cfgDefault:"2s" cfgHelper:"Time to wait for reconnect before dropping the client"` + AllowedOrigins string `cfg:"allowed-origins" env:"H2O_WAVE_ALLOWED_ORIGINS" cfgDefault:"" cfgHelper:"comma-separated list of allowed origins (e.g. http://foo.com) for websocket upgrades"` } diff --git a/server.go b/server.go index 84aa683f66..6c79477236 100644 --- a/server.go +++ b/server.go @@ -107,7 +107,7 @@ func Run(conf ServerConf) { handle("_auth/refresh", newRefreshHandler(auth, conf.Keychain)) } - handle("_s/", newSocketServer(broker, auth, conf.Editable, conf.BaseURL, conf.ForwardedHeaders, conf.PingInterval, conf.ReconnectTimeout)) + handle("_s/", newSocketServer(broker, auth, conf)) fileDir := filepath.Join(conf.DataDir, "f") handle("_f/", newFileServer(fileDir, conf.Keychain, auth, conf.BaseURL+"_f")) diff --git a/socket.go b/socket.go index c2668bd307..fe99027da9 100644 --- a/socket.go +++ b/socket.go @@ -32,14 +32,26 @@ type SocketServer struct { forwardedHeaders map[string]bool pingInterval time.Duration reconnectTimeout time.Duration + upgrader websocket.Upgrader } -func newSocketServer(broker *Broker, auth *Auth, editable bool, baseURL string, forwardedHeaders map[string]bool, pingInterval, reconnectTimeout time.Duration) *SocketServer { - return &SocketServer{broker, auth, editable, baseURL, forwardedHeaders, pingInterval, reconnectTimeout} +func newSocketServer(broker *Broker, auth *Auth, conf ServerConf) *SocketServer { + var checkOrigin func(*http.Request) bool + if conf.AllowedOrigins != nil { + checkOrigin = func(r *http.Request) bool { + return conf.AllowedOrigins[r.Header.Get("Origin")] + } + } + upgrader := websocket.Upgrader{ + ReadBufferSize: 1024, // TODO review + WriteBufferSize: 1024, // TODO review + CheckOrigin: checkOrigin, + } + return &SocketServer{broker, auth, conf.Editable, conf.BaseURL, conf.ForwardedHeaders, conf.PingInterval, conf.ReconnectTimeout, upgrader} } func (s *SocketServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { - conn, err := upgrader.Upgrade(w, r, nil) + conn, err := s.upgrader.Upgrade(w, r, nil) if err != nil { echo(Log{"t": "socket_upgrade", "err": err.Error()}) return diff --git a/website/docs/configuration.md b/website/docs/configuration.md index 780c4d5a5e..0b3b607036 100644 --- a/website/docs/configuration.md +++ b/website/docs/configuration.md @@ -6,15 +6,16 @@ title: Configuration Wave allows starting Wave server in 2 ways: -* via `wave run` command - this automatically starts Wave server (waved) under the hood, useful for development. -* via Wave server binary (waved) - useful during deployment or when you need to run your Wave server on a different machine than your app. +- via `wave run` command - this automatically starts Wave server (waved) under the hood, useful for development. +- via Wave server binary (waved) - useful during deployment or when you need to run your Wave server on a different machine than your app. Wave can be configured via configuration (`.env`) file, environment variables or command line arguments with the following priority: `cmd arg > env var > config > default`. + | ENV var or config (wave run or waved) | CLI args (waved) | Description | -|----------------------------------------|---------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| -------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | H2O_WAVE_ACCESS_KEY_ID | -access-key-id string | default API access key ID (default "access_key_id") | | H2O_WAVE_ACCESS_KEY_SECRET | -access-key-secret string | default API access key secret (default "access_key_secret") | | H2O_WAVE_ACCESS_KEYCHAIN | -access-keychain string | path to file containing API access keys (default ".wave-keychain") | @@ -23,7 +24,7 @@ Wave can be configured via configuration (`.env`) file, environment variables or | H2O_WAVE_DATA_DIR | -data-dir string | directory to store site data (default "./data"). | | H2O_WAVE_DEBUG [^1] | -debug | enable debug mode (profiling, inspection, etc.) | | H2O_WAVE_EDITABLE [^1] | -editable | allow users to edit web pages | -| H2O_WAVE_FORWARDED_HTTP_HEADERS | -forwarded-http-headers string | comma-separated list of case-insensitive HTTP header keys to forward to the Wave app from the browser WS connection. If not specified, defaults to '*' - all headers are allowed. If set to an empty string, no headers are forwarded. | +| H2O_WAVE_FORWARDED_HTTP_HEADERS | -forwarded-http-headers string | comma-separated list of case-insensitive HTTP header keys to forward to the Wave app from the browser WS connection. If not specified, defaults to '\*' - all headers are allowed. If set to an empty string, no headers are forwarded. | | H2O_WAVE_HTTP_HEADERS_FILE | -http-headers-file string | path to a MIME-formatted file containing additional HTTP headers to add to responses from the server | | H2O_WAVE_INIT | -init string | initialize site content from AOF log | | H2O_WAVE_LISTEN | -listen string | listen on this address (default ":10101") | @@ -58,7 +59,9 @@ Wave can be configured via configuration (`.env`) file, environment variables or | H2O_WAVE_WEB_DIR | -web-dir string | directory to serve web assets from (default "./www") | | H2O_WAVE_CONF | -conf string | path to a configuration file (default ".env") | | H2O_WAVE_PING_INTERVAL | -ping-interval string | how often should ping messages be sent (e.g. 60s or 1m or 0.1h) to keep the websocket connection alive (default "50s") | -| H2O_WAVE_RECONNECT_TIMEOUT | -reconnect-timeout string | Time to wait for reconnect before dropping the client (default "2s") | +| H2O_WAVE_RECONNECT_TIMEOUT | -reconnect-timeout string | Time to wait for reconnect before dropping the client (default "2s") | +| H2O_WAVE_ALLOWED_ORIGINS | -allowed-origins string | comma-separated list of allowed origins (e.g. http://foo.com) for websocket upgrades | + [^1]: `1`, `t`, `true` to enable; `0`, `f`, `false` to disable (case insensitive). [^2]: Use OS-specific path list separator to specify multiple arguments - `:` for Linux/OSX and `;` for Windows. For example, `H2O_WAVE_PUBLIC_DIR=/images/@./files/images:/downloads/@./files/downloads`. @@ -70,22 +73,22 @@ Those that do not start `waved` manually (but use `wave run` instead) and would ### Supported size units (case insensitive) -* Exabyte: `E` / `EB` / `EIB`. -* Petabyte: `P` / `PB` / `PIB`. -* Terabyte: `T` / `TB` / `TIB`. -* Gigabyte: `G` / `GB` / `GIB`. -* Megabyte: `M` / `MB` / `MIB`. -* Kilobyte: `K` / `KB` / `KIB`. -* Byte: `B` +- Exabyte: `E` / `EB` / `EIB`. +- Petabyte: `P` / `PB` / `PIB`. +- Terabyte: `T` / `TB` / `TIB`. +- Gigabyte: `G` / `GB` / `GIB`. +- Megabyte: `M` / `MB` / `MIB`. +- Kilobyte: `K` / `KB` / `KIB`. +- Byte: `B` ### Suported time units -* Nanosecond: `ns`. -* Microsecond: `us` (or `µs`). -* Milisecond `ms`. -* Second: `s`. -* Minute: `m`. -* Hour: `h`. +- Nanosecond: `ns`. +- Microsecond: `us` (or `µs`). +- Milisecond `ms`. +- Second: `s`. +- Minute: `m`. +- Hour: `h`. ### Public/Private dirs @@ -115,7 +118,7 @@ Wave serves whole directories as they are. This means that these directories are ### TLS verification -During development, you might want to test out TLS encryption, e.g. communication between Wave server and Keycloak. The easiest thing to do is to generate a self-signed certificate. However, Wave server verifies certificates for all communication by default, thus would throw an error for a self-signed one. ***FOR DEVELOPMENT PURPOSES ONLY***, it's possible to turn off the check using either `H2O_WAVE_NO_TLS_VERIFY` environment variable or `no-tls-verify` parameter. +During development, you might want to test out TLS encryption, e.g. communication between Wave server and Keycloak. The easiest thing to do is to generate a self-signed certificate. However, Wave server verifies certificates for all communication by default, thus would throw an error for a self-signed one. **_FOR DEVELOPMENT PURPOSES ONLY_**, it's possible to turn off the check using either `H2O_WAVE_NO_TLS_VERIFY` environment variable or `no-tls-verify` parameter. :::warning **Disabling TLS verification is a security risk.** Make sure TLS is not disabled in production environments. @@ -201,8 +204,8 @@ By default, Wave apps do not load any third-party trackers or capture usage data Once enabled, your app's UI will send events every time the user performs some kind of action that triggers a request from the browser to your app. Only two kinds of information are sent to the third-party trackers: -* The names of the elements that were possibly interacted with (and not values). For example, if a button named `foo` was clicked on, the value `foo=true` is tracked. -* The hash part of the URL, if any. For example if the page `/foo/bar` was navigated to, the value `#=/foo/bar` is tracked. +- The names of the elements that were possibly interacted with (and not values). For example, if a button named `foo` was clicked on, the value `foo=true` is tracked. +- The hash part of the URL, if any. For example if the page `/foo/bar` was navigated to, the value `#=/foo/bar` is tracked. ### Google Analytics