netget/server/socketio/
packet.rs1use anyhow::{bail, ensure, Context, Result};
3use serde_json::Value;
4
5pub const EIO_VERSION: &str = "4";
6pub 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#[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
56pub 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#[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 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 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
196pub 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
217pub 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}