use std::net::SocketAddr;
use std::sync::Arc;
use rsipstack::dialog::authenticate::Credential;
use rsipstack::dialog::client_dialog::ClientInviteDialog;
use rsipstack::dialog::dialog::{DialogState, DialogStateReceiver};
use rsipstack::dialog::invitation::{InviteAsyncResult, InviteOption};
use rsipstack::transport::SipAddr;
use tokio::net::{lookup_host, UdpSocket};
use tokio::task::JoinHandle;
use tracing::{debug, info};
use crate::account::SipAccount;
use crate::endpoint::SipEndpoint;
use crate::sdp::{build_sdp, parse_sdp, RemoteMedia};
type BoxError = Box<dyn std::error::Error + Send + Sync>;
pub struct AcceptedDial {
pub dialog: ClientInviteDialog,
pub remote_media: RemoteMedia,
pub rtp_socket: Arc<UdpSocket>,
pub local_rtp_addr: SocketAddr,
pub state_rx: DialogStateReceiver,
}
pub struct PendingDial {
pub dialog: ClientInviteDialog,
pub state_rx: DialogStateReceiver,
rtp_socket: Arc<UdpSocket>,
local_rtp_addr: SocketAddr,
invite_task: JoinHandle<InviteAsyncResult>,
}
impl PendingDial {
pub async fn cancel(&self) -> Result<(), BoxError> {
match self.dialog.state() {
DialogState::Confirmed(_, _) | DialogState::Terminated(_, _) => {
debug!("cancel on settled dialog; no-op");
Ok(())
}
_ => {
self.dialog.cancel().await?;
info!("sent CANCEL on outbound INVITE");
Ok(())
}
}
}
pub async fn on_confirmed(self) -> Result<AcceptedDial, BoxError> {
let (_dialog_id, resp) = self.invite_task.await??;
let resp = resp.ok_or_else::<BoxError, _>(|| "INVITE produced no final response".into())?;
if resp.status_code.kind() != rsip::StatusCodeKind::Successful {
return Err(format!("INVITE did not confirm: status {}", resp.status_code).into());
}
let remote_media = parse_sdp(&resp.body)?;
info!(
remote_addr = %remote_media.addr,
remote_port = remote_media.port,
payload_type = remote_media.payload_type,
"parsed SDP answer",
);
Ok(AcceptedDial {
dialog: self.dialog,
remote_media,
rtp_socket: self.rtp_socket,
local_rtp_addr: self.local_rtp_addr,
state_rx: self.state_rx,
})
}
}
pub struct Caller {
account: SipAccount,
endpoint: Arc<SipEndpoint>,
}
impl Caller {
pub fn new(account: SipAccount, endpoint: Arc<SipEndpoint>) -> Self {
Self { account, endpoint }
}
pub async fn dial(&self, target: rsip::Uri) -> Result<PendingDial, BoxError> {
let destination = resolve_server(&self.account).await?;
self.dial_with_destination(target, destination).await
}
pub async fn dial_with_destination(
&self,
target: rsip::Uri,
destination: Option<SipAddr>,
) -> Result<PendingDial, BoxError> {
let rtp_socket = UdpSocket::bind("0.0.0.0:0").await?;
let local_rtp_addr = rtp_socket.local_addr()?;
let rtp_port = local_rtp_addr.port();
let local_ip = self.endpoint.local_ip();
info!(local_ip = %local_ip, rtp_port, "bound RTP socket for outbound dial");
let offer = build_sdp(local_ip, rtp_port);
debug!("SDP offer:\n{}", String::from_utf8_lossy(&offer));
let opt = build_invite_option(
&self.account,
&self.endpoint.sip_addr.addr.to_string(),
target,
offer,
destination,
)?;
let (state_sender, state_rx) = self.endpoint.dialog_layer.new_dialog_state_channel();
let (dialog, invite_task) = self
.endpoint
.dialog_layer
.do_invite_async(opt, state_sender)?;
info!("INVITE on the wire");
Ok(PendingDial {
dialog,
state_rx,
rtp_socket: Arc::new(rtp_socket),
local_rtp_addr,
invite_task,
})
}
}
async fn resolve_server(account: &SipAccount) -> Result<Option<SipAddr>, BoxError> {
let host_port = format!("{}:{}", account.server(), account.port());
let mut addrs = lookup_host(&host_port).await?;
Ok(addrs.next().map(SipAddr::from))
}
fn build_invite_option(
account: &SipAccount,
contact_host: &str,
target: rsip::Uri,
offer: Vec<u8>,
destination: Option<SipAddr>,
) -> Result<InviteOption, BoxError> {
let caller_uri: rsip::Uri =
format!("sip:{}@{}", account.username, account.domain).try_into()?;
let contact_uri: rsip::Uri = format!("sip:{}@{}", account.username, contact_host).try_into()?;
let credential = Credential {
username: account.auth_username().to_string(),
password: account.password.clone(),
realm: None,
};
let display_name = if account.display_name.is_empty() {
None
} else {
Some(account.display_name.clone())
};
Ok(InviteOption {
caller_display_name: display_name,
caller: caller_uri,
callee: target,
destination,
content_type: Some("application/sdp".into()),
offer: Some(offer),
contact: contact_uri,
credential: Some(credential),
..Default::default()
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::account::Transport;
fn test_account() -> SipAccount {
SipAccount {
display_name: "Office".to_string(),
username: "1001".to_string(),
password: "secret".to_string(),
domain: "sip.example.com".to_string(),
auth_username: None,
server: Some("pbx.example.com".to_string()),
port: Some(5080),
transport: Transport::Udp,
}
}
#[test]
fn build_invite_option_composes_from_account_and_target() {
let acct = test_account();
let target: rsip::Uri = "sip:bob@example.com".try_into().unwrap();
let opt = build_invite_option(
&acct,
"192.168.1.50:5060",
target.clone(),
b"v=0\r\n".to_vec(),
None,
)
.expect("build_invite_option");
assert_eq!(opt.caller.to_string(), "sip:1001@sip.example.com");
assert_eq!(opt.callee, target);
assert_eq!(opt.contact.to_string(), "sip:1001@192.168.1.50:5060");
assert_eq!(opt.content_type.as_deref(), Some("application/sdp"));
assert_eq!(opt.caller_display_name.as_deref(), Some("Office"));
let cred = opt.credential.expect("credential should be set");
assert_eq!(cred.username, "1001");
assert_eq!(cred.password, "secret");
}
#[test]
fn build_invite_option_uses_auth_username_when_set() {
let mut acct = test_account();
acct.auth_username = Some("admin".to_string());
let target: rsip::Uri = "sip:bob@example.com".try_into().unwrap();
let opt = build_invite_option(&acct, "10.0.0.1:5060", target, vec![], None).unwrap();
let cred = opt.credential.unwrap();
assert_eq!(
cred.username, "admin",
"credential.username should follow auth_username, not the AOR user"
);
}
#[test]
fn build_invite_option_omits_display_name_when_empty() {
let mut acct = test_account();
acct.display_name = String::new();
let target: rsip::Uri = "sip:bob@example.com".try_into().unwrap();
let opt = build_invite_option(&acct, "10.0.0.1:5060", target, vec![], None).unwrap();
assert!(opt.caller_display_name.is_none());
}
#[test]
fn build_invite_option_carries_offer_body() {
let acct = test_account();
let target: rsip::Uri = "sip:bob@example.com".try_into().unwrap();
let offer = b"v=0\r\nm=audio 30000 RTP/AVP 0\r\n".to_vec();
let opt = build_invite_option(&acct, "10.0.0.1:5060", target, offer.clone(), None).unwrap();
assert_eq!(opt.offer.as_deref(), Some(offer.as_slice()));
}
#[test]
fn build_invite_option_threads_destination() {
let acct = test_account();
let target: rsip::Uri = "sip:bob@example.com".try_into().unwrap();
let dest: SipAddr = "127.0.0.1:5060".parse::<SocketAddr>().unwrap().into();
let opt = build_invite_option(&acct, "10.0.0.1:5060", target, vec![], Some(dest.clone()))
.unwrap();
assert_eq!(opt.destination, Some(dest));
}
}