ftth_rsipstack/dialog/invitation.rs
1use super::{
2 authenticate::Credential,
3 client_dialog::ClientInviteDialog,
4 dialog::{DialogInner, DialogStateSender},
5 dialog_layer::DialogLayer,
6};
7use crate::rsip;
8use crate::{
9 dialog::{dialog::Dialog, dialog_layer::DialogLayerInnerRef, DialogId},
10 transaction::{
11 key::{TransactionKey, TransactionRole},
12 make_tag,
13 transaction::Transaction,
14 },
15 transport::SipAddr,
16 Result,
17};
18use rsip::{Request, Response};
19use std::sync::Arc;
20use tracing::{debug, info, warn};
21
22/// INVITE Request Options
23///
24/// `InviteOption` contains all the parameters needed to create and send
25/// an INVITE request to establish a SIP session. This structure provides
26/// a convenient way to specify all the necessary information for initiating
27/// a call or session.
28///
29/// # Fields
30///
31/// * `caller` - URI of the calling party (From header)
32/// * `callee` - URI of the called party (To header and Request-URI)
33/// * `content_type` - MIME type of the message body (default: "application/sdp")
34/// * `offer` - Optional message body (typically SDP offer)
35/// * `contact` - Contact URI for this user agent
36/// * `credential` - Optional authentication credentials
37/// * `headers` - Optional additional headers to include
38///
39/// # Examples
40///
41/// ## Basic Voice Call
42///
43/// ```rust,no_run
44/// # use ftth_rsipstack::dialog::invitation::InviteOption;
45/// # fn example() -> ftth_rsipstack::Result<()> {
46/// # let sdp_offer_bytes = vec![];
47/// let invite_option = InviteOption {
48/// caller: "sip:alice@example.com".try_into()?,
49/// callee: "sip:bob@example.com".try_into()?,
50/// content_type: Some("application/sdp".to_string()),
51/// destination: None,
52/// offer: Some(sdp_offer_bytes),
53/// contact: "sip:alice@192.168.1.100:5060".try_into()?,
54/// credential: None,
55/// headers: None,
56/// };
57/// # Ok(())
58/// # }
59/// ```
60///
61/// ```rust,no_run
62/// # use ftth_rsipstack::dialog::dialog_layer::DialogLayer;
63/// # use ftth_rsipstack::dialog::invitation::InviteOption;
64/// # fn example() -> ftth_rsipstack::Result<()> {
65/// # let dialog_layer: DialogLayer = todo!();
66/// # let invite_option: InviteOption = todo!();
67/// let request = dialog_layer.make_invite_request(&invite_option)?;
68/// println!("Created INVITE to: {}", request.uri);
69/// # Ok(())
70/// # }
71/// ```
72///
73/// ## Call with Custom Headers
74///
75/// ```rust,no_run
76/// # use ftth_rsipstack::dialog::invitation::InviteOption;
77/// # fn example() -> ftth_rsipstack::Result<()> {
78/// # let sdp_bytes = vec![];
79/// # let auth_credential = todo!();
80/// let custom_headers = vec![
81/// rsip::Header::UserAgent("MyApp/1.0".into()),
82/// rsip::Header::Subject("Important Call".into()),
83/// ];
84///
85/// let invite_option = InviteOption {
86/// caller: "sip:alice@example.com".try_into()?,
87/// callee: "sip:bob@example.com".try_into()?,
88/// content_type: Some("application/sdp".to_string()),
89/// destination: None,
90/// offer: Some(sdp_bytes),
91/// contact: "sip:alice@192.168.1.100:5060".try_into()?,
92/// credential: Some(auth_credential),
93/// headers: Some(custom_headers),
94/// };
95/// # Ok(())
96/// # }
97/// ```
98///
99/// ## Call with Authentication
100///
101/// ```rust,no_run
102/// # use ftth_rsipstack::dialog::invitation::InviteOption;
103/// # use ftth_rsipstack::dialog::authenticate::Credential;
104/// # fn example() -> ftth_rsipstack::Result<()> {
105/// # let sdp_bytes = vec![];
106/// let credential = Credential {
107/// username: "alice".to_string(),
108/// password: "secret123".to_string(),
109/// realm: Some("example.com".to_string()),
110/// };
111///
112/// let invite_option = InviteOption {
113/// caller: "sip:alice@example.com".try_into()?,
114/// callee: "sip:bob@example.com".try_into()?,
115/// content_type: None, // Will default to "application/sdp"
116/// destination: None,
117/// offer: Some(sdp_bytes),
118/// contact: "sip:alice@192.168.1.100:5060".try_into()?,
119/// credential: Some(credential),
120/// headers: None,
121/// };
122/// # Ok(())
123/// # }
124/// ```
125#[derive(Default, Clone)]
126pub struct InviteOption {
127 pub caller: rsip::Uri,
128 pub callee: rsip::Uri,
129 pub destination: Option<SipAddr>,
130 pub content_type: Option<String>,
131 pub offer: Option<Vec<u8>>,
132 pub contact: rsip::Uri,
133 pub credential: Option<Credential>,
134 pub headers: Option<Vec<rsip::Header>>,
135}
136
137pub struct DialogGuard {
138 pub dialog_layer_inner: DialogLayerInnerRef,
139 pub id: DialogId,
140}
141
142impl DialogGuard {
143 pub fn new(dialog_layer: &Arc<DialogLayer>, id: DialogId) -> Self {
144 Self {
145 dialog_layer_inner: dialog_layer.inner.clone(),
146 id,
147 }
148 }
149}
150
151impl Drop for DialogGuard {
152 fn drop(&mut self) {
153 let dlg = match self.dialog_layer_inner.dialogs.write() {
154 Ok(mut dialogs) => match dialogs.remove(&self.id) {
155 Some(dlg) => dlg,
156 None => return,
157 },
158 _ => return,
159 };
160 let _ = tokio::spawn(async move {
161 if let Err(e) = dlg.hangup().await {
162 info!(id=%dlg.id(), "failed to hangup dialog: {}", e);
163 }
164 });
165 }
166}
167
168pub(super) struct DialogGuardForUnconfirmed<'a> {
169 pub dialog_layer_inner: &'a DialogLayerInnerRef,
170 pub id: &'a DialogId,
171}
172
173impl<'a> Drop for DialogGuardForUnconfirmed<'a> {
174 fn drop(&mut self) {
175 // If the dialog is still unconfirmed, we should try to cancel it
176 match self.dialog_layer_inner.dialogs.write() {
177 Ok(mut dialogs) => {
178 if let Some(dlg) = dialogs.get(self.id) {
179 if !dlg.can_cancel() {
180 return;
181 }
182 match dialogs.remove(self.id) {
183 Some(dlg) => {
184 info!(%self.id, "unconfirmed dialog dropped, cancelling it");
185 let _ = tokio::spawn(async move {
186 if let Err(e) = dlg.hangup().await {
187 info!(id=%dlg.id(), "failed to hangup unconfirmed dialog: {}", e);
188 }
189 });
190 }
191 None => {}
192 }
193 }
194 }
195 Err(e) => {
196 warn!(%self.id, "failed to acquire write lock on dialogs: {}", e);
197 }
198 }
199 }
200}
201
202impl DialogLayer {
203 /// Create an INVITE request from options
204 ///
205 /// Constructs a properly formatted SIP INVITE request based on the
206 /// provided options. This method handles all the required headers
207 /// and parameters according to RFC 3261.
208 ///
209 /// # Parameters
210 ///
211 /// * `opt` - INVITE options containing all necessary parameters
212 ///
213 /// # Returns
214 ///
215 /// * `Ok(Request)` - Properly formatted INVITE request
216 /// * `Err(Error)` - Failed to create request
217 ///
218 /// # Generated Headers
219 ///
220 /// The method automatically generates:
221 /// * Via header with branch parameter
222 /// * From header with tag parameter
223 /// * To header (without tag for initial request)
224 /// * Contact header
225 /// * Content-Type header
226 /// * CSeq header with incremented sequence number
227 /// * Call-ID header
228 ///
229 /// # Examples
230 ///
231 /// ```rust,no_run
232 /// # use ftth_rsipstack::dialog::dialog_layer::DialogLayer;
233 /// # use ftth_rsipstack::dialog::invitation::InviteOption;
234 /// # fn example() -> ftth_rsipstack::Result<()> {
235 /// # let dialog_layer: DialogLayer = todo!();
236 /// # let invite_option: InviteOption = todo!();
237 /// let request = dialog_layer.make_invite_request(&invite_option)?;
238 /// println!("Created INVITE to: {}", request.uri);
239 /// # Ok(())
240 /// # }
241 /// ```
242 pub fn make_invite_request(&self, opt: &InviteOption) -> Result<Request> {
243 let last_seq = self.increment_last_seq();
244 let to = rsip::typed::To {
245 display_name: None,
246 uri: opt.callee.clone(),
247 params: vec![],
248 };
249 let recipient = to.uri.clone();
250
251 let form = rsip::typed::From {
252 display_name: None,
253 uri: opt.caller.clone(),
254 params: vec![],
255 }
256 .with_tag(make_tag());
257
258 let via = self.endpoint.get_via(None, None)?;
259 let mut request =
260 self.endpoint
261 .make_request(rsip::Method::Invite, recipient, via, form, to, last_seq);
262
263 let contact = rsip::typed::Contact {
264 display_name: None,
265 uri: opt.contact.clone(),
266 params: vec![],
267 };
268
269 request
270 .headers
271 .unique_push(rsip::Header::Contact(contact.into()));
272
273 request.headers.unique_push(rsip::Header::ContentType(
274 opt.content_type
275 .clone()
276 .unwrap_or("application/sdp".to_string())
277 .into(),
278 ));
279 // can override default headers
280 if let Some(headers) = opt.headers.as_ref() {
281 for header in headers {
282 request.headers.unique_push(header.clone());
283 }
284 }
285 Ok(request)
286 }
287
288 /// Send an INVITE request and create a client dialog
289 ///
290 /// This is the main method for initiating outbound calls. It creates
291 /// an INVITE request, sends it, and manages the resulting dialog.
292 /// The method handles the complete INVITE transaction including
293 /// authentication challenges and response processing.
294 ///
295 /// # Parameters
296 ///
297 /// * `opt` - INVITE options containing all call parameters
298 /// * `state_sender` - Channel for receiving dialog state updates
299 ///
300 /// # Returns
301 ///
302 /// * `Ok((ClientInviteDialog, Option<Response>))` - Created dialog and final response
303 /// * `Err(Error)` - Failed to send INVITE or process responses
304 ///
305 /// # Call Flow
306 ///
307 /// 1. Creates INVITE request from options
308 /// 2. Creates client dialog and transaction
309 /// 3. Sends INVITE request
310 /// 4. Processes responses (1xx, 2xx, 3xx-6xx)
311 /// 5. Handles authentication challenges if needed
312 /// 6. Returns established dialog and final response
313 ///
314 /// # Examples
315 ///
316 /// ## Basic Call Setup
317 ///
318 /// ```rust,no_run
319 /// # use ftth_rsipstack::dialog::dialog_layer::DialogLayer;
320 /// # use ftth_rsipstack::dialog::invitation::InviteOption;
321 /// # async fn example() -> ftth_rsipstack::Result<()> {
322 /// # let dialog_layer: DialogLayer = todo!();
323 /// # let invite_option: InviteOption = todo!();
324 /// # let state_sender = todo!();
325 /// let (dialog, response) = dialog_layer.do_invite(invite_option, state_sender).await?;
326 ///
327 /// if let Some(resp) = response {
328 /// match resp.status_code {
329 /// rsip::StatusCode::OK => {
330 /// println!("Call answered!");
331 /// // Process SDP answer in resp.body
332 /// },
333 /// rsip::StatusCode::BusyHere => {
334 /// println!("Called party is busy");
335 /// },
336 /// _ => {
337 /// println!("Call failed: {}", resp.status_code);
338 /// }
339 /// }
340 /// }
341 /// # Ok(())
342 /// # }
343 /// ```
344 ///
345 /// ## Monitoring Dialog State
346 ///
347 /// ```rust,no_run
348 /// # use ftth_rsipstack::dialog::dialog_layer::DialogLayer;
349 /// # use ftth_rsipstack::dialog::invitation::InviteOption;
350 /// # use ftth_rsipstack::dialog::dialog::DialogState;
351 /// # async fn example() -> ftth_rsipstack::Result<()> {
352 /// # let dialog_layer: DialogLayer = todo!();
353 /// # let invite_option: InviteOption = todo!();
354 /// let (state_tx, mut state_rx) = tokio::sync::mpsc::unbounded_channel();
355 /// let (dialog, response) = dialog_layer.do_invite(invite_option, state_tx).await?;
356 ///
357 /// // Monitor dialog state changes
358 /// tokio::spawn(async move {
359 /// while let Some(state) = state_rx.recv().await {
360 /// match state {
361 /// DialogState::Early(_, resp) => {
362 /// println!("Ringing: {}", resp.status_code);
363 /// },
364 /// DialogState::Confirmed(_,_) => {
365 /// println!("Call established");
366 /// },
367 /// DialogState::Terminated(_, code) => {
368 /// println!("Call ended: {:?}", code);
369 /// break;
370 /// },
371 /// _ => {}
372 /// }
373 /// }
374 /// });
375 /// # Ok(())
376 /// # }
377 /// ```
378 ///
379 /// # Error Handling
380 ///
381 /// The method can fail for various reasons:
382 /// * Network connectivity issues
383 /// * Authentication failures
384 /// * Invalid SIP URIs or headers
385 /// * Transaction timeouts
386 /// * Protocol violations
387 ///
388 /// # Authentication
389 ///
390 /// If credentials are provided in the options, the method will
391 /// automatically handle 401/407 authentication challenges by
392 /// resending the request with proper authentication headers.
393 pub async fn do_invite(
394 &self,
395 opt: InviteOption,
396 state_sender: DialogStateSender,
397 ) -> Result<(ClientInviteDialog, Option<Response>)> {
398 let (dialog, tx) = self.create_client_invite_dialog(opt, state_sender)?;
399 let id = dialog.id();
400 self.inner
401 .dialogs
402 .write()
403 .unwrap()
404 .insert(id.clone(), Dialog::ClientInvite(dialog.clone()));
405 info!(%id, "client invite dialog created");
406
407 let _guard = DialogGuardForUnconfirmed {
408 dialog_layer_inner: &self.inner,
409 id: &id,
410 };
411
412 match dialog.process_invite(tx).await {
413 Ok((new_dialog_id, resp)) => {
414 debug!(
415 "client invite dialog confirmed: {} => {}",
416 id, new_dialog_id
417 );
418 self.inner.dialogs.write().unwrap().remove(&id);
419 // update with new dialog id
420 self.inner
421 .dialogs
422 .write()
423 .unwrap()
424 .insert(new_dialog_id, Dialog::ClientInvite(dialog.clone()));
425 return Ok((dialog, resp));
426 }
427 Err(e) => {
428 self.inner.dialogs.write().unwrap().remove(&id);
429 return Err(e);
430 }
431 }
432 }
433
434 pub fn create_client_invite_dialog(
435 &self,
436 opt: InviteOption,
437 state_sender: DialogStateSender,
438 ) -> Result<(ClientInviteDialog, Transaction)> {
439 let mut request = self.make_invite_request(&opt)?;
440 request.body = opt.offer.unwrap_or_default();
441 request.headers.unique_push(rsip::Header::ContentLength(
442 (request.body.len() as u32).into(),
443 ));
444 let key = TransactionKey::from_request(&request, TransactionRole::Client)?;
445 let mut tx = Transaction::new_client(key, request.clone(), self.endpoint.clone(), None);
446 tx.destination = opt.destination;
447
448 let id = DialogId::try_from(&request)?;
449 let mut dlg_inner = DialogInner::new(
450 TransactionRole::Client,
451 id.clone(),
452 request.clone(),
453 self.endpoint.clone(),
454 state_sender,
455 opt.credential,
456 Some(opt.contact),
457 tx.tu_sender.clone(),
458 )?;
459 dlg_inner.initial_destination = tx.destination.clone();
460
461 let dialog = ClientInviteDialog {
462 inner: Arc::new(dlg_inner),
463 };
464 Ok((dialog, tx))
465 }
466
467 pub fn confirm_client_dialog(&self, dialog: ClientInviteDialog) {
468 self.inner
469 .dialogs
470 .write()
471 .unwrap()
472 .insert(dialog.id(), Dialog::ClientInvite(dialog));
473 }
474}