Skip to content
Closed
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
54 changes: 50 additions & 4 deletions src/dialog/invitation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,15 @@ impl<'a> Drop for DialogGuardForUnconfirmed<'a> {

debug!(%self.id, "unconfirmed dialog dropped, cancelling it");
let _handle = crate::platform::spawn(async move {
let started = crate::platform::Instant::now();
let final_window = invite_tx.endpoint_inner.option.t1x64;
let mut timeout = core::pin::pin!(crate::platform::sleep(core::time::Duration::from_secs(2)));
invite_tx.stop_retransmissions();

let mut cancel_done = false;
let mut cancel = core::pin::pin!(client_dialog.cancel());
let mut final_response = None;
// Boxed so it can be dropped before the wait below.
let mut cancel = Box::pin(client_dialog.cancel());

use crate::platform::select::Which3;
loop {
Expand Down Expand Up @@ -265,6 +269,7 @@ impl<'a> Drop for DialogGuardForUnconfirmed<'a> {
status = %resp.status_code,
"received final response"
);
final_response = Some(resp);
break;
}
Some(_) => {}
Expand All @@ -273,14 +278,37 @@ impl<'a> Drop for DialogGuardForUnconfirmed<'a> {
}
}

// `cancel` (pinned above) ends its borrow here; closing the
// command channel terminates the dialog loop.
drop(invite_tx);
drop(cancel);
let _ = client_dialog.inner.transition(DialogState::Terminated(
client_dialog.id(),
TerminatedReason::UacCancel,
));
debug!(id = %client_dialog.id(), "dialog terminated");

// The callee may still answer after the CANCEL: keep the
// INVITE transaction until its final response (up to
// 64*T1) so a 2xx is ACKed, then end that session with a
// BYE (RFC 3261 §9.1, §15).
if final_response.is_none() {
let elapsed = crate::platform::Instant::now()
.checked_duration_since(started)
.unwrap_or_default();
final_response = wait_final_response(
&mut invite_tx,
final_window.saturating_sub(elapsed),
)
.await;
}
// Closing the command channel terminates the dialog loop.
drop(invite_tx);
if let Some(resp) = final_response {
if resp.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 {
warn!(id = %client_dialog.id(), error = %e, "BYE after CANCEL failed");
}
}
}
});
}
DialogState::Confirmed(_, _) => {
Expand All @@ -295,6 +323,24 @@ impl<'a> Drop for DialogGuardForUnconfirmed<'a> {
}
}

/// Wait up to `limit` for the INVITE transaction's final response.
async fn wait_final_response(
invite_tx: &mut Transaction,
limit: core::time::Duration,
) -> Option<Response> {
let wait = async {
while let Some(msg) = invite_tx.receive().await {
if let SipMessage::Response(resp) = msg {
if resp.status_code.kind() != StatusCodeKind::Provisional {
return Some(resp);
}
}
}
None
};
crate::platform::timeout(limit, wait).await.ok().flatten()
}

pub type InviteAsyncResult = Result<(DialogId, Option<Response>)>;

impl DialogLayer {
Expand Down
45 changes: 33 additions & 12 deletions src/dialog/invite_dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,18 +331,7 @@ impl InviteDialog {
self.inner.update_route_set_from_response(&resp);
}
StatusCode::OK => {
self.inner.update_route_set_from_response(&resp);
let contact = resp.contact_header()?;
self.inner.remote_contact.lock().replace(contact.clone());

let contact_uri = resp
.typed_contact_headers()?
.first()
.map(|c| c.uri.clone())
.ok_or_else(|| {
crate::Error::Error("missing Contact header".to_string())
})?;
*self.inner.remote_uri.lock() = contact_uri;
self.update_remote_target_from_2xx(&resp)?;
self.inner
.transition(DialogState::Confirmed(dialog_id.clone(), resp))?;
}
Expand All @@ -360,6 +349,38 @@ impl InviteDialog {
Ok((dialog_id, final_response))
}

/// Take the route set, Contact and remote target from a 2xx to the INVITE.
fn update_remote_target_from_2xx(&self, resp: &Response) -> Result<()> {
self.inner.update_route_set_from_response(resp);
let contact = resp.contact_header()?;
self.inner.remote_contact.lock().replace(contact.clone());

let contact_uri = resp
.typed_contact_headers()?
.first()
.map(|c| c.uri.clone())
.ok_or_else(|| crate::Error::Error("missing Contact header".to_string()))?;
*self.inner.remote_uri.lock() = contact_uri;
Ok(())
}

/// End the session a 2xx established after we cancelled the INVITE.
///
/// A CANCEL that crosses a 2xx has no effect on the INVITE (RFC 3261
/// §9.1, §15), so the UAC has to send a BYE once the 2xx is ACKed. The
/// dialog was already abandoned, so no `Confirmed` state is reported.
pub(super) async fn bye_2xx_after_cancel(&self, resp: &Response) -> Result<()> {
if let Some(tag) = resp.to_header()?.tag()? {
self.inner.update_remote_tag(tag.value())?;
}
self.update_remote_target_from_2xx(resp)?;
let request = self
.inner
.make_request(Method::Bye, None, None, None, None, None)?;
self.inner.do_request(request).await?;
Ok(())
}

// ── Shared request semantics ──────────────────────────────────────────

/// Send a BYE request to terminate the dialog.
Expand Down
1 change: 1 addition & 0 deletions src/dialog/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod test_authenticate;
mod test_cancel_2xx_race;
mod test_client_dialog;
mod test_connection_affinity;
mod test_dialog_layer;
Expand Down
Loading