1use ringo_core::account::{Account, BackendOptions};
16use ringo_core::event::{AppEvent, InviteHeaders, MediaStats};
17use serde::{Deserialize, Serialize};
18use std::io::{self, Read, Write};
19
20pub(crate) const FRAME_CONTROL: u8 = 0;
29pub(crate) const FRAME_AUDIO: u8 = 1;
31
32const MAX_FRAME_LEN: usize = 16 * 1024 * 1024;
34
35pub(crate) fn write_frame<W: Write>(w: &mut W, kind: u8, payload: &[u8]) -> io::Result<()> {
37 if payload.len() > MAX_FRAME_LEN {
38 return Err(io::Error::new(
39 io::ErrorKind::InvalidInput,
40 "frame too large",
41 ));
42 }
43 w.write_all(&[kind])?;
44 w.write_all(&(payload.len() as u32).to_le_bytes())?;
45 w.write_all(payload)?;
46 w.flush()
47}
48
49pub(crate) fn read_frame<R: Read>(r: &mut R) -> io::Result<Option<(u8, Vec<u8>)>> {
53 let mut hdr = [0u8; 5];
54 let mut filled = 0;
57 while filled < hdr.len() {
58 match r.read(&mut hdr[filled..]) {
59 Ok(0) if filled == 0 => return Ok(None), Ok(0) => {
61 return Err(io::Error::new(
62 io::ErrorKind::UnexpectedEof,
63 "EOF in frame header",
64 ));
65 }
66 Ok(n) => filled += n,
67 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
68 Err(e) => return Err(e),
69 }
70 }
71 let kind = hdr[0];
72 let len = u32::from_le_bytes([hdr[1], hdr[2], hdr[3], hdr[4]]) as usize;
73 if len > MAX_FRAME_LEN {
74 return Err(io::Error::new(
75 io::ErrorKind::InvalidData,
76 "frame too large",
77 ));
78 }
79 let mut payload = vec![0u8; len];
80 r.read_exact(&mut payload)?;
81 Ok(Some((kind, payload)))
82}
83
84pub(crate) fn pcm_to_bytes(samples: &[i16]) -> Vec<u8> {
86 let mut v = Vec::with_capacity(samples.len() * 2);
87 for &s in samples {
88 v.extend_from_slice(&s.to_le_bytes());
89 }
90 v
91}
92
93pub(crate) fn bytes_to_pcm(bytes: &[u8]) -> Vec<i16> {
95 bytes
96 .chunks_exact(2)
97 .map(|c| i16::from_le_bytes([c[0], c[1]]))
98 .collect()
99}
100
101pub(crate) const PROTO_VERSION: u32 = 1;
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
113pub(crate) struct Handshake {
114 pub proto_version: u32,
116 pub config: AgentConfig,
118}
119
120#[derive(Debug, Clone, Default, Serialize, Deserialize)]
127pub struct AgentConfig {
128 pub name: String,
130 pub account: Account,
132 pub options: BackendOptions,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(tag = "cmd", rename_all = "snake_case")]
141pub enum Command {
142 Register {
143 aor: String,
144 regint: u32,
145 },
146 Dial {
147 number: String,
148 },
149 Hangup,
150 HangupAll,
151 Accept,
152 Hold,
153 Resume,
154 Mute,
155 Dtmf {
156 digit: char,
157 },
158 SwitchLine {
159 line: usize,
160 },
161 Transfer {
162 uri: String,
163 },
164 AttendedTransferStart {
165 uri: String,
166 },
167 AttendedTransferExec,
168 AttendedTransferAbort,
169 AddHeader {
170 key: String,
171 value: String,
172 },
173 RmHeader {
174 key: String,
175 },
176 SetAudioSource {
177 spec: String,
178 },
179 ArmInviteResponse {
180 scode: u16,
181 reason: String,
182 headers: Vec<String>,
183 },
184 DisarmInviteResponse,
185 StartTxAudio {
188 rate: u32,
189 },
190 StartRxAudio,
193 Shutdown,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199#[serde(tag = "query", rename_all = "snake_case")]
200pub enum Query {
201 MediaStats,
203 ReceivedDtmf,
205 SaveAudio { prefix: String },
208 CallCount,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct QueryEnvelope {
215 pub id: u64,
216 #[serde(flatten)]
217 pub query: Query,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
224#[serde(untagged)]
225pub enum ToWorker {
226 Cmd(Command),
227 Query(QueryEnvelope),
228}
229
230#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
234pub struct WireMediaStats {
235 pub rtt_ms: f64,
236 pub jitter_ms: f64,
237 pub rx_lost: i32,
238 pub packet_loss_pct: f64,
239 pub mos: f64,
240}
241
242impl From<MediaStats> for WireMediaStats {
243 fn from(s: MediaStats) -> Self {
244 Self {
245 rtt_ms: s.rtt_ms,
246 jitter_ms: s.jitter_ms,
247 rx_lost: s.rx_lost,
248 packet_loss_pct: s.packet_loss_pct,
249 mos: s.mos,
250 }
251 }
252}
253
254impl From<WireMediaStats> for MediaStats {
255 fn from(s: WireMediaStats) -> Self {
256 Self {
257 rtt_ms: s.rtt_ms,
258 jitter_ms: s.jitter_ms,
259 rx_lost: s.rx_lost,
260 packet_loss_pct: s.packet_loss_pct,
261 mos: s.mos,
262 }
263 }
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
270#[serde(rename_all = "snake_case")]
271pub enum ReplyResult {
272 MediaStats(Option<WireMediaStats>),
273 Dtmf(String),
274 Saved(Vec<String>),
275 CallCount(u32),
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct Reply {
282 pub reply: u64,
283 pub result: ReplyResult,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct Headers {
289 pub headers: InviteHeaders,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct Ready {
297 pub ready: bool,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct RxAudioStarted {
304 pub rx_audio_rate: u32,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
312#[serde(untagged)]
313pub enum FromWorker {
314 Ready(Ready),
315 Event(WireEvent),
316 Reply(Reply),
317 Headers(Headers),
318 RxAudioStarted(RxAudioStarted),
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
324#[serde(tag = "event", rename_all = "snake_case")]
325pub enum WireEvent {
326 Registering {
327 account: String,
328 },
329 RegisterOk {
330 account: String,
331 },
332 RegisterFailed {
333 reason: String,
334 },
335 Unregistered {
336 account: String,
337 },
338 CallIncoming {
339 call_id: String,
340 number: String,
341 display_name: Option<String>,
342 },
343 CallOutgoing {
344 call_id: String,
345 number: String,
346 },
347 CallRinging {
348 call_id: String,
349 },
350 CallEstablished {
351 call_id: String,
352 },
353 CallClosed {
354 call_id: String,
355 reason: String,
356 error: bool,
357 },
358 CallDeflected {
359 from: String,
360 display_name: Option<String>,
361 target: String,
362 },
363 VoicemailStatus {
364 waiting: bool,
365 new_count: u32,
366 },
367 Response {
368 ok: bool,
369 data: String,
370 },
371 Unknown {
372 class: String,
373 type_: String,
374 },
375 BackendConnectFailed {
376 reason: String,
377 },
378}
379
380impl From<&AppEvent> for WireEvent {
381 fn from(e: &AppEvent) -> Self {
382 match e {
383 AppEvent::Registering { account } => Self::Registering {
384 account: account.clone(),
385 },
386 AppEvent::RegisterOk { account } => Self::RegisterOk {
387 account: account.clone(),
388 },
389 AppEvent::RegisterFailed { reason } => Self::RegisterFailed {
390 reason: reason.clone(),
391 },
392 AppEvent::Unregistered { account } => Self::Unregistered {
393 account: account.clone(),
394 },
395 AppEvent::CallIncoming {
396 call_id,
397 number,
398 display_name,
399 } => Self::CallIncoming {
400 call_id: call_id.clone(),
401 number: number.clone(),
402 display_name: display_name.clone(),
403 },
404 AppEvent::CallOutgoing { call_id, number } => Self::CallOutgoing {
405 call_id: call_id.clone(),
406 number: number.clone(),
407 },
408 AppEvent::CallRinging { call_id } => Self::CallRinging {
409 call_id: call_id.clone(),
410 },
411 AppEvent::CallEstablished { call_id } => Self::CallEstablished {
412 call_id: call_id.clone(),
413 },
414 AppEvent::CallClosed {
415 call_id,
416 reason,
417 error,
418 } => Self::CallClosed {
419 call_id: call_id.clone(),
420 reason: reason.clone(),
421 error: *error,
422 },
423 AppEvent::CallDeflected {
424 from,
425 display_name,
426 target,
427 } => Self::CallDeflected {
428 from: from.clone(),
429 display_name: display_name.clone(),
430 target: target.clone(),
431 },
432 AppEvent::VoicemailStatus { waiting, new_count } => Self::VoicemailStatus {
433 waiting: *waiting,
434 new_count: *new_count,
435 },
436 AppEvent::Response { ok, data } => Self::Response {
437 ok: *ok,
438 data: data.clone(),
439 },
440 AppEvent::Unknown { class, type_ } => Self::Unknown {
441 class: class.clone(),
442 type_: type_.clone(),
443 },
444 AppEvent::BackendConnectFailed { reason } => Self::BackendConnectFailed {
445 reason: reason.clone(),
446 },
447 }
448 }
449}
450
451impl From<WireEvent> for AppEvent {
452 fn from(w: WireEvent) -> Self {
453 match w {
454 WireEvent::Registering { account } => AppEvent::Registering { account },
455 WireEvent::RegisterOk { account } => AppEvent::RegisterOk { account },
456 WireEvent::RegisterFailed { reason } => AppEvent::RegisterFailed { reason },
457 WireEvent::Unregistered { account } => AppEvent::Unregistered { account },
458 WireEvent::CallIncoming {
459 call_id,
460 number,
461 display_name,
462 } => AppEvent::CallIncoming {
463 call_id,
464 number,
465 display_name,
466 },
467 WireEvent::CallOutgoing { call_id, number } => {
468 AppEvent::CallOutgoing { call_id, number }
469 }
470 WireEvent::CallRinging { call_id } => AppEvent::CallRinging { call_id },
471 WireEvent::CallEstablished { call_id } => AppEvent::CallEstablished { call_id },
472 WireEvent::CallClosed {
473 call_id,
474 reason,
475 error,
476 } => AppEvent::CallClosed {
477 call_id,
478 reason,
479 error,
480 },
481 WireEvent::CallDeflected {
482 from,
483 display_name,
484 target,
485 } => AppEvent::CallDeflected {
486 from,
487 display_name,
488 target,
489 },
490 WireEvent::VoicemailStatus { waiting, new_count } => {
491 AppEvent::VoicemailStatus { waiting, new_count }
492 }
493 WireEvent::Response { ok, data } => AppEvent::Response { ok, data },
494 WireEvent::Unknown { class, type_ } => AppEvent::Unknown { class, type_ },
495 WireEvent::BackendConnectFailed { reason } => AppEvent::BackendConnectFailed { reason },
496 }
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 #[test]
505 fn command_roundtrips_as_to_worker() {
506 let line = serde_json::to_string(&ToWorker::Cmd(Command::Dial {
507 number: "sip:1@example.com".into(),
508 }))
509 .unwrap();
510 assert_eq!(line, r#"{"cmd":"dial","number":"sip:1@example.com"}"#);
511 assert!(matches!(
512 serde_json::from_str::<ToWorker>(&line).unwrap(),
513 ToWorker::Cmd(Command::Dial { .. })
514 ));
515 }
516
517 #[test]
518 fn query_envelope_roundtrips_as_to_worker() {
519 let line = serde_json::to_string(&ToWorker::Query(QueryEnvelope {
520 id: 7,
521 query: Query::SaveAudio {
522 prefix: "rec".into(),
523 },
524 }))
525 .unwrap();
526 let back: ToWorker = serde_json::from_str(&line).unwrap();
527 match back {
528 ToWorker::Query(QueryEnvelope {
529 id,
530 query: Query::SaveAudio { prefix },
531 }) => {
532 assert_eq!((id, prefix.as_str()), (7, "rec"));
533 }
534 _ => panic!("expected save_audio query"),
535 }
536 }
537
538 #[test]
539 fn event_and_reply_disambiguate_as_from_worker() {
540 let ev = serde_json::to_string(&FromWorker::Event(WireEvent::RegisterOk {
541 account: "a".into(),
542 }))
543 .unwrap();
544 assert!(matches!(
545 serde_json::from_str::<FromWorker>(&ev).unwrap(),
546 FromWorker::Event(WireEvent::RegisterOk { .. })
547 ));
548
549 let rep = serde_json::to_string(&FromWorker::Reply(Reply {
550 reply: 7,
551 result: ReplyResult::CallCount(2),
552 }))
553 .unwrap();
554 match serde_json::from_str::<FromWorker>(&rep).unwrap() {
555 FromWorker::Reply(Reply {
556 reply,
557 result: ReplyResult::CallCount(n),
558 }) => {
559 assert_eq!((reply, n), (7, 2));
560 }
561 _ => panic!("expected call_count reply"),
562 }
563 }
564
565 #[test]
566 fn app_event_mirrors_both_ways() {
567 let ev = AppEvent::CallIncoming {
568 call_id: "abc".into(),
569 number: "sip:1@x".into(),
570 display_name: Some("Bob".into()),
571 };
572 let line = serde_json::to_string(&WireEvent::from(&ev)).unwrap();
573 let back: AppEvent = serde_json::from_str::<WireEvent>(&line).unwrap().into();
574 assert!(matches!(back, AppEvent::CallIncoming { .. }));
575 }
576
577 #[test]
578 fn ready_disambiguates_from_an_event() {
579 let ready = serde_json::to_string(&FromWorker::Ready(Ready { ready: true })).unwrap();
580 assert!(matches!(
581 serde_json::from_str::<FromWorker>(&ready).unwrap(),
582 FromWorker::Ready(Ready { ready: true })
583 ));
584 let ev = serde_json::to_string(&FromWorker::Event(WireEvent::RegisterOk {
586 account: "a".into(),
587 }))
588 .unwrap();
589 assert!(matches!(
590 serde_json::from_str::<FromWorker>(&ev).unwrap(),
591 FromWorker::Event(_)
592 ));
593 }
594
595 #[test]
596 fn handshake_carries_version_and_config() {
597 let mut cfg = AgentConfig {
598 name: "A".into(),
599 ..Default::default()
600 };
601 cfg.account.username = "alice".into();
602 let hs = Handshake {
603 proto_version: PROTO_VERSION,
604 config: cfg,
605 };
606 let line = serde_json::to_string(&hs).unwrap();
607 let back: Handshake = serde_json::from_str(&line).unwrap();
608 assert_eq!(back.proto_version, PROTO_VERSION);
609 assert_eq!(back.config.account.username, "alice");
610 }
611
612 #[test]
613 fn agent_config_roundtrips_with_embedded_account() {
614 let mut cfg = AgentConfig {
615 name: "A".into(),
616 ..Default::default()
617 };
618 cfg.account.username = "alice".into();
619 cfg.account.catchall = true;
620 cfg.options.audio_driver = Some("aubridge".into());
621 let line = serde_json::to_string(&cfg).unwrap();
622 let back: AgentConfig = serde_json::from_str(&line).unwrap();
623 assert_eq!(back.name, "A");
624 assert_eq!(back.account.username, "alice");
625 assert!(back.account.catchall);
626 assert_eq!(back.options.audio_driver.as_deref(), Some("aubridge"));
627 }
628
629 #[test]
630 fn frames_roundtrip_control_then_audio() {
631 let mut buf: Vec<u8> = Vec::new();
632 write_frame(&mut buf, FRAME_CONTROL, b"{\"cmd\":\"accept\"}").unwrap();
633 write_frame(&mut buf, FRAME_AUDIO, &pcm_to_bytes(&[1, -2, 3])).unwrap();
634 let mut r = &buf[..];
635 let (k1, p1) = read_frame(&mut r).unwrap().unwrap();
636 assert_eq!(k1, FRAME_CONTROL);
637 assert_eq!(p1, b"{\"cmd\":\"accept\"}");
638 let (k2, p2) = read_frame(&mut r).unwrap().unwrap();
639 assert_eq!(k2, FRAME_AUDIO);
640 assert_eq!(bytes_to_pcm(&p2), vec![1, -2, 3]);
641 assert!(read_frame(&mut r).unwrap().is_none()); }
643
644 #[test]
645 fn audio_frame_can_contain_newline_bytes() {
646 let samples = vec![0x0A0Ai16, 0x000A, -1];
648 let mut buf = Vec::new();
649 write_frame(&mut buf, FRAME_AUDIO, &pcm_to_bytes(&samples)).unwrap();
650 let (_k, p) = read_frame(&mut &buf[..]).unwrap().unwrap();
651 assert_eq!(bytes_to_pcm(&p), samples);
652 }
653
654 #[test]
655 fn rx_audio_started_disambiguates_from_event_and_reply() {
656 let started = serde_json::to_string(&FromWorker::RxAudioStarted(RxAudioStarted {
657 rx_audio_rate: 8000,
658 }))
659 .unwrap();
660 assert!(matches!(
661 serde_json::from_str::<FromWorker>(&started).unwrap(),
662 FromWorker::RxAudioStarted(RxAudioStarted {
663 rx_audio_rate: 8000
664 })
665 ));
666 let ev = serde_json::to_string(&FromWorker::Event(WireEvent::RegisterOk {
668 account: "a".into(),
669 }))
670 .unwrap();
671 let rep = serde_json::to_string(&FromWorker::Reply(Reply {
672 reply: 1,
673 result: ReplyResult::CallCount(0),
674 }))
675 .unwrap();
676 assert!(matches!(
677 serde_json::from_str::<FromWorker>(&ev).unwrap(),
678 FromWorker::Event(_)
679 ));
680 assert!(matches!(
681 serde_json::from_str::<FromWorker>(&rep).unwrap(),
682 FromWorker::Reply(_)
683 ));
684 }
685
686 #[test]
687 fn read_frame_distinguishes_clean_eof_from_torn_header() {
688 assert!(read_frame(&mut &b""[..]).unwrap().is_none());
690 let err = read_frame(&mut &[FRAME_CONTROL, 0x01][..]).unwrap_err();
692 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
693 }
694
695 #[test]
696 fn read_frame_rejects_oversized_length() {
697 let mut buf = vec![FRAME_AUDIO];
699 buf.extend_from_slice(&(MAX_FRAME_LEN as u32 + 1).to_le_bytes());
700 let err = read_frame(&mut &buf[..]).unwrap_err();
701 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
702 }
703
704 #[test]
705 fn read_frame_errors_on_truncated_payload() {
706 let mut buf = vec![FRAME_AUDIO];
708 buf.extend_from_slice(&4u32.to_le_bytes()); buf.extend_from_slice(&[1, 2]); let err = read_frame(&mut &buf[..]).unwrap_err();
711 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
712 }
713
714 #[test]
715 fn audio_commands_disambiguate_as_to_worker_cmd() {
716 for cmd in [Command::StartRxAudio, Command::StartTxAudio { rate: 8000 }] {
718 let line = serde_json::to_string(&ToWorker::Cmd(cmd)).unwrap();
719 assert!(
720 matches!(
721 serde_json::from_str::<ToWorker>(&line).unwrap(),
722 ToWorker::Cmd(_)
723 ),
724 "expected Cmd for {line}"
725 );
726 }
727 }
728}