Skip to content
Open
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
29 changes: 29 additions & 0 deletions src/dialog/registration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ pub struct Registration {
/// domain in SIP headers. Used for NAT traversal with load-balanced
/// proxy clusters where DNS may resolve to different IPs.
pub outbound_proxy: Option<std::net::SocketAddr>,
/// Service-Route set (RFC 3608) learned from the last successful
/// registration `200 OK`. These entries are the proxies the registrar
/// (e.g. an IMS S-CSCF) wants traversed on subsequent requests; a UA
/// preloads them as `Route` headers on later out-of-dialog requests.
/// Populated on each `200 OK`; empty when the response carried none.
pub service_route: Vec<crate::sip::typed::ServiceRoute>,
}

impl Registration {
Expand Down Expand Up @@ -173,6 +179,7 @@ impl Registration {
public_address: None,
call_id,
outbound_proxy: None,
service_route: Vec::new(),
}
}

Expand Down Expand Up @@ -209,6 +216,20 @@ impl Registration {
self.public_address.clone()
}

/// Get the Service-Route set (RFC 3608) from the last successful
/// registration.
///
/// Returns the ordered list of routes the registrar asked the user agent
/// to traverse on subsequent requests. In IMS this is the originating
/// route set advertised by the S-CSCF. The slice is empty when the last
/// `200 OK` carried no `Service-Route` header.
///
/// This accessor only exposes the learned set; applying it to outgoing
/// requests (preloading `Route` headers) is left to the caller.
pub fn service_route(&self) -> &[crate::sip::typed::ServiceRoute] {
&self.service_route
}

/// Get the registration expiration time
///
/// Returns the expiration time in seconds for the current registration.
Expand Down Expand Up @@ -551,9 +572,17 @@ impl Registration {
);
self.public_address = received;
}

// RFC 3608: adopt the Service-Route set advertised by
// the registrar as the preloaded route set for later
// out-of-dialog requests. Malformed values are ignored
// rather than failing the registration.
self.service_route = resp.typed_service_route_headers().unwrap_or_default();

