use std::collections::{HashMap, VecDeque};
use std::time::{Duration, Instant};
use crate::consts::{authmethod, control, format, frametype, iax, ie as iet, IAX_PROTO_VERSION};
use crate::frame::{self, FullFrame, MiniFrame};
use crate::ie::{self, Ie};
const RETX_AFTER: Duration = Duration::from_millis(700);
const RETX_MAX: u32 = 5;
const KEEPALIVE_EVERY: Duration = Duration::from_secs(15);
const REG_GUARD: Duration = Duration::from_secs(10); const REG_RETRY: Duration = Duration::from_secs(5);
const CALLNO_REG: u16 = 1; const CALLNO_KEEPALIVE: u16 = 2; const CALLNO_CALL_BASE: u16 = 16; const CALLNO_QUALIFY_BASE: u16 = 0x2000; const CALLNO_QUALIFY_SPAN: u16 = 0x0FFF;
fn seq_before(a: u8, b: u8) -> bool {
let d = b.wrapping_sub(a);
d != 0 && d < 128
}
fn reconstruct_ts(last: u32, ts16: u16, seen: bool) -> u32 {
if !seen {
return ts16 as u32;
}
let high = last & 0xFFFF_0000;
let cand = high | ts16 as u32;
let lower = cand.wrapping_sub(0x1_0000);
let upper = cand.wrapping_add(0x1_0000);
[lower, cand, upper]
.into_iter()
.min_by_key(|&c| (c as i64 - last as i64).unsigned_abs())
.unwrap_or(cand)
}
#[derive(Clone, Debug)]
pub struct Config {
pub name: String,
pub username: String,
pub secret: String,
pub refresh: u16,
pub audio_format: u32,
}
impl Config {
pub fn new(name: impl Into<String>, username: impl Into<String>, secret: impl Into<String>) -> Self {
Config {
name: name.into(),
username: username.into(),
secret: secret.into(),
refresh: 60,
audio_format: format::ULAW,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Event {
Registered,
RegisterLost { reason: String },
Incoming { call: u16, from: String, to: String },
Dialing { call: u16, to: String },
Ringing { call: u16 },
Answered { call: u16 },
Voice { call: u16, ts: u32, ulaw: Vec<u8> },
Ended { call: u16, reason: String },
Dtmf { call: u16, digit: char },
Log(String),
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Command {
Dial { number: String },
Answer { call: u16 },
Hangup { call: u16 },
Hold { call: u16 },
Unhold { call: u16 },
SendVoice { call: u16, ulaw: Vec<u8> },
Dtmf { call: u16, digit: char },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RegState {
Idle,
Registering,
Registered,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallState {
Trying,
Ringing,
Up,
Held,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Dir {
In,
Out,
}
struct Outstanding {
oseq: u8,
ts: u32,
bytes: Vec<u8>,
deadline: Instant,
tries: u32,
}
struct Leg {
local: u16,
remote: u16,
oseq: u8,
iseq: u8,
outstanding: VecDeque<Outstanding>,
}
impl Leg {
fn new(local: u16) -> Self {
Leg { local, remote: 0, oseq: 0, iseq: 0, outstanding: VecDeque::new() }
}
fn reset(&mut self) {
self.remote = 0;
self.oseq = 0;
self.iseq = 0;
self.outstanding.clear();
}
fn note_recv(&mut self, f: &FullFrame) {
if f.src_call != 0 {
self.remote = f.src_call;
}
self.iseq = f.oseq.wrapping_add(1);
}
fn build(&mut self, ts: u32, ft: u8, sc: u8, ies: Vec<Ie>) -> FullFrame {
let f = FullFrame::new(self.local, self.remote, ts, self.oseq, self.iseq, ft, sc, ies);
self.oseq = self.oseq.wrapping_add(1);
f
}
fn ack_bytes(&self, ts: u32) -> Vec<u8> {
FullFrame::new(self.local, self.remote, ts, self.oseq, self.iseq, frametype::IAX, iax::ACK, vec![]).encode()
}
fn track_reliable(&mut self, f: &FullFrame, now: Instant) {
self.outstanding.push_back(Outstanding {
oseq: f.oseq,
ts: f.timestamp,
bytes: f.encode(),
deadline: now + RETX_AFTER,
tries: 0,
});
}
fn ack_upto(&mut self, recv_iseq: u8, recv_ts: u32, is_ack: bool) {
self.outstanding.retain(|o| {
let acked_cumulative = seq_before(o.oseq, recv_iseq);
let acked_ts = is_ack && o.ts == recv_ts;
!(acked_cumulative || acked_ts)
});
}
}
struct CallLeg {
leg: Leg,
state: CallState,
dir: Dir,
peer: String, fmt: u32,
sent_format: bool, last_voice_ts: u32, voice_ts_seen: bool, }
pub struct PbxClient {
cfg: Config,
start: Instant,
reg: Leg,
reg_state: RegState,
refresh: u16,
calltoken: Option<Vec<u8>>,
next_reg: Instant,
next_keepalive: Instant,
calls: HashMap<u16, CallLeg>,
remote_index: HashMap<u16, u16>, callno_next: u16,
qualify_next: u16,
out: VecDeque<Vec<u8>>,
events: VecDeque<Event>,
}
impl PbxClient {
pub fn new(cfg: Config) -> Self {
let now = Instant::now();
let refresh = cfg.refresh;
PbxClient {
cfg,
start: now,
reg: Leg::new(CALLNO_REG),
reg_state: RegState::Idle,
refresh,
calltoken: None,
next_reg: now, next_keepalive: now + KEEPALIVE_EVERY,
calls: HashMap::new(),
remote_index: HashMap::new(),
callno_next: CALLNO_CALL_BASE,
qualify_next: CALLNO_QUALIFY_BASE,
out: VecDeque::new(),
events: VecDeque::new(),
}
}
pub fn name(&self) -> &str {
&self.cfg.name
}
pub fn is_registered(&self) -> bool {
self.reg_state == RegState::Registered
}
pub fn active_calls(&self) -> usize {
self.calls.len()
}
pub fn call_state(&self, call: u16) -> Option<CallState> {
self.calls.get(&call).map(|c| c.state)
}
pub fn call_ids(&self) -> Vec<u16> {
let mut v: Vec<u16> = self.calls.keys().copied().collect();
v.sort_unstable();
v
}
pub fn call_peer(&self, call: u16) -> Option<&str> {
self.calls.get(&call).map(|c| c.peer.as_str())
}
fn ts(&self) -> u32 {
self.start.elapsed().as_millis() as u32
}
fn push(&mut self, bytes: Vec<u8>) {
self.out.push_back(bytes);
}
fn emit(&mut self, ev: Event) {
self.events.push_back(ev);
}
pub fn poll_transmit(&mut self) -> Option<Vec<u8>> {
self.out.pop_front()
}
pub fn poll_event(&mut self) -> Option<Event> {
self.events.pop_front()
}
pub fn poll_timeout(&self) -> Option<Instant> {
let mut t: Option<Instant> = Some(self.next_reg);
let consider = |x: Instant, t: &mut Option<Instant>| {
*t = Some(match *t {
Some(c) if c <= x => c,
_ => x,
});
};
if self.reg_state == RegState::Registered {
consider(self.next_keepalive, &mut t);
}
for o in &self.reg.outstanding {
consider(o.deadline, &mut t);
}
for c in self.calls.values() {
for o in &c.leg.outstanding {
consider(o.deadline, &mut t);
}
}
t
}
fn alloc_callno(&mut self) -> u16 {
for _ in 0..0x7000 {
let n = self.callno_next;
self.callno_next = if self.callno_next >= 0x1FFF { CALLNO_CALL_BASE } else { self.callno_next + 1 };
if n >= CALLNO_CALL_BASE && !self.calls.contains_key(&n) {
return n;
}
}
CALLNO_CALL_BASE }
fn next_qualify_callno(&mut self) -> u16 {
let n = self.qualify_next;
self.qualify_next = CALLNO_QUALIFY_BASE + ((self.qualify_next - CALLNO_QUALIFY_BASE + 1) & CALLNO_QUALIFY_SPAN);
n
}
pub fn handle_timeout(&mut self, now: Instant) {
if self.reg.outstanding.is_empty() && now >= self.next_reg {
self.start_registration(now);
}
if self.reg_state == RegState::Registered && now >= self.next_keepalive {
let poke = FullFrame::new(CALLNO_KEEPALIVE, 0, self.ts(), 0, 0, frametype::IAX, iax::POKE, vec![]).encode();
self.push(poke);
self.next_keepalive = now + KEEPALIVE_EVERY;
}
self.retransmit_reg(now);
self.retransmit_calls(now);
}
fn start_registration(&mut self, now: Instant) {
self.reg.reset();
self.calltoken = None;
self.reg_state = RegState::Registering;
let ies = self.reg_ies();
let f = self.reg.build(self.ts(), frametype::IAX, iax::REGREQ, ies);
self.reg.track_reliable(&f, now);
self.push(f.encode());
self.next_reg = now + REG_GUARD; self.emit(Event::Log("REGREQ sended".into()));
}
fn retransmit_reg(&mut self, now: Instant) {
let mut lost = false;
for o in self.reg.outstanding.iter_mut() {
if now >= o.deadline {
if o.tries >= RETX_MAX {
lost = true;
} else {
let mut b = o.bytes.clone();
b[2] |= 0x80; self.out.push_back(b);
o.tries += 1;
o.deadline = now + RETX_AFTER;
}
}
}
if lost {
self.reg.outstanding.clear();
self.reg_state = RegState::Idle;
self.next_reg = now + REG_RETRY;
self.emit(Event::RegisterLost { reason: "no ACK after retransmissions".into() });
}
}
fn retransmit_calls(&mut self, now: Instant) {
let mut dead: Vec<u16> = Vec::new();
let mut to_send: Vec<Vec<u8>> = Vec::new();
for (&local, c) in self.calls.iter_mut() {
let mut leg_dead = false;
for o in c.leg.outstanding.iter_mut() {
if now >= o.deadline {
if o.tries >= RETX_MAX {
leg_dead = true;
} else {
let mut b = o.bytes.clone();
b[2] |= 0x80;
to_send.push(b);
o.tries += 1;
o.deadline = now + RETX_AFTER;
}
}
}
if leg_dead {
dead.push(local);
}
}
for b in to_send {
self.push(b);
}
for local in dead {
self.drop_call(local, "reliability timeout".into());
}
}
fn drop_call(&mut self, local: u16, reason: String) {
if let Some(c) = self.calls.remove(&local) {
self.remote_index.retain(|_, &mut v| v != local);
let _ = c;
self.emit(Event::Ended { call: local, reason });
}
}
pub fn handle_command(&mut self, cmd: Command, now: Instant) {
match cmd {
Command::Dial { number } => self.dial(&number, now),
Command::Answer { call } => self.answer(call, now),
Command::Hangup { call } => self.hangup(call, now),
Command::Hold { call } => self.set_hold(call, true, now),
Command::Unhold { call } => self.set_hold(call, false, now),
Command::SendVoice { call, ulaw } => self.send_voice(call, &ulaw),
Command::Dtmf { call, digit } => self.send_dtmf(call, digit, now),
}
}
fn dial(&mut self, number: &str, now: Instant) {
let local = self.alloc_callno();
let mut leg = Leg::new(local);
let ies = self.new_call_ies(number);
let f = leg.build(self.ts(), frametype::IAX, iax::NEW, ies);
leg.track_reliable(&f, now);
self.push(f.encode());
self.calls.insert(
local,
CallLeg { leg, state: CallState::Trying, dir: Dir::Out, peer: number.to_string(), fmt: self.cfg.audio_format, sent_format: false, last_voice_ts: 0, voice_ts_seen: false },
);
self.emit(Event::Dialing { call: local, to: number.to_string() });
}
fn answer(&mut self, call: u16, now: Instant) {
let ts = self.ts();
if let Some(c) = self.calls.get_mut(&call) {
if c.dir == Dir::In && c.state == CallState::Ringing {
let ans = c.leg.build(ts, frametype::CONTROL, control::ANSWER, vec![]);
c.leg.track_reliable(&ans, now);
self.push(ans.encode());
if let Some(c) = self.calls.get_mut(&call) {
c.state = CallState::Up;
c.sent_format = false;
}
self.emit(Event::Answered { call });
}
}
}
fn hangup(&mut self, call: u16, now: Instant) {
let ts = self.ts();
if let Some(c) = self.calls.get_mut(&call) {
let hb = c.leg.build(ts, frametype::IAX, iax::HANGUP, vec![Ie::str(iet::CAUSE, "Normal Clearing")]);
c.leg.track_reliable(&hb, now);
self.push(hb.encode());
}
self.drop_call(call, "local hangup".into());
}
fn send_dtmf(&mut self, call: u16, digit: char, now: Instant) {
let d = digit.to_ascii_uppercase();
if !(d.is_ascii_digit() || matches!(d, '*' | '#' | 'A'..='D')) {
return;
}
let ts = self.ts();
let bytes = self.calls.get_mut(&call).map(|c| {
let f = c.leg.build(ts, frametype::DTMF, d as u8, vec![]);
c.leg.track_reliable(&f, now);
f.encode()
});
if let Some(b) = bytes {
self.push(b);
}
}
fn set_hold(&mut self, call: u16, hold: bool, now: Instant) {
let ts = self.ts();
let bytes = self.calls.get_mut(&call).map(|c| {
let sc = if hold { control::HOLD } else { control::UNHOLD };
let f = c.leg.build(ts, frametype::CONTROL, sc, vec![]);
c.leg.track_reliable(&f, now);
c.state = if hold { CallState::Held } else { CallState::Up };
f.encode()
});
if let Some(b) = bytes {
self.push(b);
}
}
fn send_voice(&mut self, call: u16, ulaw: &[u8]) {
let ts = self.ts();
let plan = self.calls.get_mut(&call).and_then(|c| {
if c.state != CallState::Up {
return None;
}
if !c.sent_format {
c.sent_format = true;
let vf = c.leg.build(ts, frametype::VOICE, c.fmt as u8, vec![]);
Some((true, vf.encode_media(ulaw), c.leg.local))
} else {
Some((false, Vec::new(), c.leg.local))
}
});
if let Some((is_full, bytes, local)) = plan {
if is_full {
self.push(bytes);
} else {
let mf = MiniFrame { src_call: local, timestamp16: ts as u16, payload: ulaw.to_vec() };
self.push(mf.encode());
}
}
}
pub fn handle_input(&mut self, dg: &[u8], now: Instant) {
if !frame::is_full_frame(dg) {
if let Some(m) = MiniFrame::decode(dg) {
if let Some(&local) = self.remote_index.get(&m.src_call) {
if matches!(self.calls.get(&local).map(|c| c.state), Some(CallState::Up)) {
let ts = if let Some(c) = self.calls.get_mut(&local) {
let rec = reconstruct_ts(c.last_voice_ts, m.timestamp16, c.voice_ts_seen);
c.last_voice_ts = rec;
c.voice_ts_seen = true;
rec
} else {
m.timestamp16 as u32
};
self.emit(Event::Voice { call: local, ts, ulaw: m.payload });
}
}
}
return;
}
let Some(f) = FullFrame::decode(dg) else { return };
if f.frametype == frametype::IAX && (f.subclass == iax::POKE || f.subclass == iax::PING) && f.dst_call == 0 {
let src = self.next_qualify_callno();
let pong = FullFrame::new(src, f.src_call, f.timestamp, 0, f.oseq.wrapping_add(1), frametype::IAX, iax::PONG, vec![]).encode();
self.push(pong);
return;
}
if let Some(tok) = ie::find(&f.ies, iet::CALLTOKEN) {
if !tok.data.is_empty() {
let data = tok.data.clone();
if f.dst_call == self.reg.local && self.calltoken.is_none() {
self.resend_reg_with_token(data, now);
return;
}
let target = self
.calls
.iter()
.find(|(_, c)| c.dir == Dir::Out && c.state == CallState::Trying && f.dst_call == c.leg.local)
.map(|(&l, _)| l);
if let Some(local) = target {
self.resend_new_with_token(local, data, now);
return;
}
}
}
if f.frametype == frametype::IAX && f.subclass == iax::NEW {
self.on_incoming_new(&f, now);
return;
}
if f.dst_call == self.reg.local {
self.on_reg_frame(&f, now);
return;
}
if self.calls.contains_key(&f.dst_call) {
self.on_call_frame(f.dst_call, &f, now);
return;
}
match (f.frametype, f.subclass) {
(frametype::IAX, iax::ACK) | (frametype::IAX, iax::PONG) | (frametype::IAX, iax::INVAL) => {}
(ft, sc) => self.emit(Event::Log(format!("out of leg frame dst_call={} ft=0x{ft:02x} sc=0x{sc:02x}", f.dst_call))),
}
}
fn resend_reg_with_token(&mut self, token: Vec<u8>, now: Instant) {
self.calltoken = Some(token);
self.reg.reset();
let ies = self.reg_ies();
let f = self.reg.build(self.ts(), frametype::IAX, iax::REGREQ, ies);
self.reg.track_reliable(&f, now);
self.push(f.encode());
}
fn resend_new_with_token(&mut self, local: u16, token: Vec<u8>, now: Instant) {
let ts = self.ts();
let number = self.calls.get(&local).map(|c| c.peer.clone()).unwrap_or_default();
if let Some(c) = self.calls.get_mut(&local) {
c.leg.reset(); }
let mut ies = self.new_call_ies(&number);
if let Some(p) = ies.iter_mut().find(|i| i.kind == iet::CALLTOKEN) {
*p = Ie::new(iet::CALLTOKEN, token);
}
if let Some(c) = self.calls.get_mut(&local) {
let f = c.leg.build(ts, frametype::IAX, iax::NEW, ies);
c.leg.track_reliable(&f, now);
let bytes = f.encode();
self.push(bytes);
}
}
fn on_incoming_new(&mut self, f: &FullFrame, now: Instant) {
let called = ie::find(&f.ies, iet::CALLED_NUMBER).map(|i| i.as_str()).unwrap_or_default();
let caller = ie::find(&f.ies, iet::CALLING_NUMBER).map(|i| i.as_str()).unwrap_or_default();
let local = self.alloc_callno();
let mut leg = Leg::new(local);
leg.note_recv(f);
self.remote_index.insert(f.src_call, local);
let ts = self.ts();
let accept = leg.build(ts, frametype::IAX, iax::ACCEPT, vec![Ie::u32(iet::FORMAT, self.cfg.audio_format)]);
leg.track_reliable(&accept, now);
self.push(accept.encode());
let ringing = leg.build(ts, frametype::CONTROL, control::RINGING, vec![]);
leg.track_reliable(&ringing, now);
self.push(ringing.encode());
self.calls.insert(
local,
CallLeg { leg, state: CallState::Ringing, dir: Dir::In, peer: caller.clone(), fmt: self.cfg.audio_format, sent_format: false, last_voice_ts: 0, voice_ts_seen: false },
);
self.emit(Event::Incoming { call: local, from: caller, to: called });
}
fn on_reg_frame(&mut self, f: &FullFrame, now: Instant) {
self.reg.note_recv(f);
let is_ack = f.frametype == frametype::IAX && f.subclass == iax::ACK;
self.reg.ack_upto(f.iseq, f.timestamp, is_ack);
match f.subclass {
iax::REGAUTH => {
let methods = ie::find(&f.ies, iet::AUTHMETHODS).and_then(|i| i.as_u16()).unwrap_or(0);
let challenge = ie::find(&f.ies, iet::CHALLENGE).map(|i| i.as_str());
if methods & authmethod::MD5 == 0 {
self.emit(Event::Log("REGAUTH without MD5".into()));
return;
}
let Some(chal) = challenge else {
self.emit(Event::Log("REGAUTH without challenge".into()));
return;
};
let md5 = crate::md5_response(&chal, &self.cfg.secret);
let mut ies = self.reg_ies();
ies.insert(1, Ie::str(iet::MD5_RESULT, &md5)); let fr = self.reg.build(self.ts(), frametype::IAX, iax::REGREQ, ies);
self.reg.track_reliable(&fr, now);
self.push(fr.encode());
}
iax::REGACK => {
if let Some(rf) = ie::find(&f.ies, iet::REFRESH).and_then(|i| i.as_u16()) {
if rf > 0 {
self.refresh = rf;
}
}
self.reg_state = RegState::Registered;
self.next_reg = now + Duration::from_secs((self.refresh as u64 * 4) / 5);
self.next_keepalive = now + KEEPALIVE_EVERY;
let ack = self.reg.ack_bytes(self.ts());
self.push(ack);
self.emit(Event::Registered);
}
iax::REGREJ => {
let code = ie::find(&f.ies, iet::CAUSECODE).and_then(|i| i.data.first().copied());
self.calltoken = None;
self.reg_state = RegState::Idle;
self.next_reg = now + REG_RETRY;
let ack = self.reg.ack_bytes(self.ts());
self.push(ack);
self.emit(Event::RegisterLost { reason: format!("REGREJ causecode={code:?}") });
}
iax::ACK | iax::CALLTOKEN => {}
other => self.emit(Event::Log(format!("reg leg subclass=0x{other:02x}"))),
}
}
fn on_call_frame(&mut self, local: u16, f: &FullFrame, now: Instant) {
let ts = self.ts();
if let Some(c) = self.calls.get_mut(&local) {
c.leg.note_recv(f);
if f.src_call != 0 {
self.remote_index.insert(f.src_call, local);
}
let is_ack = f.frametype == frametype::IAX && f.subclass == iax::ACK;
c.leg.ack_upto(f.iseq, f.timestamp, is_ack);
}
let dir = self.calls.get(&local).map(|c| c.dir);
match (f.frametype, f.subclass) {
(frametype::VOICE, _) => {
if let Some(c) = self.calls.get_mut(&local) {
let ack = c.leg.ack_bytes(ts);
c.last_voice_ts = f.timestamp;
c.voice_ts_seen = true;
self.push(ack);
}
if matches!(self.calls.get(&local).map(|c| c.state), Some(CallState::Up)) {
self.emit(Event::Voice { call: local, ts: f.timestamp, ulaw: f.media_payload.clone() });
}
}
(frametype::IAX, iax::AUTHREQ) if dir == Some(Dir::Out) => {
let methods = ie::find(&f.ies, iet::AUTHMETHODS).and_then(|i| i.as_u16()).unwrap_or(0);
let challenge = ie::find(&f.ies, iet::CHALLENGE).map(|i| i.as_str());
if methods & authmethod::MD5 == 0 {
self.emit(Event::Log("AUTHREQ without MD5".into()));
return;
}
let Some(chal) = challenge else {
self.emit(Event::Log("AUTHREQ eithout challenge".into()));
return;
};
let md5 = crate::md5_response(&chal, &self.cfg.secret);
if let Some(c) = self.calls.get_mut(&local) {
let rep = c.leg.build(ts, frametype::IAX, iax::AUTHREP, vec![Ie::str(iet::MD5_RESULT, &md5)]);
c.leg.track_reliable(&rep, now);
self.push(rep.encode());
}
}
(frametype::IAX, iax::ACCEPT) => {
if let Some(fmt) = ie::find(&f.ies, iet::FORMAT).and_then(|i| i.as_u32()) {
if let Some(c) = self.calls.get_mut(&local) {
c.fmt = fmt;
}
}
if let Some(c) = self.calls.get_mut(&local) {
let ack = c.leg.ack_bytes(ts);
self.push(ack);
}
}
(frametype::CONTROL, control::RINGING) => {
if let Some(c) = self.calls.get_mut(&local) {
c.state = CallState::Ringing;
let ack = c.leg.ack_bytes(ts);
self.push(ack);
}
self.emit(Event::Ringing { call: local });
}
(frametype::CONTROL, control::ANSWER) => {
if let Some(c) = self.calls.get_mut(&local) {
c.state = CallState::Up;
c.sent_format = false;
let ack = c.leg.ack_bytes(ts);
self.push(ack);
}
self.emit(Event::Answered { call: local });
}
(frametype::CONTROL, control::BUSY) => {
if let Some(c) = self.calls.get_mut(&local) {
let ack = c.leg.ack_bytes(ts);
self.push(ack);
}
self.drop_call(local, "busy".into());
}
(frametype::IAX, iax::HANGUP) => {
if let Some(c) = self.calls.get_mut(&local) {
let ack = c.leg.ack_bytes(ts);
self.push(ack);
}
self.drop_call(local, "hangup by remote".into());
}
(frametype::IAX, iax::REJECT) => {
let code = ie::find(&f.ies, iet::CAUSECODE).and_then(|i| i.data.first().copied());
if let Some(c) = self.calls.get_mut(&local) {
let ack = c.leg.ack_bytes(ts);
self.push(ack);
}
self.drop_call(local, format!("refused causecode={code:?}"));
}
(frametype::IAX, iax::ACK) | (frametype::IAX, iax::PONG) => {}
(frametype::IAX, iax::LAGRQ) => {
if let Some(c) = self.calls.get_mut(&local) {
let lagrp = c.leg.build(f.timestamp, frametype::IAX, iax::LAGRP, vec![]);
self.push(lagrp.encode());
}
}
(frametype::IAX, iax::PING) => {
if let Some(c) = self.calls.get_mut(&local) {
let pong = c.leg.build(f.timestamp, frametype::IAX, iax::PONG, vec![]);
self.push(pong.encode());
}
}
(frametype::IAX, iax::LAGRP) => {} (frametype::DTMF, sc) => {
if let Some(c) = self.calls.get_mut(&local) {
let ack = c.leg.ack_bytes(ts);
self.push(ack);
}
self.emit(Event::Dtmf { call: local, digit: sc as char });
}
(frametype::CNG, _) => {
if let Some(c) = self.calls.get_mut(&local) {
let ack = c.leg.ack_bytes(ts);
self.push(ack);
}
}
(frametype::CONTROL, _) => {
if let Some(c) = self.calls.get_mut(&local) {
let ack = c.leg.ack_bytes(ts);
self.push(ack);
}
}
(ft, sc) => {
if let Some(c) = self.calls.get_mut(&local) {
let ack = c.leg.ack_bytes(ts);
self.push(ack);
}
self.emit(Event::Log(format!("call leg ft=0x{ft:02x} sc=0x{sc:02x}")));
}
}
}
fn reg_ies(&self) -> Vec<Ie> {
let mut ies = vec![Ie::str(iet::USERNAME, &self.cfg.username), Ie::u16(iet::REFRESH, self.refresh)];
match &self.calltoken {
Some(t) => ies.push(Ie::new(iet::CALLTOKEN, t.clone())),
None => ies.push(Ie::empty(iet::CALLTOKEN)),
}
ies
}
fn new_call_ies(&self, called: &str) -> Vec<Ie> {
vec![
Ie::u16(iet::VERSION, IAX_PROTO_VERSION),
Ie::str(iet::CALLING_NUMBER, &self.cfg.username),
Ie::str(iet::CALLING_NAME, "iax2"),
Ie::str(iet::USERNAME, &self.cfg.username),
Ie::u32(iet::CAPABILITY, self.cfg.audio_format),
Ie::u32(iet::FORMAT, self.cfg.audio_format),
Ie::str(iet::CALLED_NUMBER, called),
Ie::empty(iet::CALLTOKEN),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::consts::{frametype, iax, ie as iec};
fn cfg() -> Config {
Config::new("Test", "10001", "segreto")
}
fn drain_full(c: &mut PbxClient) -> Vec<FullFrame> {
let mut v = Vec::new();
while let Some(b) = c.poll_transmit() {
if frame::is_full_frame(&b) {
if let Some(f) = FullFrame::decode(&b) {
v.push(f);
}
}
}
v
}
#[test]
fn registration_handshake_with_calltoken() {
let mut c = PbxClient::new(cfg());
let now = Instant::now();
c.handle_timeout(now);
let out = drain_full(&mut c);
let regreq = out.iter().find(|f| f.subclass == iax::REGREQ).expect("REGREQ");
assert_eq!(regreq.src_call, CALLNO_REG);
assert!(ie::find(®req.ies, iec::CALLTOKEN).unwrap().data.is_empty(), "first token empty");
let token = b"TOKEN123".to_vec();
let tokframe = FullFrame::new(5, CALLNO_REG, 10, 0, 1, frametype::IAX, iax::CALLTOKEN, vec![Ie::new(iec::CALLTOKEN, token.clone())]);
c.handle_input(&tokframe.encode(), now);
let out = drain_full(&mut c);
let regreq2 = out.iter().find(|f| f.subclass == iax::REGREQ).expect("REGREQ#2");
assert_eq!(ie::find(®req2.ies, iec::CALLTOKEN).unwrap().data, token, "token incorporato");
assert_eq!(regreq2.oseq, 0, "richiesta fresca: oseq azzerato");
let regauth = FullFrame::new(
5,
CALLNO_REG,
20,
0,
2,
frametype::IAX,
iax::REGAUTH,
vec![Ie::u16(iec::AUTHMETHODS, authmethod::MD5), Ie::str(iec::CHALLENGE, "98765")],
);
c.handle_input(®auth.encode(), now);
let out = drain_full(&mut c);
let authed = out.iter().find(|f| f.subclass == iax::REGREQ).expect("REGREQ con MD5");
let md5 = ie::find(&authed.ies, iec::MD5_RESULT).expect("MD5_RESULT");
assert_eq!(md5.as_str(), crate::md5_response("98765", "segreto"));
let regack = FullFrame::new(5, CALLNO_REG, 30, 1, 3, frametype::IAX, iax::REGACK, vec![Ie::u16(iec::REFRESH, 120)]);
c.handle_input(®ack.encode(), now);
assert!(c.is_registered());
let evs: Vec<_> = std::iter::from_fn(|| c.poll_event()).collect();
assert!(evs.iter().any(|e| matches!(e, Event::Registered)), "evento Registered");
}
#[test]
fn incoming_call_rings_then_answers_then_audio() {
let now = Instant::now();
let mut c = PbxClient::new(cfg());
c.reg_state = RegState::Registered;
let new = FullFrame::new(
42,
0,
100,
0,
0,
frametype::IAX,
iax::NEW,
vec![Ie::str(iec::CALLING_NUMBER, "200"), Ie::str(iec::CALLED_NUMBER, "10001")],
);
c.handle_input(&new.encode(), now);
let out = drain_full(&mut c);
assert!(out.iter().any(|f| f.subclass == iax::ACCEPT), "ACCEPT");
assert!(out.iter().any(|f| f.frametype == frametype::CONTROL && f.subclass == control::RINGING), "RINGING");
let call = match c.poll_event() {
Some(Event::Incoming { call, from, .. }) => {
assert_eq!(from, "200");
call
}
other => panic!("atteso Incoming, trovato {other:?}"),
};
assert_eq!(c.call_state(call), Some(CallState::Ringing));
c.handle_command(Command::Answer { call }, now);
let out = drain_full(&mut c);
assert!(out.iter().any(|f| f.frametype == frametype::CONTROL && f.subclass == control::ANSWER), "ANSWER");
assert!(std::iter::from_fn(|| c.poll_event()).any(|e| matches!(e, Event::Answered { .. })));
assert_eq!(c.call_state(call), Some(CallState::Up));
let mf = MiniFrame { src_call: 42, timestamp16: 20, payload: vec![0xFF; 160] };
c.handle_input(&mf.encode(), now);
let voice = std::iter::from_fn(|| c.poll_event()).find(|e| matches!(e, Event::Voice { .. }));
assert!(voice.is_some(), "evento Voice dal mini-frame");
c.handle_command(Command::SendVoice { call, ulaw: vec![0x7F; 160] }, now);
let out = drain_full(&mut c);
assert!(out.iter().any(|f| f.frametype == frametype::VOICE), "primo full VOICE");
}
#[test]
fn two_concurrent_calls_have_isolated_reliability() {
let now = Instant::now();
let mut c = PbxClient::new(cfg());
c.reg_state = RegState::Registered;
for (src, caller) in [(42u16, "200"), (43u16, "201")] {
let new = FullFrame::new(src, 0, 100, 0, 0, frametype::IAX, iax::NEW, vec![Ie::str(iec::CALLING_NUMBER, caller), Ie::str(iec::CALLED_NUMBER, "10001")]);
c.handle_input(&new.encode(), now);
}
let calls: Vec<u16> = std::iter::from_fn(|| c.poll_event())
.filter_map(|e| if let Event::Incoming { call, .. } = e { Some(call) } else { None })
.collect();
assert_eq!(calls.len(), 2, "due chiamate concorrenti");
assert_ne!(calls[0], calls[1], "call number distinti");
let _ = drain_full(&mut c);
for &call in &calls {
assert!(c.calls.get(&call).unwrap().leg.outstanding.len() >= 1, "finestra A/B non vuota");
}
let leg0 = &c.calls[&calls[0]].leg;
let (l0, r0, iseq0) = (leg0.local, leg0.remote, leg0.oseq);
let ack = FullFrame::new(r0, l0, 5, iseq0, 0, frametype::IAX, iax::ACK, vec![]);
let before_b = c.calls[&calls[1]].leg.outstanding.len();
c.handle_input(&ack.encode(), now);
let after_b = c.calls[&calls[1]].leg.outstanding.len();
assert_eq!(before_b, after_b, "l'ACK di A non tocca la finestra di B");
}
#[test]
fn qualify_pong_echoes_timestamp_and_rotates_callno() {
let now = Instant::now();
let mut c = PbxClient::new(cfg());
c.reg_state = RegState::Registered;
let mut seen = Vec::new();
for ts_poke in [14u32, 1000, 2000] {
let poke = FullFrame::new(7, 0, ts_poke, 0, 0, frametype::IAX, iax::POKE, vec![]);
c.handle_input(&poke.encode(), now);
let out = drain_full(&mut c);
let pong = out.iter().find(|f| f.subclass == iax::PONG).expect("PONG");
assert_eq!(pong.timestamp, ts_poke, "LEZIONE 3: PONG riecheggia il ts del POKE");
seen.push(pong.src_call);
}
assert!(seen[0] != seen[1] || seen[1] != seen[2], "il callno del PONG ruota");
}
#[test]
fn in_call_ping_gets_pong_on_active_call_not_new_callno() {
let now = Instant::now();
let mut c = PbxClient::new(cfg());
c.reg_state = RegState::Registered;
let new = FullFrame::new(
42,
0,
100,
0,
0,
frametype::IAX,
iax::NEW,
vec![Ie::str(iec::CALLING_NUMBER, "200"), Ie::str(iec::CALLED_NUMBER, "10001")],
);
c.handle_input(&new.encode(), now);
let _ = drain_full(&mut c);
let call = match c.poll_event() {
Some(Event::Incoming { call, .. }) => call,
other => panic!("atteso Incoming, trovato {other:?}"),
};
c.handle_command(Command::Answer { call }, now);
let _ = drain_full(&mut c);
let _ = std::iter::from_fn(|| c.poll_event()).count();
assert_eq!(c.call_state(call), Some(CallState::Up));
let local_before = c.calls[&call].leg.local; let remote = c.calls[&call].leg.remote;
let ping = FullFrame::new(remote, local_before, 5000, 3, 4, frametype::IAX, iax::PING, vec![]);
c.handle_input(&ping.encode(), now);
let out = drain_full(&mut c);
let pong = out.iter().find(|f| f.subclass == iax::PONG).expect("PONG in risposta al PING in-call");
assert_eq!(pong.timestamp, 5000, "il PONG riecheggia il ts del PING");
assert_eq!(pong.src_call, local_before, "SCall del PONG = scall locale della call attiva, non un nuovo callno");
assert_eq!(pong.dst_call, remote, "DCall del PONG = scall del peer");
assert_eq!(c.calls.len(), 1, "nessuna call fantasma creata dal PING");
assert_eq!(c.calls[&call].leg.local, local_before, "lo scall locale della call non cambia");
assert_eq!(c.call_state(call), Some(CallState::Up), "la call resta Up");
}
#[test]
fn reconstruct_ts_handles_wrap() {
assert_eq!(reconstruct_ts(0, 1234, false), 1234);
assert_eq!(reconstruct_ts(0x0001_2000, 0x2050, true), 0x0001_2050);
assert_eq!(reconstruct_ts(0x0001_FFF0, 0x0010, true), 0x0002_0010);
assert_eq!(reconstruct_ts(0x0002_0010, 0xFFF0, true), 0x0001_FFF0);
}
#[test]
fn dtmf_send_and_receive() {
let now = Instant::now();
let mut c = PbxClient::new(cfg());
c.reg_state = RegState::Registered;
let new = FullFrame::new(42, 0, 100, 0, 0, frametype::IAX, iax::NEW, vec![Ie::str(iec::CALLING_NUMBER, "200"), Ie::str(iec::CALLED_NUMBER, "10001")]);
c.handle_input(&new.encode(), now);
let call = std::iter::from_fn(|| c.poll_event())
.find_map(|e| if let Event::Incoming { call, .. } = e { Some(call) } else { None })
.expect("Incoming");
c.handle_command(Command::Answer { call }, now);
let _ = drain_full(&mut c);
c.handle_command(Command::Dtmf { call, digit: '5' }, now);
let out = drain_full(&mut c);
let dtmf = out.iter().find(|f| f.frametype == frametype::DTMF).expect("frame DTMF");
assert_eq!(dtmf.subclass, b'5');
c.handle_command(Command::Dtmf { call, digit: 'Z' }, now);
assert!(drain_full(&mut c).iter().all(|f| f.frametype != frametype::DTMF), "cifra invalida scartata");
let rx = FullFrame::new(42, call, 200, 3, 3, frametype::DTMF, b'#', vec![]);
c.handle_input(&rx.encode(), now);
let got = std::iter::from_fn(|| c.poll_event())
.find_map(|e| if let Event::Dtmf { digit, .. } = e { Some(digit) } else { None })
.expect("Event::Dtmf");
assert_eq!(got, '#');
}
#[test]
fn lagrq_on_call_leg_replies_lagrp_echoing_timestamp() {
let now = Instant::now();
let mut c = PbxClient::new(cfg());
c.reg_state = RegState::Registered;
let new = FullFrame::new(42, 0, 100, 0, 0, frametype::IAX, iax::NEW, vec![Ie::str(iec::CALLING_NUMBER, "200"), Ie::str(iec::CALLED_NUMBER, "10001")]);
c.handle_input(&new.encode(), now);
let call = std::iter::from_fn(|| c.poll_event())
.find_map(|e| if let Event::Incoming { call, .. } = e { Some(call) } else { None })
.expect("Incoming");
c.handle_command(Command::Answer { call }, now);
let _ = drain_full(&mut c);
let lagrq = FullFrame::new(42, call, 7777, 5, 5, frametype::IAX, iax::LAGRQ, vec![]);
c.handle_input(&lagrq.encode(), now);
let out = drain_full(&mut c);
let lagrp = out.iter().find(|f| f.frametype == frametype::IAX && f.subclass == iax::LAGRP).expect("LAGRP");
assert_eq!(lagrp.timestamp, 7777, "la LAGRP deve riecheggiare il ts della LAGRQ");
assert_eq!(lagrp.iseq, lagrq.oseq.wrapping_add(1), "iseq della LAGRP = oseq LAGRQ + 1");
}
}