1use 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
22pub 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 pub fn with_config(mut self, config: WebSocketConfig) -> Self {
50 self.config = config;
51 self
52 }
53
54 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 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 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 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 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 assert!(echo_router().dispatch(handshake_request("/ws")).await.upgrades());
243
244 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 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 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 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 assert!(!message.1.values().any(|value| value.as_str() == Some("hi")));
326 }
327}