diff --git a/src/common/uri/param/mod.rs b/src/common/uri/param/mod.rs index 31f4cc1..ca1745c 100644 --- a/src/common/uri/param/mod.rs +++ b/src/common/uri/param/mod.rs @@ -169,13 +169,13 @@ pub mod tokenizer { let (rem, (_, name, value)) = tuple(( tag(";"), - take_while(I::is_token), //rfc3261 includes other chars as well, needs fixing.. + take_while(I::is_token), opt(map( tuple(( tag("="), alt(( recognize(delimited(tag("\""), take_until("\""), tag("\""))), - take_while(I::is_token), + take_while(I::is_param_value), )), )), |t| t.1, diff --git a/src/lib.rs b/src/lib.rs index c1ff1e4..5fe3e97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -317,6 +317,9 @@ pub trait AbstractInputItem: nom::AsChar + std::cmp::PartialEq + From fn is_alphabetic(c: I) -> bool; fn is_alphanumeric(c: I) -> bool; fn is_token(c: I) -> bool; + /// RFC 3261 §25.1: paramchar allows param-unreserved characters in addition to token chars. + /// param-unreserved = "[" / "]" / "/" / ":" / "&" / "+" / "$" + fn is_param_value(c: I) -> bool; } impl<'a> AbstractInput<'a, char> for &'a str { @@ -337,6 +340,10 @@ impl AbstractInputItem for char { fn is_token(c: char) -> bool { Self::is_alphanumeric(c) || "-.!%*_+`'~".contains(c) } + + fn is_param_value(c: char) -> bool { + Self::is_token(c) || "[]/:&+$".contains(c) + } } impl<'a> AbstractInput<'a, u8> for &'a [u8] { @@ -358,6 +365,10 @@ impl AbstractInputItem for u8 { is_alphanumeric(c) || "-.!%*_+`'~".contains(char::from(c)) } + + fn is_param_value(c: u8) -> bool { + Self::is_token(c) || b"[]/:&+$".contains(&c) + } } pub(crate) mod utils { diff --git a/tests/headers/record_route/typed.rs b/tests/headers/record_route/typed.rs index 392fda7..96d3574 100644 --- a/tests/headers/record_route/typed.rs +++ b/tests/headers/record_route/typed.rs @@ -50,6 +50,64 @@ mod display { } } +mod param_values_with_colons { + use super::*; + use rsip::common::uri::param::{OtherParam, OtherParamValue}; + + /// Kamailio encodes internal routing info in Record-Route URI parameters + /// using values that contain SIP URIs (e.g. du=sip:host:port). + /// The colon must be preserved per RFC 3261 §25.1 (param-unreserved). + #[test] + fn kamailio_du_param_round_trip() -> Result<(), rsip::Error> { + let input = ""; + let tokenizer = UriWithParamsListTokenizer::tokenize(input).unwrap().1; + let record_route: RecordRoute = tokenizer.try_into()?; + let uris = record_route.uris(); + + assert_eq!(uris.len(), 1); + let uri = &uris[0]; + + // Verify all params are preserved, including du with colons + assert_eq!( + uri.uri.params, + vec![ + Param::Other(OtherParam::from("lr"), Some(OtherParamValue::from("on"))), + Param::Other(OtherParam::from("ftag"), Some(OtherParamValue::from("d4nwJ0jF"))), + Param::Other( + OtherParam::from("du"), + Some(OtherParamValue::from("sip:95.143.188.49:5060")) + ), + Param::Other(OtherParam::from("did"), Some(OtherParamValue::from("893.d6d1"))), + ] + ); + + // Verify round-trip preserves the full value + // UriWithParams::Display already wraps in angle brackets + let output = uri.to_string(); + assert_eq!(output, input); + + Ok(()) + } + + #[test] + fn param_value_with_slashes_and_colons() -> Result<(), rsip::Error> { + let input = ""; + let tokenizer = UriWithParamsListTokenizer::tokenize(input).unwrap().1; + let record_route: RecordRoute = tokenizer.try_into()?; + let uri = &record_route.uris()[0]; + + let dest_param = uri.uri.params.iter().find(|p| { + matches!(p, Param::Other(name, _) if name.to_string() == "dest") + }); + assert!(dest_param.is_some()); + if let Some(Param::Other(_, Some(val))) = dest_param { + assert_eq!(val.to_string(), "sip:10.0.0.1:5080/udp"); + } + + Ok(()) + } +} + mod try_from_tokenizer { use super::*;