use std::hash::{BuildHasher, Hash, Hasher};
use std::mem::discriminant;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use rsip::headers::ToTypedHeader;
use rsip::message::HeadersExt;
use rsip::{Header, Headers, Method, Request, Response, SipMessage};
pub(crate) mod client_invite;
pub(crate) mod client_non_invite;
pub(crate) mod server_invite;
pub(crate) mod server_non_invite;
pub(crate) const MAGIC_COOKIE: &str = "z9hG4bK";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Reliability {
Reliable,
Unreliable,
}
impl Reliability {
pub(crate) fn is_reliable(self) -> bool {
matches!(self, Reliability::Reliable)
}
}
impl From<crate::account::Transport> for Reliability {
fn from(transport: crate::account::Transport) -> Self {
match transport {
crate::account::Transport::Udp => Reliability::Unreliable,
crate::account::Transport::Tcp => Reliability::Reliable,
}
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Timers {
pub t1: Duration,
pub t2: Duration,
pub t4: Duration,
}
impl Default for Timers {
fn default() -> Self {
Self {
t1: Duration::from_millis(500),
t2: Duration::from_secs(4),
t4: Duration::from_secs(5),
}
}
}
impl Timers {
pub(crate) fn timeout(&self) -> Duration {
self.t1 * 64
}
pub(crate) fn d(&self, rel: Reliability) -> Duration {
if rel.is_reliable() {
Duration::ZERO
} else {
Duration::from_secs(32)
}
}
pub(crate) fn i(&self, rel: Reliability) -> Duration {
if rel.is_reliable() {
Duration::ZERO
} else {
self.t4
}
}
pub(crate) fn j(&self, rel: Reliability) -> Duration {
if rel.is_reliable() {
Duration::ZERO
} else {
self.timeout()
}
}
pub(crate) fn k(&self, rel: Reliability) -> Duration {
if rel.is_reliable() {
Duration::ZERO
} else {
self.t4
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) enum TimerId {
A,
B,
D,
E,
F,
G,
H,
I,
J,
K,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum TxAction {
Send(SipMessage),
StartTimer { id: TimerId, after: Duration },
StopTimer(TimerId),
DeliverResponse(Response),
DeliverRequest(Request),
TimedOut,
Terminated,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct TransactionKey {
branch: String,
sent_by: String,
method: Method,
}
impl Hash for TransactionKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.branch.hash(state);
self.sent_by.hash(state);
discriminant(&self.method).hash(state);
}
}
impl TransactionKey {
pub(crate) fn from_request(req: &Request) -> Option<Self> {
Self::build(req, normalize(*req.method()))
}
pub(crate) fn from_response(resp: &Response) -> Option<Self> {
let method = resp.cseq_header().ok()?.typed().ok()?.method;
Self::build(resp, normalize(method))
}
fn build(msg: &impl HeadersExt, method: Method) -> Option<Self> {
let via = msg.via_header().ok()?.typed().ok()?;
let branch = via.branch()?.value().to_string();
let sent_by = via.sent_by().host_with_port.to_string();
Some(Self {
branch,
sent_by,
method,
})
}
pub(crate) fn invite_target(&self) -> TransactionKey {
TransactionKey {
branch: self.branch.clone(),
sent_by: self.sent_by.clone(),
method: Method::Invite,
}
}
}
fn normalize(method: Method) -> Method {
match method {
Method::Ack => Method::Invite,
other => other,
}
}
static BRANCH_COUNTER: AtomicU64 = AtomicU64::new(0);
pub(crate) fn gen_branch() -> String {
let n = BRANCH_COUNTER.fetch_add(1, Ordering::Relaxed);
let seed = std::collections::hash_map::RandomState::new().hash_one(n);
format!("{MAGIC_COOKIE}{n:x}{seed:x}")
}
pub(crate) fn via_value(sent_by: impl std::fmt::Display, branch: &str) -> String {
format!("SIP/2.0/UDP {sent_by};rport;branch={branch}")
}
pub(crate) fn gen_tag() -> String {
use std::hash::BuildHasher;
let n = BRANCH_COUNTER.fetch_add(1, Ordering::Relaxed);
let seed = std::collections::hash_map::RandomState::new().hash_one(n);
format!("wk{n:x}{seed:x}")
}
pub(crate) fn build_non_2xx_ack(invite: &Request, response: &Response) -> Option<Request> {
let mut headers = Headers::default();
headers.push(Header::Via(invite.via_header().ok()?.clone()));
headers.push(Header::From(invite.from_header().ok()?.clone()));
headers.push(Header::To(response.to_header().ok()?.clone()));
headers.push(Header::CallId(invite.call_id_header().ok()?.clone()));
let seq = invite.cseq_header().ok()?.typed().ok()?.seq;
let cseq: rsip::headers::CSeq = rsip::typed::CSeq {
seq,
method: Method::Ack,
}
.into();
headers.push(Header::CSeq(cseq));
for header in invite.headers.iter() {
if let Header::Route(route) = header {
headers.push(Header::Route(route.clone()));
}
}
headers.push(Header::MaxForwards(rsip::headers::MaxForwards::default()));
headers.push(Header::ContentLength(
rsip::headers::ContentLength::default(),
));
Some(Request {
method: Method::Ack,
uri: invite.uri.clone(),
version: invite.version.clone(),
headers,
body: Vec::new(),
})
}
#[allow(clippy::enum_variant_names)]
pub(crate) enum Transaction {
ClientInvite(client_invite::ClientInvite),
ClientNonInvite(client_non_invite::ClientNonInvite),
ServerInvite(server_invite::ServerInvite),
ServerNonInvite(server_non_invite::ServerNonInvite),
}
impl Transaction {
pub(crate) fn on_response(&mut self, resp: &Response) -> Vec<TxAction> {
match self {
Transaction::ClientInvite(t) => t.on_response(resp),
Transaction::ClientNonInvite(t) => t.on_response(resp),
Transaction::ServerInvite(_) | Transaction::ServerNonInvite(_) => Vec::new(),
}
}
pub(crate) fn on_request(&mut self, req: &Request) -> Vec<TxAction> {
match self {
Transaction::ServerInvite(t) => t.on_request(req),
Transaction::ServerNonInvite(t) => t.on_request(req),
Transaction::ClientInvite(_) | Transaction::ClientNonInvite(_) => Vec::new(),
}
}
pub(crate) fn on_timer(&mut self, id: TimerId) -> Vec<TxAction> {
match self {
Transaction::ClientInvite(t) => t.on_timer(id),
Transaction::ClientNonInvite(t) => t.on_timer(id),
Transaction::ServerInvite(t) => t.on_timer(id),
Transaction::ServerNonInvite(t) => t.on_timer(id),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn request(method: Method, branch: &str, seq: u32) -> Request {
let raw = format!(
"{m} sip:bob@example.com SIP/2.0\r\n\
Via: SIP/2.0/UDP 10.0.0.1:5060;branch={b}\r\n\
From: <sip:alice@example.com>;tag=alice\r\n\
To: <sip:bob@example.com>\r\n\
Call-ID: call-abc\r\n\
CSeq: {s} {m}\r\n\
Content-Length: 0\r\n\r\n",
m = method,
b = branch,
s = seq,
);
Request::try_from(raw.as_bytes()).expect("valid request")
}
fn response(status: u16, branch: &str, seq: u32, method: Method) -> Response {
let raw = format!(
"SIP/2.0 {code} Testing\r\n\
Via: SIP/2.0/UDP 10.0.0.1:5060;branch={b}\r\n\
From: <sip:alice@example.com>;tag=alice\r\n\
To: <sip:bob@example.com>;tag=bob\r\n\
Call-ID: call-abc\r\n\
CSeq: {s} {m}\r\n\
Content-Length: 0\r\n\r\n",
code = status,
b = branch,
s = seq,
m = method,
);
Response::try_from(raw.as_bytes()).expect("valid response")
}
#[test]
fn via_value_advertises_rport_and_keeps_branch_parseable() {
let v = via_value("192.168.1.46:54991", "z9hG4bK-rp");
assert!(v.contains(";rport"), "Via must advertise rport: {v}");
assert!(
!v.contains("rport="),
"request rport must be valueless: {v}"
);
assert!(v.contains("branch=z9hG4bK-rp"));
assert!(v.contains("192.168.1.46:54991"));
let raw = format!(
"BYE sip:bob@example.com SIP/2.0\r\n\
Via: {v}\r\n\
From: <sip:alice@example.com>;tag=alice\r\n\
To: <sip:bob@example.com>;tag=bob\r\n\
Call-ID: call-abc\r\n\
CSeq: 2 BYE\r\n\
Content-Length: 0\r\n\r\n"
);
let req = Request::try_from(raw.as_bytes()).expect("valid request");
let key = TransactionKey::from_request(&req).expect("branch parses despite bare rport");
assert_eq!(key.branch, "z9hG4bK-rp");
}
#[test]
fn gen_branch_carries_magic_cookie_and_is_unique() {
let a = gen_branch();
let b = gen_branch();
assert!(a.starts_with(MAGIC_COOKIE));
assert!(b.starts_with(MAGIC_COOKIE));
assert_ne!(a, b);
}
#[test]
fn timeout_is_64_t1() {
let t = Timers::default();
assert_eq!(t.timeout(), Duration::from_millis(500) * 64);
}
#[test]
fn soak_timers_collapse_on_reliable_transport() {
let t = Timers::default();
assert_eq!(t.d(Reliability::Reliable), Duration::ZERO);
assert_eq!(t.i(Reliability::Reliable), Duration::ZERO);
assert_eq!(t.j(Reliability::Reliable), Duration::ZERO);
assert_eq!(t.k(Reliability::Reliable), Duration::ZERO);
assert_eq!(t.d(Reliability::Unreliable), Duration::from_secs(32));
assert_eq!(t.i(Reliability::Unreliable), t.t4);
assert_eq!(t.j(Reliability::Unreliable), t.timeout());
assert_eq!(t.k(Reliability::Unreliable), t.t4);
}
#[test]
fn request_and_response_share_a_key() {
let req = request(Method::Invite, "z9hG4bK-1", 1);
let resp = response(180, "z9hG4bK-1", 1, Method::Invite);
assert_eq!(
TransactionKey::from_request(&req),
TransactionKey::from_response(&resp)
);
}
#[test]
fn ack_folds_onto_invite_but_cancel_does_not() {
let invite = request(Method::Invite, "z9hG4bK-1", 1);
let ack = request(Method::Ack, "z9hG4bK-1", 1);
let cancel = request(Method::Cancel, "z9hG4bK-1", 1);
assert_eq!(
TransactionKey::from_request(&invite),
TransactionKey::from_request(&ack),
"ACK must reach the INVITE server transaction"
);
assert_ne!(
TransactionKey::from_request(&invite),
TransactionKey::from_request(&cancel),
"CANCEL forms its own transaction"
);
}
#[test]
fn cancel_invite_target_matches_the_invite_key() {
let invite = request(Method::Invite, "z9hG4bK-cxl", 1);
let cancel = request(Method::Cancel, "z9hG4bK-cxl", 1);
let invite_key = TransactionKey::from_request(&invite).unwrap();
let cancel_key = TransactionKey::from_request(&cancel).unwrap();
assert_ne!(cancel_key, invite_key);
assert_eq!(cancel_key.invite_target(), invite_key);
let target = cancel_key.invite_target();
assert_eq!(target.branch, cancel_key.branch);
assert_eq!(target.sent_by, cancel_key.sent_by);
assert_eq!(target.method, Method::Invite);
}
#[test]
fn different_branches_yield_different_keys() {
let a = request(Method::Bye, "z9hG4bK-aaa", 2);
let b = request(Method::Bye, "z9hG4bK-bbb", 2);
assert_ne!(
TransactionKey::from_request(&a),
TransactionKey::from_request(&b)
);
}
#[test]
fn non_2xx_ack_copies_dialog_identity_and_echoes_remote_tag() {
let invite = request(Method::Invite, "z9hG4bK-9", 7);
let resp = response(486, "z9hG4bK-9", 7, Method::Invite);
let ack = build_non_2xx_ack(&invite, &resp).expect("ack built");
assert_eq!(*ack.method(), Method::Ack);
assert_eq!(ack.uri, invite.uri);
assert_eq!(
ack.via_header().unwrap().typed().unwrap().branch(),
invite.via_header().unwrap().typed().unwrap().branch()
);
let cseq = ack.cseq_header().unwrap().typed().unwrap();
assert_eq!(cseq.seq, 7);
assert_eq!(cseq.method, Method::Ack);
let to = ack.to_header().unwrap().typed().unwrap();
assert_eq!(
to.tag().map(|t| t.value().to_string()),
Some("bob".to_string())
);
}
}