use rsip::common::uri::param::{Param, Tag};
use rsip::common::uri::{UriWithParams, UriWithParamsList};
use rsip::headers::{ToTypedHeader, UntypedHeader};
use rsip::message::HeadersExt;
use rsip::{Header, Headers, Method, Request, Response, Uri};
use super::transaction::{gen_branch, via_value};
#[derive(Clone, Debug, PartialEq, Eq)]
struct Party {
display_name: Option<String>,
uri: Uri,
tag: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct DialogId {
pub call_id: String,
pub local_tag: String,
pub remote_tag: String,
}
impl DialogId {
pub(crate) fn from_request(request: &Request) -> Option<Self> {
let to = request.to_header().ok()?.typed().ok()?;
let from = request.from_header().ok()?.typed().ok()?;
Some(Self {
call_id: request.call_id_header().ok()?.value().to_string(),
local_tag: to.tag()?.value().to_string(),
remote_tag: from.tag()?.value().to_string(),
})
}
}
pub(crate) struct Dialog {
call_id: String,
local: Party,
remote: Party,
local_contact: Uri,
remote_target: Uri,
route_set: Vec<UriWithParams>,
local_seq: u32,
confirmed: bool,
}
impl Dialog {
pub(crate) fn uac(invite: &Request, response: &Response, local_contact: Uri) -> Option<Self> {
let from = invite.from_header().ok()?.typed().ok()?;
let to = response.to_header().ok()?.typed().ok()?;
let cseq = invite.cseq_header().ok()?.typed().ok()?;
let mut route_set = collect_routes(response.headers.iter());
route_set.reverse();
Some(Self {
call_id: invite.call_id_header().ok()?.value().to_string(),
local: Party {
display_name: from.display_name.clone(),
uri: from.uri.clone(),
tag: tag_of(&from.params)?,
},
remote: Party {
display_name: to.display_name.clone(),
uri: to.uri.clone(),
tag: tag_of(&to.params)?,
},
local_contact,
remote_target: response.contact_header().ok()?.typed().ok()?.uri,
route_set,
local_seq: cseq.seq,
confirmed: response.status_code().code() >= 200,
})
}
pub(crate) fn uas(invite: &Request, local_tag: String, local_contact: Uri) -> Option<Self> {
let from = invite.from_header().ok()?.typed().ok()?;
let to = invite.to_header().ok()?.typed().ok()?;
let route_set = collect_routes(invite.headers.iter());
Some(Self {
call_id: invite.call_id_header().ok()?.value().to_string(),
local: Party {
display_name: to.display_name.clone(),
uri: to.uri.clone(),
tag: local_tag,
},
remote: Party {
display_name: from.display_name.clone(),
uri: from.uri.clone(),
tag: tag_of(&from.params)?,
},
local_contact,
remote_target: invite.contact_header().ok()?.typed().ok()?.uri,
route_set,
local_seq: 0,
confirmed: true,
})
}
pub(crate) fn id(&self) -> DialogId {
DialogId {
call_id: self.call_id.clone(),
local_tag: self.local.tag.clone(),
remote_tag: self.remote.tag.clone(),
}
}
pub(crate) fn is_confirmed(&self) -> bool {
self.confirmed
}
pub(crate) fn confirm(&mut self) {
self.confirmed = true;
}
pub(crate) fn remote_target(&self) -> &Uri {
&self.remote_target
}
pub(crate) fn new_request(&mut self, method: Method) -> Request {
self.local_seq += 1;
let seq = self.local_seq;
self.compose(method, seq, Vec::new(), Vec::new())
}
pub(crate) fn new_request_with(
&mut self,
method: Method,
extra_headers: Vec<Header>,
body: Vec<u8>,
) -> Request {
self.local_seq += 1;
let seq = self.local_seq;
self.compose(method, seq, extra_headers, body)
}
pub(crate) fn ack_2xx(&self, invite_cseq: u32) -> Request {
self.compose(Method::Ack, invite_cseq, Vec::new(), Vec::new())
}
fn compose(
&self,
method: Method,
seq: u32,
extra_headers: Vec<Header>,
body: Vec<u8>,
) -> Request {
let branch = gen_branch();
let mut headers = Headers::default();
let host = self.local_contact.host_with_port.to_string();
headers.push(Header::Via(rsip::headers::Via::new(via_value(
&host, &branch,
))));
headers.push(Header::MaxForwards(rsip::headers::MaxForwards::default()));
let from = rsip::typed::From {
display_name: self.local.display_name.clone(),
uri: self.local.uri.clone(),
params: vec![Param::Tag(Tag::new(self.local.tag.clone()))],
};
let to = rsip::typed::To {
display_name: self.remote.display_name.clone(),
uri: self.remote.uri.clone(),
params: vec![Param::Tag(Tag::new(self.remote.tag.clone()))],
};
headers.push(Header::From(from.into()));
headers.push(Header::To(to.into()));
headers.push(Header::CallId(rsip::headers::CallId::new(
self.call_id.clone(),
)));
headers.push(Header::CSeq(rsip::typed::CSeq { seq, method }.into()));
if !self.route_set.is_empty() {
let list: UriWithParamsList = self.route_set.clone().into();
headers.push(Header::Route(rsip::typed::Route(list).into()));
}
let contact = rsip::typed::Contact {
display_name: None,
uri: self.local_contact.clone(),
params: vec![],
};
headers.push(Header::Contact(contact.into()));
for header in extra_headers {
headers.push(header);
}
headers.push(Header::ContentLength(rsip::headers::ContentLength::from(
body.len() as u32,
)));
Request {
method,
uri: self.remote_target.clone(),
version: rsip::Version::V2,
headers,
body,
}
}
}
fn tag_of(params: &[Param]) -> Option<String> {
params.iter().find_map(|p| match p {
Param::Tag(tag) => Some(tag.value().to_string()),
_ => None,
})
}
fn collect_routes<'a>(headers: impl Iterator<Item = &'a Header>) -> Vec<UriWithParams> {
let mut routes = Vec::new();
for header in headers {
if let Header::RecordRoute(rr) = header {
if let Ok(typed) = rr.typed() {
routes.extend(typed.uris().iter().cloned());
}
}
}
routes
}
#[cfg(test)]
mod tests {
use super::*;
fn invite() -> Request {
let raw = "INVITE sip:bob@biloxi.example.com SIP/2.0\r\n\
Via: SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK-inv\r\n\
Record-Route: <sip:p1.example.com;lr>\r\n\
From: \"Alice\" <sip:alice@atlanta.example.com>;tag=alice\r\n\
To: <sip:bob@biloxi.example.com>\r\n\
Contact: <sip:alice@10.0.0.1:5060>\r\n\
Call-ID: call-dlg\r\n\
CSeq: 314 INVITE\r\n\
Content-Length: 0\r\n\r\n";
Request::try_from(raw.as_bytes()).unwrap()
}
fn ok_response() -> Response {
let raw = "SIP/2.0 200 OK\r\n\
Via: SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK-inv\r\n\
Record-Route: <sip:p1.example.com;lr>\r\n\
Record-Route: <sip:p2.example.com;lr>\r\n\
From: \"Alice\" <sip:alice@atlanta.example.com>;tag=alice\r\n\
To: <sip:bob@biloxi.example.com>;tag=bob\r\n\
Contact: <sip:bob@192.168.9.9:5060>\r\n\
Call-ID: call-dlg\r\n\
CSeq: 314 INVITE\r\n\
Content-Length: 0\r\n\r\n";
Response::try_from(raw.as_bytes()).unwrap()
}
fn local_contact() -> Uri {
Uri::try_from("sip:alice@10.0.0.1:5060").unwrap()
}
#[test]
fn uac_dialog_captures_identity_and_target() {
let dialog = Dialog::uac(&invite(), &ok_response(), local_contact()).unwrap();
assert!(dialog.is_confirmed());
assert_eq!(
dialog.id(),
DialogId {
call_id: "call-dlg".into(),
local_tag: "alice".into(),
remote_tag: "bob".into(),
}
);
assert_eq!(
dialog.remote_target().to_string(),
"sip:bob@192.168.9.9:5060"
);
}
#[test]
fn in_dialog_request_uses_target_route_set_and_tags() {
let mut dialog = Dialog::uac(&invite(), &ok_response(), local_contact()).unwrap();
let bye = dialog.new_request(Method::Bye);
assert_eq!(bye.uri.to_string(), "sip:bob@192.168.9.9:5060");
assert_eq!(*bye.method(), Method::Bye);
let cseq = bye.cseq_header().unwrap().typed().unwrap();
assert_eq!(cseq.seq, 315);
assert_eq!(cseq.method, Method::Bye);
assert_eq!(
bye.from_header()
.unwrap()
.typed()
.unwrap()
.tag()
.unwrap()
.value(),
"alice"
);
assert_eq!(
bye.to_header()
.unwrap()
.typed()
.unwrap()
.tag()
.unwrap()
.value(),
"bob"
);
let route = bye
.headers
.iter()
.find_map(|h| match h {
Header::Route(r) => Some(r.to_string()),
_ => None,
})
.expect("Route header present");
let p2 = route.find("p2").expect("p2 in route");
let p1 = route.find("p1").expect("p1 in route");
assert!(p2 < p1, "UAC route set must be reversed: {route}");
let branch = bye
.via_header()
.unwrap()
.typed()
.unwrap()
.branch()
.unwrap()
.to_string();
assert!(branch.starts_with("z9hG4bK"));
}
#[test]
fn in_dialog_request_via_advertises_rport() {
let mut dialog = Dialog::uac(&invite(), &ok_response(), local_contact()).unwrap();
let bye = dialog.new_request(Method::Bye);
let via = bye.via_header().unwrap().to_string();
assert!(via.contains(";rport"), "in-dialog Via lacks rport: {via}");
}
#[test]
fn route_set_is_replayed_on_every_request() {
let mut dialog = Dialog::uac(&invite(), &ok_response(), local_contact()).unwrap();
let _bye = dialog.new_request(Method::Bye);
let reinvite = dialog.new_request(Method::Invite);
assert!(reinvite
.headers
.iter()
.any(|h| matches!(h, Header::Route(_))));
assert_eq!(reinvite.cseq_header().unwrap().typed().unwrap().seq, 316);
}
#[test]
fn new_request_with_attaches_body_headers_and_content_length() {
let mut dialog = Dialog::uac(&invite(), &ok_response(), local_contact()).unwrap();
let body = b"Signal=5\nDuration=160".to_vec();
let info = dialog.new_request_with(
Method::Info,
vec![Header::ContentType(rsip::headers::ContentType::new(
"application/dtmf-relay",
))],
body.clone(),
);
assert_eq!(*info.method(), Method::Info);
assert_eq!(info.body, body);
let len = info
.headers
.iter()
.find_map(|h| match h {
Header::ContentLength(c) => Some(c.value().to_string()),
_ => None,
})
.expect("Content-Length present");
assert_eq!(len, body.len().to_string());
assert!(info.headers.iter().any(|h| matches!(
h,
Header::ContentType(ct) if ct.value() == "application/dtmf-relay"
)));
}
#[test]
fn uas_dialog_orients_parties_and_matches_inbound() {
let dialog = Dialog::uas(&invite(), "ourtag".into(), local_contact()).unwrap();
let id = dialog.id();
assert_eq!(id.local_tag, "ourtag");
assert_eq!(id.remote_tag, "alice");
let bye_raw = "BYE sip:bob@10.0.0.1:5060 SIP/2.0\r\n\
Via: SIP/2.0/UDP 1.2.3.4:5060;branch=z9hG4bK-bye\r\n\
From: \"Alice\" <sip:alice@atlanta.example.com>;tag=alice\r\n\
To: <sip:bob@biloxi.example.com>;tag=ourtag\r\n\
Call-ID: call-dlg\r\n\
CSeq: 1 BYE\r\n\
Content-Length: 0\r\n\r\n";
let inbound = Request::try_from(bye_raw.as_bytes()).unwrap();
assert_eq!(DialogId::from_request(&inbound), Some(dialog.id()));
}
#[test]
fn early_dialog_from_provisional_is_not_confirmed() {
let raw = "SIP/2.0 180 Ringing\r\n\
Via: SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK-inv\r\n\
From: \"Alice\" <sip:alice@atlanta.example.com>;tag=alice\r\n\
To: <sip:bob@biloxi.example.com>;tag=bob\r\n\
Contact: <sip:bob@192.168.9.9:5060>\r\n\
Call-ID: call-dlg\r\n\
CSeq: 314 INVITE\r\n\
Content-Length: 0\r\n\r\n";
let ringing = Response::try_from(raw.as_bytes()).unwrap();
let mut dialog = Dialog::uac(&invite(), &ringing, local_contact()).unwrap();
assert!(!dialog.is_confirmed());
dialog.confirm();
assert!(dialog.is_confirmed());
}
}