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
16 changes: 16 additions & 0 deletions src/dialog/invitation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ pub struct InviteOption {
/// generated. Reuse the same value on transferred calls (REFER/Replaces)
/// to keep the session identifiable across dialogs.
pub session_id: Option<String>,
/// RFC 3608: preloaded route set for this out-of-dialog request. Each entry
/// is emitted as a `Route` header, in order, ahead of the caller-supplied
/// headers. Typically obtained from
/// [`Registration::preloaded_route_set`](crate::dialog::registration::Registration::preloaded_route_set)
/// so the request follows the path an IMS S-CSCF advertised at
/// registration. Empty by default, leaving existing behaviour unchanged.
pub route_set: Vec<crate::sip::typed::Route>,
}

pub struct DialogGuard {
Expand Down Expand Up @@ -364,6 +371,15 @@ impl DialogLayer {
call_id,
);

// RFC 3608: preload the Service-Route set learned at registration as
// Route headers, in order, so this out-of-dialog request traverses the
// proxies the registrar (e.g. an IMS S-CSCF) requires. Plain push, not
// unique_push, because a route set legitimately has several Route
// headers.
for route in &opt.route_set {
request.headers.push(route.clone().into());
}

let contact = if let Some(ref addr) = transport_addr {
let mut uri = opt.contact.clone();
uri.host_with_port = addr.addr.clone();
Expand Down
14 changes: 14 additions & 0 deletions src/dialog/registration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,20 @@ impl Registration {
&self.service_route
}

/// Build the preloaded `Route` set for out-of-dialog requests from the
/// learned Service-Route set (RFC 3608 §5.2).
///
/// The returned routes are in the order the registrar sent them and can be
/// assigned to [`InviteOption::route_set`] (or otherwise pushed as `Route`
/// headers) so an initial request such as an INVITE traverses the
/// registrar's required path. Returns an empty vector when the last
/// registration carried no Service-Route.
///
/// [`InviteOption::route_set`]: crate::dialog::invitation::InviteOption::route_set
pub fn preloaded_route_set(&self) -> Vec<crate::sip::typed::Route> {
self.service_route.iter().cloned().map(Into::into).collect()
}

/// Get the registration expiration time
///
/// Returns the expiration time in seconds for the current registration.
Expand Down
75 changes: 75 additions & 0 deletions src/dialog/tests/test_dialog_layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,3 +597,78 @@ async fn test_make_invite_request_with_tls_transport_uses_sips_scheme() -> crate

Ok(())
}

#[tokio::test]
async fn test_make_invite_request_preloads_service_route() -> crate::Result<()> {
let token = CancellationToken::new();
let tl = TransportLayer::new(token.child_token());

// A UDP address so get_via has something to work with.
let udp_conn = UdpConnection::create_connection("127.0.0.1:0".parse()?, None, None).await?;
tl.add_transport(crate::transport::SipConnection::Udp(udp_conn));

let endpoint = EndpointBuilder::new()
.with_user_agent("rsipstack-test")
.with_transport_layer(tl)
.build();
let dialog_layer = DialogLayer::new(endpoint.inner.clone());

// Two-hop route set as an IMS S-CSCF would advertise via Service-Route.
let route_set = vec![
crate::sip::typed::Route::parse("<sip:scscf.home.net;lr>")?,
crate::sip::typed::Route::parse("<sip:pcscf.visited.net;lr>")?,
];

let opt = crate::dialog::invitation::InviteOption {
caller: crate::sip::Uri::try_from("sip:alice@example.com")?,
callee: crate::sip::Uri::try_from("sip:bob@example.com")?,
contact: crate::sip::Uri::try_from("sip:alice@192.168.1.10:5060")?,
route_set,
..Default::default()
};

let request = dialog_layer.make_invite_request(&opt)?;

// Both hops must be preloaded as Route headers, in the advertised order.
let routes = request.typed_route_headers()?;
assert_eq!(
routes.len(),
2,
"both Service-Route hops should be preloaded"
);
assert_eq!(routes[0].uri.to_string(), "sip:scscf.home.net;lr");
assert_eq!(routes[1].uri.to_string(), "sip:pcscf.visited.net;lr");

Ok(())
}

#[tokio::test]
async fn test_make_invite_request_without_route_set_has_no_route() -> crate::Result<()> {
let token = CancellationToken::new();
let tl = TransportLayer::new(token.child_token());
let udp_conn = UdpConnection::create_connection("127.0.0.1:0".parse()?, None, None).await?;
tl.add_transport(crate::transport::SipConnection::Udp(udp_conn));

let endpoint = EndpointBuilder::new()
.with_user_agent("rsipstack-test")
.with_transport_layer(tl)
.build();
let dialog_layer = DialogLayer::new(endpoint.inner.clone());

let opt = crate::dialog::invitation::InviteOption {
caller: crate::sip::Uri::try_from("sip:alice@example.com")?,
callee: crate::sip::Uri::try_from("sip:bob@example.com")?,
contact: crate::sip::Uri::try_from("sip:alice@192.168.1.10:5060")?,
..Default::default()
};

let request = dialog_layer.make_invite_request(&opt)?;

// Default (empty) route set must not add any Route header.
assert!(
request.route_headers().is_empty(),
"no Route header expected when route_set is empty"
);

Ok(())
}
23 changes: 23 additions & 0 deletions src/sip/headers/typed/service_route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,19 @@ impl std::convert::From<ServiceRoute> for Header {
}
}

impl std::convert::From<ServiceRoute> for super::Route {
/// Convert a learned `Service-Route` entry into the `Route` header a user
/// agent preloads on subsequent requests (RFC 3608 §5.2). The name-addr is
/// carried over verbatim; only the header field name differs on the wire.
fn from(r: ServiceRoute) -> super::Route {
super::Route {
display_name: r.display_name,
uri: r.uri,
params: r.params,
}
}
}

impl<'a> super::TypedHeader<'a> for ServiceRoute {}

#[cfg(test)]
Expand Down Expand Up @@ -157,4 +170,14 @@ mod tests {
let reparsed = ServiceRoute::parse(header.value()).unwrap();
assert_eq!(sr, reparsed);
}

#[test]
fn service_route_into_route_preserves_name_addr() {
let sr = ServiceRoute::parse("<sip:scscf.home.net;lr>").unwrap();
let route: crate::sip::typed::Route = sr.clone().into();
assert_eq!(route.uri, sr.uri);
assert_eq!(route.display_name, sr.display_name);
assert_eq!(route.params, sr.params);
assert!(route.has_lr());
}
}
Loading