From 6cffbdbb1371daa6d93900c29f66df1e293996c7 Mon Sep 17 00:00:00 2001 From: yeoleobun Date: Tue, 10 Mar 2026 15:23:21 +0800 Subject: [PATCH] fix: register contact parseing --- src/dialog/registration.rs | 17 ++-- src/rsip_ext.rs | 186 +++++++++++++++++++++++++++++++++++-- 2 files changed, 189 insertions(+), 14 deletions(-) diff --git a/src/dialog/registration.rs b/src/dialog/registration.rs index 2548c9da..399c3ed7 100644 --- a/src/dialog/registration.rs +++ b/src/dialog/registration.rs @@ -13,11 +13,8 @@ use crate::{ transport::SipAddr, Result, }; -use rsip::{ - prelude::{HeadersExt, ToTypedHeader}, - Param, Response, SipMessage, StatusCode, -}; -use tracing::debug; +use rsip::{Param, Response, SipMessage, StatusCode}; +use tracing::{debug, warn}; /// SIP Registration Client /// @@ -469,11 +466,15 @@ impl Registration { let received = resp.via_received(); // Update contact header from response - match resp.contact_header() { + match resp.typed_contact_headers() { Ok(contact) => { - self.contact = contact.typed().ok(); + if let Some(contact) = contact.first().cloned() { + self.contact = Some(contact); + } + } + Err(e) => { + warn!("failed to parse contact: {:?}", e); } - Err(_) => {} }; if self.public_address != received { debug!( diff --git a/src/rsip_ext.rs b/src/rsip_ext.rs index 283a44af..a2ef3071 100644 --- a/src/rsip_ext.rs +++ b/src/rsip_ext.rs @@ -22,6 +22,7 @@ pub trait RsipResponseExt { fn reason_phrase(&self) -> Option<&str>; fn via_received(&self) -> Option; fn content_type(&self) -> Option; + fn typed_contact_headers(&self) -> Result>; fn contact_uri(&self) -> Result; fn remote_uri(&self, destination: Option<&SipAddr>) -> Result; } @@ -61,14 +62,20 @@ impl RsipResponseExt for rsip::Response { None } + fn typed_contact_headers(&self) -> Result> { + let contact = match self.contact_header() { + Ok(contact) => contact, + Err(rsip::Error::MissingHeader(_)) => return Ok(Vec::new()), + Err(e) => return Err(Error::from(e)), + }; + parse_typed_contact_header_list(contact.value()) + } + fn contact_uri(&self) -> Result { - let contact = self.contact_header()?; - if let Ok(typed_contact) = contact.typed() { - Ok(typed_contact.uri) + if let Some(contact) = self.typed_contact_headers()?.first() { + Ok(contact.uri.clone()) } else { - let mut uri = extract_uri_from_contact(contact.value())?; - uri.headers.clear(); - Ok(uri) + Err(Error::Error("missing Contact header".to_string())) } } @@ -140,6 +147,132 @@ pub fn extract_uri_from_contact(line: &str) -> Result { return Ok(uri); } +pub fn parse_typed_contact_header_list(line: &str) -> Result> { + let trimmed = line.trim(); + if trimmed.is_empty() { + return Err(Error::Error("empty Contact header".to_string())); + } + + let values = split_contact_header_values(trimmed)?; + let mut contacts = Vec::with_capacity(values.len()); + for value in values { + contacts.push(parse_typed_contact(value.as_str())?); + } + + Ok(contacts) +} + +pub fn parse_typed_contact(line: &str) -> Result { + if let Ok(contact) = rsip::headers::Contact::from(line).typed() { + return Ok(contact); + } + + let trimmed = line.trim(); + if trimmed.is_empty() { + return Err(Error::Error("empty Contact header".to_string())); + } + + let (display_name, uri_part, header_params_part) = if let Some(start) = trimmed.find('<') { + let end = trimmed[start..] + .find('>') + .map(|offset| start + offset) + .ok_or_else(|| Error::Error("invalid Contact header: missing '>'".to_string()))?; + let display = trimmed[..start].trim(); + let display_name = if display.is_empty() { + None + } else { + Some(display.trim_matches('"').to_string()) + }; + let uri = &trimmed[start + 1..end]; + let params = trimmed[end + 1..].trim(); + (display_name, uri, params) + } else { + let (uri, params) = split_uri_and_header_params(trimmed); + (None, uri, params) + }; + + let mut uri = extract_uri_from_contact(uri_part)?; + uri.headers.clear(); + + let params = parse_contact_header_params(header_params_part)?; + + Ok(rsip::typed::Contact { + display_name, + uri, + params, + }) +} + +pub fn split_contact_header_values(line: &str) -> Result> { + let mut values = Vec::new(); + let mut current = String::new(); + let mut in_quotes = false; + let mut angle_depth = 0usize; + + for ch in line.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 value = current.trim(); + if !value.is_empty() { + values.push(value.to_string()); + } + current.clear(); + } + _ => current.push(ch), + } + } + + let value = current.trim(); + if !value.is_empty() { + values.push(value.to_string()); + } + + if values.is_empty() { + return Err(Error::Error("empty Contact header".to_string())); + } + + Ok(values) +} + +fn split_uri_and_header_params(input: &str) -> (&str, &str) { + let path = input.split_once('?').map_or(input, |(path, _)| path); + if let Some(idx) = path.find(';') { + (&input[..idx], &input[idx..]) + } else { + (input, "") + } +} + +fn parse_contact_header_params(input: &str) -> Result> { + let trimmed = input.trim(); + if trimmed.is_empty() { + return Ok(Vec::new()); + } + + let params = separated_list0(char(';'), custom_contact_param) + .parse(trimmed.trim_start_matches(';')) + .map_err(|_| Error::Error(format!("invalid Contact header params: {}", input)))? + .1; + + params + .into_iter() + .filter(|param| !param.name.is_empty()) + .map(|param| rsip::Param::try_from((param.name, param.value)).map_err(Error::from)) + .collect() +} + fn apply_tokenizer_params(uri: &mut rsip::Uri, tokenizer: &CustomContactTokenizer) { for (name, value) in tokenizer.params.iter().map(|p| (p.name, p.value)) { if name.eq_ignore_ascii_case("transport") { @@ -356,3 +489,44 @@ fn test_rsip_headers_ext() { ] ); } + +#[test] +fn test_parse_typed_contact_headers_from_masked_kamailio_response() { + use rsip::Response; + + let response: Response = concat!( + "SIP/2.0 200 OK\r\n", + "Via: SIP/2.0/UDP 192.0.2.10:13050;branch=z9hG4bK-test;rport=60326;received=198.51.100.20\r\n", + "From: ;tag=from-tag\r\n", + "To: ;tag=to-tag\r\n", + "CSeq: 1 REGISTER\r\n", + "Call-ID: test-call-id@example.com\r\n", + "Contact: ;expires=573;+sip.instance=\"\", ;expires=3600\r\n", + "Content-Length: 0\r\n", + "\r\n" + ) + .try_into() + .expect("failed to parse response"); + + let contacts = response + .typed_contact_headers() + .expect("failed to parse typed Contact headers"); + + assert_eq!(contacts.len(), 2); + assert_eq!(contacts[0].uri.to_string(), "sip:1001@198.51.100.20:56734"); + assert_eq!(contacts[1].uri.to_string(), "sip:1001@192.0.2.10:13050"); + assert_eq!( + contacts[0].expires().map(|expires| expires.value()), + Some("573") + ); + assert_eq!( + contacts[1].expires().map(|expires| expires.value()), + Some("3600") + ); + assert!(contacts[0].params.iter().any(|param| matches!( + param, + rsip::Param::Other(name, Some(value)) + if name.value().eq_ignore_ascii_case("+sip.instance") + && value.value() == "" + ))); +}