Skip to main content

rustlavel_ws/
route.rs

1//! Mounting a socket on the router.
2//!
3//! ```ignore
4//! r.get("/ws", websocket(|mut socket, _request| async move {
5//!     while let Some(message) = socket.recv().await {
6//!         let _ = socket.send(message).await;
7//!     }
8//! }));
9//! ```
10//!
11//! That is the whole echo server. The handshake, the 101, the framing, the
12//! pongs and the close are all behind `websocket`; the application writes an
13//! async closure that owns the socket for as long as the client is connected.
14
15use crate::connection::{WebSocket, WebSocketConfig};
16use crate::handshake;
17use rustlavel_http::handler::BoxFuture;
18use rustlavel_http::{Handler, IntoResponse, Request, Response, Upgrade, Upgraded};
19use rustlavel_core::{Event, events};
20use std::sync::{Arc, Mutex};
21
22/// A route that answers the WebSocket handshake and hands the socket to
23/// `handler`.
24///
25/// The closure receives the [`Request`] as well, because everything a socket
26/// needs to know about who is connecting — the session, a query parameter, the
27/// authenticated user attached by middleware — lives on it and is gone once the
28/// connection stops being HTTP.
29pub fn websocket<F, Fut>(handler: F) -> WebSocketRoute<F>
30where
31    F: Fn(WebSocket, Request) -> Fut + Send + Sync + 'static,
32    Fut: Future<Output = ()> + Send + 'static,
33{
34    WebSocketRoute {
35        handler: Arc::new(handler),
36        config: WebSocketConfig::default(),
37        protocols: Vec::new(),
38    }
39}
40
41pub struct WebSocketRoute<F> {
42    handler: Arc<F>,
43    config: WebSocketConfig,
44    protocols: Vec<String>,
45}
46
47impl<F> WebSocketRoute<F> {
48    /// Limits and timers for sockets opened on this route.
49    pub fn with_config(mut self, config: WebSocketConfig) -> Self {
50        self.config = config;
51        self
52    }
53
54    /// The subprotocols this route understands, most preferred first.
55    pub fn protocols(mut self, protocols: &[&str]) -> Self {
56        self.protocols = protocols.iter().map(|p| (*p).to_string()).collect();
57        self
58    }
59}
60
61impl<F, Fut> Handler for WebSocketRoute<F>
62where
63    F: Fn(WebSocket, Request) -> Fut + Send + Sync + 'static,
64    Fut: Future<Output = ()> + Send + 'static,
65{
66    fn call(&self, request: Request) -> BoxFuture<Response> {
67        let handshake = match handshake::negotiate(&request, &self.protocols) {
68            Ok(handshake) => handshake,
69            // A malformed handshake is an ordinary HTTP failure, and stays one:
70            // the connection is never upgraded, so the client gets a 400 body
71            // it can actually read.
72            Err(error) => return Box::pin(async move { error.into_response() }),
73        };
74
75        let response = handshake::response(&handshake);
76        let upgrade = SocketUpgrade {
77            handler: Arc::clone(&self.handler),
78            config: self.config.clone(),
79            path: request.path().to_string(),
80            protocol: handshake.protocol.clone(),
81            // The request is moved through to the closure. It cannot be cloned
82            // and `Upgrade::run` only has `&self`, so it waits in a cell that
83            // the single upgrade takes it out of.
84            request: Mutex::new(Some(request)),
85        };
86
87        Box::pin(async move { response.upgrading(upgrade) })
88    }
89}
90
91struct SocketUpgrade<F> {
92    handler: Arc<F>,
93    config: WebSocketConfig,
94    path: String,
95    protocol: Option<String>,
96    request: Mutex<Option<Request>>,
97}
98
99impl<F, Fut> Upgrade for SocketUpgrade<F>
100where
101    F: Fn(WebSocket, Request) -> Fut + Send + Sync + 'static,
102    Fut: Future<Output = ()> + Send + 'static,
103{
104    fn run(&self, connection: Upgraded) -> BoxFuture<()> {
105        let taken = self.request.lock().ok().and_then(|mut slot| slot.take());
106        let Some(request) = taken else {
107            // Only reachable if the server ran one upgrade twice.
108            return Box::pin(async {});
109        };
110
111        let socket = WebSocket::new(connection, self.config.clone())
112            .on_path(self.path.clone())
113            .with_protocol(self.protocol.clone());
114
115        if events::has_subscribers() {
116            Event::new("ws.connected")
117                .with("path", self.path.as_str())
118                .with("protocol", self.protocol.clone())
119                .dispatch();
120        }
121
122        let handler = Arc::clone(&self.handler);
123        Box::pin(async move { handler(socket, request).await })
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::connection::WebSocketConfig;
131    use crate::frame::{Frame, OpCode, Role};
132    use crate::message::Message;
133    use rustlavel_http::{Method, Router, Status};
134    use tokio::io::{AsyncReadExt, AsyncWriteExt};
135
136    fn handshake_request(target: &str) -> Request {
137        Request::new(Method::Get, target)
138            .with_header("upgrade", "websocket")
139            .with_header("connection", "Upgrade")
140            .with_header("sec-websocket-version", "13")
141            .with_header("sec-websocket-key", "dGhlIHNhbXBsZSBub25jZQ==")
142    }
143
144    fn echo_router() -> Router {
145        let mut router = Router::new();
146        router.get(
147            "/ws",
148            websocket(|mut socket: WebSocket, _request: Request| async move {
149                while let Some(message) = socket.recv().await {
150                    if socket.send(message).await.is_err() {
151                        break;
152                    }
153                }
154            }),
155        );
156        router.finalize();
157        router
158    }
159
160    #[tokio::test]
161    async fn a_valid_handshake_answers_101_and_takes_the_connection() {
162        let response = echo_router().dispatch(handshake_request("/ws")).await;
163
164        assert_eq!(response.status, Status(101));
165        assert_eq!(
166            response.headers.get("sec-websocket-accept"),
167            Some("s3pPLMBiTxaQ9kYGzzhZRbK+xOo=")
168        );
169        assert!(response.upgrades(), "the response must claim the socket");
170    }
171
172    #[tokio::test]
173    async fn a_handshake_missing_a_header_answers_400_and_keeps_the_connection() {
174        let mut request = handshake_request("/ws");
175        request.headers_mut().remove("sec-websocket-version");
176
177        let response = echo_router().dispatch(request).await;
178
179        assert_eq!(response.status, Status::BAD_REQUEST);
180        assert!(!response.upgrades());
181        assert!(response.body_string().contains("Sec-WebSocket-Version"));
182    }
183
184    #[tokio::test]
185    async fn the_route_negotiates_a_subprotocol() {
186        let mut router = Router::new();
187        router.get(
188            "/ws",
189            websocket(|_socket: WebSocket, _request: Request| async {}).protocols(&["rustlavel"]),
190        );
191        router.finalize();
192
193        let request = handshake_request("/ws").with_header("sec-websocket-protocol", "rustlavel");
194        let response = router.dispatch(request).await;
195
196        assert_eq!(response.headers.get("sec-websocket-protocol"), Some("rustlavel"));
197    }
198
199    /// The upgrade a route attaches, built directly.
200    ///
201    /// `Response::take_upgrade` is crate-private to `rustlavel-http` — the
202    /// server is its only caller — so a test outside that crate cannot pull the
203    /// upgrade back out of a dispatched response. Constructing the same value
204    /// exercises the same code the server runs.
205    fn upgrade_for<F, Fut>(handler: F, request: Request) -> SocketUpgrade<F>
206    where
207        F: Fn(WebSocket, Request) -> Fut + Send + Sync + 'static,
208        Fut: Future<Output = ()> + Send + 'static,
209    {
210        SocketUpgrade {
211            handler: Arc::new(handler),
212            config: WebSocketConfig { idle_timeout: None, ..WebSocketConfig::default() },
213            path: request.path().to_string(),
214            protocol: None,
215            request: Mutex::new(Some(request)),
216        }
217    }
218
219    async fn run_over_a_pipe(
220        upgrade: impl Upgrade,
221        buffered: Vec<u8>,
222    ) -> (tokio::io::DuplexStream, tokio::task::JoinHandle<()>) {
223        let (client, server) = tokio::io::duplex(4096);
224        let (reader, writer) = tokio::io::split(server);
225        let running = tokio::spawn(async move {
226            upgrade
227                .run(Upgraded { reader: Box::new(reader), writer: Box::new(writer), buffered })
228                .await;
229        });
230        (client, running)
231    }
232
233    async fn read_one(client: &mut tokio::io::DuplexStream) -> Frame {
234        let mut received = vec![0u8; 256];
235        let read = client.read(&mut received).await.unwrap();
236        Frame::decode(&received[..read], Role::Client, 1024).unwrap().unwrap().0
237    }
238
239    #[tokio::test]
240    async fn an_echo_handler_answers_over_the_upgraded_connection() {
241        // The route claims the socket…
242        assert!(echo_router().dispatch(handshake_request("/ws")).await.upgrades());
243
244        // …and this is what it does with it.
245        let upgrade = upgrade_for(
246            |mut socket: WebSocket, _request: Request| async move {
247                while let Some(message) = socket.recv().await {
248                    if socket.send(message).await.is_err() {
249                        break;
250                    }
251                }
252            },
253            handshake_request("/ws"),
254        );
255
256        let (mut client, running) = run_over_a_pipe(upgrade, Vec::new()).await;
257
258        let masked = Frame::client(OpCode::Text, b"ping me".to_vec(), [1, 2, 3, 4]);
259        client.write_all(&masked.encode()).await.unwrap();
260
261        let frame = read_one(&mut client).await;
262        assert_eq!(
263            Message::from_data(frame.opcode, frame.payload).unwrap(),
264            Message::text("ping me")
265        );
266
267        drop(client);
268        running.await.unwrap();
269    }
270
271    #[tokio::test]
272    async fn the_handler_receives_the_request_that_opened_the_socket() {
273        let upgrade = upgrade_for(
274            |mut socket: WebSocket, request: Request| async move {
275                // Everything about who is connecting lives on the request, and
276                // is gone once the connection stops being HTTP.
277                let room = request.query("room").unwrap_or("lobby").to_string();
278                let _ = socket.send(Message::text(room)).await;
279            },
280            handshake_request("/ws?room=kitchen"),
281        );
282
283        let (mut client, _running) = run_over_a_pipe(upgrade, Vec::new()).await;
284
285        assert_eq!(read_one(&mut client).await.payload, b"kitchen");
286    }
287
288    #[tokio::test]
289    async fn opening_a_socket_is_reported_to_instrumentation() {
290        // `events` is process-wide, so this test owns the subscriber list for
291        // its duration and clears it again on the way out.
292        let _guard = crate::testing::events_lock();
293        let seen = Arc::new(Mutex::new(Vec::new()));
294        let sink = Arc::clone(&seen);
295        events::subscribe(move |event: &Event| {
296            // Other tests open sockets on this same process-wide bus, so the
297            // path this test alone uses is what tells its events apart.
298            if event.field("path").and_then(|value| value.as_str()) == Some("/ws-instrumented") {
299                sink.lock().unwrap_or_else(|p| p.into_inner()).push((event.kind, event.fields.clone()));
300            }
301        });
302
303        let upgrade = upgrade_for(
304            |mut socket: WebSocket, _request: Request| async move {
305                while socket.recv().await.is_some() {}
306            },
307            handshake_request("/ws-instrumented"),
308        );
309        let early = Frame::client(OpCode::Text, b"hi".to_vec(), [9, 9, 9, 9]).encode();
310        let (client, running) = run_over_a_pipe(upgrade, early).await;
311
312        drop(client);
313        running.await.unwrap();
314
315        let kinds: Vec<&str> = seen.lock().unwrap_or_else(|p| p.into_inner()).iter().map(|(kind, _)| *kind).collect();
316        assert!(kinds.contains(&"ws.connected"), "{kinds:?}");
317        assert!(kinds.contains(&"ws.message"), "{kinds:?}");
318        assert!(kinds.contains(&"ws.disconnected"), "{kinds:?}");
319
320        let recorded = seen.lock().unwrap_or_else(|p| p.into_inner());
321        let message = recorded.iter().find(|(kind, _)| *kind == "ws.message").unwrap();
322        assert_eq!(message.1.get("kind").and_then(|v| v.as_str()), Some("text"));
323        assert_eq!(message.1.get("bytes").and_then(|v| v.as_i64()), Some(2));
324        // The payload itself is never recorded.
325        assert!(!message.1.values().any(|value| value.as_str() == Some("hi")));
326    }
327}