use std::net::SocketAddr;
use rsip::headers::{ToTypedHeader, UntypedHeader};
use rsip::message::HeadersExt;
use rsip::{Header, Headers, Method, Request, StatusCode, Uri};
use super::auth::Credentials;
use super::dialog::Dialog;
use super::transaction::{gen_branch, via_value};
pub(crate) struct CallConfig {
pub target: Uri,
pub from: Uri,
pub contact: Uri,
pub from_tag: String,
pub call_id: String,
pub sdp: Vec<u8>,
pub extra_headers: Vec<Header>,
pub username: String,
pub password: String,
}
impl CallConfig {
fn creds(&self) -> Credentials<'_> {
Credentials {
username: &self.username,
password: &self.password,
}
}
pub(crate) fn creds_for_retry(&self) -> Credentials<'_> {
self.creds()
}
}
pub(crate) enum CallOutcome {
Answered {
dialog: Box<Dialog>,
response: Box<rsip::Response>,
},
Rejected(StatusCode),
Unauthorized,
TimedOut,
EngineStopped,
}
pub(crate) fn build_invite(cfg: &CallConfig, cseq: u32, local_addr: SocketAddr) -> Request {
let mut headers = Headers::default();
headers.push(Header::Via(rsip::headers::Via::new(via_value(
local_addr,
&gen_branch(),
))));
headers.push(Header::MaxForwards(rsip::headers::MaxForwards::default()));
let from = rsip::typed::From {
display_name: None,
uri: cfg.from.clone(),
params: vec![rsip::common::uri::param::Param::Tag(
rsip::common::uri::param::Tag::new(cfg.from_tag.clone()),
)],
};
let to = rsip::typed::To {
display_name: None,
uri: cfg.target.clone(),
params: vec![],
};
let contact = rsip::typed::Contact {
display_name: None,
uri: cfg.contact.clone(),
params: vec![],
};
headers.push(Header::From(from.into()));
headers.push(Header::To(to.into()));
headers.push(Header::Contact(contact.into()));
headers.push(Header::CallId(rsip::headers::CallId::new(
cfg.call_id.clone(),
)));
headers.push(Header::CSeq(
rsip::typed::CSeq {
seq: cseq,
method: Method::Invite,
}
.into(),
));
for header in &cfg.extra_headers {
headers.push(header.clone());
}
if !cfg.sdp.is_empty() {
headers.push(Header::ContentType(rsip::headers::ContentType::new(
"application/sdp",
)));
}
headers.push(Header::ContentLength(rsip::headers::ContentLength::from(
cfg.sdp.len() as u32,
)));
Request {
method: Method::Invite,
uri: cfg.target.clone(),
version: rsip::Version::V2,
headers,
body: cfg.sdp.clone(),
}
}
pub(crate) fn build_cancel(invite: &Request) -> Option<Request> {
let via = invite.via_header().ok()?.clone();
let from = invite.from_header().ok()?.clone();
let to = invite.to_header().ok()?.clone();
let call_id = invite.call_id_header().ok()?.clone();
let seq = cseq_of(invite);
let mut headers = Headers::default();
headers.push(Header::Via(via));
headers.push(Header::MaxForwards(rsip::headers::MaxForwards::default()));
headers.push(Header::From(from));
headers.push(Header::To(to));
headers.push(Header::CallId(call_id));
headers.push(Header::CSeq(
rsip::typed::CSeq {
seq,
method: Method::Cancel,
}
.into(),
));
headers.push(Header::ContentLength(
rsip::headers::ContentLength::default(),
));
Some(Request {
method: Method::Cancel,
uri: invite.uri.clone(),
version: rsip::Version::V2,
headers,
body: Vec::new(),
})
}
pub(crate) fn cseq_of(request: &Request) -> u32 {
request
.cseq_header()
.ok()
.and_then(|c| c.typed().ok())
.map(|c| c.seq)
.unwrap_or(1)
}
#[cfg(test)]
mod tests {
use super::*;
fn config() -> CallConfig {
CallConfig {
target: Uri::try_from("sip:bob@example.com").unwrap(),
from: Uri::try_from("sip:alice@example.com").unwrap(),
contact: Uri::try_from("sip:alice@127.0.0.1:5060").unwrap(),
from_tag: "alicetag".into(),
call_id: "call-xyz".into(),
sdp: b"v=0\r\n".to_vec(),
extra_headers: Vec::new(),
username: "alice".into(),
password: "secret".into(),
}
}
#[test]
fn build_invite_carries_sdp() {
let cfg = config();
let invite = build_invite(&cfg, 1, "127.0.0.1:5060".parse().unwrap());
assert_eq!(*invite.method(), Method::Invite);
assert_eq!(invite.body, b"v=0\r\n");
assert!(invite
.headers
.iter()
.any(|h| matches!(h, Header::ContentType(_))));
}
#[test]
fn build_invite_via_advertises_rport() {
let cfg = config();
let invite = build_invite(&cfg, 1, "127.0.0.1:5060".parse().unwrap());
let via = invite.via_header().unwrap().to_string();
assert!(via.contains(";rport"), "INVITE Via lacks rport: {via}");
let cancel = build_cancel(&invite).expect("cancel built");
let cancel_via = cancel.via_header().unwrap().to_string();
assert!(
cancel_via.contains(";rport"),
"CANCEL Via lacks rport: {cancel_via}"
);
}
#[test]
fn build_invite_carries_extra_headers() {
let mut cfg = config();
cfg.extra_headers = vec![Header::Supported("timer".into())];
let invite = build_invite(&cfg, 1, "127.0.0.1:5060".parse().unwrap());
assert!(invite.to_string().contains("Supported: timer"));
}
#[test]
fn build_cancel_matches_invite_branch_and_cseq() {
let cfg = config();
let invite = build_invite(&cfg, 4, "127.0.0.1:5060".parse().unwrap());
let cancel = build_cancel(&invite).expect("cancel built");
assert_eq!(*cancel.method(), Method::Cancel);
assert_eq!(cancel.uri, invite.uri);
let inv_branch = invite
.via_header()
.unwrap()
.typed()
.unwrap()
.branch()
.unwrap()
.to_string();
let can_branch = cancel
.via_header()
.unwrap()
.typed()
.unwrap()
.branch()
.unwrap()
.to_string();
assert_eq!(can_branch, inv_branch, "CANCEL reuses the INVITE branch");
let cseq = cancel.cseq_header().unwrap().typed().unwrap();
assert_eq!(cseq.seq, 4);
assert_eq!(cseq.method, Method::Cancel);
}
#[test]
fn cseq_of_reads_sequence() {
let cfg = config();
let invite = build_invite(&cfg, 7, "127.0.0.1:5060".parse().unwrap());
assert_eq!(cseq_of(&invite), 7);
}
}