debug!(
status = %resp.status_code,
contact = ?self.contact.as_ref().map(|c| c.uri.to_string()),
service_route = self.service_route.len(),
"registration do_request done"
);
return Ok(resp);
Expand Down
7 changes: 7 additions & 0 deletions src/sip/headers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub enum Header {
Require(Require),
RetryAfter(RetryAfter),
Route(Route),
ServiceRoute(ServiceRoute),
Server(Server),
Subject(Subject),
SubscriptionState(SubscriptionState),
Expand Down Expand Up @@ -108,6 +109,7 @@ impl std::fmt::Display for Header {
Self::Require(inner) => write!(f, "{}", inner),
Self::RetryAfter(inner) => write!(f, "{}", inner),
Self::Route(inner) => write!(f, "{}", inner),
Self::ServiceRoute(inner) => write!(f, "{}", inner),
Self::Server(inner) => write!(f, "{}", inner),
Self::Subject(inner) => write!(f, "{}", inner),
Self::SubscriptionState(inner) => write!(f, "{}", inner),
Expand Down Expand Up @@ -178,6 +180,7 @@ impl Header {
Self::Require(_) => "Require",
Self::RetryAfter(_) => "Retry-After",
Self::Route(_) => "Route",
Self::ServiceRoute(_) => "Service-Route",
Self::Server(_) => "Server",
Self::Subject(_) => "Subject",
Self::SubscriptionState(_) => "Subscription-State",
Expand Down Expand Up @@ -246,6 +249,7 @@ impl Header {
Self::Require(h) => h.value(),
Self::RetryAfter(h) => h.value(),
Self::Route(h) => h.value(),
Self::ServiceRoute(h) => h.value(),
Self::Server(h) => h.value(),
Self::Subject(h) => h.value(),
Self::SubscriptionState(h) => h.value(),
Expand Down Expand Up @@ -459,6 +463,9 @@ pub fn make_header(name: &str, value: String) -> Header {
n if n.eq_ignore_ascii_case("Require") => Header::Require(Require::new(value)),
n if n.eq_ignore_ascii_case("Retry-After") => Header::RetryAfter(RetryAfter::new(value)),
n if n.eq_ignore_ascii_case("Route") => Header::Route(Route::new(value)),
n if n.eq_ignore_ascii_case("Service-Route") => {
Header::ServiceRoute(ServiceRoute::new(value))
}
n if n.eq_ignore_ascii_case("Server") => Header::Server(Server::new(value)),
n if n.eq_ignore_ascii_case("Subject") || n.eq_ignore_ascii_case("s") => {
Header::Subject(Subject::new(value))
Expand Down
2 changes: 2 additions & 0 deletions src/sip/headers/typed/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub use proxy_authenticate::ProxyAuthenticate;
pub use proxy_authorization::ProxyAuthorization;
pub use record_route::RecordRoute;
pub use route::Route;
pub use service_route::ServiceRoute;
pub use to::To;
pub use via::Via;
pub use www_authenticate::WwwAuthenticate;
Expand All @@ -46,6 +47,7 @@ pub mod proxy_authenticate;
pub mod proxy_authorization;
pub mod record_route;
pub mod route;
pub mod service_route;
pub mod to;
pub mod via;
pub mod www_authenticate;
160 changes: 160 additions & 0 deletions src/sip/headers/typed/service_route.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
use super::parse_helpers::parse_display_uri_params_str;
use crate::sip::{uri::Param, uri::ParamsExt, Error, Header, Uri};

/// Typed `Service-Route` header (RFC 3608).
///
/// A registrar returns one or more `Service-Route` headers in a REGISTER
/// `200 OK` to tell the user agent which proxies to traverse for subsequent
/// requests within the registration. In IMS these entries form the originating
/// route set advertised by the S-CSCF; the UA preloads them as `Route` headers
/// on later requests such as the initial INVITE.
///
/// The grammar mirrors `Route`/`Record-Route`: a comma-separated list of
/// name-addr values, each with optional URI and header parameters.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ServiceRoute {
pub display_name: Option<String>,
pub uri: Uri,
pub params: Vec<Param>,
}

fn split_service_route_values(s: &str) -> Vec<String> {
let mut values = Vec::new();
let mut current = String::new();
let mut angle_depth = 0usize;
let mut in_quotes = false;
for ch in s.chars() {
match ch {
'"' => {
in_quotes = !in_quotes;
current.push(ch);
}
'<' if !in_quotes => {
angle_depth += 1;
current.push(ch);
}
'>' if !in_quotes => {
angle_depth = angle_depth.saturating_sub(1);
current.push(ch);
}
',' if !in_quotes && angle_depth == 0 => {
let v = current.trim().to_string();
if !v.is_empty() {
values.push(v);
}
current.clear();
}
_ => current.push(ch),
}
}
let v = current.trim().to_string();
if !v.is_empty() {
values.push(v);
}
values
}

impl ServiceRoute {
pub fn parse(s: &str) -> Result<Self, Error> {
let (display_name, uri, params) = parse_display_uri_params_str(s)?;
Ok(ServiceRoute {
display_name,
uri,
params,
})
}

/// Parse a single `Service-Route` header value that may contain several
/// comma-separated entries into one `ServiceRoute` per entry.
pub fn parse_header_list(s: &str) -> Result<Vec<Self>, Error> {
split_service_route_values(s)
.into_iter()
.map(|v| Self::parse(&v))
.collect()
}

pub fn has_lr(&self) -> bool {
self.uri.has_lr()
}
}

impl std::fmt::Display for ServiceRoute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.display_name {
Some(name) => write!(f, "\"{}\" <{}>", name, self.uri)?,
None => write!(f, "<{}>", self.uri)?,
}
for p in &self.params {
write!(f, "{}", p)?;
}
Ok(())
}
}

impl std::convert::From<Uri> for ServiceRoute {
fn from(uri: Uri) -> Self {
Self {
display_name: None,
uri,
params: vec![],
}
}
}

impl std::convert::From<ServiceRoute> for String {
fn from(r: ServiceRoute) -> String {
r.to_string()
}
}

impl std::convert::From<ServiceRoute> for Header {
fn from(r: ServiceRoute) -> Header {
Header::ServiceRoute(crate::sip::headers::untyped::ServiceRoute::new(
r.to_string(),
))
}
}

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

#[cfg(test)]
mod tests {
use super::ServiceRoute;

#[test]
fn service_route_single_lr() {
let sr = ServiceRoute::parse("<sip:scscf.home.net;lr>").unwrap();
assert_eq!(sr.uri.to_string(), "sip:scscf.home.net;lr");
assert!(sr.has_lr());
}

#[test]
fn service_route_display_roundtrip() {
let s = "<sip:scscf.home.net;lr>";
let sr = ServiceRoute::parse(s).unwrap();
assert_eq!(sr.to_string(), s);
}

#[test]
fn service_route_multi_uri() {
// 3GPP TS 24.229 style: S-CSCF followed by P-CSCF in the route set.
let routes =
ServiceRoute::parse_header_list("<sip:scscf.home.net;lr>, <sip:pcscf.visited.net;lr>")
.unwrap();
assert_eq!(routes.len(), 2);
assert_eq!(routes[0].uri.to_string(), "sip:scscf.home.net;lr");
assert_eq!(routes[1].uri.to_string(), "sip:pcscf.visited.net;lr");
assert!(routes[0].has_lr());
assert!(routes[1].has_lr());
}

#[test]
fn service_route_to_header_and_back() {
let sr = ServiceRoute::parse("<sip:scscf.home.net;lr>").unwrap();
let header: crate::sip::Header = sr.clone().into();
// The untyped header value drops the surrounding header name but keeps
// the name-addr, so re-parsing yields the same typed value.
let reparsed = ServiceRoute::parse(header.value()).unwrap();
assert_eq!(sr, reparsed);
}
}
1 change: 1 addition & 0 deletions src/sip/headers/untyped.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ untyped_header!(RSeq, "RSeq", Header::RSeq);
untyped_header!(RAck, "RAck", Header::RAck);
untyped_header!(Privacy, "Privacy", Header::Privacy);
untyped_header!(Path, "Path", Header::Path);
untyped_header!(ServiceRoute, "Service-Route", Header::ServiceRoute);
untyped_header!(Identity, "Identity", Header::Identity);
untyped_header!(UserToUser, "User-to-User", Header::UserToUser);
untyped_header!(SessionId, "Session-ID", Header::SessionId);
Expand Down
47 changes: 47 additions & 0 deletions src/sip/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,21 @@ pub trait HeadersExt: HasHeaders {
}
Ok(routes)
}
fn service_route_headers(&self) -> Vec<&ServiceRoute> {
all_headers!(self.headers().iter(), Header::ServiceRoute)
}
fn service_route_header(&self) -> Option<&ServiceRoute> {
header_opt!(self.headers().iter(), Header::ServiceRoute)
}
fn typed_service_route_headers(&self) -> Result<Vec<crate::sip::typed::ServiceRoute>, Error> {
let mut routes = Vec::new();
for r in self.service_route_headers() {
routes.extend(crate::sip::typed::ServiceRoute::parse_header_list(
r.value(),
)?);
}
Ok(routes)
}
fn user_agent_header(&self) -> Option<&UserAgent> {
header_opt!(self.headers().iter(), Header::UserAgent)
}
Expand Down Expand Up @@ -1174,6 +1189,38 @@ mod tests {
assert!(paths[0].value().contains("edge.restsend.com"));
}

#[test]
fn new_headers_service_route_header() {
// REGISTER 200 OK carrying an IMS-style Service-Route set. RFC 3608
// allows the entries to arrive either as one comma-separated header or
// as several header lines; both must fold into the same route set.
let msg: SipMessage = concat!(
"SIP/2.0 200 OK\r\n",
"Via: SIP/2.0/TCP edge.home.net;branch=z9hG4bKtest\r\n",
"From: <sip:alice@home.net>;tag=abc\r\n",
"To: <sip:alice@home.net>;tag=xyz\r\n",
"Call-ID: sr-test@edge.home.net\r\n",
"CSeq: 1 REGISTER\r\n",
"Service-Route: <sip:scscf.home.net;lr>\r\n",
"Service-Route: <sip:pcscf.visited.net;lr>\r\n",
"Contact: <sip:alice@192.0.2.5:5060>;expires=600\r\n",
"Content-Length: 0\r\n",
"\r\n"
)
.try_into()
.unwrap();

let raw = msg.service_route_headers();
assert_eq!(raw.len(), 2);

let routes = msg.typed_service_route_headers().unwrap();
assert_eq!(routes.len(), 2);
assert_eq!(routes[0].uri.to_string(), "sip:scscf.home.net;lr");
assert_eq!(routes[1].uri.to_string(), "sip:pcscf.visited.net;lr");
assert!(routes[0].has_lr());
assert!(routes[1].has_lr());
}

#[test]
fn header_value_helper_case_insensitive() {
let req: Request = invite_request().try_into().unwrap();
Expand Down
4 changes: 2 additions & 2 deletions src/sip/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ pub mod param {
pub mod typed {
pub use super::headers::typed::{
Allow, Authorization, CSeq, Contact, From, HistoryInfo, HistoryInfoEntry, Identity,
ProxyAuthenticate, ProxyAuthorization, RecordRoute, Route, To, Tokenize, TypedHeader, Via,
WwwAuthenticate,
ProxyAuthenticate, ProxyAuthorization, RecordRoute, Route, ServiceRoute, To, Tokenize,
TypedHeader, Via, WwwAuthenticate,
};
pub mod tokenizers {
pub use crate::sip::headers::typed::tokenizers::{AuthTokenizer, CseqTokenizer};
Expand Down