Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions src/dialog/client_dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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(
Expand Down
31 changes: 29 additions & 2 deletions src/dialog/dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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<Option<SipAddr>>,
/// `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<DialogState>;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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<bool> {
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.
Expand Down
36 changes: 22 additions & 14 deletions src/dialog/invitation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response>) {
pub(super) async fn bye_if_2xx(client_dialog: &InviteDialog, resp: Option<Response>) {
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 {
Expand Down Expand Up @@ -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`].
///
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`].
///
Expand Down Expand Up @@ -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,
Expand Down
94 changes: 81 additions & 13 deletions src/dialog/invite_dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Vec<Header>>) -> Result<()> {
if self.inner.can_cancel() {
if self.role() != TransactionRole::Client {
return Ok(());
}
self.cancel().await
} else {
self.bye_with_headers(headers).await
Expand All @@ -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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading