Skip to main content

trillium_websockets/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(
3    clippy::dbg_macro,
4    missing_copy_implementations,
5    rustdoc::missing_crate_level_docs,
6    missing_debug_implementations,
7    missing_docs,
8    nonstandard_style,
9    unused_qualifications
10)]
11
12//! # A websocket trillium handler
13//!
14//! There are three primary ways to use this crate
15//!
16//! ## With an async function that receives a [`WebSocketConn`]
17//!
18//! This is the simplest way to use trillium websockets, but does not
19//! provide any of the affordances that implementing the
20//! [`WebSocketHandler`] trait does. It is best for very simple websockets
21//! or for usages that require moving the WebSocketConn elsewhere in an
22//! application. The WebSocketConn is fully owned at this point, and will
23//! disconnect when dropped, not when the async function passed to
24//! `websocket` completes.
25//!
26//! ```
27//! use futures_lite::stream::StreamExt;
28//! use trillium_websockets::{Message, WebSocketConn, websocket};
29//!
30//! let handler = websocket(|mut conn: WebSocketConn| async move {
31//!     while let Some(Ok(Message::Text(input))) = conn.next().await {
32//!         conn.send_string(format!("received your message: {}", &input))
33//!             .await;
34//!     }
35//! });
36//! # // tests at tests/tests.rs for example simplicity
37//! ```
38//!
39//!
40//! ## Implementing [`WebSocketHandler`]
41//!
42//! [`WebSocketHandler`] provides support for sending outbound messages as a
43//! stream, and simplifies common patterns like executing async code on
44//! received messages.
45//!
46//! ## Using [`JsonWebSocketHandler`]
47//!
48//! [`JsonWebSocketHandler`] provides a thin serialization and
49//! deserialization layer on top of [`WebSocketHandler`] for this common
50//! use case.  See the [`JsonWebSocketHandler`] documentation for example
51//! usage. In order to use this trait, the `json` cargo feature must be
52//! enabled.
53//!
54//! ## Origin checking
55//!
56//! Browsers do not apply CORS to websocket handshakes, so a page on any site can open a socket to
57//! any server, and the browser attaches the visitor's cookies to that handshake. RFC 6455 §10.2
58//! assigns the origin check to the server for exactly this reason; skipping it is the
59//! cross-site websocket hijacking vulnerability.
60//!
61//! By default this handler accepts a handshake only if its `Origin` names the same host as the
62//! request's `Host` or `:authority`, or if there is no `Origin` at all, which means the client is
63//! not a browser. Rejected handshakes receive a `403 Forbidden` and log which origin was refused.
64//!
65//! ```
66//! # use trillium_websockets::{WebSocket, WebSocketConn};
67//! # let handler = |_: WebSocketConn| async {};
68//! WebSocket::new(handler); // same-origin (default)
69//! //
70//! # let handler = |_: WebSocketConn| async {};
71//! WebSocket::new(handler).allow_origins(["https://app.example.com"]);
72//! # let handler = |_: WebSocketConn| async {};
73//! WebSocket::new(handler).allow_origin_fn(|origin| origin == Some("https://app.example.com"));
74//! # let handler = |_: WebSocketConn| async {};
75//! WebSocket::new(handler).allow_any_origin(); // opt out
76//! ```
77//!
78//! An application that serves its pages from one host and its sockets from another — say pages on
79//! `app.example.com` and sockets on `api.example.com` — must name the page origin with
80//! [`allow_origins`][WebSocket::allow_origins].
81//!
82//! ## Message size
83//!
84//! Inbound messages are assembled in memory before they reach a handler, up to tungstenite's
85//! default of 64 MiB per message. Applications that exchange small messages should lower that
86//! with [`with_protocol_config`][WebSocket::with_protocol_config].
87
88#[cfg(test)]
89#[doc = include_str!("../README.md")]
90mod readme {}
91
92mod bidirectional_stream;
93mod origin;
94mod websocket_connection;
95mod websocket_handler;
96
97pub use async_tungstenite::{
98    self,
99    tungstenite::{
100        self, Message,
101        protocol::{Role, WebSocketConfig},
102    },
103};
104use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
105use bidirectional_stream::{BidirectionalStream, Direction};
106use futures_lite::stream::StreamExt;
107use origin::{OriginPolicy, OriginPredicate};
108use sha1::{Digest, Sha1};
109use std::{
110    net::IpAddr,
111    ops::{Deref, DerefMut},
112};
113use trillium::{
114    Conn, Handler, Info, KnownHeaderName,
115    KnownHeaderName::{
116        Connection, SecWebsocketAccept, SecWebsocketKey, SecWebsocketProtocol, SecWebsocketVersion,
117        Upgrade as UpgradeHeader,
118    },
119    Method, Status, Upgrade, Version,
120};
121pub use websocket_connection::WebSocketConn;
122pub use websocket_handler::WebSocketHandler;
123
124const WEBSOCKET_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
125
126#[derive(thiserror::Error, Debug)]
127#[non_exhaustive]
128/// An Error type that represents all exceptional conditions that can be encoutered in the operation
129/// of this crate
130pub enum Error {
131    #[error(transparent)]
132    /// an error in the underlying websocket implementation
133    WebSocket(#[from] tungstenite::Error),
134
135    #[cfg(feature = "json")]
136    #[error(transparent)]
137    /// an error in json serialization or deserialization
138    Json(#[from] serde_json::Error),
139}
140
141/// a Result type for this crate
142pub type Result<T = Message> = std::result::Result<T, Error>;
143
144#[cfg(feature = "json")]
145mod json;
146
147#[cfg(feature = "json")]
148pub use json::{JsonHandler, JsonWebSocketHandler, json_websocket};
149
150/// The trillium handler.
151/// See crate-level docs for example usage.
152#[derive(Debug)]
153pub struct WebSocket<H> {
154    handler: H,
155    protocols: Vec<String>,
156    config: Option<WebSocketConfig>,
157    required: bool,
158    origin_policy: OriginPolicy,
159}
160
161impl<H> Deref for WebSocket<H> {
162    type Target = H;
163
164    fn deref(&self) -> &Self::Target {
165        &self.handler
166    }
167}
168
169impl<H> DerefMut for WebSocket<H> {
170    fn deref_mut(&mut self) -> &mut Self::Target {
171        &mut self.handler
172    }
173}
174
175/// Builds a new trillium handler from the provided
176/// WebSocketHandler. Alias for [`WebSocket::new`]
177pub fn websocket<H>(websocket_handler: H) -> WebSocket<H>
178where
179    H: WebSocketHandler,
180{
181    WebSocket::new(websocket_handler)
182}
183
184impl<H> WebSocket<H>
185where
186    H: WebSocketHandler,
187{
188    async fn run_h1(&self, mut conn: Conn) -> Conn {
189        if !upgrade_requested(&conn) {
190            if self.required {
191                return conn.with_status(Status::UpgradeRequired).halt();
192            } else {
193                return conn;
194            }
195        }
196
197        if !self.origin_policy.allows(&conn) {
198            return reject_origin(conn);
199        }
200
201        if !supported_websocket_version(&conn) {
202            return reject_unsupported_version(conn);
203        }
204
205        let websocket_peer_ip = WebsocketPeerIp(conn.peer_ip());
206
207        let Some(sec_websocket_key) = conn.request_headers().get_str(SecWebsocketKey) else {
208            return conn.with_status(Status::BadRequest).halt();
209        };
210        let sec_websocket_accept = websocket_accept_hash(sec_websocket_key);
211
212        let protocol = websocket_protocol(&conn, &self.protocols);
213
214        let headers = conn.response_headers_mut();
215
216        headers.extend([
217            (UpgradeHeader, "websocket"),
218            (Connection, "Upgrade"),
219            (SecWebsocketVersion, "13"),
220        ]);
221
222        headers.insert(SecWebsocketAccept, sec_websocket_accept);
223
224        if let Some(protocol) = protocol {
225            headers.insert(SecWebsocketProtocol, protocol);
226        }
227
228        conn.halt()
229            .with_state(websocket_peer_ip)
230            .with_state(IsWebsocket)
231            .with_status(Status::SwitchingProtocols)
232    }
233
234    /// Build a new WebSocket with an async handler function that
235    /// receives a [`WebSocketConn`]
236    pub fn new(handler: H) -> Self {
237        Self {
238            handler,
239            protocols: Default::default(),
240            config: None,
241            required: false,
242            origin_policy: OriginPolicy::default(),
243        }
244    }
245
246    /// Accept handshakes only from pages on these origins.
247    ///
248    /// ```
249    /// # use trillium_websockets::{WebSocket, WebSocketConn};
250    /// # let websocket = WebSocket::new(|_: WebSocketConn| async {});
251    /// websocket.allow_origins(["https://app.example.com", "https://admin.example.com"]);
252    /// ```
253    ///
254    /// Origins are compared exactly, after normalizing default ports and idn hosts. Nothing is
255    /// matched by prefix or suffix, because an allowed origin of `example.com` matched by suffix
256    /// also admits `evil-example.com`. For a family of subdomains, use
257    /// [`allow_origin_fn`][Self::allow_origin_fn].
258    ///
259    /// A request with no `Origin` header is allowed, as it did not come from a browser.
260    ///
261    /// # Panics
262    ///
263    /// Panics if any of the provided strings is not a url with a scheme and a host, or if it
264    /// carries a path, query, fragment, or userinfo.
265    pub fn allow_origins<'a>(mut self, origins: impl IntoIterator<Item = &'a str>) -> Self {
266        self.origin_policy = OriginPolicy::list(origins);
267        self
268    }
269
270    /// Accept handshakes for which this predicate returns true.
271    ///
272    /// The argument is the raw `Origin` header, so `None` (a non-browser client) stays
273    /// distinguishable from `Some("null")` (a sandboxed iframe or a `file://` page, which is
274    /// attacker-reachable and should generally be rejected).
275    ///
276    /// ```
277    /// # use trillium_websockets::{WebSocket, WebSocketConn};
278    /// # let websocket = WebSocket::new(|_: WebSocketConn| async {});
279    /// websocket.allow_origin_fn(|origin| match origin {
280    ///     None => true,
281    ///     Some(origin) => origin
282    ///         .strip_prefix("https://")
283    ///         .is_some_and(|host| host == "example.com" || host.ends_with(".example.com")),
284    /// });
285    /// ```
286    pub fn allow_origin_fn<F>(mut self, predicate: F) -> Self
287    where
288        F: Fn(Option<&str>) -> bool + Send + Sync + 'static,
289    {
290        self.origin_policy = OriginPolicy::Predicate(OriginPredicate::from(predicate));
291        self
292    }
293
294    /// Accept handshakes from any origin, disabling the same-origin default.
295    ///
296    /// Any page on the web can then open a socket to this handler and act with the browser's
297    /// ambient authority — the visitor's cookies are attached to the handshake. Only do this if
298    /// the socket is either unauthenticated or authenticated by something the page cannot
299    /// replay, such as a token the client sends in its first message.
300    pub fn allow_any_origin(mut self) -> Self {
301        self.origin_policy = OriginPolicy::Any;
302        self
303    }
304
305    /// `protocols` is a sequence of known protocols. On successful handshake,
306    /// the returned response headers contain the first protocol in this list
307    /// which the server also knows.
308    pub fn with_protocols(self, protocols: &[&str]) -> Self {
309        Self {
310            protocols: protocols.iter().map(ToString::to_string).collect(),
311            ..self
312        }
313    }
314
315    /// configure the websocket protocol
316    pub fn with_protocol_config(self, config: WebSocketConfig) -> Self {
317        Self {
318            config: Some(config),
319            ..self
320        }
321    }
322
323    /// configure this handler to halt and send back a [`426 Upgrade
324    /// Required`][Status::UpgradeRequired] if a websocket cannot be negotiated
325    pub fn required(mut self) -> Self {
326        self.required = true;
327        self
328    }
329}
330
331struct IsWebsocket;
332
333#[cfg(test)]
334mod tests;
335
336// this is a workaround for the fact that Upgrade is a public struct,
337// so adding peer_ip to that struct would be a breaking change. We
338// stash a copy in state for now.
339struct WebsocketPeerIp(Option<IpAddr>);
340
341impl<H> Handler for WebSocket<H>
342where
343    H: WebSocketHandler,
344{
345    async fn run(&self, mut conn: Conn) -> Conn {
346        match conn.http_version() {
347            Version::Http1_0 | Version::Http1_1 => self.run_h1(conn).await,
348            // Extended-CONNECT bootstrap of WebSockets — RFC 8441 (h2) and RFC 9220 (h3) define
349            // the same shape: `:method = CONNECT`, `:protocol = websocket`, no SHA1/Key/Accept
350            // handshake. The server replies with status 200 and the stream stays open as a
351            // bidirectional byte channel carrying WebSocket frames.
352            Version::Http2 | Version::Http3 => {
353                if extended_connect_websocket_request(&conn) {
354                    if !self.origin_policy.allows(&conn) {
355                        return reject_origin(conn);
356                    }
357
358                    if !supported_websocket_version(&conn) {
359                        return reject_unsupported_version(conn);
360                    }
361
362                    let websocket_peer_ip = WebsocketPeerIp(conn.peer_ip());
363                    let protocol = websocket_protocol(&conn, &self.protocols);
364
365                    if let Some(protocol) = protocol {
366                        conn.response_headers_mut()
367                            .insert(SecWebsocketProtocol, protocol);
368                    }
369
370                    conn.halt()
371                        .with_state(websocket_peer_ip)
372                        .with_state(IsWebsocket)
373                        .with_status(Status::Ok)
374                } else if self.required {
375                    conn.with_status(Status::UpgradeRequired).halt()
376                } else {
377                    conn
378                }
379            }
380            _ => {
381                if self.required {
382                    conn.with_status(Status::UpgradeRequired).halt()
383                } else {
384                    conn
385                }
386            }
387        }
388    }
389
390    async fn init(&mut self, info: &mut Info) {
391        // Required for h2 (RFC 8441 §3) and h3 (RFC 9220 §3) clients to attempt the extended
392        // CONNECT bootstrap of WebSockets. Harmless on h1.
393        info.config_mut().set_extended_connect_enabled(true);
394    }
395
396    fn has_upgrade(&self, upgrade: &Upgrade) -> bool {
397        upgrade.state().contains::<IsWebsocket>()
398    }
399
400    async fn upgrade(&self, mut upgrade: Upgrade) {
401        let peer_ip = upgrade
402            .state_mut()
403            .take::<WebsocketPeerIp>()
404            .and_then(|i| i.0);
405        let mut conn = WebSocketConn::new(upgrade, self.config, Role::Server).await;
406        conn.set_peer_ip(peer_ip);
407
408        let Some((mut conn, outbound)) = self.handler.connect(conn).await else {
409            return;
410        };
411
412        let inbound = conn.take_inbound_stream();
413
414        let mut stream = std::pin::pin!(BidirectionalStream { inbound, outbound });
415        loop {
416            // The conn's own flush-on-pending lives in its Stream impl, but this loop polls the
417            // taken inbound stream directly, so it flushes buffered sends before parking itself.
418            let next = futures_lite::future::poll_fn(|cx| {
419                let poll = stream.as_mut().poll_next(cx);
420                if !matches!(poll, std::task::Poll::Ready(Some(_)))
421                    && let std::task::Poll::Ready(Err(e)) = conn.poll_flush_sink(cx)
422                {
423                    log::debug!("websocket flush error: {e}");
424                }
425                poll
426            })
427            .await;
428
429            let Some(message) = next else { break };
430            match message {
431                Direction::Inbound(Ok(Message::Close(close_frame))) => {
432                    self.handler.disconnect(&mut conn, close_frame).await;
433                    break;
434                }
435
436                Direction::Inbound(Ok(message)) => {
437                    self.handler.inbound(message, &mut conn).await;
438                }
439
440                Direction::Outbound(message) => {
441                    if let Err(e) = self.handler.send(message, &mut conn).await {
442                        log::warn!("outbound websocket error: {:?}", e);
443                        break;
444                    }
445                }
446
447                _ => {
448                    self.handler.disconnect(&mut conn, None).await;
449                    break;
450                }
451            }
452        }
453
454        if let Some(err) = conn.close().await.err() {
455            log::warn!("websocket close error: {:?}", err);
456        };
457    }
458}
459
460fn websocket_protocol(conn: &Conn, protocols: &[String]) -> Option<String> {
461    conn.request_headers()
462        .token_iter(SecWebsocketProtocol)
463        .find(|req_p| protocols.iter().any(|x| x == req_p))
464        .map(str::to_owned)
465}
466
467fn connection_is_upgrade(conn: &Conn) -> bool {
468    conn.request_headers()
469        .token_iter(Connection)
470        .any(|c| c.eq_ignore_ascii_case("upgrade"))
471}
472
473fn upgrade_to_websocket(conn: &Conn) -> bool {
474    conn.request_headers()
475        .eq_ignore_ascii_case(UpgradeHeader, "websocket")
476}
477
478fn supported_websocket_version(conn: &Conn) -> bool {
479    conn.request_headers().get_str(SecWebsocketVersion) == Some("13")
480}
481
482fn reject_origin(conn: Conn) -> Conn {
483    log::warn!(
484        "rejecting websocket handshake from origin {:?} for authority {:?}. If this is expected, \
485         configure the origins that may open a websocket with `WebSocket::allow_origins([..])`, \
486         `WebSocket::allow_origin_fn(..)`, or `WebSocket::allow_any_origin()`.",
487        conn.request_headers().get_str(KnownHeaderName::Origin),
488        conn.host()
489    );
490
491    conn.with_status(Status::Forbidden).halt()
492}
493
494fn reject_unsupported_version(conn: Conn) -> Conn {
495    conn.with_status(Status::UpgradeRequired)
496        .with_response_header(SecWebsocketVersion, "13")
497        .halt()
498}
499
500fn upgrade_requested(conn: &Conn) -> bool {
501    conn.method() == Method::Get
502        && conn.http_version() == Version::Http1_1
503        && connection_is_upgrade(conn)
504        && upgrade_to_websocket(conn)
505}
506
507/// Detect a WebSocket bootstrap over extended CONNECT (RFC 8441 for h2, RFC 9220 for h3).
508///
509/// The peer must use `CONNECT` and carry a `:protocol` pseudo-header equal to "websocket"
510/// (case-insensitive per the RFCs).
511fn extended_connect_websocket_request(conn: &Conn) -> bool {
512    if conn.method() != Method::Connect {
513        return false;
514    }
515    let inner: &trillium_http::Conn<Box<dyn trillium::Transport>> = conn.as_ref();
516    inner
517        .protocol()
518        .is_some_and(|p| p.eq_ignore_ascii_case("websocket"))
519}
520
521/// Generate a random key suitable for Sec-WebSocket-Key
522pub fn websocket_key() -> String {
523    BASE64.encode(fastrand::u128(..).to_ne_bytes())
524}
525
526/// Generate the expected Sec-WebSocket-Accept hash from the Sec-WebSocket-Key
527pub fn websocket_accept_hash(websocket_key: &str) -> String {
528    let hash = Sha1::new()
529        .chain_update(websocket_key)
530        .chain_update(WEBSOCKET_GUID)
531        .finalize();
532    BASE64.encode(&hash[..])
533}