1use serde_json::{json, Value};
32
33pub const MAGIC_V2: &[u8; 4] = b" V2";
35
36pub const FRAME_RESPONSE: u32 = 0;
37pub const FRAME_ERROR: u32 = 1;
38pub const FRAME_MESSAGE: u32 = 2;
39
40pub const MAX_LINE: usize = 1024;
44
45pub const MAX_MSG_SIZE: usize = 1024 * 1024;
47
48pub const MAX_BODY_SIZE: usize = 5 * 1024 * 1024;
50
51pub const MAX_MPUB_MESSAGES: usize = (MAX_BODY_SIZE - 4) / 5;
54
55pub const MAX_RDY_COUNT: u64 = 2500;
57
58pub const MAX_REQ_TIMEOUT_MS: i64 = 60 * 60 * 1000;
60
61pub const MSG_TIMEOUT_MS: i64 = 60_000;
63pub const MAX_MSG_TIMEOUT_MS: i64 = 15 * 60 * 1000;
64
65pub const MAX_HEARTBEAT_MS: i64 = 60_000;
67pub const MIN_HEARTBEAT_MS: i64 = 1000;
68
69pub const MSG_ID_LEN: usize = 16;
71
72pub const MAX_NAME_LEN: usize = 64;
74
75pub const HEARTBEAT: &[u8] = b"_heartbeat_";
77
78pub const ERROR_CODES: &[&str] = &[
80 "E_INVALID",
81 "E_BAD_BODY",
82 "E_BAD_TOPIC",
83 "E_BAD_CHANNEL",
84 "E_BAD_MESSAGE",
85 "E_PUB_FAILED",
86 "E_MPUB_FAILED",
87 "E_DPUB_FAILED",
88 "E_FIN_FAILED",
89 "E_REQ_FAILED",
90 "E_TOUCH_FAILED",
91 "E_AUTH_FAILED",
92 "E_UNAUTHORIZED",
93 "E_AUTH_DISABLED",
94 "E_TOO_MANY_CHANNEL_CONSUMERS",
95];
96
97pub fn is_fatal(code: &str) -> bool {
100 !matches!(code, "E_FIN_FAILED" | "E_REQ_FAILED" | "E_TOUCH_FAILED")
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum Command {
106 Identify(Vec<u8>),
107 Sub {
108 topic: String,
109 channel: String,
110 },
111 Rdy(u64),
112 Fin(String),
113 Req {
114 id: String,
115 timeout_ms: i64,
116 },
117 Touch(String),
118 Pub {
119 topic: String,
120 body: Vec<u8>,
121 },
122 Mpub {
123 topic: String,
124 messages: Vec<Vec<u8>>,
125 },
126 Dpub {
127 topic: String,
128 defer_ms: i64,
129 body: Vec<u8>,
130 },
131 Nop,
132 Cls,
133 Auth(Vec<u8>),
134}
135
136impl Command {
137 pub fn name(&self) -> &'static str {
139 match self {
140 Command::Identify(_) => "IDENTIFY",
141 Command::Sub { .. } => "SUB",
142 Command::Rdy(_) => "RDY",
143 Command::Fin(_) => "FIN",
144 Command::Req { .. } => "REQ",
145 Command::Touch(_) => "TOUCH",
146 Command::Pub { .. } => "PUB",
147 Command::Mpub { .. } => "MPUB",
148 Command::Dpub { .. } => "DPUB",
149 Command::Nop => "NOP",
150 Command::Cls => "CLS",
151 Command::Auth(_) => "AUTH",
152 }
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct WireError {
160 pub code: &'static str,
161 pub message: String,
162 pub fatal: bool,
163}
164
165impl WireError {
166 fn fatal(code: &'static str, message: impl Into<String>) -> Self {
167 Self {
168 code,
169 message: message.into(),
170 fatal: true,
171 }
172 }
173
174 pub fn decision(&self) -> &'static str {
176 if self.message.contains("too big") || self.message.contains("too long") {
177 "fail_closed_too_large"
178 } else if self.message.contains("message count") {
179 "fail_closed_mpub_count"
180 } else {
181 "fail_closed_bad_command"
182 }
183 }
184
185 pub fn to_frame(&self) -> Vec<u8> {
186 error_frame(self.code, &self.message)
187 }
188}
189
190pub fn valid_name(name: &str) -> bool {
193 if name.is_empty() || name.len() > MAX_NAME_LEN {
194 return false;
195 }
196 let stem = name.strip_suffix("#ephemeral").unwrap_or(name);
197 !stem.is_empty()
198 && stem
199 .bytes()
200 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
201}
202
203pub fn is_ephemeral(name: &str) -> bool {
205 name.ends_with("#ephemeral")
206}
207
208pub fn parse_command(buf: &[u8]) -> Result<Option<(Command, usize)>, WireError> {
215 let window = &buf[..buf.len().min(MAX_LINE)];
216 let Some(nl) = window.iter().position(|b| *b == b'\n') else {
217 if buf.len() >= MAX_LINE {
218 return Err(WireError::fatal(
219 "E_INVALID",
220 format!("command line too long (over {MAX_LINE} bytes)"),
221 ));
222 }
223 return Ok(None);
224 };
225 let mut line = &buf[..nl];
226 if line.last() == Some(&b'\r') {
227 line = &line[..line.len() - 1];
228 }
229 let line = String::from_utf8_lossy(line).into_owned();
230 let params: Vec<&str> = line.split(' ').collect();
231 let after_line = nl + 1;
232
233 let body = |label: &str, min: i64, max: usize| -> Result<Option<(Vec<u8>, usize)>, WireError> {
234 let rest = &buf[after_line..];
235 if rest.len() < 4 {
236 return Ok(None);
237 }
238 let size = i32::from_be_bytes(rest[..4].try_into().unwrap()) as i64;
239 let (code, what) = match label {
240 "PUB" | "DPUB" => ("E_BAD_MESSAGE", "message"),
241 _ => ("E_BAD_BODY", "body"),
242 };
243 if size < min {
244 let kind = if what == "message" {
245 "message body"
246 } else {
247 "body"
248 };
249 return Err(WireError::fatal(
250 code,
251 format!("{label} invalid {kind} size {size}"),
252 ));
253 }
254 if size as usize > max {
255 return Err(WireError::fatal(
256 code,
257 format!("{label} {what} too big {size} > {max}"),
258 ));
259 }
260 let size = size as usize;
261 if rest.len() < 4 + size {
262 return Ok(None);
263 }
264 Ok(Some((rest[4..4 + size].to_vec(), after_line + 4 + size)))
265 };
266
267 let topic_param = |label: &str| -> Result<String, WireError> {
268 let topic = params[1];
269 if !valid_name(topic) {
270 return Err(WireError::fatal(
271 "E_BAD_TOPIC",
272 format!("{label} topic name {topic:?} is not valid"),
273 ));
274 }
275 Ok(topic.to_string())
276 };
277
278 let command = match params[0] {
279 "IDENTIFY" => match body("IDENTIFY", 1, MAX_BODY_SIZE)? {
280 None => return Ok(None),
281 Some((body, used)) => return Ok(Some((Command::Identify(body), used))),
282 },
283 "AUTH" => match body("AUTH", 1, MAX_BODY_SIZE)? {
284 None => return Ok(None),
285 Some((body, used)) => return Ok(Some((Command::Auth(body), used))),
286 },
287 "PUB" => {
288 if params.len() < 2 {
289 return Err(WireError::fatal(
290 "E_INVALID",
291 "PUB insufficient number of parameters",
292 ));
293 }
294 let topic = topic_param("PUB")?;
295 match body("PUB", 1, MAX_MSG_SIZE)? {
296 None => return Ok(None),
297 Some((body, used)) => return Ok(Some((Command::Pub { topic, body }, used))),
298 }
299 }
300 "DPUB" => {
301 if params.len() < 3 {
302 return Err(WireError::fatal(
303 "E_INVALID",
304 "DPUB insufficient number of parameters",
305 ));
306 }
307 let topic = topic_param("DPUB")?;
308 let defer_ms: i64 = params[2].parse().map_err(|_| {
309 WireError::fatal(
310 "E_INVALID",
311 format!("DPUB could not parse timeout {}", params[2]),
312 )
313 })?;
314 if !(0..=MAX_REQ_TIMEOUT_MS).contains(&defer_ms) {
315 return Err(WireError::fatal(
316 "E_INVALID",
317 format!("DPUB timeout {defer_ms} out of range 0-{MAX_REQ_TIMEOUT_MS}"),
318 ));
319 }
320 match body("DPUB", 1, MAX_MSG_SIZE)? {
321 None => return Ok(None),
322 Some((body, used)) => {
323 return Ok(Some((
324 Command::Dpub {
325 topic,
326 defer_ms,
327 body,
328 },
329 used,
330 )))
331 }
332 }
333 }
334 "MPUB" => {
335 if params.len() < 2 {
336 return Err(WireError::fatal(
337 "E_INVALID",
338 "MPUB insufficient number of parameters",
339 ));
340 }
341 let topic = topic_param("MPUB")?;
342 let rest = &buf[after_line..];
344 if rest.len() >= 8 {
345 let size = i32::from_be_bytes(rest[..4].try_into().unwrap()) as i64;
346 if (1..=MAX_BODY_SIZE as i64).contains(&size) {
347 let count = i32::from_be_bytes(rest[4..8].try_into().unwrap()) as i64;
348 check_mpub_count(count)?;
349 }
350 }
351 match body("MPUB", 1, MAX_BODY_SIZE)? {
352 None => return Ok(None),
353 Some((body, used)) => {
354 let messages = split_mpub(&body)?;
355 return Ok(Some((Command::Mpub { topic, messages }, used)));
356 }
357 }
358 }
359 "SUB" => {
360 if params.len() < 3 {
361 return Err(WireError::fatal(
362 "E_INVALID",
363 "SUB insufficient number of parameters",
364 ));
365 }
366 let topic = topic_param("SUB")?;
367 let channel = params[2];
368 if !valid_name(channel) {
369 return Err(WireError::fatal(
370 "E_BAD_CHANNEL",
371 format!("SUB channel name {channel:?} is not valid"),
372 ));
373 }
374 Command::Sub {
375 topic,
376 channel: channel.to_string(),
377 }
378 }
379 "RDY" => {
380 let count: i64 = match params.get(1) {
381 None => 1,
382 Some(p) => p.parse().map_err(|_| {
383 WireError::fatal("E_INVALID", format!("RDY could not parse count {p}"))
384 })?,
385 };
386 if count < 0 || count as u64 > MAX_RDY_COUNT {
387 return Err(WireError::fatal(
388 "E_INVALID",
389 format!("RDY count {count} out of range 0-{MAX_RDY_COUNT}"),
390 ));
391 }
392 Command::Rdy(count as u64)
393 }
394 "FIN" => {
395 if params.len() < 2 {
396 return Err(WireError::fatal(
397 "E_INVALID",
398 "FIN insufficient number of params",
399 ));
400 }
401 Command::Fin(message_id(params[1])?)
402 }
403 "TOUCH" => {
404 if params.len() < 2 {
405 return Err(WireError::fatal(
406 "E_INVALID",
407 "TOUCH insufficient number of params",
408 ));
409 }
410 Command::Touch(message_id(params[1])?)
411 }
412 "REQ" => {
413 if params.len() < 3 {
414 return Err(WireError::fatal(
415 "E_INVALID",
416 "REQ insufficient number of params",
417 ));
418 }
419 let id = message_id(params[1])?;
420 let timeout_ms: i64 = params[2].parse().map_err(|_| {
421 WireError::fatal(
422 "E_INVALID",
423 format!("REQ could not parse timeout {}", params[2]),
424 )
425 })?;
426 Command::Req {
428 id,
429 timeout_ms: timeout_ms.clamp(0, MAX_REQ_TIMEOUT_MS),
430 }
431 }
432 "NOP" => Command::Nop,
433 "CLS" => Command::Cls,
434 other => {
435 return Err(WireError::fatal(
436 "E_INVALID",
437 format!(
438 "invalid command {}",
439 crate::utils::sanitize::line_field(other)
440 ),
441 ))
442 }
443 };
444 Ok(Some((command, after_line)))
445}
446
447fn check_mpub_count(count: i64) -> Result<(), WireError> {
448 if count <= 0 || count as usize > MAX_MPUB_MESSAGES {
449 return Err(WireError::fatal(
450 "E_BAD_BODY",
451 format!("MPUB invalid message count {count}"),
452 ));
453 }
454 Ok(())
455}
456
457fn message_id(param: &str) -> Result<String, WireError> {
458 if param.len() != MSG_ID_LEN || !param.bytes().all(|byte| byte.is_ascii_hexdigit()) {
462 return Err(WireError::fatal("E_INVALID", "Invalid Message ID"));
463 }
464 Ok(param.to_string())
465}
466
467pub fn split_mpub(body: &[u8]) -> Result<Vec<Vec<u8>>, WireError> {
469 if body.len() < 4 {
470 return Err(WireError::fatal(
471 "E_BAD_BODY",
472 "MPUB failed to read message count",
473 ));
474 }
475 let count = i32::from_be_bytes(body[..4].try_into().unwrap()) as i64;
476 check_mpub_count(count)?;
477 let mut at = 4usize;
478 let mut messages = Vec::with_capacity((count as usize).min(body.len() / 5));
481 for _ in 0..count {
482 if body.len() < at + 4 {
483 return Err(WireError::fatal(
484 "E_BAD_MESSAGE",
485 "MPUB failed to read message body size",
486 ));
487 }
488 let size = i32::from_be_bytes(body[at..at + 4].try_into().unwrap()) as i64;
489 at += 4;
490 if size <= 0 {
491 return Err(WireError::fatal(
492 "E_BAD_MESSAGE",
493 format!("MPUB invalid message({}) body size {size}", messages.len()),
494 ));
495 }
496 if size as usize > MAX_MSG_SIZE {
497 return Err(WireError::fatal(
498 "E_BAD_MESSAGE",
499 format!("MPUB message too big {size} > {MAX_MSG_SIZE}"),
500 ));
501 }
502 let size = size as usize;
503 if body.len() < at + size {
504 return Err(WireError::fatal(
505 "E_BAD_MESSAGE",
506 "MPUB failed to read message body",
507 ));
508 }
509 messages.push(body[at..at + size].to_vec());
510 at += size;
511 }
512 Ok(messages)
513}
514
515pub fn encode_command(command: &Command) -> Vec<u8> {
518 let with_body = |line: String, body: &[u8]| {
519 let mut out = line.into_bytes();
520 out.extend_from_slice(&(body.len() as u32).to_be_bytes());
521 out.extend_from_slice(body);
522 out
523 };
524 match command {
525 Command::Identify(body) => with_body("IDENTIFY\n".to_string(), body),
526 Command::Auth(body) => with_body("AUTH\n".to_string(), body),
527 Command::Pub { topic, body } => with_body(format!("PUB {topic}\n"), body),
528 Command::Dpub {
529 topic,
530 defer_ms,
531 body,
532 } => with_body(format!("DPUB {topic} {defer_ms}\n"), body),
533 Command::Mpub { topic, messages } => {
534 let mut body = (messages.len() as u32).to_be_bytes().to_vec();
535 for m in messages {
536 body.extend_from_slice(&(m.len() as u32).to_be_bytes());
537 body.extend_from_slice(m);
538 }
539 with_body(format!("MPUB {topic}\n"), &body)
540 }
541 Command::Sub { topic, channel } => format!("SUB {topic} {channel}\n").into_bytes(),
542 Command::Rdy(n) => format!("RDY {n}\n").into_bytes(),
543 Command::Fin(id) => format!("FIN {id}\n").into_bytes(),
544 Command::Req { id, timeout_ms } => format!("REQ {id} {timeout_ms}\n").into_bytes(),
545 Command::Touch(id) => format!("TOUCH {id}\n").into_bytes(),
546 Command::Nop => b"NOP\n".to_vec(),
547 Command::Cls => b"CLS\n".to_vec(),
548 }
549}
550
551pub fn encode_frame(frame_type: u32, data: &[u8]) -> Vec<u8> {
553 let mut out = Vec::with_capacity(8 + data.len());
554 out.extend_from_slice(&((data.len() + 4) as u32).to_be_bytes());
555 out.extend_from_slice(&frame_type.to_be_bytes());
556 out.extend_from_slice(data);
557 out
558}
559
560pub fn response_frame(data: &[u8]) -> Vec<u8> {
561 encode_frame(FRAME_RESPONSE, data)
562}
563
564pub fn error_frame(code: &str, message: &str) -> Vec<u8> {
566 let message = crate::utils::sanitize::line_field(message);
567 let text = if message.trim().is_empty() {
568 code.to_string()
569 } else {
570 format!("{code} {}", message.trim())
571 };
572 encode_frame(FRAME_ERROR, text.as_bytes())
573}
574
575pub fn message_frame(
577 timestamp_ns: i64,
578 attempts: u16,
579 id: &[u8; MSG_ID_LEN],
580 body: &[u8],
581) -> Vec<u8> {
582 let mut data = Vec::with_capacity(8 + 2 + MSG_ID_LEN + body.len());
583 data.extend_from_slice(×tamp_ns.to_be_bytes());
584 data.extend_from_slice(&attempts.to_be_bytes());
585 data.extend_from_slice(id);
586 data.extend_from_slice(body);
587 encode_frame(FRAME_MESSAGE, &data)
588}
589
590pub fn message_id_for(n: u64) -> [u8; MSG_ID_LEN] {
592 let mut id = [0u8; MSG_ID_LEN];
593 id.copy_from_slice(format!("{n:016x}").as_bytes());
594 id
595}
596
597#[derive(Debug, Clone, PartialEq, Eq)]
599pub struct Frame {
600 pub frame_type: u32,
601 pub data: Vec<u8>,
602}
603
604#[derive(Debug, Clone, PartialEq, Eq)]
606pub struct Message {
607 pub timestamp_ns: i64,
608 pub attempts: u16,
609 pub id: [u8; MSG_ID_LEN],
610 pub body: Vec<u8>,
611}
612
613pub const MAX_FRAME_DATA: usize = 8 + 2 + MSG_ID_LEN + MAX_MSG_SIZE;
615
616pub fn parse_frame(buf: &[u8]) -> Result<Option<(Frame, usize)>, &'static str> {
619 if buf.len() < 4 {
620 return Ok(None);
621 }
622 let size = u32::from_be_bytes(buf[..4].try_into().unwrap()) as usize;
623 if size < 4 {
624 return Err("frame size below the four-byte frame type");
625 }
626 if size - 4 > MAX_FRAME_DATA {
627 return Err("frame size past the largest message frame");
628 }
629 if buf.len() < 4 + size {
630 return Ok(None);
631 }
632 let frame_type = u32::from_be_bytes(buf[4..8].try_into().unwrap());
633 Ok(Some((
634 Frame {
635 frame_type,
636 data: buf[8..4 + size].to_vec(),
637 },
638 4 + size,
639 )))
640}
641
642pub fn parse_message(data: &[u8]) -> Option<Message> {
644 if data.len() < 8 + 2 + MSG_ID_LEN {
645 return None;
646 }
647 let mut id = [0u8; MSG_ID_LEN];
648 id.copy_from_slice(&data[10..10 + MSG_ID_LEN]);
649 Some(Message {
650 timestamp_ns: i64::from_be_bytes(data[..8].try_into().unwrap()),
651 attempts: u16::from_be_bytes(data[8..10].try_into().unwrap()),
652 id,
653 body: data[10 + MSG_ID_LEN..].to_vec(),
654 })
655}
656
657#[derive(Debug, Clone, PartialEq)]
659pub struct Identify {
660 pub feature_negotiation: bool,
661 pub heartbeat_ms: Option<i64>,
663 pub msg_timeout_ms: i64,
664 pub client_id: String,
665 pub hostname: String,
666 pub user_agent: String,
667}
668
669pub fn parse_identify(body: &[u8]) -> Result<Identify, WireError> {
671 let value: Value = serde_json::from_slice(body)
672 .map_err(|_| WireError::fatal("E_BAD_BODY", "IDENTIFY failed to decode JSON body"))?;
673 if !value.is_object() {
674 return Err(WireError::fatal(
675 "E_BAD_BODY",
676 "IDENTIFY failed to decode JSON body",
677 ));
678 }
679 let text = |k: &str| {
680 value
681 .get(k)
682 .and_then(Value::as_str)
683 .map(|s| crate::utils::truncate_for_log(&crate::utils::sanitize::line_field(s), 128))
684 .unwrap_or_default()
685 };
686 let heartbeat = value
687 .get("heartbeat_interval")
688 .and_then(Value::as_i64)
689 .unwrap_or(0);
690 let heartbeat_ms = match heartbeat {
691 -1 => None,
692 0 => Some(0),
693 n if (MIN_HEARTBEAT_MS..=MAX_HEARTBEAT_MS).contains(&n) => Some(n),
694 n => {
695 return Err(WireError::fatal(
696 "E_BAD_BODY",
697 format!("IDENTIFY heartbeat interval ({n}) is invalid"),
698 ))
699 }
700 };
701 let msg_timeout = value
702 .get("msg_timeout")
703 .and_then(Value::as_i64)
704 .unwrap_or(0);
705 let msg_timeout_ms = match msg_timeout {
706 0 => MSG_TIMEOUT_MS,
707 n if (1000..=MAX_MSG_TIMEOUT_MS).contains(&n) => n,
708 n => {
709 return Err(WireError::fatal(
710 "E_BAD_BODY",
711 format!("IDENTIFY msg timeout ({n}) is invalid"),
712 ))
713 }
714 };
715 Ok(Identify {
716 feature_negotiation: value
717 .get("feature_negotiation")
718 .and_then(Value::as_bool)
719 .unwrap_or(false),
720 heartbeat_ms,
721 msg_timeout_ms,
722 client_id: text("client_id"),
723 hostname: text("hostname"),
724 user_agent: text("user_agent"),
725 })
726}
727
728pub fn identify_response(msg_timeout_ms: i64) -> Vec<u8> {
731 json!({
732 "max_rdy_count": MAX_RDY_COUNT,
733 "version": concat!("netget-", env!("CARGO_PKG_VERSION")),
734 "max_msg_timeout": MAX_MSG_TIMEOUT_MS,
735 "msg_timeout": msg_timeout_ms,
736 "tls_v1": false,
737 "deflate": false,
738 "deflate_level": 6,
739 "max_deflate_level": 6,
740 "snappy": false,
741 "sample_rate": 0,
742 "auth_required": false,
743 "output_buffer_size": 16384,
744 "output_buffer_timeout": 250
745 })
746 .to_string()
747 .into_bytes()
748}