From 66d5887e1a9c2e5d7eb0c3af803f64d68bce8edc Mon Sep 17 00:00:00 2001 From: Tyson George Date: Sun, 11 Oct 2026 17:37:08 -0400 Subject: [PATCH] fix(dialog): cancel() in Calling defers the CANCEL to the first provisional InviteDialog::cancel() returned Ok(()) and did nothing for a UAC whose INVITE had no response yet, and for any UAS dialog. For do_invite_async it is the only way to abandon a call, so the cancelled call could still be established. In Calling, cancel() now records the request and returns Ok(()). process_invite sends the CANCEL once the first provisional arrives (RFC 3261 section 9.1). After cancel(), a 2xx to the INVITE is ended with a BYE (the transaction ACKs it), the dialog reports Terminated(UacCancel) and is not registered as confirmed, the same outcome as a dropped do_invite future. A challenge to a cancelled INVITE is not answered with credentials. The legacy ClientInviteDialog::process_invite does the same. On a UAS dialog cancel() returns an error: CANCEL is a UAC request and an unanswered INVITE ends with reject(). hangup() on such a dialog keeps returning Ok(()). --- src/dialog/client_dialog.rs | 19 +- src/dialog/dialog.rs | 31 ++- src/dialog/invitation.rs | 36 ++- src/dialog/invite_dialog.rs | 94 ++++++- src/dialog/tests/test_cancel_2xx_race.rs | 260 +++++++++++++++++- .../tests/test_invite_auth_challenge.rs | 25 ++ src/dialog/tests/test_uas_ack_timeout.rs | 29 ++ 7 files changed, 459 insertions(+), 35 deletions(-) diff --git a/src/dialog/client_dialog.rs b/src/dialog/client_dialog.rs index dafa0b7b..f1a6abab 100644 --- a/src/dialog/client_dialog.rs +++ b/src/dialog/client_dialog.rs @@ -250,9 +250,12 @@ impl ClientInviteDialog { /// /// # Returns /// - /// * `Ok(())` - CANCEL was sent successfully + /// * `Ok(())` - CANCEL was sent successfully, or deferred until the + /// first provisional response when none has arrived yet (RFC 3261 §9.1) /// * `Err(Error)` - Failed to send CANCEL request /// + /// See [`InviteDialog::cancel`](super::invite_dialog::InviteDialog::cancel). + /// /// # Examples /// /// ```rust,no_run @@ -746,21 +749,27 @@ impl ClientInviteDialog { let peer_uuid = resp.session_id_header().and_then(|s| s.local_uuid()); self.inner.observe_peer_session_uuid(peer_uuid); + let unified = + super::invite_dialog::InviteDialog::from_inner(self.inner.clone()); if status == StatusCode::Trying { self.inner.transition(DialogState::Trying(self.id()))?; + unified.send_deferred_cancel(); continue; } if matches!(status.kind(), crate::sip::StatusCodeKind::Provisional) { + unified.send_deferred_cancel(); self.inner.handle_provisional_response(&resp).await?; self.inner.transition(DialogState::Early(self.id(), resp))?; + unified.send_deferred_cancel(); continue; } if matches!( status, StatusCode::ProxyAuthenticationRequired | StatusCode::Unauthorized - ) { + ) && !self.inner.cancel_requested.load(Ordering::SeqCst) + { if auth_sent { final_response = Some(resp.clone()); debug!(id = %self.id(), ?status, "received auth response after auth sent"); @@ -831,8 +840,10 @@ impl ClientInviteDialog { crate::Error::Error("missing Contact header".to_string()) })?; *self.inner.remote_uri.lock() = contact_uri; - self.inner - .transition(DialogState::Confirmed(dialog_id.clone(), resp))?; + let confirmed = DialogState::Confirmed(dialog_id.clone(), resp.clone()); + if self.inner.confirm_unless_cancelled(confirmed)? { + unified.bye_2xx_of_cancelled(resp); + } } _ => { self.inner.transition(DialogState::Terminated( diff --git a/src/dialog/dialog.rs b/src/dialog/dialog.rs index a4b6483e..35974adc 100644 --- a/src/dialog/dialog.rs +++ b/src/dialog/dialog.rs @@ -25,7 +25,7 @@ use crate::{ transport::{SipAddr, SipConnection}, Result, }; -use core::sync::atomic::{AtomicU32, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use futures::FutureExt; use tracing::{debug, info, warn}; @@ -340,6 +340,10 @@ pub struct DialogInner { /// connection is unavailable: immune to missing received/rport params and /// valid even after the socket object died. pub(super) dialback_target: Mutex>, + /// `cancel()` was called on this UAC dialog's INVITE. + pub(super) cancel_requested: AtomicBool, + /// That CANCEL waits for a provisional response (RFC 3261 §9.1). + pub(super) cancel_deferred: AtomicBool, } pub type DialogStateReceiver = UnboundedReceiver; @@ -501,6 +505,8 @@ impl DialogInner { remote_ack: Mutex::new(None), server_connection: Mutex::new(None), dialback_target: Mutex::new(None), + cancel_requested: AtomicBool::new(false), + cancel_deferred: AtomicBool::new(false), }) } /// Extract RFC 7989 Session-ID state from the initial request. @@ -1567,6 +1573,8 @@ impl DialogInner { remote_ack: Mutex::new(None), server_connection: Mutex::new(None), dialback_target: Mutex::new(None), + cancel_requested: AtomicBool::new(false), + cancel_deferred: AtomicBool::new(false), })) } fn build_restored_initial_request( @@ -1645,7 +1653,26 @@ impl DialogInner { // Decide under the state lock and notify only transitions that are // applied, while still holding it, so notifications follow the order // in which the state changed. - let mut old_state = self.state.lock(); + self.transition_locked(&mut self.state.lock(), state) + } + + /// The 2xx to the INVITE: `confirmed`, or `Terminated(UacCancel)` once + /// `cancel()` was called. Decided under the state lock `cancel()` takes, + /// so a racing `cancel()` is either seen here or sees `Confirmed`. + /// Returns whether the INVITE was cancelled. + pub(super) fn confirm_unless_cancelled(&self, confirmed: DialogState) -> Result { + let id = self.id.lock().clone(); + let mut state = self.state.lock(); + if self.cancel_requested.load(Ordering::SeqCst) { + let cancelled = DialogState::Terminated(id, TerminatedReason::UacCancel); + self.transition_locked(&mut state, cancelled)?; + return Ok(true); + } + self.transition_locked(&mut state, confirmed)?; + Ok(false) + } + + fn transition_locked(&self, old_state: &mut DialogState, state: DialogState) -> Result<()> { // Late updates after termination (e.g. CANCEL's 200 arriving after // the INVITE's 487, or a second Terminated from a racing teardown) // are neither applied nor broadcast: observers already saw Terminated. diff --git a/src/dialog/invitation.rs b/src/dialog/invitation.rs index cc99dc92..944adeaa 100644 --- a/src/dialog/invitation.rs +++ b/src/dialog/invitation.rs @@ -359,7 +359,7 @@ async fn wait_response( } /// End the session a 2xx to an abandoned INVITE established. -async fn bye_if_2xx(client_dialog: &InviteDialog, resp: Option) { +pub(super) async fn bye_if_2xx(client_dialog: &InviteDialog, resp: Option) { if let Some(resp) = resp.filter(|r| r.status_code.kind() == StatusCodeKind::Successful) { info!(id = %client_dialog.id(), "2xx after CANCEL, sending BYE"); if let Err(e) = client_dialog.bye_2xx_after_cancel(&resp).await { @@ -661,7 +661,8 @@ impl DialogLayer { /// # Important: Cleanup Required /// /// When a 2xx response is received, the confirmed dialog is registered in - /// the internal dialog registry. **The caller is responsible for removing it** + /// the internal dialog registry (not when [`InviteDialog::cancel`] was + /// called: that 2xx is ended with a BYE). **The caller is responsible for removing it** /// after the dialog terminates by calling [`DialogLayer::remove_dialog`] /// with the dialog's [`DialogId`]. /// @@ -712,13 +713,16 @@ impl DialogLayer { Some(ref r) if r.status_code.kind() == crate::sip::StatusCodeKind::Successful => { - debug!( - "client invite dialog confirmed: {} => {}", - id, new_dialog_id - ); - self.inner - .dialogs - .insert(new_dialog_id.to_string(), Dialog::Invite(dialog.clone())); + // A cancelled INVITE's 2xx was BYE'd: nothing to register. + if !dialog.inner.is_terminated() { + debug!( + "client invite dialog confirmed: {} => {}", + id, new_dialog_id + ); + self.inner + .dialogs + .insert(new_dialog_id.to_string(), Dialog::Invite(dialog.clone())); + } // RFC 6026 §7.2: the client transaction is in Accepted // with Timer M (64*T1) armed. Keep receiving until it @@ -757,7 +761,8 @@ impl DialogLayer { /// # Important: Cleanup Required /// /// On a successful (2xx) response the confirmed dialog is registered in - /// the internal dialog registry. **The caller must remove it** after the + /// the internal dialog registry (not when [`InviteDialog::cancel`] was + /// called: that 2xx is ended with a BYE). **The caller must remove it** after the /// dialog terminates by calling [`DialogLayer::remove_dialog`] with the /// confirmed dialog's [`DialogId`]. /// @@ -799,10 +804,13 @@ impl DialogLayer { .unwrap_or(false); if is_2xx { - debug!("client invite dialog confirmed: {} => {}", id0, new_id); - inner - .dialogs - .insert(new_id.to_string(), Dialog::Invite(dialog_clone.clone())); + // A cancelled INVITE's 2xx was BYE'd: nothing to register. + if !dialog_clone.inner.is_terminated() { + debug!("client invite dialog confirmed: {} => {}", id0, new_id); + inner + .dialogs + .insert(new_id.to_string(), Dialog::Invite(dialog_clone.clone())); + } // RFC 6026 §7.2: keep draining the Accepted transaction // until Timer M ends it (re-ACKs retransmitted 2xx, diff --git a/src/dialog/invite_dialog.rs b/src/dialog/invite_dialog.rs index cf771341..c5c8fee0 100644 --- a/src/dialog/invite_dialog.rs +++ b/src/dialog/invite_dialog.rs @@ -28,7 +28,8 @@ use tracing::{debug, info, trace, warn}; /// /// Role-specific methods (e.g. `ringing`/`accept`/`reject` for UAS, or /// `cancel`/`hangup`/`options`/`process_invite` for UAC) are no-ops when -/// invoked on the "wrong" role. +/// invoked on the "wrong" role, except `cancel` and `process_invite`, which +/// return an error for a UAS. #[derive(Clone)] pub struct InviteDialog { pub(super) inner: DialogInnerRef, @@ -184,10 +185,14 @@ impl InviteDialog { } /// Terminate the dialog: send BYE when confirmed, CANCEL when still early. - /// Role-agnostic — `cancel()` internally guards against non-UAC dialogs, - /// and `bye_with_headers()` handles the confirmed UAS/UAC case uniformly. + /// Role-agnostic: an unanswered UAS dialog is left alone (it ends with + /// `reject()`), and `bye_with_headers()` handles the confirmed UAS/UAC + /// case uniformly. pub async fn hangup_with_headers(&self, headers: Option>) -> Result<()> { if self.inner.can_cancel() { + if self.role() != TransactionRole::Client { + return Ok(()); + } self.cancel().await } else { self.bye_with_headers(headers).await @@ -202,19 +207,73 @@ impl InviteDialog { } /// Send a CANCEL request to abort an unanswered INVITE. - /// No-op for UAS dialogs. + /// + /// * `Calling` (no response yet): returns `Ok(())` at once. The CANCEL + /// is sent when the first provisional response arrives (RFC 3261 + /// §9.1); `process_invite` (`do_invite`, `do_invite_async`) sends it. + /// * `Trying` / `Early`: sends the CANCEL and returns its outcome. + /// + /// In both cases a 2xx to the INVITE is ACKed and then ended with a BYE + /// (with `auto_ack_2xx` off the ACK stays the caller's), the dialog + /// reports `Terminated(UacCancel)`, never `Confirmed`, and is not + /// registered in the dialog layer. A challenge (401/407) is final. + /// Once a final response arrived there is nothing to cancel: `Ok(())`. + /// A UAS cannot send CANCEL: an error; end the INVITE with `reject()`. pub async fn cancel(&self) -> Result<()> { - if self.role() != TransactionRole::Client - || !matches!( - self.state(), - DialogState::Trying(_) | DialogState::Early(_, _) - ) + if self.role() != TransactionRole::Client { + return Err(crate::Error::DialogError( + "cancel is a UAC operation; use reject()".to_string(), + self.id(), + StatusCode::ServerInternalError, + )); + } { - return Ok(()); + // Decided under the state lock, so `process_invite` either sees + // the deferred CANCEL after leaving Calling or we see it left. + let state = self.inner.state.lock(); + match *state { + DialogState::Calling(_) => { + self.inner.cancel_requested.store(true, Ordering::SeqCst); + self.inner.cancel_deferred.store(true, Ordering::SeqCst); + return Ok(()); + } + DialogState::Trying(_) | DialogState::Early(_, _) => { + self.inner.cancel_requested.store(true, Ordering::SeqCst); + } + _ => return Ok(()), + } } self.send_cancel().await } + /// Send the CANCEL `cancel()` deferred in Calling, once the INVITE got a + /// provisional response. Skipped when a final response was processed + /// before the task ran. + pub(super) fn send_deferred_cancel(&self) { + if !self.inner.cancel_deferred.swap(false, Ordering::SeqCst) { + return; + } + let dialog = self.clone(); + let _handle = crate::platform::spawn(async move { + if !dialog.inner.can_cancel() { + return; + } + if let Err(e) = dialog.send_cancel().await { + warn!(id = %dialog.id(), error = %e, "deferred cancel failed"); + } + }); + } + + /// End the session a 2xx to a cancelled INVITE established: the CANCEL + /// had no effect on it (RFC 3261 §9.1, §15). The transaction ACKs the + /// 2xx (unless `auto_ack_2xx` is off), then a BYE follows. + pub(super) fn bye_2xx_of_cancelled(&self, resp: Response) { + let dialog = self.clone(); + let _handle = crate::platform::spawn(async move { + super::invitation::bye_if_2xx(&dialog, Some(resp)).await; + }); + } + /// Send the CANCEL for the initial INVITE, whatever the dialog state. pub(super) async fn send_cancel(&self) -> Result<()> { debug!(id = %self.id(), "sending cancel request"); @@ -285,19 +344,26 @@ impl InviteDialog { if status == StatusCode::Trying { self.inner.transition(DialogState::Trying(self.id()))?; + self.send_deferred_cancel(); continue; } if matches!(status.kind(), crate::sip::StatusCodeKind::Provisional) { + // Not held back by a PRACK (RFC 3262); again after + // the transition for a cancel() racing this one. + self.send_deferred_cancel(); self.inner.handle_provisional_response(&resp).await?; self.inner.transition(DialogState::Early(self.id(), resp))?; + self.send_deferred_cancel(); continue; } + // A challenge to a cancelled INVITE is its final response. if matches!( status, StatusCode::ProxyAuthenticationRequired | StatusCode::Unauthorized - ) { + ) && !self.inner.cancel_requested.load(Ordering::SeqCst) + { if auth_sent { final_response = Some(resp.clone()); debug!(id = %self.id(), ?status, "received auth response after auth sent"); @@ -356,8 +422,10 @@ impl InviteDialog { } StatusCode::OK => { self.update_remote_target_from_2xx(&resp)?; - self.inner - .transition(DialogState::Confirmed(dialog_id.clone(), resp))?; + let confirmed = DialogState::Confirmed(dialog_id.clone(), resp.clone()); + if self.inner.confirm_unless_cancelled(confirmed)? { + self.bye_2xx_of_cancelled(resp); + } } _ => { self.inner.transition(DialogState::Terminated( diff --git a/src/dialog/tests/test_cancel_2xx_race.rs b/src/dialog/tests/test_cancel_2xx_race.rs index 946a01cc..b57f5030 100644 --- a/src/dialog/tests/test_cancel_2xx_race.rs +++ b/src/dialog/tests/test_cancel_2xx_race.rs @@ -10,12 +10,14 @@ //! has to end (CANCEL, and ACK + BYE for a 2xx) even though the layer entry is //! already gone. use crate::dialog::{ + client_dialog::ClientInviteDialog, dialog::{DialogState, DialogStateReceiver, TerminatedReason}, dialog_layer::DialogLayer, invitation::InviteOption, DialogId, }; -use crate::sip::{prelude::HeadersExt, Method, Request, SipMessage, Uri}; +use crate::sip::{prelude::HeadersExt, Method, Request, SipMessage, StatusCode, Uri}; +use crate::transaction::endpoint::EndpointOption; use crate::transport::{udp::UdpConnection, TransportLayer}; use crate::EndpointBuilder; use std::net::SocketAddr; @@ -48,13 +50,18 @@ async fn recv_request(socket: &UdpSocket, method: Method, wait: Duration) -> (Re /// Fail if the peer receives a BYE within `wait`. async fn assert_no_bye(socket: &UdpSocket, wait: Duration, what: &str) { + assert_no_request(socket, Method::Bye, wait, what).await +} + +/// Fail if the peer receives a `method` request within `wait`. +async fn assert_no_request(socket: &UdpSocket, method: Method, wait: Duration, what: &str) { let mut buf = vec![0u8; 4096]; let deadline = tokio::time::Instant::now() + wait; while let Ok(Ok((len, _))) = tokio::time::timeout_at(deadline, socket.recv_from(&mut buf)).await { let text = std::str::from_utf8(&buf[..len]).unwrap(); if let Ok(SipMessage::Request(req)) = SipMessage::try_from(text) { - assert_ne!(req.method, Method::Bye, "{what}"); + assert_ne!(req.method, method, "{what}"); } } } @@ -98,6 +105,10 @@ struct Uac { } async fn setup(token: &CancellationToken) -> crate::Result { + setup_with(token, EndpointOption::default()).await +} + +async fn setup_with(token: &CancellationToken, option: EndpointOption) -> crate::Result { let peer = UdpSocket::bind("127.0.0.1:0").await?; let peer_port = peer.local_addr()?.port(); @@ -114,6 +125,7 @@ async fn setup(token: &CancellationToken) -> crate::Result { .with_user_agent("rsipstack-test") .with_transport_layer(transport_layer) .with_cancel_token(token.child_token()) + .with_option(option) .build(); let endpoint_inner = endpoint.inner.clone(); tokio::spawn(async move { @@ -543,3 +555,247 @@ async fn test_dropped_before_provisional_final_failure_is_acked_only() -> crate: async fn test_removed_before_provisional_final_failure_is_acked_only() -> crate::Result<()> { run_dropped_before_provisional(486, true).await } + +/// `cancel()` on a `do_invite_async` call, in Calling (no response yet) or +/// in Early (after the 180). In Calling the CANCEL is deferred to the first +/// provisional (RFC 3261 §9.1). `first` is the peer's first response (100, +/// 180, 200 or 486); after a provisional the peer answers the CANCEL and +/// ends the INVITE with `last` (487, or a 200 that crosses the CANCEL). +async fn run_cancel_called(in_calling: bool, first: u16, last: u16) -> crate::Result<()> { + run_cancel_called_via(false, in_calling, first, last).await +} + +/// The same, with `legacy` driving the INVITE through the legacy +/// `ClientInviteDialog::process_invite` instead of `do_invite_async`. +async fn run_cancel_called_via( + legacy: bool, + in_calling: bool, + first: u16, + last: u16, +) -> crate::Result<()> { + let token = CancellationToken::new(); + let Uac { + dialog_layer, + option, + peer, + } = setup(&token).await?; + let wait = Duration::from_secs(2); + + let (state_sender, mut states) = unbounded_channel(); + let (dialog, join) = if legacy { + let (dialog, mut tx) = dialog_layer.create_client_invite_dialog(option, state_sender)?; + let legacy = ClientInviteDialog { + inner: dialog.inner.clone(), + }; + let join = tokio::spawn(async move { legacy.process_invite(&mut tx).await }); + (dialog, join) + } else { + dialog_layer.do_invite_async(option, state_sender)? + }; + let (inv, uac) = recv_request(&peer, Method::Invite, wait).await; + calling_id(&mut states).await; + if in_calling { + dialog.cancel().await?; + assert_no_request( + &peer, + Method::Cancel, + Duration::from_millis(1100), + "no CANCEL before a provisional", + ) + .await; + } + let provisional = matches!(first, 100 | 180); + reply(&peer, uac, &inv, first, "Response").await; + if !in_calling { + wait_for_state(&mut states, "Early", wait, |s| { + matches!(s, DialogState::Early(_, _)) + }) + .await; + // In Early cancel() waits for the CANCEL's response. + let dialog = dialog.clone(); + tokio::spawn(async move { dialog.cancel().await }); + } + if provisional { + let (cancel, _) = recv_request(&peer, Method::Cancel, wait).await; + assert_eq!(cancel.cseq_header()?.seq()?, inv.cseq_header()?.seq()?); + if last == 200 { + // The callee answered while the CANCEL was in flight. + reply(&peer, uac, &inv, 200, "OK").await; + reply(&peer, uac, &cancel, 200, "OK").await; + } else { + reply(&peer, uac, &cancel, 200, "OK").await; + reply(&peer, uac, &inv, last, "Request Terminated").await; + } + } + let answered = if provisional { last } else { first }; + let (ack, _) = recv_request(&peer, Method::Ack, wait).await; + assert_in_dialog(&ack, &inv, "ACK"); + if answered == 200 { + let (bye, _) = recv_request(&peer, Method::Bye, wait).await; + assert_in_dialog(&bye, &inv, "BYE"); + reply(&peer, uac, &bye, 200, "OK").await; + } else { + assert_no_bye(&peer, Duration::from_millis(500), "an unanswered call").await; + } + // Nothing left to cancel after a final response. + assert_no_request( + &peer, + Method::Cancel, + Duration::from_millis(300), + "a late CANCEL", + ) + .await; + + let (_, resp) = tokio::time::timeout(wait, join) + .await + .expect("join") + .expect("invite task")?; + assert_eq!(resp.map(|r| u16::from(r.status_code)), Some(answered)); + tokio::time::sleep(Duration::from_millis(200)).await; + let seen: Vec<_> = std::iter::from_fn(|| states.try_recv().ok()).collect(); + assert!( + !seen.iter().any(|s| matches!(s, DialogState::Confirmed(..))), + "a cancelled call must not report Confirmed, got {seen:?}" + ); + let terminations: Vec<_> = seen + .iter() + .filter(|s| matches!(s, DialogState::Terminated(..))) + .collect(); + assert_eq!(terminations.len(), 1, "one Terminated, got {seen:?}"); + if answered == 200 { + assert!(matches!( + terminations[0], + DialogState::Terminated(_, TerminatedReason::UacCancel) + )); + } + assert!(dialog.state().is_terminated()); + assert!( + dialog_layer.is_empty(), + "a cancelled call is not registered" + ); + token.cancel(); + Ok(()) +} + +#[tokio::test] +async fn test_cancel_in_calling_is_sent_after_the_180() -> crate::Result<()> { + run_cancel_called(true, 180, 487).await +} + +#[tokio::test] +async fn test_cancel_in_calling_is_sent_after_the_100() -> crate::Result<()> { + run_cancel_called(true, 100, 487).await +} + +#[tokio::test] +async fn test_cancel_in_calling_2xx_crossing_the_cancel_is_acked_and_byed() -> crate::Result<()> { + run_cancel_called(true, 180, 200).await +} + +#[tokio::test] +async fn test_cancel_in_calling_2xx_without_provisional_is_acked_and_byed() -> crate::Result<()> { + run_cancel_called(true, 200, 200).await +} + +#[tokio::test] +async fn test_cancel_in_calling_final_failure_is_acked_only() -> crate::Result<()> { + run_cancel_called(true, 486, 486).await +} + +/// The same `cancel()` in Early: a 2xx that crosses the CANCEL is ACKed and +/// BYE'd, as for a dropped `do_invite` future. +#[tokio::test] +async fn test_cancel_in_early_2xx_crossing_the_cancel_is_acked_and_byed() -> crate::Result<()> { + run_cancel_called(false, 180, 200).await?; + run_cancel_called(false, 180, 487).await +} + +/// The legacy `ClientInviteDialog::process_invite` sends the deferred +/// CANCEL and BYEs the 2xx alike. +#[tokio::test] +async fn test_cancel_in_calling_with_the_legacy_client_dialog() -> crate::Result<()> { + run_cancel_called_via(true, true, 180, 487).await?; + run_cancel_called_via(true, true, 200, 200).await +} + +/// A reliable 183 (RFC 3262) after `cancel()` in Calling: the CANCEL is not +/// held back until the PRACK, which the peer never answers, completes. +#[tokio::test] +async fn test_cancel_in_calling_is_not_held_back_by_the_prack() -> crate::Result<()> { + let token = CancellationToken::new(); + let Uac { + dialog_layer, + mut option, + peer, + } = setup(&token).await?; + option.support_prack = true; + let (state_sender, mut states) = unbounded_channel(); + let (dialog, _join) = dialog_layer.do_invite_async(option, state_sender)?; + let (inv, uac) = recv_request(&peer, Method::Invite, Duration::from_secs(2)).await; + calling_id(&mut states).await; + dialog.cancel().await?; + let contact = format!("sip:bob@{}", peer.local_addr()?); + let reliable = response(&inv, 183, "Session Progress", &contact).replace( + "Content-Length: 0", + "Require: 100rel\r\nRSeq: 1\r\nContent-Length: 0", + ); + peer.send_to(reliable.as_bytes(), uac).await?; + // The PRACK goes out and is never answered; the CANCEL arrives well + // before the PRACK's own timeout (64*T1). + let mut buf = vec![0u8; 4096]; + let (mut prack, mut cancel) = (false, false); + while !(prack && cancel) { + let (len, _) = tokio::time::timeout(Duration::from_secs(1), peer.recv_from(&mut buf)) + .await + .unwrap_or_else(|_| panic!("PRACK seen: {prack}, CANCEL seen: {cancel}"))?; + if let Ok(SipMessage::Request(req)) = SipMessage::try_from(&buf[..len]) { + prack |= req.method == Method::PRack; + cancel |= req.method == Method::Cancel + && req.cseq_header()?.seq()? == inv.cseq_header()?.seq()?; + } + } + token.cancel(); + Ok(()) +} + +/// `cancel()` in Calling and no response at all: nothing but INVITE +/// retransmissions is sent, and Timer B ends the INVITE (408). +#[tokio::test] +async fn test_cancel_in_calling_without_response_times_out() -> crate::Result<()> { + let token = CancellationToken::new(); + let t1 = Duration::from_millis(20); + let option = EndpointOption { + t1, + t1x64: t1 * 64, + ..Default::default() + }; + let Uac { + dialog_layer, + option, + peer, + } = setup_with(&token, option).await?; + let (state_sender, mut states) = unbounded_channel(); + let (dialog, join) = dialog_layer.do_invite_async(option, state_sender)?; + recv_request(&peer, Method::Invite, Duration::from_secs(2)).await; + calling_id(&mut states).await; + dialog.cancel().await?; + assert_no_request( + &peer, + Method::Cancel, + t1 * 64 + Duration::from_millis(500), + "no CANCEL without a provisional", + ) + .await; + let (_, resp) = tokio::time::timeout(Duration::from_secs(2), join) + .await + .expect("join") + .expect("invite task")?; + assert_eq!( + resp.map(|r| r.status_code), + Some(StatusCode::RequestTimeout) + ); + assert!(dialog.state().is_terminated()); + assert!(dialog_layer.is_empty()); + token.cancel(); + Ok(()) +} diff --git a/src/dialog/tests/test_invite_auth_challenge.rs b/src/dialog/tests/test_invite_auth_challenge.rs index 161864a5..e3cd7ada 100644 --- a/src/dialog/tests/test_invite_auth_challenge.rs +++ b/src/dialog/tests/test_invite_auth_challenge.rs @@ -282,3 +282,28 @@ async fn test_invite_challenge_after_authenticated_retry_is_final_response() -> p.token.cancel(); Ok(()) } + +/// `cancel()` before the challenge: a cancelled INVITE is not resent with +/// credentials, the challenge is its final response. +#[tokio::test] +async fn test_cancelled_invite_is_not_retried_with_credential() -> crate::Result<()> { + let (p, uas_incoming) = peers().await?; + let mut seen = spawn_uas(uas_incoming, StatusCode::Unauthorized, p.uas_port, true); + let (state_tx, _state_rx) = unbounded_channel(); + let opt = invite_option(&p, Some(credential())); + let layer = std::sync::Arc::new(p.uac_layer); + let (dialog, join) = layer.do_invite_async(opt, state_tx)?; + dialog.cancel().await?; + + let (_, resp) = tokio::time::timeout(PROMPT, join) + .await + .expect("cancelled INVITE did not complete") + .expect("invite task")?; + assert_eq!(resp.map(|r| r.status_code), Some(StatusCode::Unauthorized)); + assert!(dialog.state().is_terminated()); + assert_eq!(seen.recv().await, Some(false)); + assert_no_more_invites(&mut seen).await; + + p.token.cancel(); + Ok(()) +} diff --git a/src/dialog/tests/test_uas_ack_timeout.rs b/src/dialog/tests/test_uas_ack_timeout.rs index e6f73224..ba98c7f6 100644 --- a/src/dialog/tests/test_uas_ack_timeout.rs +++ b/src/dialog/tests/test_uas_ack_timeout.rs @@ -1073,3 +1073,32 @@ async fn test_unacked_reinvite_2xx_ends_the_session_on_a_legacy_client_dialog() async fn test_acked_reinvite_keeps_a_legacy_client_dialog() -> crate::Result<()> { legacy_client_dialog_reinvite(true).await } + +/// CANCEL is a UAC request (RFC 3261 §9): `cancel()` on a server dialog +/// fails and sends nothing; an unanswered INVITE is ended with `reject()`. +#[tokio::test] +async fn test_cancel_on_a_server_dialog_fails() -> crate::Result<()> { + let token = CancellationToken::new(); + let (mut states, mut dialogs, peer) = setup(&token, short_timers()).await?; + + peer.send_request(Method::Invite, 1, None).await; + let dialog = tokio::time::timeout(Duration::from_secs(2), dialogs.recv()) + .await + .expect("timeout waiting for the server dialog") + .unwrap(); + let error = dialog.cancel().await.expect_err("cancel() on a UAS"); + assert!(error.to_string().contains("reject()"), "{error}"); + let sent = peer.collect(Instant::now() + T1 * 4).await; + assert!( + sent.iter() + .all(|(_, m)| matches!(m, SipMessage::Response(r) if r.status_code.code() == 100)), + "nothing but 100 Trying, got {sent:?}" + ); + assert!(!dialog.state().is_terminated()); + // hangup() on the unanswered server dialog keeps its behavior. + dialog.hangup().await?; + assert!(!dialog.state().is_terminated()); + assert!(!std::iter::from_fn(|| states.try_recv().ok()).any(|s| s.is_terminated())); + token.cancel(); + Ok(()) +}