1use std::net::SocketAddr;
34use std::sync::Arc;
35
36use rsipstack::dialog::authenticate::Credential;
37use rsipstack::dialog::client_dialog::ClientInviteDialog;
38use rsipstack::dialog::dialog::{DialogState, DialogStateReceiver};
39use rsipstack::dialog::invitation::{InviteAsyncResult, InviteOption};
40use rsipstack::transport::SipAddr;
41use tokio::net::{lookup_host, UdpSocket};
42use tokio::task::JoinHandle;
43use tracing::{debug, info};
44
45use crate::account::SipAccount;
46use crate::endpoint::SipEndpoint;
47use crate::sdp::{build_sdp, parse_sdp, RemoteMedia};
48
49type BoxError = Box<dyn std::error::Error + Send + Sync>;
50
51pub struct AcceptedDial {
59 pub dialog: ClientInviteDialog,
61 pub remote_media: RemoteMedia,
63 pub rtp_socket: Arc<UdpSocket>,
65 pub local_rtp_addr: SocketAddr,
67 pub state_rx: DialogStateReceiver,
70}
71
72pub struct PendingDial {
88 pub dialog: ClientInviteDialog,
91 pub state_rx: DialogStateReceiver,
93 rtp_socket: Arc<UdpSocket>,
94 local_rtp_addr: SocketAddr,
95 invite_task: JoinHandle<InviteAsyncResult>,
96}
97
98impl PendingDial {
99 pub async fn cancel(&self) -> Result<(), BoxError> {
104 match self.dialog.state() {
105 DialogState::Confirmed(_, _) | DialogState::Terminated(_, _) => {
106 debug!("cancel on settled dialog; no-op");
107 Ok(())
108 }
109 _ => {
110 self.dialog.cancel().await?;
111 info!("sent CANCEL on outbound INVITE");
112 Ok(())
113 }
114 }
115 }
116
117 pub async fn on_confirmed(self) -> Result<AcceptedDial, BoxError> {
125 let (_dialog_id, resp) = self.invite_task.await??;
126 let resp = resp.ok_or_else::<BoxError, _>(|| "INVITE produced no final response".into())?;
127 if resp.status_code.kind() != rsip::StatusCodeKind::Successful {
128 return Err(format!("INVITE did not confirm: status {}", resp.status_code).into());
129 }
130 let remote_media = parse_sdp(&resp.body)?;
131 info!(
132 remote_addr = %remote_media.addr,
133 remote_port = remote_media.port,
134 payload_type = remote_media.payload_type,
135 "parsed SDP answer",
136 );
137 Ok(AcceptedDial {
138 dialog: self.dialog,
139 remote_media,
140 rtp_socket: self.rtp_socket,
141 local_rtp_addr: self.local_rtp_addr,
142 state_rx: self.state_rx,
143 })
144 }
145}
146
147pub struct Caller {
149 account: SipAccount,
150 endpoint: Arc<SipEndpoint>,
151}
152
153impl Caller {
154 pub fn new(account: SipAccount, endpoint: Arc<SipEndpoint>) -> Self {
156 Self { account, endpoint }
157 }
158
159 pub async fn dial(&self, target: rsip::Uri) -> Result<PendingDial, BoxError> {
168 let destination = resolve_server(&self.account).await?;
169 self.dial_with_destination(target, destination).await
170 }
171
172 pub async fn dial_with_destination(
176 &self,
177 target: rsip::Uri,
178 destination: Option<SipAddr>,
179 ) -> Result<PendingDial, BoxError> {
180 let rtp_socket = UdpSocket::bind("0.0.0.0:0").await?;
181 let local_rtp_addr = rtp_socket.local_addr()?;
182 let rtp_port = local_rtp_addr.port();
183 let local_ip = self.endpoint.local_ip();
184 info!(local_ip = %local_ip, rtp_port, "bound RTP socket for outbound dial");
185
186 let offer = build_sdp(local_ip, rtp_port);
187 debug!("SDP offer:\n{}", String::from_utf8_lossy(&offer));
188
189 let opt = build_invite_option(
190 &self.account,
191 &self.endpoint.sip_addr.addr.to_string(),
192 target,
193 offer,
194 destination,
195 )?;
196 let (state_sender, state_rx) = self.endpoint.dialog_layer.new_dialog_state_channel();
197 let (dialog, invite_task) = self
198 .endpoint
199 .dialog_layer
200 .do_invite_async(opt, state_sender)?;
201 info!("INVITE on the wire");
202
203 Ok(PendingDial {
204 dialog,
205 state_rx,
206 rtp_socket: Arc::new(rtp_socket),
207 local_rtp_addr,
208 invite_task,
209 })
210 }
211}
212
213async fn resolve_server(account: &SipAccount) -> Result<Option<SipAddr>, BoxError> {
216 let host_port = format!("{}:{}", account.server(), account.port());
217 let mut addrs = lookup_host(&host_port).await?;
218 Ok(addrs.next().map(SipAddr::from))
219}
220
221fn build_invite_option(
224 account: &SipAccount,
225 contact_host: &str,
226 target: rsip::Uri,
227 offer: Vec<u8>,
228 destination: Option<SipAddr>,
229) -> Result<InviteOption, BoxError> {
230 let caller_uri: rsip::Uri =
231 format!("sip:{}@{}", account.username, account.domain).try_into()?;
232 let contact_uri: rsip::Uri = format!("sip:{}@{}", account.username, contact_host).try_into()?;
233 let credential = Credential {
234 username: account.auth_username().to_string(),
235 password: account.password.clone(),
236 realm: None,
237 };
238 let display_name = if account.display_name.is_empty() {
239 None
240 } else {
241 Some(account.display_name.clone())
242 };
243 Ok(InviteOption {
244 caller_display_name: display_name,
245 caller: caller_uri,
246 callee: target,
247 destination,
248 content_type: Some("application/sdp".into()),
249 offer: Some(offer),
250 contact: contact_uri,
251 credential: Some(credential),
252 ..Default::default()
253 })
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use crate::account::Transport;
260
261 fn test_account() -> SipAccount {
262 SipAccount {
263 display_name: "Office".to_string(),
264 username: "1001".to_string(),
265 password: "secret".to_string(),
266 domain: "sip.example.com".to_string(),
267 auth_username: None,
268 server: Some("pbx.example.com".to_string()),
269 port: Some(5080),
270 transport: Transport::Udp,
271 }
272 }
273
274 #[test]
275 fn build_invite_option_composes_from_account_and_target() {
276 let acct = test_account();
277 let target: rsip::Uri = "sip:bob@example.com".try_into().unwrap();
278 let opt = build_invite_option(
279 &acct,
280 "192.168.1.50:5060",
281 target.clone(),
282 b"v=0\r\n".to_vec(),
283 None,
284 )
285 .expect("build_invite_option");
286
287 assert_eq!(opt.caller.to_string(), "sip:1001@sip.example.com");
288 assert_eq!(opt.callee, target);
289 assert_eq!(opt.contact.to_string(), "sip:1001@192.168.1.50:5060");
290 assert_eq!(opt.content_type.as_deref(), Some("application/sdp"));
291 assert_eq!(opt.caller_display_name.as_deref(), Some("Office"));
292
293 let cred = opt.credential.expect("credential should be set");
294 assert_eq!(cred.username, "1001");
295 assert_eq!(cred.password, "secret");
296 }
297
298 #[test]
299 fn build_invite_option_uses_auth_username_when_set() {
300 let mut acct = test_account();
301 acct.auth_username = Some("admin".to_string());
302 let target: rsip::Uri = "sip:bob@example.com".try_into().unwrap();
303 let opt = build_invite_option(&acct, "10.0.0.1:5060", target, vec![], None).unwrap();
304 let cred = opt.credential.unwrap();
305 assert_eq!(
306 cred.username, "admin",
307 "credential.username should follow auth_username, not the AOR user"
308 );
309 }
310
311 #[test]
312 fn build_invite_option_omits_display_name_when_empty() {
313 let mut acct = test_account();
314 acct.display_name = String::new();
315 let target: rsip::Uri = "sip:bob@example.com".try_into().unwrap();
316 let opt = build_invite_option(&acct, "10.0.0.1:5060", target, vec![], None).unwrap();
317 assert!(opt.caller_display_name.is_none());
318 }
319
320 #[test]
321 fn build_invite_option_carries_offer_body() {
322 let acct = test_account();
323 let target: rsip::Uri = "sip:bob@example.com".try_into().unwrap();
324 let offer = b"v=0\r\nm=audio 30000 RTP/AVP 0\r\n".to_vec();
325 let opt = build_invite_option(&acct, "10.0.0.1:5060", target, offer.clone(), None).unwrap();
326 assert_eq!(opt.offer.as_deref(), Some(offer.as_slice()));
327 }
328
329 #[test]
330 fn build_invite_option_threads_destination() {
331 let acct = test_account();
332 let target: rsip::Uri = "sip:bob@example.com".try_into().unwrap();
333 let dest: SipAddr = "127.0.0.1:5060".parse::<SocketAddr>().unwrap().into();
334 let opt = build_invite_option(&acct, "10.0.0.1:5060", target, vec![], Some(dest.clone()))
335 .unwrap();
336 assert_eq!(opt.destination, Some(dest));
337 }
338}