Skip to main content

churust_core/
ws.rs

1//! WebSocket support (feature `ws`).
2//!
3//! A handler upgrades a request by taking the [`WebSocketUpgrade`] extractor and
4//! calling [`WebSocketUpgrade::on_upgrade`]:
5//!
6//! ```no_run
7//! use churust_core::Churust;
8//! use churust_core::ws::WebSocketUpgrade;
9//!
10//! # fn build() {
11//! Churust::server().routing(|r| {
12//!     r.get("/echo", |ws: WebSocketUpgrade| async move {
13//!         ws.on_upgrade(|mut sock| async move {
14//!             while let Some(Ok(msg)) = sock.recv().await {
15//!                 if sock.send(msg).await.is_err() { break; }
16//!             }
17//!         })
18//!     });
19//! });
20//! # }
21//! ```
22
23use crate::call::Call;
24use crate::error::{Error, Result};
25use crate::extract::FromCallParts;
26use crate::response::Response;
27use async_trait::async_trait;
28use futures_util::{SinkExt, StreamExt};
29use http::header::{
30    CONNECTION, SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_PROTOCOL,
31    SEC_WEBSOCKET_VERSION, UPGRADE,
32};
33use http::{HeaderMap, HeaderValue, StatusCode};
34use hyper::upgrade::OnUpgrade;
35use hyper_util::rt::TokioIo;
36use std::future::Future;
37use std::sync::{Arc, Mutex};
38use tokio_tungstenite::tungstenite::protocol::{Role, WebSocketConfig};
39use tokio_tungstenite::WebSocketStream;
40
41/// A cloneable, takeable holder for hyper's pending connection upgrade. The
42/// engine inserts one into a [`Call`]'s extensions for WebSocket
43/// handshake requests; [`WebSocketUpgrade`] takes it back out.
44#[derive(Clone)]
45pub struct OnUpgradeHandle(Arc<Mutex<Option<OnUpgrade>>>);
46
47/// Frame and message size caps for an upgraded socket, seeded into the call by
48/// the engine so `on_upgrade` can apply them without reaching for global state.
49///
50/// The two are separate on purpose: a peer that respects the frame cap can
51/// still send an unbounded number of small continuation frames that reassemble
52/// into one enormous message.
53#[derive(Debug, Clone, Copy)]
54pub struct WsLimits {
55    /// Maximum size of a single frame, in bytes.
56    pub max_frame_bytes: usize,
57    /// Maximum size of a reassembled message, in bytes.
58    pub max_message_bytes: usize,
59}
60
61impl OnUpgradeHandle {
62    /// Wrap a pending upgrade.
63    pub fn new(on_upgrade: OnUpgrade) -> Self {
64        Self(Arc::new(Mutex::new(Some(on_upgrade))))
65    }
66
67    /// Take the upgrade future (can only succeed once).
68    pub(crate) fn take(&self) -> Option<OnUpgrade> {
69        self.0.lock().ok().and_then(|mut guard| guard.take())
70    }
71}
72
73impl std::fmt::Debug for OnUpgradeHandle {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.write_str("OnUpgradeHandle")
76    }
77}
78
79/// True if the request headers request a WebSocket upgrade (`Connection:
80/// upgrade` + `Upgrade: websocket`, case-insensitive).
81pub(crate) fn is_upgrade_request(headers: &HeaderMap) -> bool {
82    let connection_upgrade = headers
83        .get(CONNECTION)
84        .and_then(|v| v.to_str().ok())
85        .map(|v| {
86            v.to_ascii_lowercase()
87                .split(',')
88                .any(|p| p.trim() == "upgrade")
89        })
90        .unwrap_or(false);
91    let upgrade_websocket = headers
92        .get(UPGRADE)
93        .and_then(|v| v.to_str().ok())
94        .map(|v| v.eq_ignore_ascii_case("websocket"))
95        .unwrap_or(false);
96    connection_upgrade && upgrade_websocket
97}
98
99use tokio_tungstenite::tungstenite::Message as TMessage;
100
101/// A WebSocket message. A deliberately small enum so user code never has to name
102/// `tungstenite` types.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub enum Message {
105    /// A UTF-8 text frame.
106    Text(String),
107    /// A binary frame.
108    Binary(Vec<u8>),
109    /// A ping control frame (payload echoed back by the peer as a pong).
110    Ping(Vec<u8>),
111    /// A pong control frame.
112    Pong(Vec<u8>),
113    /// A close frame (connection is closing).
114    Close,
115}
116
117impl From<Message> for TMessage {
118    fn from(m: Message) -> Self {
119        match m {
120            Message::Text(s) => TMessage::Text(s.into()),
121            Message::Binary(b) => TMessage::Binary(b.into()),
122            Message::Ping(b) => TMessage::Ping(b.into()),
123            Message::Pong(b) => TMessage::Pong(b.into()),
124            Message::Close => TMessage::Close(None),
125        }
126    }
127}
128
129impl From<TMessage> for Message {
130    fn from(m: TMessage) -> Self {
131        match m {
132            TMessage::Text(s) => Message::Text(s.to_string()),
133            TMessage::Binary(b) => Message::Binary(b.to_vec()),
134            TMessage::Ping(b) => Message::Ping(b.to_vec()),
135            TMessage::Pong(b) => Message::Pong(b.to_vec()),
136            TMessage::Close(_) => Message::Close,
137            // Raw frames are not surfaced to user code.
138            _ => Message::Close,
139        }
140    }
141}
142
143/// An established WebSocket connection. Obtained inside the
144/// [`WebSocketUpgrade::on_upgrade`] callback.
145/// The configured WebSocket idle bound, seeded into the call by the engine.
146#[derive(Debug, Clone, Copy)]
147pub struct WsIdleTimeout(pub u64);
148
149/// An established WebSocket connection, handed to the `on_upgrade` callback.
150pub struct WebSocket {
151    inner: WebSocketStream<TokioIo<hyper::upgrade::Upgraded>>,
152    /// Milliseconds since the socket opened, at the last frame in either
153    /// direction. Read by the idle reaper, which cannot see into the
154    /// application's callback and so has no other way to tell a busy socket
155    /// from a dead one.
156    activity: std::sync::Arc<WsActivity>,
157}
158
159/// When an upgraded socket last carried a frame.
160pub(crate) struct WsActivity {
161    last_ms: std::sync::atomic::AtomicU64,
162    origin: tokio::time::Instant,
163}
164
165impl WsActivity {
166    pub(crate) fn new() -> Self {
167        Self {
168            last_ms: std::sync::atomic::AtomicU64::new(0),
169            origin: tokio::time::Instant::now(),
170        }
171    }
172
173    fn touch(&self) {
174        self.last_ms.store(
175            self.origin.elapsed().as_millis() as u64,
176            std::sync::atomic::Ordering::Relaxed,
177        );
178    }
179
180    /// `None` once the socket has been quiet for at least `idle_ms`, otherwise
181    /// how much longer to wait before asking again.
182    pub(crate) fn idle_for(&self, idle_ms: u64) -> Option<std::time::Duration> {
183        let last = self.last_ms.load(std::sync::atomic::Ordering::Relaxed);
184        let quiet = (self.origin.elapsed().as_millis() as u64).saturating_sub(last);
185        match idle_ms.checked_sub(quiet) {
186            Some(0) | None => None,
187            Some(remaining) => Some(std::time::Duration::from_millis(remaining)),
188        }
189    }
190}
191
192impl WebSocket {
193    /// Receive the next message. `None` when the connection has closed.
194    pub async fn recv(&mut self) -> Option<Result<Message>> {
195        let got = self.inner.next().await;
196        self.activity.touch();
197        match got {
198            Some(Ok(msg)) => Some(Ok(msg.into())),
199            Some(Err(e)) => Some(Err(Error::internal(format!("websocket recv: {e}")))),
200            None => None,
201        }
202    }
203
204    /// Send a message.
205    pub async fn send(&mut self, msg: Message) -> Result<()> {
206        let out = self
207            .inner
208            .send(msg.into())
209            .await
210            .map_err(|e| Error::internal(format!("websocket send: {e}")));
211        self.activity.touch();
212        out
213    }
214
215    /// Convenience: send a text message.
216    pub async fn send_text(&mut self, text: impl Into<String>) -> Result<()> {
217        self.send(Message::Text(text.into())).await
218    }
219
220    /// Convenience: send a binary message.
221    pub async fn send_binary(&mut self, bytes: impl Into<Vec<u8>>) -> Result<()> {
222        self.send(Message::Binary(bytes.into())).await
223    }
224
225    /// Close the connection.
226    pub async fn close(&mut self) -> Result<()> {
227        self.inner
228            .close(None)
229            .await
230            .map_err(|e| Error::internal(format!("websocket close: {e}")))
231    }
232}
233
234/// Extractor that represents a pending WebSocket upgrade. A handler takes it as
235/// an argument, then calls [`on_upgrade`](WebSocketUpgrade::on_upgrade).
236///
237/// Extraction fails with **426 Upgrade Required** if the request is not a valid
238/// WebSocket handshake.
239pub struct WebSocketUpgrade {
240    on_upgrade: OnUpgrade,
241    accept_key: HeaderValue,
242    protocol: Option<HeaderValue>,
243    limits: WsLimits,
244    /// This connection's share of the accept budget, moved into the socket
245    /// task so the permit is held for as long as the WebSocket is open.
246    conn_guard: Option<crate::engine::ConnGuard>,
247    /// How long the socket may sit with no frame before it is closed.
248    idle_timeout_ms: u64,
249}
250
251#[async_trait]
252impl FromCallParts for WebSocketUpgrade {
253    async fn from_call_parts(call: &mut Call) -> Result<Self> {
254        let version_ok = call
255            .header(SEC_WEBSOCKET_VERSION.as_str())
256            .map(|v| v == "13")
257            .unwrap_or(false);
258        if !is_upgrade_request(call.headers()) || !version_ok {
259            return Err(Error::new(
260                StatusCode::UPGRADE_REQUIRED,
261                "expected a WebSocket upgrade request",
262            )
263            .with_response_header(UPGRADE, HeaderValue::from_static("websocket")));
264        }
265
266        let key = call
267            .header(SEC_WEBSOCKET_KEY.as_str())
268            .ok_or_else(|| Error::bad_request("missing Sec-WebSocket-Key"))?;
269        let accept = tokio_tungstenite::tungstenite::handshake::derive_accept_key(key.as_bytes());
270        let accept_key =
271            HeaderValue::from_str(&accept).map_err(|_| Error::internal("invalid accept key"))?;
272
273        let protocol = call
274            .header(SEC_WEBSOCKET_PROTOCOL.as_str())
275            .and_then(|p| p.split(',').next())
276            .and_then(|p| HeaderValue::from_str(p.trim()).ok());
277
278        let handle = call.get::<OnUpgradeHandle>().ok_or_else(|| {
279            Error::new(
280                StatusCode::UPGRADE_REQUIRED,
281                "WebSocket upgrade unavailable (no pending connection upgrade)",
282            )
283        })?;
284        let on_upgrade = handle
285            .take()
286            .ok_or_else(|| Error::internal("WebSocket upgrade already consumed"))?;
287
288        // Absent only when a call was built without the engine (unit tests);
289        // the conservative defaults then apply.
290        let limits = call.get::<WsLimits>().unwrap_or(WsLimits {
291            max_frame_bytes: 1 << 20,
292            max_message_bytes: 4 << 20,
293        });
294
295        // The connection's share of `max_connections` and of the drain. hyper
296        // resolves an upgraded connection as soon as it dispatches the `101`,
297        // so without carrying this into the socket task a live WebSocket held
298        // no permit at all and the cap bounded nothing for WebSocket traffic.
299        // Absent when a call was built without the engine (unit tests).
300        let conn_guard = call.get::<crate::engine::ConnGuard>();
301        // Absent when a call was built without the engine (unit tests); the
302        // default bound then applies.
303        let idle_timeout_ms = call
304            .get::<WsIdleTimeout>()
305            .map(|WsIdleTimeout(ms)| ms)
306            .unwrap_or(300_000);
307
308        Ok(WebSocketUpgrade {
309            on_upgrade,
310            limits,
311            accept_key,
312            protocol,
313            conn_guard,
314            idle_timeout_ms,
315        })
316    }
317}
318
319impl WebSocketUpgrade {
320    /// Finish the handshake: spawn a task that runs `callback` with the
321    /// established [`WebSocket`] once the upgrade completes, and return the
322    /// `101 Switching Protocols` response the engine will send.
323    pub fn on_upgrade<F, Fut>(self, callback: F) -> Response
324    where
325        F: FnOnce(WebSocket) -> Fut + Send + 'static,
326        Fut: Future<Output = ()> + Send + 'static,
327    {
328        let WebSocketUpgrade {
329            on_upgrade,
330            accept_key,
331            protocol,
332            limits,
333            conn_guard,
334            idle_timeout_ms,
335        } = self;
336
337        tokio::spawn(async move {
338            // Held for the socket's lifetime, so the budget is returned when
339            // the WebSocket ends rather than when the handshake finished.
340            let _conn_guard = conn_guard;
341            if let Ok(upgraded) = on_upgrade.await {
342                // Bound both a single frame and a reassembled message: a peer
343                // respecting the frame cap can still stream unbounded
344                // continuation frames into one enormous message.
345                let mut ws_config = WebSocketConfig::default();
346                ws_config.max_frame_size = Some(limits.max_frame_bytes);
347                ws_config.max_message_size = Some(limits.max_message_bytes);
348
349                let stream = WebSocketStream::from_raw_socket(
350                    TokioIo::new(upgraded),
351                    Role::Server,
352                    Some(ws_config),
353                )
354                .await;
355
356                let activity = std::sync::Arc::new(WsActivity::new());
357                let socket = WebSocket {
358                    inner: stream,
359                    activity: activity.clone(),
360                };
361
362                // Race the application's callback against an idle reaper. No
363                // HTTP-level timeout survives the upgrade — `header_read_timeout`
364                // applies before there is a socket and `request_timeout_ms`
365                // wrapped a request that already completed — so without this a
366                // peer that finishes the handshake and then says nothing holds
367                // this connection's permit until the process restarts.
368                //
369                // Dropping the callback future closes the socket and releases
370                // the guard. That is abrupt by design: the peer is not
371                // answering, so there is nobody to negotiate a close with.
372                if idle_timeout_ms == 0 {
373                    callback(socket).await;
374                } else {
375                    let work = callback(socket);
376                    tokio::pin!(work);
377                    let reaper =
378                        tokio::time::sleep(std::time::Duration::from_millis(idle_timeout_ms));
379                    tokio::pin!(reaper);
380                    loop {
381                        tokio::select! {
382                            _ = &mut work => break,
383                            _ = reaper.as_mut() => match activity.idle_for(idle_timeout_ms) {
384                                // Quiet for the whole period: drop it.
385                                None => {
386                                    tracing::debug!("closing an idle WebSocket");
387                                    break;
388                                }
389                                // Traffic since the timer was armed.
390                                Some(remaining) => reaper
391                                    .as_mut()
392                                    .reset(tokio::time::Instant::now() + remaining),
393                            },
394                        }
395                    }
396                }
397            }
398        });
399
400        let mut res = Response::new(StatusCode::SWITCHING_PROTOCOLS);
401        res.headers
402            .insert(UPGRADE, HeaderValue::from_static("websocket"));
403        res.headers
404            .insert(CONNECTION, HeaderValue::from_static("upgrade"));
405        res.headers.insert(SEC_WEBSOCKET_ACCEPT, accept_key);
406        if let Some(p) = protocol {
407            res.headers.insert(SEC_WEBSOCKET_PROTOCOL, p);
408        }
409        res
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416    use crate::{Churust, TestClient};
417    use tokio_tungstenite::tungstenite::Message as TMessage;
418
419    #[tokio::test]
420    async fn plain_get_to_ws_route_is_426() {
421        let app = Churust::server()
422            .routing(|r| {
423                r.get("/ws", |ws: WebSocketUpgrade| async move {
424                    ws.on_upgrade(|_sock| async {})
425                });
426            })
427            .build();
428        // A normal GET (no upgrade headers, no captured handle) must be rejected.
429        let res = TestClient::new(app).get("/ws").send().await;
430        assert_eq!(res.status(), http::StatusCode::UPGRADE_REQUIRED);
431    }
432
433    #[test]
434    fn message_round_trips_through_tungstenite() {
435        let cases = [
436            Message::Text("hi".into()),
437            Message::Binary(vec![1, 2, 3]),
438            Message::Ping(vec![9]),
439            Message::Pong(vec![8]),
440            Message::Close,
441        ];
442        for m in cases {
443            let t: TMessage = m.clone().into();
444            let back: Message = t.into();
445            assert_eq!(m, back);
446        }
447    }
448
449    #[test]
450    fn accept_key_matches_rfc6455_example() {
451        // RFC 6455 §1.3 worked example.
452        let accept = tokio_tungstenite::tungstenite::handshake::derive_accept_key(
453            b"dGhlIHNhbXBsZSBub25jZQ==",
454        );
455        assert_eq!(accept, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
456    }
457}