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                // The drain signal is a third way this socket can end, and it
373                // has to be watched here. Holding a drain token makes the
374                // shutdown *wait* for this socket; nothing was telling the socket
375                // to stop. The connection loop that watches the signal for an
376                // HTTP connection has already exited for an upgraded one — hyper
377                // resolves the connection when the `101` goes out — so every
378                // shutdown burned the whole grace period whenever a WebSocket was
379                // live, and at `shutdown_timeout_ms = 0`, which means "wait as
380                // long as it takes", it never returned at all.
381                //
382                // Dropping the callback future is what ends the socket, and doing
383                // that closes it. A WebSocket has no request boundary to finish
384                // at: an application that wants a clean `Close` frame and a
385                // last word to its peer has to be the one to send it, which it
386                // can do because it is holding the socket.
387                // `None` when there is no connection budget to belong to — a
388                // `TestClient` call, or any path that never went through the
389                // accept loop. Nothing can signal such a socket, so its drain
390                // branch simply never fires.
391                let drain_watch = conn_guard.clone();
392                let draining = async move {
393                    match drain_watch {
394                        Some(guard) => guard.draining().await,
395                        None => std::future::pending::<()>().await,
396                    }
397                };
398                tokio::pin!(draining);
399
400                if idle_timeout_ms == 0 {
401                    let work = callback(socket);
402                    tokio::pin!(work);
403                    tokio::select! {
404                        _ = &mut work => {}
405                        _ = draining.as_mut() => {
406                            tracing::debug!("closing a WebSocket for shutdown");
407                        }
408                    }
409                } else {
410                    let work = callback(socket);
411                    tokio::pin!(work);
412                    let reaper =
413                        tokio::time::sleep(std::time::Duration::from_millis(idle_timeout_ms));
414                    tokio::pin!(reaper);
415                    loop {
416                        tokio::select! {
417                            _ = &mut work => break,
418                            _ = draining.as_mut() => {
419                                tracing::debug!("closing a WebSocket for shutdown");
420                                break;
421                            }
422                            _ = reaper.as_mut() => match activity.idle_for(idle_timeout_ms) {
423                                // Quiet for the whole period: drop it.
424                                None => {
425                                    tracing::debug!("closing an idle WebSocket");
426                                    break;
427                                }
428                                // Traffic since the timer was armed.
429                                Some(remaining) => reaper
430                                    .as_mut()
431                                    .reset(tokio::time::Instant::now() + remaining),
432                            },
433                        }
434                    }
435                }
436                // Dropped here rather than at the top of the task, so the permit
437                // and the drain token are returned only once the socket is
438                // genuinely finished.
439                drop(conn_guard);
440            }
441        });
442
443        let mut res = Response::new(StatusCode::SWITCHING_PROTOCOLS);
444        res.headers
445            .insert(UPGRADE, HeaderValue::from_static("websocket"));
446        res.headers
447            .insert(CONNECTION, HeaderValue::from_static("upgrade"));
448        res.headers.insert(SEC_WEBSOCKET_ACCEPT, accept_key);
449        if let Some(p) = protocol {
450            res.headers.insert(SEC_WEBSOCKET_PROTOCOL, p);
451        }
452        res
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459    use crate::{Churust, TestClient};
460    use tokio_tungstenite::tungstenite::Message as TMessage;
461
462    #[tokio::test]
463    async fn plain_get_to_ws_route_is_426() {
464        let app = Churust::server()
465            .routing(|r| {
466                r.get("/ws", |ws: WebSocketUpgrade| async move {
467                    ws.on_upgrade(|_sock| async {})
468                });
469            })
470            .build();
471        // A normal GET (no upgrade headers, no captured handle) must be rejected.
472        let res = TestClient::new(app).get("/ws").send().await;
473        assert_eq!(res.status(), http::StatusCode::UPGRADE_REQUIRED);
474    }
475
476    #[test]
477    fn message_round_trips_through_tungstenite() {
478        let cases = [
479            Message::Text("hi".into()),
480            Message::Binary(vec![1, 2, 3]),
481            Message::Ping(vec![9]),
482            Message::Pong(vec![8]),
483            Message::Close,
484        ];
485        for m in cases {
486            let t: TMessage = m.clone().into();
487            let back: Message = t.into();
488            assert_eq!(m, back);
489        }
490    }
491
492    #[test]
493    fn accept_key_matches_rfc6455_example() {
494        // RFC 6455 §1.3 worked example.
495        let accept = tokio_tungstenite::tungstenite::handshake::derive_accept_key(
496            b"dGhlIHNhbXBsZSBub25jZQ==",
497        );
498        assert_eq!(accept, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
499    }
500}