use crate::api::types::DialogIdentity;
use crate::cleanup_diag::{self, CleanupStage};
use crate::errors::{Result, SessionError};
use crate::session_store::SessionStore;
use crate::state_table::types::{DialogId, SessionId};
use dashmap::DashMap;
use rvoip_infra_common::events::{
coordinator::GlobalEventCoordinator,
cross_crate::{RvoipCrossCrateEvent, SessionToDialogEvent},
};
use rvoip_sip_core::{Response, StatusCode, Uri};
use rvoip_sip_dialog::{
api::unified::{
ByeRequestOptions, CancelRequestOptions, InfoRequestOptions, MessageRequestOptions,
NotifyRequestOptions, ReferRequestOptions, SubscribeRequestOptions, UnifiedDialogApi,
UpdateRequestOptions,
},
transaction::TransactionKey,
DialogId as RvoipDialogId,
};
use std::str::FromStr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Default)]
pub(crate) struct RegistrationResponseMetadata {
pub(crate) service_route: Option<Vec<String>>,
pub(crate) pub_gruu: Option<String>,
pub(crate) temp_gruu: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) enum RegisterAttemptOutcome {
Registered {
accepted_expires: u32,
metadata: RegistrationResponseMetadata,
},
Unregistered,
AuthChallenge {
status_code: u16,
challenge: String,
},
IntervalTooBrief {
min_expires: u32,
},
Failure {
status_code: u16,
reason: String,
},
}
pub struct DialogAdapter {
pub(crate) dialog_api: Arc<UnifiedDialogApi>,
pub(crate) store: Arc<SessionStore>,
pub(crate) session_to_dialog: Arc<DashMap<SessionId, RvoipDialogId>>,
pub(crate) dialog_to_session: Arc<DashMap<RvoipDialogId, SessionId>>,
pub(crate) callid_to_session: Arc<DashMap<String, SessionId>>,
pub(crate) outgoing_invite_tx: Arc<DashMap<SessionId, TransactionKey>>,
pub(crate) auto_emit_extra_headers: Vec<rvoip_sip_core::types::TypedHeader>,
pub(crate) global_coordinator: Arc<GlobalEventCoordinator>,
pub(crate) state_machine: Arc<std::sync::OnceLock<Arc<crate::state_machine::StateMachine>>>,
pub(crate) outbound_proxy_uri: Option<rvoip_sip_core::types::uri::Uri>,
pub(crate) outbound_contact_params:
Option<rvoip_sip_core::types::outbound::OutboundContactParams>,
pub(crate) symmetric_flow_params:
Option<rvoip_sip_core::types::outbound::OutboundContactParams>,
registration_auto_refresh: bool,
registration_refresh_jitter_percent: u8,
registration_refresh_tasks: Arc<DashMap<SessionId, tokio::task::AbortHandle>>,
cleanup_attempt_total: Arc<AtomicU64>,
cleanup_mapped_total: Arc<AtomicU64>,
cleanup_missing_total: Arc<AtomicU64>,
cleanup_call_ids_removed_total: Arc<AtomicU64>,
cleanup_outgoing_invite_removed_total: Arc<AtomicU64>,
pub(crate) trace_redactor: Option<Arc<dyn crate::api::trace_redactor::TraceRedactor>>,
}
impl DialogAdapter {
pub fn new(
dialog_api: Arc<UnifiedDialogApi>,
store: Arc<SessionStore>,
global_coordinator: Arc<GlobalEventCoordinator>,
outbound_proxy_uri: Option<rvoip_sip_core::types::uri::Uri>,
outbound_contact_params: Option<rvoip_sip_core::types::outbound::OutboundContactParams>,
symmetric_flow_params: Option<rvoip_sip_core::types::outbound::OutboundContactParams>,
registration_auto_refresh: bool,
registration_refresh_jitter_percent: u8,
auto_emit_extra_headers: Vec<rvoip_sip_core::types::TypedHeader>,
trace_redactor: Option<Arc<dyn crate::api::trace_redactor::TraceRedactor>>,
) -> Self {
Self {
dialog_api,
store,
session_to_dialog: Arc::new(DashMap::new()),
dialog_to_session: Arc::new(DashMap::new()),
callid_to_session: Arc::new(DashMap::new()),
outgoing_invite_tx: Arc::new(DashMap::new()),
auto_emit_extra_headers,
global_coordinator,
state_machine: Arc::new(std::sync::OnceLock::new()),
outbound_proxy_uri,
outbound_contact_params,
symmetric_flow_params,
registration_auto_refresh,
registration_refresh_jitter_percent,
registration_refresh_tasks: Arc::new(DashMap::new()),
cleanup_attempt_total: Arc::new(AtomicU64::new(0)),
cleanup_mapped_total: Arc::new(AtomicU64::new(0)),
cleanup_missing_total: Arc::new(AtomicU64::new(0)),
cleanup_call_ids_removed_total: Arc::new(AtomicU64::new(0)),
cleanup_outgoing_invite_removed_total: Arc::new(AtomicU64::new(0)),
trace_redactor,
}
}
pub fn init_state_machine(
&self,
state_machine: Arc<crate::state_machine::StateMachine>,
) -> std::result::Result<(), Arc<crate::state_machine::StateMachine>> {
self.state_machine.set(state_machine)
}
fn publish_api_event(&self, api_event: crate::api::events::Event) {
let wrapped = crate::adapters::SessionApiCrossCrateEvent::new(api_event);
let coordinator = self.global_coordinator.clone();
tokio::spawn(async move {
if let Err(e) = coordinator.publish(wrapped).await {
tracing::warn!("Failed to publish app-level dialog adapter event: {}", e);
}
});
}
pub(crate) fn outbound_transport_context_for_uri(
&self,
request_uri: &str,
) -> crate::auth::SipTransportSecurityContext {
let Ok(uri) = Uri::from_str(request_uri) else {
return crate::auth::SipTransportSecurityContext::from_request_uri_transport_hint(
request_uri,
);
};
let transaction_manager = self
.dialog_api
.dialog_manager()
.core()
.transaction_manager();
let transport = transaction_manager.get_best_transport_for_uri(&uri);
let mut context =
crate::auth::SipTransportSecurityContext::from_transport_name(transport.to_string());
if let Some(info) = transaction_manager.get_transport_info(transport) {
context.local_addr = info.local_addr.map(|addr| addr.to_string());
}
context
}
pub(crate) fn outbound_transport_context_for_response(
&self,
response: &Response,
fallback_request_uri: &str,
) -> crate::auth::SipTransportSecurityContext {
self.dialog_api
.outbound_transport_context_for_response(response)
.map(|context| {
crate::auth::SipTransportSecurityContext::from_transport_context(&context)
})
.unwrap_or_else(|| self.outbound_transport_context_for_uri(fallback_request_uri))
}
pub(crate) fn abort_registration_refresh(&self, session_id: &SessionId) {
let guard = cleanup_diag::stage_guard(CleanupStage::TimerTaskShutdown, &session_id.0);
if let Some((_, handle)) = self.registration_refresh_tasks.remove(session_id) {
handle.abort();
}
guard.finish_success();
}
#[cfg(feature = "perf-tests")]
pub(crate) fn perf_diagnostic_counts(&self) -> serde_json::Value {
serde_json::json!({
"session_to_dialog": self.session_to_dialog.len(),
"dialog_to_session": self.dialog_to_session.len(),
"callid_to_session": self.callid_to_session.len(),
"outgoing_invite_tx": self.outgoing_invite_tx.len(),
"registration_refresh_tasks": self.registration_refresh_tasks.len(),
"lifecycle": {
"cleanup_attempt_total": self.cleanup_attempt_total.load(Ordering::Relaxed),
"cleanup_mapped_total": self.cleanup_mapped_total.load(Ordering::Relaxed),
"cleanup_missing_total": self.cleanup_missing_total.load(Ordering::Relaxed),
"cleanup_call_ids_removed_total": self.cleanup_call_ids_removed_total.load(Ordering::Relaxed),
"cleanup_outgoing_invite_removed_total": self.cleanup_outgoing_invite_removed_total.load(Ordering::Relaxed),
},
})
}
pub(crate) fn abort_all_registration_refreshes(&self) {
let guard = cleanup_diag::stage_guard(CleanupStage::TimerTaskShutdown, "all");
let handles: Vec<_> = self
.registration_refresh_tasks
.iter()
.map(|entry| entry.value().clone())
.collect();
self.registration_refresh_tasks.clear();
for handle in handles {
handle.abort();
}
guard.finish_success();
}
fn compute_registration_refresh_at(&self, now: Instant, accepted_expires: u32) -> Instant {
let base_secs = ((accepted_expires as f64) * 0.85).floor().max(1.0) as u64;
let jitter_cap_secs =
(base_secs * u64::from(self.registration_refresh_jitter_percent)) / 100;
let jitter_secs = if jitter_cap_secs == 0 {
0
} else {
use rand::Rng;
rand::thread_rng().gen_range(0..=jitter_cap_secs)
};
now + Duration::from_secs(base_secs.saturating_sub(jitter_secs).max(1))
}
fn schedule_registration_refresh(
&self,
session_id: SessionId,
next_refresh_at: Option<Instant>,
) {
self.abort_registration_refresh(&session_id);
if !self.registration_auto_refresh {
return;
}
let Some(next_refresh_at) = next_refresh_at else {
return;
};
let Some(state_machine) = self.state_machine.get().cloned() else {
return;
};
let session_id_for_task = session_id.clone();
let tasks = self.registration_refresh_tasks.clone();
let adapter = self.clone();
let handle = tokio::spawn(async move {
tokio::time::sleep_until(tokio::time::Instant::from_std(next_refresh_at)).await;
tasks.remove(&session_id_for_task);
match state_machine
.process_event(
&session_id_for_task,
crate::state_table::types::EventType::RefreshRegistration,
)
.await
{
Ok(result) if result.transition.is_some() => {}
Ok(_) => {
tracing::warn!(
"Automatic registration refresh had no state-table transition for session {}; falling back to direct REGISTER",
session_id_for_task
);
if let Err(e) = adapter
.send_registration_refresh_direct(&session_id_for_task)
.await
{
tracing::warn!(
"Automatic direct registration refresh failed for session {}: {}",
session_id_for_task,
e
);
}
}
Err(e) => {
tracing::warn!(
"Automatic registration refresh failed for session {}: {}",
session_id_for_task,
e
);
}
}
})
.abort_handle();
self.registration_refresh_tasks.insert(session_id, handle);
}
async fn send_registration_refresh_direct(&self, session_id: &SessionId) -> Result<()> {
let session = self.store.get_session(session_id).await?;
if !session.is_registered {
tracing::debug!(
"Skipping automatic registration refresh for unregistered session {}",
session_id
);
return Ok(());
}
let from_uri = session
.local_uri
.clone()
.ok_or_else(|| SessionError::InternalError("local_uri not set for refresh".into()))?;
let registrar_uri = session
.registrar_uri
.clone()
.or_else(|| session.remote_uri.clone())
.ok_or_else(|| {
SessionError::InternalError("registrar_uri not set for refresh".into())
})?;
let contact_uri = session
.registration_contact
.clone()
.or_else(|| session.local_uri.clone())
.ok_or_else(|| SessionError::InternalError("contact_uri not set for refresh".into()))?;
let expires = session.registration_expires.unwrap_or(3600);
let auth = session
.auth
.clone()
.or_else(|| session.credentials.clone().map(Into::into));
drop(session);
let mut attempt_expires = expires;
for _ in 0..=2 {
match self
.send_register(
session_id,
®istrar_uri,
&from_uri,
&contact_uri,
attempt_expires,
auth.as_ref(),
Vec::new(),
)
.await?
{
RegisterAttemptOutcome::Registered {
accepted_expires,
metadata,
} => {
return self
.apply_registration_success(
session_id,
®istrar_uri,
&from_uri,
&contact_uri,
accepted_expires,
metadata,
)
.await;
}
RegisterAttemptOutcome::IntervalTooBrief { min_expires } => {
let mut session = self.store.get_session(session_id).await?;
session.registration_expires = Some(min_expires);
session.registration_retry_count += 1;
self.store.update_session(session).await?;
attempt_expires = min_expires;
}
RegisterAttemptOutcome::Failure {
status_code,
reason,
} => {
return self
.apply_registration_failure(session_id, ®istrar_uri, status_code, reason)
.await;
}
RegisterAttemptOutcome::AuthChallenge { status_code, .. } => {
return self
.apply_registration_failure(
session_id,
®istrar_uri,
status_code,
"automatic registration refresh received a new auth challenge",
)
.await;
}
RegisterAttemptOutcome::Unregistered => {
return self
.apply_unregistration_success(session_id, ®istrar_uri)
.await;
}
}
}
self.apply_registration_failure(
session_id,
®istrar_uri,
423,
"registration refresh failed with repeated 423 Interval Too Brief responses",
)
.await
}
pub(crate) fn accepted_registration_expires(
response: &Response,
requested_contact_uri: &str,
fallback_expires: u32,
) -> u32 {
use rvoip_sip_core::types::headers::HeaderAccess;
use rvoip_sip_core::types::{header::HeaderName, TypedHeader};
let requested = requested_contact_uri.trim().trim_matches(['<', '>']);
let mut first_contact_expires = None;
for contact in response.headers.iter().filter_map(|header| match header {
TypedHeader::Contact(contact) => Some(contact),
_ => None,
}) {
for address in contact.addresses() {
let expires = address
.get_param("expires")
.flatten()
.and_then(|value| value.parse::<u32>().ok());
if first_contact_expires.is_none() {
first_contact_expires = expires;
}
if address.uri.to_string() == requested {
if let Some(expires) = expires {
return expires;
}
}
}
}
first_contact_expires
.or_else(|| {
response
.raw_header_value(&HeaderName::Expires)
.and_then(|value| value.trim().parse::<u32>().ok())
})
.unwrap_or(fallback_expires)
}
pub(crate) fn response_registration_metadata(
response: &Response,
) -> RegistrationResponseMetadata {
use rvoip_sip_core::types::outbound::read_gruu_contact_params;
use rvoip_sip_core::types::TypedHeader;
let service_route = {
let routes: Vec<String> = response
.headers
.iter()
.filter_map(|header| match header {
TypedHeader::ServiceRoute(route) => Some(route.uris()),
_ => None,
})
.flatten()
.map(|uri| uri.to_string())
.collect();
if routes.is_empty() {
None
} else {
Some(routes)
}
};
let mut pub_gruu = None;
let mut temp_gruu = None;
for contact in response.headers.iter().filter_map(|header| match header {
TypedHeader::Contact(contact) => Some(contact),
_ => None,
}) {
for address in contact.addresses() {
let params = read_gruu_contact_params(address);
if pub_gruu.is_none() {
pub_gruu = params.pub_gruu;
}
if temp_gruu.is_none() {
temp_gruu = params.temp_gruu;
}
}
}
RegistrationResponseMetadata {
service_route,
pub_gruu,
temp_gruu,
}
}
pub(crate) fn register_attempt_outcome_from_response(
response: &Response,
contact_uri: &str,
expires: u32,
) -> RegisterAttemptOutcome {
match response.status_code() {
200..=299 => {
if expires == 0 {
RegisterAttemptOutcome::Unregistered
} else {
RegisterAttemptOutcome::Registered {
accepted_expires: Self::accepted_registration_expires(
response,
contact_uri,
expires,
),
metadata: Self::response_registration_metadata(response),
}
}
}
401 | 407 => {
use rvoip_sip_core::types::headers::HeaderAccess;
let header_name = if response.status_code() == 407 {
rvoip_sip_core::types::header::HeaderName::ProxyAuthenticate
} else {
rvoip_sip_core::types::header::HeaderName::WwwAuthenticate
};
if let Some(challenge) = response.raw_header_value(&header_name) {
RegisterAttemptOutcome::AuthChallenge {
status_code: response.status_code(),
challenge,
}
} else {
RegisterAttemptOutcome::Failure {
status_code: response.status_code(),
reason: "REGISTER challenge response did not include challenge header"
.to_string(),
}
}
}
423 => {
use rvoip_sip_core::types::headers::HeaderAccess;
match response
.raw_header_value(&rvoip_sip_core::types::header::HeaderName::MinExpires)
.and_then(|s| s.trim().parse::<u32>().ok())
{
Some(min_expires) if min_expires > 0 && min_expires <= 7200 => {
RegisterAttemptOutcome::IntervalTooBrief { min_expires }
}
Some(min_expires) => RegisterAttemptOutcome::Failure {
status_code: response.status_code(),
reason: format!(
"423 Interval Too Brief included invalid Min-Expires={}",
min_expires
),
},
None => RegisterAttemptOutcome::Failure {
status_code: response.status_code(),
reason: "423 Interval Too Brief without Min-Expires header".to_string(),
},
}
}
_ => RegisterAttemptOutcome::Failure {
status_code: response.status_code(),
reason: response.reason_phrase().to_string(),
},
}
}
pub(crate) async fn apply_registration_success(
&self,
session_id: &SessionId,
registrar_uri: &str,
from_uri: &str,
contact_uri: &str,
accepted_expires: u32,
metadata: RegistrationResponseMetadata,
) -> Result<()> {
let now = Instant::now();
let next_refresh_at = if self.registration_auto_refresh && accepted_expires > 0 {
Some(self.compute_registration_refresh_at(now, accepted_expires))
} else {
None
};
let mut session = self.store.get_session(session_id).await?;
session.is_registered = true;
session.registration_expires = Some(accepted_expires);
session.registration_accepted_expires = Some(accepted_expires);
session.registration_registered_at = Some(now);
session.registration_next_refresh_at = next_refresh_at;
session.registration_last_failure = None;
session.registration_retry_count = 0;
session.registration_service_route = metadata.service_route;
session.registration_pub_gruu = metadata.pub_gruu;
session.registration_temp_gruu = metadata.temp_gruu;
self.store.update_session(session).await?;
tracing::info!(
"✅ Registration successful - session {} marked as registered",
session_id.0
);
self.publish_api_event(crate::api::events::Event::RegistrationSuccess {
registrar: registrar_uri.to_string(),
expires: accepted_expires,
contact: contact_uri.to_string(),
});
self.schedule_registration_refresh(session_id.clone(), next_refresh_at);
self.start_symmetric_registration_keepalive(from_uri, registrar_uri)
.await;
Ok(())
}
pub(crate) async fn apply_unregistration_success(
&self,
session_id: &SessionId,
registrar_uri: &str,
) -> Result<()> {
self.abort_registration_refresh(session_id);
let mut session = self.store.get_session(session_id).await?;
session.is_registered = false;
session.registration_accepted_expires = None;
session.registration_registered_at = None;
session.registration_next_refresh_at = None;
session.registration_last_failure = None;
session.registration_retry_count = 0;
session.registration_service_route = None;
session.registration_pub_gruu = None;
session.registration_temp_gruu = None;
self.store.update_session(session).await?;
tracing::info!(
"✅ Unregistration successful - session {} marked as unregistered",
session_id.0
);
self.publish_api_event(crate::api::events::Event::UnregistrationSuccess {
registrar: registrar_uri.to_string(),
});
Ok(())
}
pub(crate) async fn apply_registration_failure(
&self,
session_id: &SessionId,
registrar_uri: &str,
status_code: u16,
reason: impl Into<String>,
) -> Result<()> {
self.abort_registration_refresh(session_id);
let reason = reason.into();
let mut session = self.store.get_session(session_id).await?;
let failure_summary = if session.registration_retry_count > 0 {
format!(
"{} after {} retry attempt(s)",
reason, session.registration_retry_count
)
} else {
reason.clone()
};
session.is_registered = false;
session.registration_accepted_expires = None;
session.registration_registered_at = None;
session.registration_next_refresh_at = None;
session.registration_last_failure = Some(failure_summary.clone());
session.registration_service_route = None;
session.registration_pub_gruu = None;
session.registration_temp_gruu = None;
self.store.update_session(session).await?;
self.publish_api_event(crate::api::events::Event::RegistrationFailed {
registrar: registrar_uri.to_string(),
status_code,
reason: failure_summary,
});
Ok(())
}
pub(crate) async fn apply_unregistration_failure(
&self,
session_id: &SessionId,
registrar_uri: &str,
reason: impl Into<String>,
) -> Result<()> {
self.abort_registration_refresh(session_id);
let reason = reason.into();
let mut session = self.store.get_session(session_id).await?;
session.is_registered = false;
session.registration_accepted_expires = None;
session.registration_registered_at = None;
session.registration_next_refresh_at = None;
session.registration_last_failure = Some(reason.clone());
session.registration_retry_count = 0;
session.registration_service_route = None;
session.registration_pub_gruu = None;
session.registration_temp_gruu = None;
self.store.update_session(session).await?;
self.publish_api_event(crate::api::events::Event::UnregistrationFailed {
registrar: registrar_uri.to_string(),
reason,
});
Ok(())
}
pub async fn send_response_by_dialog(
&self,
_dialog_id: DialogId,
status_code: u16,
_reason: &str,
) -> Result<()> {
tracing::warn!(
"send_response_by_dialog called but conversion not implemented - status: {}",
status_code
);
Ok(())
}
pub async fn send_bye(&self, dialog_id: crate::types::DialogId) -> Result<()> {
let rvoip_dialog_id: RvoipDialogId = dialog_id.into();
if let Some(entry) = self.dialog_to_session.get(&rvoip_dialog_id) {
let session_id = entry.value().clone();
drop(entry);
self.dialog_api
.send_bye_with_options(&rvoip_dialog_id, ByeRequestOptions::default())
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send BYE: {}", e)))?;
tracing::info!("Sent BYE for session {}", session_id.0);
} else {
tracing::warn!("No session found for dialog {}", dialog_id);
}
Ok(())
}
pub async fn send_reinvite(
&self,
dialog_id: crate::types::DialogId,
sdp: String,
) -> Result<()> {
let rvoip_dialog_id: RvoipDialogId = dialog_id.into();
if let Some(entry) = self.dialog_to_session.get(&rvoip_dialog_id) {
let session_id = entry.value().clone();
drop(entry);
self.dialog_api
.send_update_with_options(
&rvoip_dialog_id,
UpdateRequestOptions {
sdp: Some(sdp),
..Default::default()
},
)
.await
.map_err(|e| {
SessionError::DialogError(format!("Failed to send re-INVITE: {}", e))
})?;
tracing::info!("Sent re-INVITE for session {}", session_id.0);
} else {
tracing::warn!("No session found for dialog {}", dialog_id);
}
Ok(())
}
pub async fn send_refer(
&self,
dialog_id: crate::types::DialogId,
target: &str,
attended: bool,
) -> Result<()> {
let rvoip_dialog_id: RvoipDialogId = dialog_id.into();
if let Some(entry) = self.dialog_to_session.get(&rvoip_dialog_id) {
let session_id = entry.value().clone();
drop(entry);
let _ = attended;
self.dialog_api
.send_refer_with_options(
&rvoip_dialog_id,
ReferRequestOptions {
refer_to: target.to_string(),
..Default::default()
},
)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send REFER: {}", e)))?;
tracing::info!("Sent REFER to {} for session {}", target, session_id.0);
} else {
tracing::warn!("No session found for dialog {}", dialog_id);
}
Ok(())
}
pub async fn get_remote_uri(&self, _dialog_id: crate::types::DialogId) -> Result<String> {
Ok("sip:remote@example.com".to_string())
}
#[allow(clippy::too_many_arguments)]
pub async fn resend_invite_with_auth(
&self,
session_id: &SessionId,
sdp: Option<String>,
auth_header_name: &str,
auth_header_value: String,
extras: Vec<rvoip_sip_core::types::TypedHeader>,
from_display: Option<String>,
contact_override: Option<String>,
) -> Result<()> {
use tokio::time::{Duration, Instant};
let start = Instant::now();
let dialog_id = loop {
if let Some(entry) = self.session_to_dialog.get(session_id) {
break entry.value().clone();
}
if start.elapsed() >= Duration::from_secs(1) {
return Err(SessionError::SessionNotFound(session_id.0.clone()));
}
tokio::time::sleep(Duration::from_millis(10)).await;
};
self.dialog_api
.send_invite_with_auth(
&dialog_id,
sdp,
auth_header_name,
auth_header_value,
extras,
from_display,
contact_override,
)
.await
.map_err(|e| {
SessionError::DialogError(format!(
"resend_invite_with_auth failed for session {}: {}",
session_id.0, e
))
})?;
Ok(())
}
pub async fn resend_invite_with_session_timer_override(
&self,
session_id: &SessionId,
sdp: Option<String>,
session_secs: u32,
min_se: u32,
) -> Result<()> {
use tokio::time::{Duration, Instant};
let start = Instant::now();
let dialog_id = loop {
if let Some(entry) = self.session_to_dialog.get(session_id) {
break entry.value().clone();
}
if start.elapsed() >= Duration::from_secs(1) {
return Err(SessionError::SessionNotFound(session_id.0.clone()));
}
tokio::time::sleep(Duration::from_millis(10)).await;
};
self.dialog_api
.send_invite_with_session_timer_override(&dialog_id, sdp, session_secs, min_se)
.await
.map_err(|e| {
SessionError::DialogError(format!(
"resend_invite_with_session_timer_override failed for session {}: {}",
session_id.0, e
))
})?;
Ok(())
}
pub async fn peer_supports_100rel(&self, session_id: &SessionId) -> Result<bool> {
let dialog_id = self
.session_to_dialog
.get(session_id)
.map(|e| e.value().clone())
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?;
let dialog = self
.dialog_api
.get_dialog_info(&dialog_id)
.await
.map_err(|e| {
SessionError::DialogError(format!(
"peer_supports_100rel: failed to read dialog {}: {}",
dialog_id, e
))
})?;
Ok(dialog.peer_supports_100rel)
}
pub async fn send_invite_with_details(
&self,
session_id: &SessionId,
from: &str,
to: &str,
sdp: Option<String>,
) -> Result<()> {
let call_id = format!("{}@rvoip-sip", session_id.0);
self.callid_to_session
.insert(call_id.clone(), session_id.clone());
let call_handle = self
.dialog_api
.make_call_for_session(&session_id.0, from, to, sdp, Some(call_id.clone()))
.await
.map_err(|e| SessionError::DialogError(format!("Failed to make call: {}", e)))?;
let dialog_id = call_handle.call_id().clone();
self.session_to_dialog
.insert(session_id.clone(), dialog_id.clone());
self.dialog_to_session
.insert(dialog_id.clone(), session_id.clone());
let event = SessionToDialogEvent::StoreDialogMapping {
session_id: session_id.0.clone(),
dialog_id: dialog_id.to_string(),
};
self.global_coordinator
.publish(Arc::new(RvoipCrossCrateEvent::SessionToDialog(event)))
.await
.map_err(|e| {
SessionError::InternalError(format!("Failed to publish StoreDialogMapping: {}", e))
})?;
tracing::info!(
"Published StoreDialogMapping for session {} -> dialog {}",
session_id.0,
dialog_id
);
tracing::debug!(
"Dialog {} created for session {} - ACK will be handled by dialog-core",
dialog_id,
session_id.0
);
tracing::debug!("Dialog {} created for session {}", dialog_id, session_id.0);
Ok(())
}
pub async fn send_invite_with_extra_headers(
&self,
session_id: &SessionId,
from: &str,
to: &str,
sdp: Option<String>,
extra_headers: Vec<rvoip_sip_core::types::TypedHeader>,
) -> Result<()> {
self.send_invite_with_extra_headers_inner(
session_id,
from,
to,
sdp,
extra_headers,
true, )
.await
}
pub async fn send_invite_with_extra_headers_no_global_proxy(
&self,
session_id: &SessionId,
from: &str,
to: &str,
sdp: Option<String>,
extra_headers: Vec<rvoip_sip_core::types::TypedHeader>,
) -> Result<()> {
self.send_invite_with_extra_headers_inner(session_id, from, to, sdp, extra_headers, false)
.await
}
async fn send_invite_with_extra_headers_inner(
&self,
session_id: &SessionId,
from: &str,
to: &str,
sdp: Option<String>,
extra_headers: Vec<rvoip_sip_core::types::TypedHeader>,
apply_global_proxy: bool,
) -> Result<()> {
let call_id = format!("{}@rvoip-sip", session_id.0);
self.callid_to_session
.insert(call_id.clone(), session_id.clone());
let headers = if apply_global_proxy {
prepend_outbound_proxy_route(extra_headers, self.outbound_proxy_uri.as_ref())
} else {
extra_headers
};
if apply_global_proxy && self.outbound_proxy_uri.is_some() {
tracing::debug!(
"E4 outbound proxy: prepended Route to INVITE for session {}",
session_id.0
);
}
let call_handle = self
.dialog_api
.make_call_with_extra_headers_for_session(
&session_id.0,
from,
to,
sdp,
Some(call_id.clone()),
headers,
)
.await
.map_err(|e| {
SessionError::DialogError(format!("Failed to make call with extra headers: {}", e))
})?;
let dialog_id = call_handle.call_id().clone();
self.session_to_dialog
.insert(session_id.clone(), dialog_id.clone());
self.dialog_to_session
.insert(dialog_id.clone(), session_id.clone());
let event = SessionToDialogEvent::StoreDialogMapping {
session_id: session_id.0.clone(),
dialog_id: dialog_id.to_string(),
};
self.global_coordinator
.publish(Arc::new(RvoipCrossCrateEvent::SessionToDialog(event)))
.await
.map_err(|e| {
SessionError::InternalError(format!("Failed to publish StoreDialogMapping: {}", e))
})?;
tracing::info!(
"send_invite_with_extra_headers: published StoreDialogMapping for session {} -> dialog {} ({} extra header(s))",
session_id.0,
dialog_id,
self.session_to_dialog
.get(session_id)
.map(|_| "ok")
.unwrap_or("missing"),
);
Ok(())
}
pub async fn send_invite_with_options(
&self,
session_id: &SessionId,
mut opts: rvoip_sip_dialog::api::unified::InviteRequestOptions,
apply_global_proxy: bool,
) -> Result<()> {
let call_id = format!("{}@rvoip-sip", session_id.0);
self.callid_to_session
.insert(call_id.clone(), session_id.clone());
if apply_global_proxy {
opts.extra_headers =
prepend_outbound_proxy_route(opts.extra_headers, self.outbound_proxy_uri.as_ref());
if self.outbound_proxy_uri.is_some() {
tracing::debug!(
"E4 outbound proxy: prepended Route to INVITE for session {}",
session_id.0
);
}
}
opts.call_id = Some(call_id.clone());
let call_handle = self
.dialog_api
.send_invite_with_options_for_session(&session_id.0, opts)
.await
.map_err(|e| {
SessionError::DialogError(format!("Failed to send INVITE with options: {}", e))
})?;
let dialog_id = call_handle.call_id().clone();
self.session_to_dialog
.insert(session_id.clone(), dialog_id.clone());
self.dialog_to_session
.insert(dialog_id.clone(), session_id.clone());
let event = SessionToDialogEvent::StoreDialogMapping {
session_id: session_id.0.clone(),
dialog_id: dialog_id.to_string(),
};
self.global_coordinator
.publish(Arc::new(RvoipCrossCrateEvent::SessionToDialog(event)))
.await
.map_err(|e| {
SessionError::InternalError(format!("Failed to publish StoreDialogMapping: {}", e))
})?;
Ok(())
}
pub async fn send_200_ok(&self, session_id: &SessionId, sdp: Option<String>) -> Result<()> {
self.send_response(session_id, 200, sdp).await
}
pub async fn send_response_with_sdp(
&self,
session_id: &SessionId,
code: u16,
_reason: &str,
sdp: &str,
) -> Result<()> {
self.send_response(session_id, code, Some(sdp.to_string()))
.await
}
pub async fn send_response_session(
&self,
session_id: &SessionId,
code: u16,
_reason: &str,
) -> Result<()> {
self.send_response(session_id, code, None).await
}
pub async fn send_error_response(
&self,
session_id: &SessionId,
code: StatusCode,
_reason: &str,
) -> Result<()> {
self.send_response(session_id, code.as_u16(), None).await
}
pub async fn send_redirect_response_with_options(
&self,
session_id: &SessionId,
status: u16,
contacts: Vec<String>,
extra_headers: Vec<rvoip_sip_core::types::TypedHeader>,
) -> Result<()> {
tracing::info!(
"DialogAdapter sending {} redirect for session {} with {} contact(s), {} staged extras",
status,
session_id.0,
contacts.len(),
extra_headers.len()
);
self.dialog_api
.send_redirect_response_with_extras_for_session(
&session_id.0,
status,
contacts,
extra_headers,
)
.await
.map_err(|e| {
tracing::error!(
"Failed to send redirect (with extras) for session {}: {}",
session_id.0,
e
);
SessionError::DialogError(format!("Failed to send redirect: {}", e))
})
}
pub async fn send_redirect_response(
&self,
session_id: &SessionId,
status: u16,
contacts: Vec<String>,
) -> Result<()> {
tracing::info!(
"DialogAdapter sending {} redirect for session {} with {} contact(s)",
status,
session_id.0,
contacts.len()
);
self.dialog_api
.send_redirect_response_for_session(&session_id.0, status, contacts)
.await
.map_err(|e| {
tracing::error!(
"Failed to send redirect for session {}: {}",
session_id.0,
e
);
SessionError::DialogError(format!("Failed to send redirect: {}", e))
})
}
pub async fn send_response_with_options(
&self,
session_id: &SessionId,
code: u16,
sdp: Option<String>,
extra_headers: Vec<rvoip_sip_core::types::TypedHeader>,
) -> Result<()> {
tracing::info!(
"DialogAdapter sending {} response for session {} with {} staged extras",
code,
session_id.0,
extra_headers.len()
);
self.dialog_api
.send_response_with_extras_for_session(&session_id.0, code, sdp, extra_headers)
.await
.map_err(|e| {
tracing::error!(
"Failed to send response (with extras) for session {}: {}",
session_id.0,
e
);
SessionError::DialogError(format!("Failed to send response: {}", e))
})
}
pub async fn send_response(
&self,
session_id: &SessionId,
code: u16,
sdp: Option<String>,
) -> Result<()> {
tracing::info!(
"DialogAdapter sending {} response for session {} with SDP: {}",
code,
session_id.0,
sdp.is_some()
);
self.dialog_api
.send_response_for_session(&session_id.0, code, sdp)
.await
.map_err(|e| {
tracing::error!(
"Failed to send response for session {}: {}",
session_id.0,
e
);
SessionError::DialogError(format!("Failed to send response: {}", e))
})
}
pub async fn send_response_for_transaction(
&self,
session_id: &SessionId,
transaction_id: &TransactionKey,
code: u16,
sdp: Option<String>,
) -> Result<()> {
tracing::info!(
"DialogAdapter sending {} response for session {} via transaction {} with SDP: {}",
code,
session_id.0,
transaction_id,
sdp.is_some()
);
self.dialog_api
.send_response_for_session_transaction(&session_id.0, transaction_id, code, sdp)
.await
.map_err(|e| {
tracing::error!(
"Failed to send response for session {} via transaction {}: {}",
session_id.0,
transaction_id,
e
);
SessionError::DialogError(format!("Failed to send response: {}", e))
})
}
pub async fn send_ack(&self, session_id: &SessionId, response: &Response) -> Result<()> {
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?
.clone();
if let Some(tx_id) = self.outgoing_invite_tx.get(session_id) {
self.dialog_api
.send_ack_for_2xx_response(&dialog_id, &tx_id, response)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send ACK: {}", e)))?;
self.outgoing_invite_tx.remove(session_id);
} else {
tracing::debug!(
"No transaction ID stored for session {}, ACK may fail",
session_id.0
);
}
Ok(())
}
pub async fn send_bye_session(&self, session_id: &SessionId) -> Result<()> {
let dialog_id = {
let entry = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?;
entry.value().clone()
};
self.dialog_api
.send_bye_with_options(&dialog_id, ByeRequestOptions::default())
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send BYE: {}", e)))?;
Ok(())
}
pub async fn send_bye_session_with_reason(
&self,
session_id: &SessionId,
reason: rvoip_sip_core::types::reason::Reason,
) -> Result<()> {
let dialog_id = {
let entry = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?;
entry.value().clone()
};
self.dialog_api
.send_bye_with_options(
&dialog_id,
ByeRequestOptions {
reason: Some(reason.to_string()),
..Default::default()
},
)
.await
.map_err(|e| {
SessionError::DialogError(format!("Failed to send BYE with Reason: {}", e))
})?;
Ok(())
}
pub async fn send_update_with_options(
&self,
session_id: &SessionId,
mut opts: rvoip_sip_dialog::api::unified::UpdateRequestOptions,
) -> Result<()> {
opts.extra_headers = apply_outbound_extras_policy(
rvoip_sip_core::types::Method::Update,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
)?;
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?
.clone();
self.dialog_api
.send_update_with_options(&dialog_id, opts)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send UPDATE: {}", e)))?;
Ok(())
}
pub async fn send_reinvite_with_options(
&self,
session_id: &SessionId,
mut opts: rvoip_sip_dialog::api::unified::ReInviteRequestOptions,
) -> Result<()> {
opts.extra_headers = apply_outbound_extras_policy(
rvoip_sip_core::types::Method::Invite,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
)?;
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?
.clone();
self.dialog_api
.send_reinvite_with_options(&dialog_id, opts)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send re-INVITE: {}", e)))?;
Ok(())
}
pub async fn send_refer_with_options(
&self,
session_id: &SessionId,
mut opts: rvoip_sip_dialog::api::unified::ReferRequestOptions,
) -> Result<()> {
opts.extra_headers = apply_outbound_extras_policy(
rvoip_sip_core::types::Method::Refer,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
)?;
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?
.clone();
self.dialog_api
.send_refer_with_options(&dialog_id, opts)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send REFER: {}", e)))?;
Ok(())
}
pub async fn send_info_with_options(
&self,
session_id: &SessionId,
mut opts: rvoip_sip_dialog::api::unified::InfoRequestOptions,
) -> Result<()> {
opts.extra_headers = apply_outbound_extras_policy(
rvoip_sip_core::types::Method::Info,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
)?;
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?
.clone();
self.dialog_api
.send_info_with_options(&dialog_id, opts)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send INFO: {}", e)))?;
Ok(())
}
pub async fn send_bye_with_options(
&self,
session_id: &SessionId,
mut opts: rvoip_sip_dialog::api::unified::ByeRequestOptions,
) -> Result<()> {
opts.extra_headers = apply_outbound_extras_policy(
rvoip_sip_core::types::Method::Bye,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
)?;
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?
.clone();
self.dialog_api
.send_bye_with_options(&dialog_id, opts)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send BYE: {}", e)))?;
Ok(())
}
pub async fn send_notify_with_options(
&self,
session_id: &SessionId,
mut opts: rvoip_sip_dialog::api::unified::NotifyRequestOptions,
) -> Result<()> {
opts.extra_headers = apply_outbound_extras_policy(
rvoip_sip_core::types::Method::Notify,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
)?;
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?
.clone();
self.dialog_api
.send_notify_with_options(&dialog_id, opts)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send NOTIFY: {}", e)))?;
Ok(())
}
pub async fn send_message_oob_with_options(
&self,
mut opts: rvoip_sip_dialog::api::unified::MessageRequestOptions,
) -> Result<rvoip_sip_core::Response> {
opts.extra_headers = apply_outbound_extras_policy(
rvoip_sip_core::types::Method::Message,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
)?;
self.dialog_api
.send_message_out_of_dialog_with_options(opts)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send MESSAGE: {}", e)))
}
pub async fn send_options_oob_with_options(
&self,
mut opts: rvoip_sip_dialog::api::unified::OptionsRequestOptions,
) -> Result<rvoip_sip_core::Response> {
opts.extra_headers = apply_outbound_extras_policy(
rvoip_sip_core::types::Method::Options,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
)?;
self.dialog_api
.send_options_out_of_dialog_with_options(opts)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send OPTIONS: {}", e)))
}
pub async fn send_subscribe_oob_with_options(
&self,
target: &str,
mut opts: rvoip_sip_dialog::api::unified::SubscribeRequestOptions,
) -> Result<rvoip_sip_core::Response> {
opts.extra_headers = apply_outbound_extras_policy(
rvoip_sip_core::types::Method::Subscribe,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
)?;
self.dialog_api
.send_subscribe_with_options(target, opts)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send SUBSCRIBE: {}", e)))
}
pub async fn send_bye_with_auth(
&self,
session_id: &SessionId,
mut opts: rvoip_sip_dialog::api::unified::ByeRequestOptions,
auth_header_name: &str,
auth_header_value: String,
) -> Result<()> {
opts.extra_headers = apply_outbound_extras_policy_with_auth(
rvoip_sip_core::types::Method::Bye,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
auth_header_name,
auth_header_value,
)?;
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?
.clone();
self.dialog_api
.send_bye_with_options(&dialog_id, opts)
.await
.map_err(|e| {
SessionError::DialogError(format!("Failed to send BYE with auth: {}", e))
})?;
Ok(())
}
pub async fn send_refer_with_auth(
&self,
session_id: &SessionId,
mut opts: rvoip_sip_dialog::api::unified::ReferRequestOptions,
auth_header_name: &str,
auth_header_value: String,
) -> Result<()> {
opts.extra_headers = apply_outbound_extras_policy_with_auth(
rvoip_sip_core::types::Method::Refer,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
auth_header_name,
auth_header_value,
)?;
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?
.clone();
self.dialog_api
.send_refer_with_options(&dialog_id, opts)
.await
.map_err(|e| {
SessionError::DialogError(format!("Failed to send REFER with auth: {}", e))
})?;
Ok(())
}
pub async fn send_notify_with_auth(
&self,
session_id: &SessionId,
mut opts: rvoip_sip_dialog::api::unified::NotifyRequestOptions,
auth_header_name: &str,
auth_header_value: String,
) -> Result<()> {
opts.extra_headers = apply_outbound_extras_policy_with_auth(
rvoip_sip_core::types::Method::Notify,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
auth_header_name,
auth_header_value,
)?;
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?
.clone();
self.dialog_api
.send_notify_with_options(&dialog_id, opts)
.await
.map_err(|e| {
SessionError::DialogError(format!("Failed to send NOTIFY with auth: {}", e))
})?;
Ok(())
}
pub async fn send_info_with_auth(
&self,
session_id: &SessionId,
mut opts: rvoip_sip_dialog::api::unified::InfoRequestOptions,
auth_header_name: &str,
auth_header_value: String,
) -> Result<()> {
opts.extra_headers = apply_outbound_extras_policy_with_auth(
rvoip_sip_core::types::Method::Info,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
auth_header_name,
auth_header_value,
)?;
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?
.clone();
self.dialog_api
.send_info_with_options(&dialog_id, opts)
.await
.map_err(|e| {
SessionError::DialogError(format!("Failed to send INFO with auth: {}", e))
})?;
Ok(())
}
pub async fn send_update_with_auth(
&self,
session_id: &SessionId,
mut opts: rvoip_sip_dialog::api::unified::UpdateRequestOptions,
auth_header_name: &str,
auth_header_value: String,
) -> Result<()> {
opts.extra_headers = apply_outbound_extras_policy_with_auth(
rvoip_sip_core::types::Method::Update,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
auth_header_name,
auth_header_value,
)?;
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?
.clone();
self.dialog_api
.send_update_with_options(&dialog_id, opts)
.await
.map_err(|e| {
SessionError::DialogError(format!("Failed to send UPDATE with auth: {}", e))
})?;
Ok(())
}
pub async fn send_message_oob_with_auth(
&self,
mut opts: rvoip_sip_dialog::api::unified::MessageRequestOptions,
auth_header_name: &str,
auth_header_value: String,
) -> Result<rvoip_sip_core::Response> {
opts.extra_headers = apply_outbound_extras_policy_with_auth(
rvoip_sip_core::types::Method::Message,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
auth_header_name,
auth_header_value,
)?;
self.dialog_api
.send_message_out_of_dialog_with_options(opts)
.await
.map_err(|e| {
SessionError::DialogError(format!("Failed to send MESSAGE with auth: {}", e))
})
}
pub async fn send_options_oob_with_auth(
&self,
mut opts: rvoip_sip_dialog::api::unified::OptionsRequestOptions,
auth_header_name: &str,
auth_header_value: String,
) -> Result<rvoip_sip_core::Response> {
opts.extra_headers = apply_outbound_extras_policy_with_auth(
rvoip_sip_core::types::Method::Options,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
auth_header_name,
auth_header_value,
)?;
self.dialog_api
.send_options_out_of_dialog_with_options(opts)
.await
.map_err(|e| {
SessionError::DialogError(format!("Failed to send OPTIONS with auth: {}", e))
})
}
pub async fn send_subscribe_oob_with_auth(
&self,
target: &str,
mut opts: rvoip_sip_dialog::api::unified::SubscribeRequestOptions,
auth_header_name: &str,
auth_header_value: String,
) -> Result<rvoip_sip_core::Response> {
opts.extra_headers = apply_outbound_extras_policy_with_auth(
rvoip_sip_core::types::Method::Subscribe,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
auth_header_name,
auth_header_value,
)?;
self.dialog_api
.send_subscribe_with_options(target, opts)
.await
.map_err(|e| {
SessionError::DialogError(format!("Failed to send SUBSCRIBE with auth: {}", e))
})
}
pub async fn send_cancel(&self, session_id: &SessionId) -> Result<()> {
let dialog_id = {
let entry = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?;
entry.value().clone()
};
self.dialog_api
.send_cancel_with_options(&dialog_id, CancelRequestOptions::default())
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send CANCEL: {}", e)))?;
Ok(())
}
pub async fn send_register_with_options(
&self,
mut opts: rvoip_sip_dialog::api::unified::RegisterRequestOptions,
) -> Result<rvoip_sip_core::Response> {
opts.extra_headers = apply_outbound_extras_policy(
rvoip_sip_core::types::Method::Register,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
)?;
self.dialog_api
.send_register_with_options(opts)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send REGISTER: {}", e)))
}
pub async fn send_cancel_with_options(
&self,
session_id: &SessionId,
mut opts: rvoip_sip_dialog::api::unified::CancelRequestOptions,
) -> Result<()> {
opts.extra_headers = apply_outbound_extras_policy(
rvoip_sip_core::types::Method::Cancel,
opts.extra_headers,
self.outbound_proxy_uri.as_ref(),
)?;
let dialog_id = {
let entry = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?;
entry.value().clone()
};
self.dialog_api
.send_cancel_with_options(&dialog_id, opts)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send CANCEL: {}", e)))?;
Ok(())
}
pub async fn send_info(
&self,
session_id: &SessionId,
content_type: &str,
body: &[u8],
) -> Result<()> {
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?
.clone();
self.dialog_api
.send_info_with_options(
&dialog_id,
InfoRequestOptions {
content_type: content_type.to_string(),
body: bytes::Bytes::copy_from_slice(body),
..Default::default()
},
)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send INFO: {}", e)))?;
tracing::debug!(
session = %session_id.0,
content_type = %content_type,
body_len = body.len(),
"Sent INFO"
);
Ok(())
}
pub async fn send_refer_session(&self, session_id: &SessionId, refer_to: &str) -> Result<()> {
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?
.clone();
self.dialog_api
.send_refer_with_options(
&dialog_id,
ReferRequestOptions {
refer_to: refer_to.to_string(),
..Default::default()
},
)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send REFER: {}", e)))?;
tracing::info!("Sent REFER to {} for session {}", refer_to, session_id.0);
Ok(())
}
pub async fn dialog_identity(&self, session_id: &SessionId) -> Result<Option<DialogIdentity>> {
let dialog_id = match self.session_to_dialog.get(session_id) {
Some(entry) => entry.clone(),
None => return Ok(None),
};
let dialog = match self.dialog_api.get_dialog_info(&dialog_id).await {
Ok(d) => d,
Err(_) => return Ok(None),
};
Ok(Some(DialogIdentity {
call_id: dialog.call_id,
local_tag: dialog.local_tag,
remote_tag: dialog.remote_tag,
}))
}
pub async fn send_reinvite_session(&self, session_id: &SessionId, sdp: String) -> Result<()> {
use rvoip_sip_core::Method;
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::SessionNotFound(session_id.0.clone()))?
.clone();
self.dialog_api
.send_request_in_dialog(&dialog_id, Method::Invite, Some(bytes::Bytes::from(sdp)))
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send re-INVITE: {}", e)))?;
Ok(())
}
pub async fn cleanup_session(&self, session_id: &SessionId) -> Result<()> {
let guard = cleanup_diag::stage_guard(CleanupStage::DialogCleanup, &session_id.0);
self.cleanup_attempt_total.fetch_add(1, Ordering::Relaxed);
if let Some(dialog_id) = self.session_to_dialog.remove(session_id) {
self.cleanup_mapped_total.fetch_add(1, Ordering::Relaxed);
self.dialog_to_session.remove(&dialog_id.1);
self.dialog_api
.dialog_manager()
.core()
.cleanup_dialog_storage_and_transactions(&dialog_id.1)
.await;
} else {
self.cleanup_missing_total.fetch_add(1, Ordering::Relaxed);
}
let call_ids_to_remove: Vec<_> = self
.callid_to_session
.iter()
.filter(|entry| entry.value() == session_id)
.map(|entry| entry.key().clone())
.collect();
for call_id in call_ids_to_remove {
self.callid_to_session.remove(&call_id);
self.cleanup_call_ids_removed_total
.fetch_add(1, Ordering::Relaxed);
}
if self.outgoing_invite_tx.remove(session_id).is_some() {
self.cleanup_outgoing_invite_removed_total
.fetch_add(1, Ordering::Relaxed);
}
tracing::debug!(
"Cleaned up dialog adapter mappings for session {}",
session_id.0
);
guard.finish_success();
Ok(())
}
async fn start_symmetric_registration_keepalive(&self, from_uri: &str, registrar_uri: &str) {
let Some(params) = self.symmetric_flow_params.as_ref() else {
return;
};
let dest_uri = match registrar_uri.parse::<rvoip_sip_core::Uri>() {
Ok(uri) => uri,
Err(e) => {
tracing::warn!(
"symmetric registered-flow: invalid registrar URI {}: {}",
registrar_uri,
e
);
return;
}
};
let Some(destination) =
rvoip_sip_dialog::dialog::dialog_utils::resolve_uri_to_socketaddr(&dest_uri).await
else {
tracing::warn!(
"symmetric registered-flow: could not resolve registrar URI {} for keep-alive",
registrar_uri
);
return;
};
self.dialog_api.dialog_manager().core().start_outbound_ping(
(
from_uri.to_string(),
params.reg_id,
params.instance_urn.clone(),
),
destination,
);
tracing::info!(
"symmetric registered-flow: keep-alive ping started for AoR {} (reg-id={}, instance={}) → {}",
from_uri,
params.reg_id,
params.instance_urn,
destination
);
}
pub(crate) async fn send_register(
&self,
session_id: &SessionId,
registrar_uri: &str,
from_uri: &str,
contact_uri: &str,
expires: u32,
auth: Option<&crate::auth::SipClientAuth>,
extras: Vec<rvoip_sip_core::types::TypedHeader>,
) -> Result<RegisterAttemptOutcome> {
tracing::info!(
"Sending REGISTER for session {} to {} (expires={})",
session_id.0,
registrar_uri,
expires
);
let (authorization, proxy_authorization) = if let Some(auth) = auth {
let mut session = self.store.get_session(session_id).await?;
if let Some(challenge_raw) = session.auth_challenge_raw.clone().or_else(|| {
session.auth_challenge.as_ref().map(|challenge| {
rvoip_auth_core::DigestAuthenticator::new(challenge.realm.clone())
.format_www_authenticate(challenge)
})
}) {
let digest_challenge = session.auth_challenge.clone().or_else(|| {
rvoip_auth_core::DigestAuthenticator::parse_challenge(&challenge_raw).ok()
});
let nc_value = if let Some(challenge) = digest_challenge.as_ref() {
let nc_key = (challenge.realm.clone(), challenge.nonce.clone());
*session
.digest_nc
.entry(nc_key)
.and_modify(|n| *n += 1)
.or_insert(1)
} else {
1
};
let transport_context = session
.pending_auth_transport
.clone()
.unwrap_or_else(|| self.outbound_transport_context_for_uri(registrar_uri));
session.pending_auth_transport = None;
self.store.update_session(session.clone()).await?;
tracing::info!(
"🔍 CLIENT: Computing auth for REGISTER uri={}, nc={}",
registrar_uri,
nc_value
);
let selected = auth.authorization_for_challenge_with_transport_context(
&challenge_raw,
"REGISTER",
registrar_uri,
nc_value,
None,
&transport_context,
)?;
tracing::info!(
"🔍 CLIENT: Computed REGISTER auth using {:?}",
selected.scheme
);
let status = session
.pending_auth
.as_ref()
.map(|(status, _)| *status)
.unwrap_or(401);
if status == 407 {
(None, Some(selected.value))
} else {
(Some(selected.value), None)
}
} else {
tracing::debug!("No challenge stored, sending without auth");
(None, None)
}
} else {
(None, None)
};
let rewritten_contact = if let Some(public) = self.dialog_api.discovered_public_addr().await
{
let rewritten = rewrite_contact_host(contact_uri, public);
if rewritten != contact_uri {
tracing::info!(
"RFC 3581/5626: rewriting REGISTER Contact {} → {} (NAT-discovered)",
contact_uri,
rewritten
);
}
rewritten
} else {
contact_uri.to_string()
};
let (registration_call_id, registration_cseq) = {
let mut session = self.store.get_session(session_id).await?;
let call_id = session
.registration_call_id
.get_or_insert_with(|| format!("reg-{}", uuid::Uuid::new_v4()))
.clone();
session.registration_cseq = session.registration_cseq.saturating_add(1);
let cseq = session.registration_cseq;
self.store.update_session(session).await?;
(call_id, cseq)
};
let response = self
.dialog_api
.send_register_with_options(rvoip_sip_dialog::api::unified::RegisterRequestOptions {
registrar_uri: registrar_uri.to_string(),
aor_uri: from_uri.to_string(),
contact_uri: rewritten_contact,
expires,
authorization,
proxy_authorization,
call_id: Some(registration_call_id),
cseq: Some(registration_cseq),
outbound_contact: self.outbound_contact_params.clone(),
outbound_proxy_uri: self.outbound_proxy_uri.clone(),
extra_headers: extras,
refresh: false,
})
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send REGISTER: {}", e)))?;
tracing::info!(
"REGISTER response received: {} for session {}",
response.status_code(),
session_id.0
);
let register_transport = self
.dialog_api
.outbound_transport_context_for_response(&response)
.map(|context| {
crate::auth::SipTransportSecurityContext::from_transport_context(&context)
});
if let Ok(mut session) = self.store.get_session(session_id).await {
session.pending_auth_transport = register_transport;
self.store.update_session(session).await?;
}
match response.status_code() {
200..=299 => {
let is_unregister = expires == 0;
if is_unregister {
Ok(RegisterAttemptOutcome::Unregistered)
} else {
let accepted_expires =
Self::accepted_registration_expires(&response, contact_uri, expires);
let metadata = Self::response_registration_metadata(&response);
Ok(RegisterAttemptOutcome::Registered {
accepted_expires,
metadata,
})
}
}
401 | 407 => {
use rvoip_sip_core::types::headers::HeaderAccess;
let header_name = if response.status_code() == 407 {
rvoip_sip_core::types::header::HeaderName::ProxyAuthenticate
} else {
rvoip_sip_core::types::header::HeaderName::WwwAuthenticate
};
let challenges = response
.raw_headers(&header_name)
.into_iter()
.filter_map(|bytes| String::from_utf8(bytes).ok())
.collect::<Vec<_>>();
if !challenges.is_empty() {
Ok(RegisterAttemptOutcome::AuthChallenge {
status_code: response.status_code(),
challenge: challenges.join(", "),
})
} else {
tracing::warn!(
"REGISTER {} without challenge header",
response.status_code()
);
Ok(RegisterAttemptOutcome::Failure {
status_code: response.status_code(),
reason: "REGISTER challenge response did not include challenge header"
.to_string(),
})
}
}
423 => {
use rvoip_sip_core::types::headers::HeaderAccess;
let min_expires = response
.raw_header_value(&rvoip_sip_core::types::header::HeaderName::MinExpires)
.and_then(|s| s.trim().parse::<u32>().ok());
match min_expires {
Some(min_expires) if min_expires > 0 && min_expires <= 7200 => {
Ok(RegisterAttemptOutcome::IntervalTooBrief { min_expires })
}
Some(min_expires) => Ok(RegisterAttemptOutcome::Failure {
status_code: response.status_code(),
reason: format!(
"423 Interval Too Brief included invalid Min-Expires={}",
min_expires
),
}),
None => Ok(RegisterAttemptOutcome::Failure {
status_code: response.status_code(),
reason: "423 Interval Too Brief without Min-Expires header".to_string(),
}),
}
}
_ => {
tracing::warn!(
"❌ Registration failed with status {}",
response.status_code()
);
Ok(RegisterAttemptOutcome::Failure {
status_code: response.status_code(),
reason: response.reason_phrase().to_string(),
})
}
}
}
pub async fn send_subscribe(
&self,
session_id: &SessionId,
from_uri: &str,
to_uri: &str,
event_package: &str,
expires: u32,
) -> Result<()> {
tracing::info!(
"Sending SUBSCRIBE for session {} from {} to {} for event {}",
session_id.0,
from_uri,
to_uri,
event_package
);
let response = self
.dialog_api
.send_subscribe_with_options(
to_uri,
SubscribeRequestOptions {
event: event_package.to_string(),
expires,
from_uri: Some(from_uri.to_string()),
contact_uri: Some(from_uri.to_string()),
..Default::default()
},
)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send SUBSCRIBE: {}", e)))?;
tracing::info!(
"SUBSCRIBE response: {} for session {}",
response.status_code(),
session_id.0
);
if response.status_code() == 200 || response.status_code() == 202 {
let event = RvoipCrossCrateEvent::DialogToSession(
rvoip_infra_common::events::cross_crate::DialogToSessionEvent::SubscriptionAccepted {
session_id: session_id.0.clone(),
}
);
let _ = self.global_coordinator.publish(Arc::new(event)).await;
} else if response.status_code() >= 400 {
let event = RvoipCrossCrateEvent::DialogToSession(
rvoip_infra_common::events::cross_crate::DialogToSessionEvent::SubscriptionFailed {
session_id: session_id.0.clone(),
status_code: response.status_code(),
},
);
let _ = self.global_coordinator.publish(Arc::new(event)).await;
}
Ok(())
}
pub async fn send_notify(
&self,
session_id: &SessionId,
event_package: &str,
body: Option<String>,
subscription_state: Option<String>,
) -> Result<()> {
tracing::info!(
"Sending NOTIFY for session {} with event {} and state {:?}",
session_id.0,
event_package,
subscription_state
);
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::DialogError("No dialog for session".to_string()))?
.clone();
self.dialog_api
.send_notify_with_options(
&dialog_id,
NotifyRequestOptions {
event: event_package.to_string(),
subscription_state: subscription_state.unwrap_or_default(),
body: body.map(bytes::Bytes::from),
..Default::default()
},
)
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send NOTIFY: {}", e)))?;
tracing::info!("NOTIFY sent successfully for session {}", session_id.0);
Ok(())
}
pub async fn send_refer_notify(
&self,
session_id: &SessionId,
status_code: u16,
reason: &str,
) -> Result<()> {
tracing::info!(
"Sending REFER NOTIFY for session {} with status {} {}",
session_id.0,
status_code,
reason
);
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::DialogError("No dialog for session".to_string()))?
.clone();
self.dialog_api
.send_refer_notify(&dialog_id, status_code, reason)
.await
.map_err(|e| {
SessionError::DialogError(format!("Failed to send REFER NOTIFY: {}", e))
})?;
tracing::info!(
"REFER NOTIFY sent successfully for session {}",
session_id.0
);
Ok(())
}
pub async fn send_message(
&self,
session_id: &SessionId,
from_uri: &str,
to_uri: &str,
body: String,
in_dialog: bool,
) -> Result<()> {
tracing::info!(
"Sending MESSAGE for session {} from {} to {} (in_dialog: {})",
session_id.0,
from_uri,
to_uri,
in_dialog
);
if in_dialog {
let dialog_id = self
.session_to_dialog
.get(session_id)
.ok_or_else(|| SessionError::DialogError("No dialog for session".to_string()))?
.clone();
self.dialog_api
.send_request_in_dialog(
&dialog_id,
rvoip_sip_core::Method::Message,
Some(bytes::Bytes::from(body)),
)
.await
.map_err(|e| {
SessionError::DialogError(format!("Failed to send MESSAGE in dialog: {}", e))
})?;
} else {
let response = self
.dialog_api
.send_message_out_of_dialog_with_options(MessageRequestOptions {
from_uri: from_uri.to_string(),
to_uri: to_uri.to_string(),
content_type: String::from("text/plain"),
body: bytes::Bytes::from(body),
..Default::default()
})
.await
.map_err(|e| SessionError::DialogError(format!("Failed to send MESSAGE: {}", e)))?;
if response.status_code() == 200 {
let event = RvoipCrossCrateEvent::DialogToSession(
rvoip_infra_common::events::cross_crate::DialogToSessionEvent::MessageDelivered {
session_id: session_id.0.clone(),
}
);
let _ = self.global_coordinator.publish(Arc::new(event)).await;
} else if response.status_code() >= 400 {
let event = RvoipCrossCrateEvent::DialogToSession(
rvoip_infra_common::events::cross_crate::DialogToSessionEvent::MessageFailed {
session_id: session_id.0.clone(),
status_code: response.status_code(),
},
);
let _ = self.global_coordinator.publish(Arc::new(event)).await;
}
}
tracing::info!("MESSAGE sent successfully for session {}", session_id.0);
Ok(())
}
pub async fn start(&self) -> Result<()> {
self.dialog_api
.start()
.await
.map_err(|e| SessionError::DialogError(format!("Failed to start dialog API: {}", e)))?;
Ok(())
}
pub async fn stop(&self) -> Result<()> {
self.dialog_api
.stop()
.await
.map_err(|e| SessionError::DialogError(format!("Failed to stop dialog API: {}", e)))?;
Ok(())
}
}
impl Clone for DialogAdapter {
fn clone(&self) -> Self {
Self {
dialog_api: self.dialog_api.clone(),
store: self.store.clone(),
session_to_dialog: self.session_to_dialog.clone(),
dialog_to_session: self.dialog_to_session.clone(),
callid_to_session: self.callid_to_session.clone(),
outgoing_invite_tx: self.outgoing_invite_tx.clone(),
auto_emit_extra_headers: self.auto_emit_extra_headers.clone(),
global_coordinator: self.global_coordinator.clone(),
state_machine: self.state_machine.clone(),
outbound_proxy_uri: self.outbound_proxy_uri.clone(),
outbound_contact_params: self.outbound_contact_params.clone(),
symmetric_flow_params: self.symmetric_flow_params.clone(),
registration_auto_refresh: self.registration_auto_refresh,
registration_refresh_jitter_percent: self.registration_refresh_jitter_percent,
registration_refresh_tasks: self.registration_refresh_tasks.clone(),
cleanup_attempt_total: self.cleanup_attempt_total.clone(),
cleanup_mapped_total: self.cleanup_mapped_total.clone(),
cleanup_missing_total: self.cleanup_missing_total.clone(),
cleanup_call_ids_removed_total: self.cleanup_call_ids_removed_total.clone(),
cleanup_outgoing_invite_removed_total: self
.cleanup_outgoing_invite_removed_total
.clone(),
trace_redactor: self.trace_redactor.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
fn pub_addr() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7)), 54321)
}
#[test]
fn rewrite_contact_swaps_host_port_after_user() {
let input = "sip:alice@192.168.1.10:5060";
assert_eq!(
rewrite_contact_host(input, pub_addr()),
"sip:alice@203.0.113.7:54321"
);
}
#[test]
fn rewrite_contact_preserves_uri_params() {
let input = "sip:alice@192.168.1.10:5060;transport=tcp";
assert_eq!(
rewrite_contact_host(input, pub_addr()),
"sip:alice@203.0.113.7:54321;transport=tcp"
);
}
#[test]
fn rewrite_contact_handles_no_port_in_input() {
let input = "sip:alice@192.168.1.10";
assert_eq!(
rewrite_contact_host(input, pub_addr()),
"sip:alice@203.0.113.7:54321"
);
}
#[test]
fn rewrite_contact_handles_no_user() {
let input = "sip:192.168.1.10:5060";
assert_eq!(
rewrite_contact_host(input, pub_addr()),
"sip:203.0.113.7:54321"
);
}
#[test]
fn rewrite_contact_passes_through_sips_scheme() {
let input = "sips:alice@192.168.1.10:5061;transport=tls";
assert_eq!(
rewrite_contact_host(input, pub_addr()),
"sips:alice@203.0.113.7:54321;transport=tls"
);
}
use rvoip_sip_core::types::{uri::Uri, TypedHeader};
use std::str::FromStr;
#[test]
fn prepend_outbound_proxy_route_with_proxy_adds_first_route() {
let proxy = Uri::from_str("sip:sbc.example.com;lr").unwrap();
let headers = prepend_outbound_proxy_route(Vec::new(), Some(&proxy));
assert_eq!(headers.len(), 1);
match &headers[0] {
TypedHeader::Route(route) => {
assert_eq!(route.len(), 1);
assert_eq!(route[0].0.uri.to_string(), "sip:sbc.example.com;lr");
}
other => panic!("expected TypedHeader::Route, got {:?}", other),
}
}
#[test]
fn prepend_outbound_proxy_route_without_proxy_is_identity() {
let pai_uri = Uri::from_str("sip:alice@pai.example.com").unwrap();
let existing = vec![TypedHeader::PAssertedIdentity(
rvoip_sip_core::types::p_asserted_identity::PAssertedIdentity::with_uri(pai_uri),
)];
let headers = prepend_outbound_proxy_route(existing.clone(), None);
assert_eq!(headers.len(), existing.len());
assert!(matches!(headers[0], TypedHeader::PAssertedIdentity(_)));
}
#[test]
fn prepend_outbound_proxy_route_preserves_existing_before_route() {
let proxy = Uri::from_str("sip:sbc.example.com;lr").unwrap();
let pai_uri = Uri::from_str("sip:alice@pai.example.com").unwrap();
let existing = vec![TypedHeader::PAssertedIdentity(
rvoip_sip_core::types::p_asserted_identity::PAssertedIdentity::with_uri(pai_uri),
)];
let headers = prepend_outbound_proxy_route(existing, Some(&proxy));
assert_eq!(headers.len(), 2);
assert!(matches!(headers[0], TypedHeader::Route(_)));
assert!(matches!(headers[1], TypedHeader::PAssertedIdentity(_)));
}
}
pub(crate) fn rewrite_contact_host(input: &str, public: std::net::SocketAddr) -> String {
let (host_section, params_suffix) = match input.find(';') {
Some(idx) => (&input[..idx], &input[idx..]),
None => (input, ""),
};
let (scheme_prefix, after_scheme) = match host_section.find(':') {
Some(idx) => (&host_section[..=idx], &host_section[idx + 1..]),
None => return input.to_string(), };
let (user_at, _existing_host_port) = match after_scheme.find('@') {
Some(idx) => (&after_scheme[..=idx], &after_scheme[idx + 1..]),
None => ("", after_scheme),
};
format!("{}{}{}{}", scheme_prefix, user_at, public, params_suffix)
}
pub fn strip_via_below_top(request: &mut rvoip_sip_core::Request) -> usize {
use rvoip_sip_core::types::TypedHeader;
let mut seen_first_via = false;
let mut removed = 0;
request.headers.retain(|h| {
if matches!(h, TypedHeader::Via(_)) {
if seen_first_via {
removed += 1;
false
} else {
seen_first_via = true;
true
}
} else {
true
}
});
removed
}
pub fn strip_record_route_below_self(
request: &mut rvoip_sip_core::Request,
self_host: &str,
) -> usize {
use rvoip_sip_core::types::TypedHeader;
let self_lower = self_host.to_ascii_lowercase();
let mut removed = 0;
for header in request.headers.iter_mut() {
if let TypedHeader::RecordRoute(rr) = header {
let before = rr.0.len();
rr.0.retain(|entry| {
let host = entry.0.uri.host.to_string().to_ascii_lowercase();
host == self_lower
});
removed += before - rr.0.len();
}
}
request.headers.retain(|h| match h {
TypedHeader::RecordRoute(rr) => !rr.0.is_empty(),
_ => true,
});
removed
}
pub(crate) fn prepend_outbound_proxy_route(
extra_headers: Vec<rvoip_sip_core::types::TypedHeader>,
outbound_proxy_uri: Option<&rvoip_sip_core::types::uri::Uri>,
) -> Vec<rvoip_sip_core::types::TypedHeader> {
let mut headers = extra_headers;
if let Some(uri) = outbound_proxy_uri {
use rvoip_sip_core::types::{route::Route, TypedHeader};
headers.insert(0, TypedHeader::Route(Route::with_uri(uri.clone())));
}
headers
}
pub(crate) fn apply_outbound_extras_policy(
method: rvoip_sip_core::types::Method,
extras: Vec<rvoip_sip_core::types::TypedHeader>,
outbound_proxy_uri: Option<&rvoip_sip_core::types::uri::Uri>,
) -> Result<Vec<rvoip_sip_core::types::TypedHeader>> {
if let Err(violations) = crate::api::headers::policy::validate_outbound(method, &extras) {
let first = violations.into_iter().next().expect("non-empty on Err");
return Err(SessionError::HeaderPolicy {
method: first.method,
header: first.name,
reason: crate::api::headers::ViolationReason::StackManaged,
});
}
Ok(prepend_outbound_proxy_route(extras, outbound_proxy_uri))
}
pub(crate) fn apply_outbound_extras_policy_with_auth(
method: rvoip_sip_core::types::Method,
extras: Vec<rvoip_sip_core::types::TypedHeader>,
outbound_proxy_uri: Option<&rvoip_sip_core::types::uri::Uri>,
auth_header_name: &str,
auth_header_value: String,
) -> Result<Vec<rvoip_sip_core::types::TypedHeader>> {
let mut validated = apply_outbound_extras_policy(method, extras, outbound_proxy_uri)?;
let header_name = match auth_header_name {
"Proxy-Authorization" => rvoip_sip_core::types::header::HeaderName::ProxyAuthorization,
_ => rvoip_sip_core::types::header::HeaderName::Authorization,
};
validated.push(rvoip_sip_core::types::TypedHeader::Other(
header_name,
rvoip_sip_core::types::headers::HeaderValue::Raw(auth_header_value.into_bytes()),
));
Ok(validated)
}