1pub mod actions;
5pub mod packet;
6
7use crate::llm::action_helper::call_llm;
8use crate::llm::actions::protocol_trait::ActionResult;
9use crate::logging::emit::Log;
10use crate::protocol::{Event, SpawnContext};
11use crate::server::accept_bounded::{accept_bounded, ConnectionLimiter, DEFAULT_MAX_CONNECTIONS};
12use crate::server::connection::ConnectionId;
13use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
14use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
15use crate::utils::clock::Instant;
16use bytes::Bytes;
17use futures::{SinkExt, StreamExt};
18use http_body_util::{BodyExt, Full, Limited};
19use hyper::{
20 body::Incoming, header, server::conn::http1, service::service_fn, Method, Request, Response,
21 StatusCode,
22};
23use hyper_util::rt::{TokioIo, TokioTimer};
24use packet::{Eio, Kind, Sio};
25use serde_json::{json, Value};
26use std::collections::{HashMap, HashSet};
27use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
28use std::{convert::Infallible, net::SocketAddr, sync::Arc, time::Duration};
29use tokio::sync::{mpsc, Mutex, Notify};
30use tokio_tungstenite::tungstenite::{
31 handshake::derive_accept_key,
32 protocol::{Role, WebSocketConfig},
33 Message,
34};
35
36pub const DEFAULT_PATH: &str = "/socket.io/";
37pub const DEFAULT_NAMESPACES: &[&str] = &["/"];
38pub const DEFAULT_PING_INTERVAL: Duration = Duration::from_secs(25);
39pub const DEFAULT_PING_TIMEOUT: Duration = Duration::from_secs(20);
40const MAX_SESSIONS: usize = 256;
41const QUEUE: usize = 256;
42const MAX_ROOMS: usize = 64;
43const HEADER_TIMEOUT: Duration = Duration::from_secs(30);
44const BODY_TIMEOUT: Duration = Duration::from_secs(30);
45
46struct Session {
47 sid: String,
48 conn: ConnectionId,
49 tx: mpsc::Sender<String>,
50 rx: Mutex<mpsc::Receiver<String>>,
51 websocket: AtomicBool,
52 sockets: Mutex<HashMap<String, String>>,
54 last_pong: std::sync::Mutex<Instant>,
55 closed: AtomicBool,
56 overflow: AtomicBool,
58 closing: Notify,
59 acks: Mutex<HashMap<u64, (String, String)>>,
60 next_ack: AtomicU64,
61 inbound: Mutex<()>,
62}
63
64struct Shared {
65 ctx: SpawnContext,
66 path: String,
67 namespaces: Vec<String>,
68 ping_interval: Duration,
69 ping_timeout: Duration,
70 sessions: Mutex<HashMap<String, Arc<Session>>>,
71 sockets: Mutex<HashMap<String, (String, String, HashSet<String>)>>,
73}
74
75pub async fn spawn(ctx: SpawnContext) -> anyhow::Result<SocketAddr> {
76 let p = ctx.startup_params.as_ref();
77 let path = p
78 .map(|p| p.get_optional_string("path"))
79 .transpose()?
80 .flatten()
81 .unwrap_or_else(|| DEFAULT_PATH.to_owned());
82 anyhow::ensure!(
83 path.starts_with('/')
84 && path.len() <= 128
85 && path
86 .bytes()
87 .all(|b| b.is_ascii_alphanumeric() || b"/-_.".contains(&b)),
88 "path must be an absolute path"
89 );
90 let path = format!("{}/", path.trim_end_matches('/'));
91 let namespaces: Vec<String> = match p
92 .map(|p| p.get_optional_array("namespaces"))
93 .transpose()?
94 .flatten()
95 {
96 None => DEFAULT_NAMESPACES.iter().map(|s| s.to_string()).collect(),
97 Some(list) => list
98 .iter()
99 .map(|n| {
100 n.as_str()
101 .filter(|n| packet::namespace_ok(n))
102 .map(str::to_owned)
103 .ok_or_else(|| anyhow::anyhow!("namespaces are paths like /admin"))
104 })
105 .collect::<anyhow::Result<_>>()?,
106 };
107 anyhow::ensure!(
108 !namespaces.is_empty() && namespaces.len() <= 32,
109 "1 to 32 namespaces"
110 );
111 let ms = |k: &str, d: Duration| -> anyhow::Result<Duration> {
112 let v = p
113 .map(|p| p.get_optional_u64(k))
114 .transpose()?
115 .flatten()
116 .unwrap_or(d.as_millis() as u64);
117 anyhow::ensure!((1000..=120_000).contains(&v), "{k} must be 1000..=120000");
118 Ok(Duration::from_millis(v))
119 };
120 let ping_interval = ms("ping_interval_ms", DEFAULT_PING_INTERVAL)?;
121 let ping_timeout = ms("ping_timeout_ms", DEFAULT_PING_TIMEOUT)?;
122 let listener = tokio::net::TcpListener::bind(ctx.legacy_listen_addr()).await?;
123 let local = listener.local_addr()?;
124 Log::new(Some(&ctx.status_tx)).info(format!(
125 "Socket.IO server at http://{local}{path} (namespaces {})",
126 namespaces.join(", ")
127 ));
128 let shared = Arc::new(Shared {
129 ctx: ctx.clone(),
130 path,
131 namespaces,
132 ping_interval,
133 ping_timeout,
134 sessions: Mutex::default(),
135 sockets: Mutex::default(),
136 });
137 let server_id = ctx.server_id;
138 let accept = tokio::spawn(async move {
139 let limiter = ConnectionLimiter::new(DEFAULT_MAX_CONNECTIONS);
140 loop {
141 let (stream, peer, permit) = match accept_bounded(&listener, &limiter, b"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", "Socket.IO", Some(&shared.ctx.status_tx)).await {
142 Ok(v) => v,
143 Err(_) => break,
144 };
145 let child = shared.clone();
146 shared.ctx.state.spawn_server_task(server_id, async move {
147 let _permit = permit;
148 let svc = child.clone();
149 let upgrade: UpgradeSlot = Arc::default();
150 let svc_upgrade = upgrade.clone();
151 let service = service_fn(move |req| {
152 let shared = svc.clone();
153 let upgrade = svc_upgrade.clone();
154 async move { Ok::<_, Infallible>(route(&shared, peer, req, &upgrade).await) }
155 });
156 let mut builder = http1::Builder::new();
157 builder.timer(TokioTimer::new()).header_read_timeout(HEADER_TIMEOUT).max_headers(64).max_buf_size(64 * 1024);
158 let _ = builder.serve_connection(TokioIo::new(stream), service).with_upgrades().await;
159 let pending = upgrade.lock().await.take();
160 if let Some((on_upgrade, session, fresh)) = pending {
161 if let Ok(upgraded) = on_upgrade.await {
162 let mut cfg = WebSocketConfig::default();
163 cfg.max_message_size = Some(packet::MAX_PAYLOAD);
164 cfg.max_frame_size = Some(packet::MAX_PAYLOAD);
165 let ws = tokio_tungstenite::WebSocketStream::from_raw_socket(TokioIo::new(upgraded), Role::Server, Some(cfg)).await;
166 websocket(&child, session, ws, fresh).await;
167 }
168 }
169 }).await;
170 }
171 });
172 ctx.state.register_server_task(server_id, accept).await;
173 Ok(local)
174}
175
176type UpgradeSlot = Arc<Mutex<Option<(hyper::upgrade::OnUpgrade, Arc<Session>, bool)>>>;
177type Reply = Response<Full<Bytes>>;
178
179fn text(status: u16, body: String) -> Reply {
180 let mut r = Response::new(Full::new(Bytes::from(body)));
181 *r.status_mut() = StatusCode::from_u16(status).unwrap_or(StatusCode::BAD_REQUEST);
182 r.headers_mut().insert(
183 header::CONTENT_TYPE,
184 header::HeaderValue::from_static("text/plain; charset=UTF-8"),
185 );
186 r
187}
188
189fn eio_error(code: u8, message: &str) -> Reply {
191 let mut r = text(400, json!({"code": code, "message": message}).to_string());
192 r.headers_mut().insert(
193 header::CONTENT_TYPE,
194 header::HeaderValue::from_static("application/json"),
195 );
196 r
197}
198
199fn outcome(ctx: &SpawnContext, conn: ConnectionId, operation: &str, decision: &str) {
200 let summary = format!("Socket.IO session {conn} operation={operation} decision={decision}");
201 let log = Log::new(Some(&ctx.status_tx));
202 if decision.starts_with("fail_closed_") || decision == "model_silent" {
203 log.error(summary);
204 } else {
205 log.info(summary);
206 }
207}
208
209fn query(req: &Request<Incoming>) -> HashMap<String, String> {
210 req.uri()
211 .query()
212 .unwrap_or("")
213 .split('&')
214 .filter_map(|p| {
215 let (k, v) = p.split_once('=')?;
216 Some((k.to_owned(), v.to_owned()))
217 })
218 .collect()
219}
220
221fn open_packet(shared: &Shared, sid: &str, upgrades: bool) -> String {
222 let data = json!({"sid": sid, "upgrades": if upgrades { json!(["websocket"]) } else { json!([]) }, "pingInterval": shared.ping_interval.as_millis() as u64, "pingTimeout": shared.ping_timeout.as_millis() as u64, "maxPayload": packet::MAX_PAYLOAD});
223 Eio::Open(data.to_string()).encode()
224}
225
226async fn new_session(
227 shared: &Arc<Shared>,
228 peer: SocketAddr,
229 websocket: bool,
230) -> Option<Arc<Session>> {
231 let ctx = &shared.ctx;
232 let mut sessions = shared.sessions.lock().await;
233 if sessions.len() >= MAX_SESSIONS {
234 return None;
235 }
236 let (tx, rx) = mpsc::channel(QUEUE);
237 let conn = ConnectionId::new(ctx.state.get_next_unified_id().await);
238 let now = Instant::now();
239 ctx.state
240 .add_connection_to_server(
241 ctx.server_id,
242 ConnectionState {
243 id: conn,
244 remote_addr: peer,
245 local_addr: peer,
246 bytes_sent: 0,
247 bytes_received: 0,
248 packets_sent: 0,
249 packets_received: 0,
250 last_activity: now,
251 status: ConnectionStatus::Active,
252 status_changed_at: now,
253 protocol_info: ProtocolConnectionInfo::empty(),
254 },
255 )
256 .await;
257 let session = Arc::new(Session {
258 sid: packet::new_id(),
259 conn,
260 tx,
261 rx: Mutex::new(rx),
262 websocket: AtomicBool::new(websocket),
263 sockets: Mutex::default(),
264 last_pong: std::sync::Mutex::new(now),
265 closed: AtomicBool::new(false),
266 overflow: AtomicBool::new(false),
267 closing: Notify::new(),
268 acks: Mutex::default(),
269 next_ack: AtomicU64::new(1),
270 inbound: Mutex::new(()),
271 });
272 sessions.insert(session.sid.clone(), session.clone());
273 drop(sessions);
274 let peer_rx = crate::server::peer_support::register_peer_channel(
275 &ctx.state,
276 ctx.server_id,
277 conn.as_u32(),
278 )
279 .await;
280 ctx.state
281 .spawn_server_task(
282 ctx.server_id,
283 peer_commands(shared.clone(), session.clone(), peer_rx),
284 )
285 .await;
286 ctx.state
287 .spawn_server_task(ctx.server_id, heartbeat(shared.clone(), session.clone()))
288 .await;
289 Log::new(Some(&ctx.status_tx)).info(format!(
290 "Socket.IO session {conn} opened over {}",
291 if websocket { "websocket" } else { "polling" }
292 ));
293 Some(session)
294}
295
296async fn heartbeat(shared: Arc<Shared>, session: Arc<Session>) {
297 loop {
298 tokio::select! {
299 _ = tokio::time::sleep(shared.ping_interval) => {}
300 _ = session.closing.notified() => {
301 if session.overflow.load(Ordering::SeqCst) {
302 close_session(&shared, &session, "transport error").await;
303 }
304 return;
305 }
306 }
307 if session.closed.load(Ordering::SeqCst) {
308 return;
309 }
310 let since_pong = session
311 .last_pong
312 .lock()
313 .map(|t| t.elapsed())
314 .unwrap_or_default();
315 if since_pong > shared.ping_interval + shared.ping_timeout {
316 close_session(&shared, &session, "ping timeout").await;
317 return;
318 }
319 if session
320 .tx
321 .try_send(Eio::Ping(String::new()).encode())
322 .is_err()
323 {
324 close_session(&shared, &session, "transport error").await;
325 return;
326 }
327 tokio::select! {
329 _ = tokio::time::sleep(shared.ping_timeout) => {}
330 _ = session.closing.notified() => {
331 if session.overflow.load(Ordering::SeqCst) {
332 close_session(&shared, &session, "transport error").await;
333 }
334 return;
335 }
336 }
337 let since_pong = session
338 .last_pong
339 .lock()
340 .map(|t| t.elapsed())
341 .unwrap_or_default();
342 if since_pong > shared.ping_timeout + Duration::from_millis(50)
343 && !session.closed.load(Ordering::SeqCst)
344 {
345 close_session(&shared, &session, "ping timeout").await;
346 return;
347 }
348 }
349}
350
351async fn close_session(shared: &Arc<Shared>, session: &Arc<Session>, reason: &str) {
352 if session.closed.swap(true, Ordering::SeqCst) {
353 return;
354 }
355 let _ = session.tx.try_send(Eio::Close.encode());
356 session.closing.notify_waiters();
357 shared.sessions.lock().await.remove(&session.sid);
358 let ctx = &shared.ctx;
359 ctx.state
360 .remove_peer_handle(ctx.server_id, session.conn.as_u32())
361 .await;
362 ctx.state
363 .update_connection_status(ctx.server_id, session.conn, ConnectionStatus::Closed)
364 .await;
365 let sockets: Vec<(String, String)> = session.sockets.lock().await.drain().collect();
366 for (nsp, socket) in sockets {
367 shared.sockets.lock().await.remove(&socket);
368 notify_disconnect(shared, session, &nsp, &socket, reason).await;
369 }
370 Log::new(Some(&ctx.status_tx)).info(format!(
371 "Socket.IO session {} closed: {reason}",
372 session.conn
373 ));
374 let _ = ctx.status_tx.send("__UPDATE_UI__".into());
375}
376
377async fn route(
378 shared: &Arc<Shared>,
379 peer: SocketAddr,
380 mut req: Request<Incoming>,
381 upgrade: &UpgradeSlot,
382) -> Reply {
383 let path = req.uri().path();
384 if path != shared.path && format!("{path}/") != shared.path {
385 return text(404, "not found".into());
386 }
387 let q = query(&req);
388 if q.get("EIO").map(String::as_str) != Some(packet::EIO_VERSION) {
389 return eio_error(5, "Unsupported protocol version");
390 }
391 let session = match q.get("sid") {
392 None => None,
393 Some(sid) => match shared.sessions.lock().await.get(sid).cloned() {
394 Some(s) => Some(s),
395 None => return eio_error(1, "Session ID unknown"),
396 },
397 };
398 match (q.get("transport").map(String::as_str), req.method()) {
399 (Some("websocket"), &Method::GET) => {
400 let h = req.headers();
401 let key = h
402 .get(header::SEC_WEBSOCKET_KEY)
403 .and_then(|v| v.to_str().ok())
404 .map(str::to_owned);
405 let is_ws = h
406 .get(header::UPGRADE)
407 .and_then(|v| v.to_str().ok())
408 .is_some_and(|v| v.eq_ignore_ascii_case("websocket"));
409 let (Some(key), true) = (key, is_ws) else {
410 return eio_error(3, "Bad request");
411 };
412 let (session, fresh) = match session {
413 Some(s) => {
414 if s.websocket.load(Ordering::SeqCst) {
415 return eio_error(3, "Bad request");
416 }
417 (s, false)
418 }
419 None => match new_session(shared, peer, true).await {
420 Some(s) => (s, true),
421 None => return eio_error(3, "Too many sessions"),
422 },
423 };
424 *upgrade.lock().await = Some((hyper::upgrade::on(&mut req), session, fresh));
425 let mut r = Response::new(Full::new(Bytes::new()));
426 *r.status_mut() = StatusCode::SWITCHING_PROTOCOLS;
427 let hs = r.headers_mut();
428 hs.insert(
429 header::UPGRADE,
430 header::HeaderValue::from_static("websocket"),
431 );
432 hs.insert(
433 header::CONNECTION,
434 header::HeaderValue::from_static("Upgrade"),
435 );
436 if let Ok(v) = header::HeaderValue::from_str(&derive_accept_key(key.as_bytes())) {
437 hs.insert(header::SEC_WEBSOCKET_ACCEPT, v);
438 }
439 r
440 }
441 (Some("polling"), &Method::GET) => match session {
442 None => match new_session(shared, peer, false).await {
443 Some(s) => text(200, open_packet(shared, &s.sid, true)),
444 None => eio_error(3, "Too many sessions"),
445 },
446 Some(s) => poll(shared, &s).await,
447 },
448 (Some("polling"), &Method::POST) => {
449 let Some(s) = session else {
450 return eio_error(1, "Session ID unknown");
451 };
452 let body = match tokio::time::timeout(
453 BODY_TIMEOUT,
454 Limited::new(req.into_body(), packet::MAX_PAYLOAD).collect(),
455 )
456 .await
457 {
458 Ok(Ok(b)) => b.to_bytes(),
459 _ => {
460 close_session(shared, &s, "transport error").await;
461 return eio_error(3, "Bad request");
462 }
463 };
464 let packets = std::str::from_utf8(&body).ok().map(packet::split_payload);
465 match packets {
466 Some(Ok(list)) => {
467 let ctx = &shared.ctx;
468 ctx.state
469 .update_connection_stats(
470 ctx.server_id,
471 s.conn,
472 Some(body.len() as u64),
473 None,
474 Some(list.len() as u64),
475 None,
476 )
477 .await;
478 for p in list {
479 process(shared, &s, p).await;
480 }
481 text(200, "ok".into())
482 }
483 _ => {
484 close_session(shared, &s, "parse error").await;
485 eio_error(3, "Bad request")
486 }
487 }
488 }
489 (Some("polling" | "websocket"), _) => eio_error(2, "Bad handshake method"),
490 _ => eio_error(0, "Transport unknown"),
491 }
492}
493
494async fn poll(shared: &Arc<Shared>, s: &Arc<Session>) -> Reply {
497 if s.websocket.load(Ordering::SeqCst) {
498 return eio_error(3, "Bad request");
499 }
500 let Ok(mut rx) = s.rx.try_lock() else {
501 close_session(shared, s, "transport error").await;
502 return eio_error(3, "Bad request");
503 };
504 let first = tokio::select! {
505 p = rx.recv() => p,
506 _ = tokio::time::sleep(shared.ping_interval + shared.ping_timeout) => Some(Eio::Noop.encode()),
507 };
508 let Some(first) = first else {
509 return text(200, Eio::Close.encode());
510 };
511 let mut packets = vec![first];
512 let mut size = packets[0].len();
513 while packets.len() < packet::MAX_PACKETS && size < packet::MAX_PAYLOAD / 2 {
514 match rx.try_recv() {
515 Ok(p) => {
516 size += p.len();
517 packets.push(p);
518 }
519 Err(_) => break,
520 }
521 }
522 let body = packet::join_payload(&packets);
523 let ctx = &shared.ctx;
524 ctx.state
525 .update_connection_stats(
526 ctx.server_id,
527 s.conn,
528 None,
529 Some(body.len() as u64),
530 None,
531 Some(packets.len() as u64),
532 )
533 .await;
534 text(200, body)
535}
536
537async fn websocket(
538 shared: &Arc<Shared>,
539 session: Arc<Session>,
540 ws: tokio_tungstenite::WebSocketStream<TokioIo<hyper::upgrade::Upgraded>>,
541 fresh: bool,
542) {
543 let (mut sink, mut stream) = ws.split();
544 if fresh {
545 if sink
546 .send(Message::Text(open_packet(shared, &session.sid, false)))
547 .await
548 .is_err()
549 {
550 close_session(shared, &session, "transport error").await;
551 return;
552 }
553 } else {
554 let probe = tokio::time::timeout(Duration::from_secs(10), stream.next()).await;
557 if !matches!(probe, Ok(Some(Ok(Message::Text(ref t)))) if t == "2probe") {
558 return;
559 }
560 if sink.send(Message::Text("3probe".into())).await.is_err() {
561 return;
562 }
563 let _ = session.tx.try_send(Eio::Noop.encode());
564 let upgrade = tokio::time::timeout(Duration::from_secs(10), stream.next()).await;
565 if !matches!(upgrade, Ok(Some(Ok(Message::Text(ref t)))) if t == "5") {
566 return;
567 }
568 session.websocket.store(true, Ordering::SeqCst);
569 }
570 let writer_session = session.clone();
571 let writer_shared = shared.clone();
572 let writer = tokio::spawn(async move {
573 let mut rx = writer_session.rx.lock().await;
574 while let Some(p) = rx.recv().await {
575 let closing = p == Eio::Close.encode();
576 let n = p.len() as u64;
577 if sink.send(Message::Text(p)).await.is_err() {
578 break;
579 }
580 let ctx = &writer_shared.ctx;
581 ctx.state
582 .update_connection_stats(
583 ctx.server_id,
584 writer_session.conn,
585 None,
586 Some(n),
587 None,
588 Some(1),
589 )
590 .await;
591 if closing {
592 break;
593 }
594 }
595 let _ = sink.close().await;
596 });
597 loop {
598 let msg = tokio::select! {
599 m = stream.next() => m,
600 _ = session.closing.notified() => break,
601 };
602 let text = match msg {
603 Some(Ok(Message::Text(t))) => t,
604 Some(Ok(Message::Ping(_) | Message::Pong(_) | Message::Frame(_))) => continue,
605 Some(Ok(Message::Binary(_))) => {
606 Log::new(Some(&shared.ctx.status_tx)).warn(format!(
607 "Socket.IO session {}: binary frames are not supported",
608 session.conn
609 ));
610 continue;
611 }
612 _ => break,
613 };
614 let ctx = &shared.ctx;
615 ctx.state
616 .update_connection_stats(
617 ctx.server_id,
618 session.conn,
619 Some(text.len() as u64),
620 None,
621 Some(1),
622 None,
623 )
624 .await;
625 match Eio::decode(&text) {
626 Ok(p) => process(shared, &session, p).await,
627 Err(_) => break,
628 }
629 if session.closed.load(Ordering::SeqCst) {
630 break;
631 }
632 }
633 close_session(shared, &session, "transport close").await;
634 let _ = tokio::time::timeout(Duration::from_secs(2), writer).await;
635}
636
637async fn send(session: &Session, p: Sio) -> bool {
638 session
639 .tx
640 .try_send(Eio::Message(p.encode()).encode())
641 .is_ok()
642}
643
644async fn process(shared: &Arc<Shared>, session: &Arc<Session>, p: Eio) {
645 let _turn = session.inbound.lock().await;
646 match p {
647 Eio::Pong(_) => {
648 if let Ok(mut t) = session.last_pong.lock() {
649 *t = Instant::now();
650 }
651 }
652 Eio::Ping(d) => {
653 let _ = session.tx.try_send(Eio::Pong(d).encode());
654 }
655 Eio::Close => {
656 drop(_turn);
657 close_session(shared, session, "client disconnect").await;
658 }
659 Eio::Message(m) => match Sio::decode(&m) {
660 Ok(sio) => socket_packet(shared, session, sio).await,
661 Err(e) => Log::new(Some(&shared.ctx.status_tx))
662 .warn(format!("Socket.IO session {}: {e}", session.conn)),
663 },
664 Eio::Open(_) | Eio::Upgrade | Eio::Noop => {}
665 }
666}
667
668async fn ask(
670 shared: &Shared,
671 session: &Session,
672 event: Event,
673 operation: &str,
674 allowed: &[&str],
675) -> Result<Vec<Value>, ()> {
676 let ctx = &shared.ctx;
677 let result = match call_llm(
678 &ctx.llm_client,
679 &ctx.state,
680 ctx.server_id,
681 Some(session.conn),
682 &event,
683 &actions::SocketIoProtocol,
684 )
685 .await
686 {
687 Ok(r) => r,
688 Err(_) => {
689 outcome(ctx, session.conn, operation, "fail_closed_llm_error");
690 return Err(());
691 }
692 };
693 if !result.failures.is_empty() {
694 outcome(ctx, session.conn, operation, "fail_closed_invalid_reply");
695 return Err(());
696 }
697 let mut out = Vec::new();
698 let mut pending: Vec<ActionResult> = result.protocol_results.into_iter().rev().collect();
699 while let Some(r) = pending.pop() {
700 match r {
701 ActionResult::Custom { name, data } if allowed.contains(&name.as_str()) => {
702 out.push(data)
703 }
704 ActionResult::Multiple(items) => pending.extend(items.into_iter().rev()),
705 _ => {}
706 }
707 }
708 Ok(out)
709}
710
711async fn socket_packet(shared: &Arc<Shared>, session: &Arc<Session>, p: Sio) {
712 let ctx = &shared.ctx;
713 let socket = session.sockets.lock().await.get(&p.nsp).cloned();
714 match p.kind {
715 Kind::Connect => {
716 if socket.is_some() {
717 return;
718 }
719 if !shared.namespaces.contains(&p.nsp) {
720 outcome(ctx, session.conn, "connect", "protocol_refusal");
721 send(
722 session,
723 Sio::new(
724 Kind::ConnectError,
725 &p.nsp,
726 None,
727 Some(json!({"message": "Invalid namespace"})),
728 ),
729 )
730 .await;
731 return;
732 }
733 let event = Event::new(
734 &actions::CONNECT_EVENT,
735 json!({"namespace": p.nsp, "auth": p.data, "session_id": session.sid, "transport": if session.websocket.load(Ordering::SeqCst) { "websocket" } else { "polling" }}),
736 );
737 let answers = match ask(
738 shared,
739 session,
740 event,
741 "connect",
742 &[
743 "socketio_accept",
744 "socketio_reject",
745 "socketio_emit",
746 "socketio_join",
747 ],
748 )
749 .await
750 {
751 Ok(a) => a,
752 Err(()) => {
753 send(session, Sio::new(Kind::ConnectError, &p.nsp, None, Some(json!({"message": crate::utils::wire_failure::WireFailure::Unavailable.text()})))).await;
754 return;
755 }
756 };
757 let accept = answers.iter().position(|a| a["type"] == "socketio_accept");
758 let reject = answers.iter().find(|a| a["type"] == "socketio_reject");
759 match (accept, reject) {
760 (Some(_), None) => {
761 let sid = packet::new_id();
762 session
763 .sockets
764 .lock()
765 .await
766 .insert(p.nsp.clone(), sid.clone());
767 shared.sockets.lock().await.insert(
768 sid.clone(),
769 (session.sid.clone(), p.nsp.clone(), HashSet::new()),
770 );
771 send(
772 session,
773 Sio::new(Kind::Connect, &p.nsp, None, Some(json!({"sid": sid}))),
774 )
775 .await;
776 outcome(ctx, session.conn, "connect", "model_answer");
777 let rest: Vec<Value> = answers
778 .into_iter()
779 .filter(|a| a["type"] != "socketio_accept")
780 .collect();
781 apply(shared, session, &p.nsp, &sid, None, rest).await;
782 }
783 (None, Some(r)) => {
784 outcome(ctx, session.conn, "connect", "model_reject");
785 let mut data = json!({"message": r["message"]});
786 if let Some(d) = r.get("data").filter(|d| d.is_object()) {
787 data["data"] = d.clone();
788 }
789 send(
790 session,
791 Sio::new(Kind::ConnectError, &p.nsp, None, Some(data)),
792 )
793 .await;
794 }
795 _ => {
796 outcome(
797 ctx,
798 session.conn,
799 "connect",
800 if answers.is_empty() {
801 "model_silent"
802 } else {
803 "fail_closed_invalid_reply"
804 },
805 );
806 send(session, Sio::new(Kind::ConnectError, &p.nsp, None, Some(json!({"message": crate::utils::wire_failure::WireFailure::Unavailable.text()})))).await;
807 }
808 }
809 }
810 Kind::Disconnect => {
811 if let Some(sid) = socket {
812 session.sockets.lock().await.remove(&p.nsp);
813 shared.sockets.lock().await.remove(&sid);
814 notify_disconnect(shared, session, &p.nsp, &sid, "client namespace disconnect")
815 .await;
816 }
817 }
818 Kind::Event => {
819 let Some(sid) = socket else {
820 Log::new(Some(&ctx.status_tx)).warn(format!(
821 "Socket.IO session {}: event for unconnected namespace {}",
822 session.conn, p.nsp
823 ));
824 return;
825 };
826 let (name, args) = match p.event() {
827 Ok(e) => e,
828 Err(e) => {
829 Log::new(Some(&ctx.status_tx))
830 .warn(format!("Socket.IO session {}: {e}", session.conn));
831 return;
832 }
833 };
834 let rooms: Vec<String> = shared
835 .sockets
836 .lock()
837 .await
838 .get(&sid)
839 .map(|s| s.2.iter().cloned().collect())
840 .unwrap_or_default();
841 let event = Event::new(
842 &actions::EVENT_EVENT,
843 json!({"socket_id": sid, "namespace": p.nsp, "event": name, "args": args, "ack_requested": p.id.is_some(), "rooms": rooms}),
844 );
845 if let Ok(answers) = ask(
846 shared,
847 session,
848 event,
849 "event",
850 &[
851 "socketio_emit",
852 "socketio_ack",
853 "socketio_join",
854 "socketio_leave",
855 "socketio_disconnect_socket",
856 ],
857 )
858 .await
859 {
860 outcome(ctx, session.conn, "event", "model_answer");
861 apply(shared, session, &p.nsp, &sid, p.id, answers).await;
862 }
863 }
864 Kind::Ack => {
865 let (Some(sid), Some(id)) = (socket, p.id) else {
866 return;
867 };
868 let Some((nsp, event_name)) = session.acks.lock().await.remove(&id) else {
869 return;
870 };
871 if nsp != p.nsp {
872 return;
873 }
874 let args = p
875 .data
876 .as_ref()
877 .and_then(Value::as_array)
878 .cloned()
879 .unwrap_or_default();
880 let event = Event::new(
881 &actions::ACK_EVENT,
882 json!({"socket_id": sid, "namespace": nsp, "event": event_name, "args": args}),
883 );
884 if let Ok(answers) = ask(
885 shared,
886 session,
887 event,
888 "ack",
889 &[
890 "socketio_emit",
891 "socketio_join",
892 "socketio_leave",
893 "socketio_disconnect_socket",
894 ],
895 )
896 .await
897 {
898 outcome(ctx, session.conn, "ack", "model_answer");
899 apply(shared, session, &nsp, &sid, None, answers).await;
900 }
901 }
902 Kind::ConnectError | Kind::BinaryEvent | Kind::BinaryAck => {}
903 }
904}
905
906async fn notify_disconnect(
907 shared: &Arc<Shared>,
908 session: &Arc<Session>,
909 nsp: &str,
910 socket: &str,
911 reason: &str,
912) {
913 let event = Event::new(
914 &actions::DISCONNECT_EVENT,
915 json!({"socket_id": socket, "namespace": nsp, "reason": reason}),
916 );
917 if let Ok(answers) = ask(shared, session, event, "disconnect", &["socketio_emit"]).await {
918 apply(shared, session, nsp, socket, None, answers).await;
919 }
920}
921
922async fn apply(
925 shared: &Arc<Shared>,
926 session: &Arc<Session>,
927 nsp: &str,
928 sid: &str,
929 mut ack: Option<u64>,
930 answers: Vec<Value>,
931) {
932 let ctx = &shared.ctx;
933 for a in answers {
934 match a["type"].as_str().unwrap_or_default() {
935 "socketio_emit" => {
936 let ns = a["namespace"].as_str().unwrap_or(nsp).to_owned();
937 let to = a["to"].as_str().unwrap_or("sender");
938 let include_sender = a["include_sender"].as_bool().unwrap_or(true);
939 let mut payload = vec![a["event"].clone()];
940 payload.extend(a["args"].as_array().cloned().unwrap_or_default());
941 let targets: Vec<String> = {
942 let sockets = shared.sockets.lock().await;
943 match to {
944 "sender" => vec![sid.to_owned()],
945 "namespace" => sockets
946 .iter()
947 .filter(|(_, v)| v.1 == ns)
948 .map(|(k, _)| k.clone())
949 .collect(),
950 t if t.starts_with("room:") => {
951 let room = &t[5..];
952 sockets
953 .iter()
954 .filter(|(_, v)| v.1 == ns && v.2.contains(room))
955 .map(|(k, _)| k.clone())
956 .collect()
957 }
958 t => vec![t.trim_start_matches("socket:").to_owned()],
959 }
960 };
961 let targets: Vec<String> = targets
962 .into_iter()
963 .filter(|t| include_sender || t != sid)
964 .collect();
965 let want_ack = a["ack"].as_bool().unwrap_or(false) && targets.len() == 1;
966 for t in targets {
967 let Some((tsid, tns)) = shared
968 .sockets
969 .lock()
970 .await
971 .get(&t)
972 .map(|v| (v.0.clone(), v.1.clone()))
973 else {
974 continue;
975 };
976 let Some(ts) = shared.sessions.lock().await.get(&tsid).cloned() else {
977 continue;
978 };
979 let id = if want_ack {
980 let id = ts.next_ack.fetch_add(1, Ordering::Relaxed);
981 ts.acks.lock().await.insert(
982 id,
983 (
984 tns.clone(),
985 a["event"].as_str().unwrap_or_default().to_owned(),
986 ),
987 );
988 Some(id)
989 } else {
990 None
991 };
992 if !send(
993 &ts,
994 Sio::new(Kind::Event, &tns, id, Some(Value::Array(payload.clone()))),
995 )
996 .await
997 {
998 Log::new(Some(&ctx.status_tx)).warn(format!(
999 "Socket.IO session {}: queue full, closing",
1000 ts.conn
1001 ));
1002 ts.overflow.store(true, Ordering::SeqCst);
1003 ts.closing.notify_waiters();
1004 }
1005 }
1006 }
1007 "socketio_ack" => {
1008 if let Some(id) = ack.take() {
1009 let args = a["args"].as_array().cloned().unwrap_or_default();
1010 send(
1011 session,
1012 Sio::new(Kind::Ack, nsp, Some(id), Some(Value::Array(args))),
1013 )
1014 .await;
1015 }
1016 }
1017 "socketio_join" | "socketio_leave" => {
1018 let room = a["room"].as_str().unwrap_or_default().to_owned();
1019 if let Some(entry) = shared.sockets.lock().await.get_mut(sid) {
1020 if a["type"] == "socketio_join" {
1021 if entry.2.len() < MAX_ROOMS {
1022 entry.2.insert(room);
1023 }
1024 } else {
1025 entry.2.remove(&room);
1026 }
1027 }
1028 }
1029 "socketio_disconnect_socket" => {
1030 if session.sockets.lock().await.remove(nsp).is_some() {
1031 shared.sockets.lock().await.remove(sid);
1032 send(session, Sio::new(Kind::Disconnect, nsp, None, None)).await;
1033 }
1034 }
1035 _ => {}
1036 }
1037 }
1038}
1039
1040async fn peer_commands(
1041 shared: Arc<Shared>,
1042 session: Arc<Session>,
1043 mut rx: mpsc::Receiver<ClientCommand>,
1044) {
1045 while let Some(command) = rx.recv().await {
1046 let a = command.action.clone();
1047 let result = actions::check_action(&a).and_then(|()| match a["type"].as_str() {
1048 Some("socketio_emit" | "socketio_disconnect_socket" | "disconnect") => Ok(()),
1049 _ => anyhow::bail!("a Socket.IO session accepts socketio_emit, socketio_disconnect_socket or disconnect"),
1050 });
1051 let outcome = match result {
1052 Err(e) => ClientSendOutcome::Rejected {
1053 error: e.to_string(),
1054 },
1055 Ok(()) if a["type"] == "disconnect" => {
1056 close_session(&shared, &session, "server disconnect").await;
1057 ClientSendOutcome::Disconnected
1058 }
1059 Ok(()) => {
1060 let nsp = a["namespace"].as_str().unwrap_or("/").to_owned();
1061 match session.sockets.lock().await.get(&nsp).cloned() {
1062 None => ClientSendOutcome::Rejected {
1063 error: format!("this session has no socket in {nsp}"),
1064 },
1065 Some(sid) => {
1066 apply(&shared, &session, &nsp, &sid, None, vec![a.clone()]).await;
1067 ClientSendOutcome::Sent {
1068 bytes_sent: a.to_string().len(),
1069 }
1070 }
1071 }
1072 }
1073 };
1074 shared
1075 .ctx
1076 .state
1077 .record_access_log(
1078 crate::state::AccessLogOwner::Server(shared.ctx.server_id.as_u32()),
1079 "Socket.IO",
1080 Some(session.conn.as_u32()),
1081 "injected_action",
1082 json!({"type": a["type"], "event": a["event"]}),
1083 vec![serde_json::to_value(&outcome).unwrap_or(Value::Null)],
1084 )
1085 .await;
1086 let _ = command.reply_tx.send(Ok(outcome));
1087 if session.closed.load(Ordering::SeqCst) {
1088 return;
1089 }
1090 }
1091}