Skip to main content

netget/server/socketio/
packet.rs

1//! Engine.IO v4 and Socket.IO v5 packet codecs, shared by the server and the client.
2use anyhow::{bail, ensure, Context, Result};
3use serde_json::Value;
4
5pub const EIO_VERSION: &str = "4";
6/// Engine.IO v4 separates packets in a polling payload with the record separator.
7pub const SEPARATOR: char = '\u{1e}';
8pub const MAX_PAYLOAD: usize = 1_000_000;
9pub const MAX_PACKETS: usize = 256;
10pub const MAX_EVENT_NAME: usize = 128;
11pub const MAX_ARGS: usize = 16;
12
13/// Engine.IO packet types.
14#[derive(Debug, Clone, PartialEq)]
15pub enum Eio {
16    Open(String),
17    Close,
18    Ping(String),
19    Pong(String),
20    Message(String),
21    Upgrade,
22    Noop,
23}
24
25impl Eio {
26    pub fn decode(s: &str) -> Result<Self> {
27        let mut chars = s.chars();
28        let kind = chars.next().context("empty Engine.IO packet")?;
29        let rest = chars.as_str().to_owned();
30        Ok(match kind {
31            '0' => Eio::Open(rest),
32            '1' => Eio::Close,
33            '2' => Eio::Ping(rest),
34            '3' => Eio::Pong(rest),
35            '4' => Eio::Message(rest),
36            '5' => Eio::Upgrade,
37            '6' => Eio::Noop,
38            'b' => bail!("binary Engine.IO packets are not supported"),
39            other => bail!("unknown Engine.IO packet type {other:?}"),
40        })
41    }
42
43    pub fn encode(&self) -> String {
44        match self {
45            Eio::Open(d) => format!("0{d}"),
46            Eio::Close => "1".into(),
47            Eio::Ping(d) => format!("2{d}"),
48            Eio::Pong(d) => format!("3{d}"),
49            Eio::Message(d) => format!("4{d}"),
50            Eio::Upgrade => "5".into(),
51            Eio::Noop => "6".into(),
52        }
53    }
54}
55
56/// Split a polling payload into packets (Engine.IO v4: record separator).
57pub fn split_payload(body: &str) -> Result<Vec<Eio>> {
58    ensure!(
59        body.len() <= MAX_PAYLOAD,
60        "payload over {MAX_PAYLOAD} bytes"
61    );
62    let parts: Vec<&str> = body.split(SEPARATOR).collect();
63    ensure!(
64        parts.len() <= MAX_PACKETS,
65        "more than {MAX_PACKETS} packets in one payload"
66    );
67    parts.into_iter().map(Eio::decode).collect()
68}
69
70pub fn join_payload(packets: &[String]) -> String {
71    packets.join(&SEPARATOR.to_string())
72}
73
74/// Socket.IO packet types.
75#[derive(Debug, Clone, Copy, PartialEq)]
76pub enum Kind {
77    Connect = 0,
78    Disconnect = 1,
79    Event = 2,
80    Ack = 3,
81    ConnectError = 4,
82    BinaryEvent = 5,
83    BinaryAck = 6,
84}
85
86#[derive(Debug, Clone, PartialEq)]
87pub struct Sio {
88    pub kind: Kind,
89    pub nsp: String,
90    pub id: Option<u64>,
91    pub data: Option<Value>,
92}
93
94impl Sio {
95    pub fn new(kind: Kind, nsp: &str, id: Option<u64>, data: Option<Value>) -> Self {
96        Self {
97            kind,
98            nsp: nsp.to_owned(),
99            id,
100            data,
101        }
102    }
103
104    /// `<type>[<attachments>-][<nsp>,][<id>][<json>]`.
105    pub fn decode(s: &str) -> Result<Self> {
106        let b = s.as_bytes();
107        let kind = match b.first().copied() {
108            Some(b'0') => Kind::Connect,
109            Some(b'1') => Kind::Disconnect,
110            Some(b'2') => Kind::Event,
111            Some(b'3') => Kind::Ack,
112            Some(b'4') => Kind::ConnectError,
113            Some(b'5') => Kind::BinaryEvent,
114            Some(b'6') => Kind::BinaryAck,
115            _ => bail!("unknown Socket.IO packet type"),
116        };
117        ensure!(
118            !matches!(kind, Kind::BinaryEvent | Kind::BinaryAck),
119            "binary Socket.IO packets are not supported"
120        );
121        let mut i = 1;
122        let mut nsp = "/".to_owned();
123        if b.get(i) == Some(&b'/') {
124            let end = s[i..].find(',').map(|e| e + i).unwrap_or(s.len());
125            nsp = s[i..end].to_owned();
126            ensure!(
127                namespace_ok(&nsp),
128                "namespace must be / followed by letters, digits, -, _ or ."
129            );
130            i = (end + 1).min(s.len());
131        }
132        let start = i;
133        while i < b.len() && b[i].is_ascii_digit() {
134            i += 1;
135        }
136        let id = if i > start {
137            ensure!(i - start <= 15, "acknowledgement id too long");
138            Some(s[start..i].parse::<u64>()?)
139        } else {
140            None
141        };
142        let data = if i < b.len() {
143            let v: Value =
144                serde_json::from_str(&s[i..]).context("Socket.IO payload is not JSON")?;
145            ensure!(
146                crate::utils::json_budget::within_budget(&v, MAX_PAYLOAD, 20_000, 32),
147                "Socket.IO payload exceeds the bounds"
148            );
149            Some(v)
150        } else {
151            None
152        };
153        Ok(Self {
154            kind,
155            nsp,
156            id,
157            data,
158        })
159    }
160
161    pub fn encode(&self) -> String {
162        let mut out = (self.kind as u8).to_string();
163        if self.nsp != "/" {
164            out.push_str(&self.nsp);
165            out.push(',');
166        }
167        if let Some(id) = self.id {
168            out.push_str(&id.to_string());
169        }
170        if let Some(d) = &self.data {
171            out.push_str(&d.to_string());
172        }
173        out
174    }
175
176    /// An EVENT's name and arguments.
177    pub fn event(&self) -> Result<(String, Vec<Value>)> {
178        let list = self
179            .data
180            .as_ref()
181            .and_then(Value::as_array)
182            .context("an event payload is an array")?;
183        let name = list
184            .first()
185            .and_then(Value::as_str)
186            .context("an event starts with its name")?;
187        ensure!(
188            !name.is_empty() && name.len() <= MAX_EVENT_NAME,
189            "event name must be 1..{MAX_EVENT_NAME} characters"
190        );
191        ensure!(list.len() - 1 <= MAX_ARGS, "at most {MAX_ARGS} arguments");
192        Ok((name.to_owned(), list[1..].to_vec()))
193    }
194}
195
196/// Event names Socket.IO reserves on the wire.
197pub fn reserved(name: &str) -> bool {
198    matches!(
199        name,
200        "connect"
201            | "connect_error"
202            | "disconnect"
203            | "disconnecting"
204            | "newListener"
205            | "removeListener"
206    )
207}
208
209pub fn namespace_ok(nsp: &str) -> bool {
210    nsp.starts_with('/')
211        && nsp.len() <= 128
212        && nsp
213            .bytes()
214            .all(|b| b.is_ascii_alphanumeric() || b"/-_.".contains(&b))
215}
216
217/// A random session / socket id (20 URL-safe characters, as Engine.IO uses).
218pub fn new_id() -> String {
219    use base64::Engine;
220    let raw: [u8; 15] = rand::random();
221    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(raw)
222}