Skip to main content

netget/server/socketio/
mod.rs

1//! Socket.IO v5 over Engine.IO v4. Rust owns sessions, transports (long-polling, WebSocket,
2//! upgrade), heartbeats, packet framing, namespaces, acknowledgement ids and rooms; the handler
3//! decides connections and what to emit.
4pub 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    /// namespace → socket id
53    sockets: Mutex<HashMap<String, String>>,
54    last_pong: std::sync::Mutex<Instant>,
55    closed: AtomicBool,
56    /// Set when the outbound queue overflowed; the heartbeat task then closes the session.
57    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    /// socket id → (session id, namespace, rooms)
72    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
189/// Engine.IO's error body: `{"code": N, "message": ...}` with 400.
190fn 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        // The pong must arrive within pingTimeout.
328        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
494/// A long-poll: wait for queued packets (a ping arrives within pingInterval at the latest) and
495/// return them record-separated. A second concurrent poll on one session is a protocol error.
496async 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        // Upgrade from polling: "2probe" → "3probe", flush the pending poll with a noop, then
555        // wait for "5" before switching.
556        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
668/// Ask the handler about `event`; `Err(())` after logging when it cannot answer.
669async 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
922/// Carry out the handler's actions for socket `sid` in `nsp`. `ack` is the id the client
923/// waits on, answered at most once.
924async 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}