use sip_header::SipHeader;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidHeaderName(String);
impl std::fmt::Display for InvalidHeaderName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"invalid SIP header name {:?}: contains \\n or \\r",
self.0
)
}
}
impl std::error::Error for InvalidHeaderName {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SipHeaderPrefix {
Invite,
Request,
Response,
Provisional,
Bye,
NoBye,
}
impl SipHeaderPrefix {
pub fn as_str(&self) -> &'static str {
match self {
Self::Invite => "sip_i_",
Self::Request => "sip_h_",
Self::Response => "sip_rh_",
Self::Provisional => "sip_ph_",
Self::Bye => "sip_bye_h_",
Self::NoBye => "sip_nobye_h_",
}
}
}
impl std::fmt::Display for SipHeaderPrefix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseSipPassthroughError(pub String);
impl std::fmt::Display for ParseSipPassthroughError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "not a SIP passthrough variable: {}", self.0)
}
}
impl std::error::Error for ParseSipPassthroughError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SipPassthroughHeader {
prefix: SipHeaderPrefix,
canonical_name: String,
wire: String,
}
const PREFIX_PATTERNS: &[(SipHeaderPrefix, &str)] = &[
(SipHeaderPrefix::NoBye, "sip_nobye_h_"),
(SipHeaderPrefix::Bye, "sip_bye_h_"),
(SipHeaderPrefix::Provisional, "sip_ph_"),
(SipHeaderPrefix::Response, "sip_rh_"),
(SipHeaderPrefix::Invite, "sip_i_"),
(SipHeaderPrefix::Request, "sip_h_"),
];
fn validate_header_name(name: &str) -> Result<(), InvalidHeaderName> {
if name.is_empty() || name.contains('\n') || name.contains('\r') {
return Err(InvalidHeaderName(name.to_string()));
}
Ok(())
}
fn build_wire(prefix: SipHeaderPrefix, canonical: &str) -> String {
match prefix {
SipHeaderPrefix::Invite => {
let mut wire = String::with_capacity(6 + canonical.len());
wire.push_str("sip_i_");
for ch in canonical.chars() {
if ch == '-' {
wire.push('_');
} else {
wire.push(ch.to_ascii_lowercase());
}
}
wire
}
_ => {
let pfx = prefix.as_str();
let mut wire = String::with_capacity(pfx.len() + canonical.len());
wire.push_str(pfx);
wire.push_str(canonical);
wire
}
}
}
impl SipPassthroughHeader {
pub fn new(prefix: SipHeaderPrefix, header: SipHeader) -> Self {
let canonical = header
.as_str()
.to_string();
let wire = build_wire(prefix, &canonical);
Self {
prefix,
canonical_name: canonical,
wire,
}
}
pub fn new_raw(
prefix: SipHeaderPrefix,
name: impl Into<String>,
) -> Result<Self, InvalidHeaderName> {
let canonical = name.into();
validate_header_name(&canonical)?;
let wire = build_wire(prefix, &canonical);
Ok(Self {
prefix,
canonical_name: canonical,
wire,
})
}
pub fn invite(header: SipHeader) -> Self {
Self::new(SipHeaderPrefix::Invite, header)
}
pub fn invite_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
Self::new_raw(SipHeaderPrefix::Invite, name)
}
pub fn request(header: SipHeader) -> Self {
Self::new(SipHeaderPrefix::Request, header)
}
pub fn request_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
Self::new_raw(SipHeaderPrefix::Request, name)
}
pub fn response(header: SipHeader) -> Self {
Self::new(SipHeaderPrefix::Response, header)
}
pub fn response_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
Self::new_raw(SipHeaderPrefix::Response, name)
}
pub fn provisional(header: SipHeader) -> Self {
Self::new(SipHeaderPrefix::Provisional, header)
}
pub fn provisional_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
Self::new_raw(SipHeaderPrefix::Provisional, name)
}
pub fn bye(header: SipHeader) -> Self {
Self::new(SipHeaderPrefix::Bye, header)
}
pub fn bye_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
Self::new_raw(SipHeaderPrefix::Bye, name)
}
pub fn no_bye(header: SipHeader) -> Self {
Self::new(SipHeaderPrefix::NoBye, header)
}
pub fn no_bye_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
Self::new_raw(SipHeaderPrefix::NoBye, name)
}
pub fn prefix(&self) -> SipHeaderPrefix {
self.prefix
}
pub fn canonical_name(&self) -> &str {
&self.canonical_name
}
pub fn as_str(&self) -> &str {
&self.wire
}
pub fn extract_from(&self, message: &str) -> Vec<String> {
sip_header::extract_header(message, &self.canonical_name)
}
pub fn is_array_header(&self) -> bool {
if self.prefix != SipHeaderPrefix::Invite {
return false;
}
self.canonical_name
.parse::<SipHeader>()
.map(|h| h.is_multi_valued())
.unwrap_or(false)
}
}
impl super::VariableName for SipPassthroughHeader {
fn as_str(&self) -> &str {
&self.wire
}
}
impl std::fmt::Display for SipPassthroughHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.wire)
}
}
impl AsRef<str> for SipPassthroughHeader {
fn as_ref(&self) -> &str {
&self.wire
}
}
impl From<SipPassthroughHeader> for String {
fn from(h: SipPassthroughHeader) -> Self {
h.wire
}
}
impl std::str::FromStr for SipPassthroughHeader {
type Err = ParseSipPassthroughError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
for &(prefix, pat) in PREFIX_PATTERNS {
if let Some(suffix) = s.strip_prefix(pat) {
if suffix.is_empty() {
return Err(ParseSipPassthroughError(s.to_string()));
}
let canonical = match prefix {
SipHeaderPrefix::Invite => {
let with_hyphens = suffix.replace('_', "-");
match with_hyphens.parse::<SipHeader>() {
Ok(h) => h
.as_str()
.to_string(),
Err(_) => with_hyphens,
}
}
_ => match suffix.parse::<SipHeader>() {
Ok(h) => h
.as_str()
.to_string(),
Err(_) => suffix.to_string(),
},
};
return Ok(Self {
prefix,
canonical_name: canonical,
wire: s.to_string(),
});
}
}
let lower = s.to_ascii_lowercase();
if lower != s {
for &(prefix, pat) in PREFIX_PATTERNS {
if let Some(suffix) = lower.strip_prefix(pat) {
if suffix.is_empty() {
return Err(ParseSipPassthroughError(s.to_string()));
}
let canonical = match prefix {
SipHeaderPrefix::Invite => {
let with_hyphens = suffix.replace('_', "-");
match with_hyphens.parse::<SipHeader>() {
Ok(h) => h
.as_str()
.to_string(),
Err(_) => with_hyphens,
}
}
_ => match suffix.parse::<SipHeader>() {
Ok(h) => h
.as_str()
.to_string(),
Err(_) => suffix.to_string(),
},
};
let wire = build_wire(prefix, &canonical);
return Ok(Self {
prefix,
canonical_name: canonical,
wire,
});
}
}
}
Err(ParseSipPassthroughError(s.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invite_typed_wire_format() {
assert_eq!(
SipPassthroughHeader::invite(SipHeader::CallInfo).as_str(),
"sip_i_call_info"
);
assert_eq!(
SipPassthroughHeader::invite(SipHeader::PAssertedIdentity).as_str(),
"sip_i_p_asserted_identity"
);
assert_eq!(
SipPassthroughHeader::invite(SipHeader::Via).as_str(),
"sip_i_via"
);
assert_eq!(
SipPassthroughHeader::invite(SipHeader::CallId).as_str(),
"sip_i_call_id"
);
}
#[test]
fn request_typed_wire_format() {
assert_eq!(
SipPassthroughHeader::request(SipHeader::CallInfo).as_str(),
"sip_h_Call-Info"
);
assert_eq!(
SipPassthroughHeader::request(SipHeader::PAssertedIdentity).as_str(),
"sip_h_P-Asserted-Identity"
);
}
#[test]
fn response_typed_wire_format() {
assert_eq!(
SipPassthroughHeader::response(SipHeader::CallInfo).as_str(),
"sip_rh_Call-Info"
);
}
#[test]
fn provisional_typed_wire_format() {
assert_eq!(
SipPassthroughHeader::provisional(SipHeader::AlertInfo).as_str(),
"sip_ph_Alert-Info"
);
}
#[test]
fn bye_typed_wire_format() {
assert_eq!(
SipPassthroughHeader::bye(SipHeader::Reason).as_str(),
"sip_bye_h_Reason"
);
}
#[test]
fn no_bye_typed_wire_format() {
assert_eq!(
SipPassthroughHeader::no_bye(SipHeader::Reason).as_str(),
"sip_nobye_h_Reason"
);
}
#[test]
fn raw_custom_header() {
assert_eq!(
SipPassthroughHeader::request_raw("X-Tenant")
.unwrap()
.as_str(),
"sip_h_X-Tenant"
);
assert_eq!(
SipPassthroughHeader::invite_raw("X-Custom")
.unwrap()
.as_str(),
"sip_i_x_custom"
);
}
#[test]
fn raw_rejects_newlines() {
assert!(SipPassthroughHeader::request_raw("X-Bad\nHeader").is_err());
assert!(SipPassthroughHeader::request_raw("X-Bad\rHeader").is_err());
assert!(SipPassthroughHeader::request_raw("").is_err());
}
#[test]
fn display_matches_as_str() {
let h = SipPassthroughHeader::request(SipHeader::CallInfo);
assert_eq!(h.to_string(), h.as_str());
}
#[test]
fn from_str_request() {
let parsed: SipPassthroughHeader = "sip_h_Call-Info"
.parse()
.unwrap();
assert_eq!(parsed.prefix(), SipHeaderPrefix::Request);
assert_eq!(parsed.canonical_name(), "Call-Info");
assert_eq!(parsed.as_str(), "sip_h_Call-Info");
}
#[test]
fn from_str_invite() {
let parsed: SipPassthroughHeader = "sip_i_call_info"
.parse()
.unwrap();
assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
assert_eq!(parsed.canonical_name(), "Call-Info");
assert_eq!(parsed.as_str(), "sip_i_call_info");
}
#[test]
fn from_str_invite_p_asserted_identity() {
let parsed: SipPassthroughHeader = "sip_i_p_asserted_identity"
.parse()
.unwrap();
assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
assert_eq!(parsed.canonical_name(), "P-Asserted-Identity");
}
#[test]
fn from_str_response() {
let parsed: SipPassthroughHeader = "sip_rh_Call-Info"
.parse()
.unwrap();
assert_eq!(parsed.prefix(), SipHeaderPrefix::Response);
assert_eq!(parsed.canonical_name(), "Call-Info");
}
#[test]
fn from_str_bye() {
let parsed: SipPassthroughHeader = "sip_bye_h_Reason"
.parse()
.unwrap();
assert_eq!(parsed.prefix(), SipHeaderPrefix::Bye);
assert_eq!(parsed.canonical_name(), "Reason");
}
#[test]
fn from_str_no_bye() {
let parsed: SipPassthroughHeader = "sip_nobye_h_Reason"
.parse()
.unwrap();
assert_eq!(parsed.prefix(), SipHeaderPrefix::NoBye);
assert_eq!(parsed.canonical_name(), "Reason");
}
#[test]
fn from_str_unknown_custom_header() {
let parsed: SipPassthroughHeader = "sip_h_X-Tenant"
.parse()
.unwrap();
assert_eq!(parsed.prefix(), SipHeaderPrefix::Request);
assert_eq!(parsed.canonical_name(), "X-Tenant");
}
#[test]
fn from_str_invite_unknown_custom() {
let parsed: SipPassthroughHeader = "sip_i_x_custom"
.parse()
.unwrap();
assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
assert_eq!(parsed.canonical_name(), "x-custom");
}
#[test]
fn from_str_case_insensitive_invite() {
let parsed: SipPassthroughHeader = "SIP_I_CALL_INFO"
.parse()
.unwrap();
assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
assert_eq!(parsed.canonical_name(), "Call-Info");
}
#[test]
fn from_str_rejects_no_prefix() {
assert!("call_info"
.parse::<SipPassthroughHeader>()
.is_err());
assert!("sip_call_info"
.parse::<SipPassthroughHeader>()
.is_err());
}
#[test]
fn from_str_rejects_empty_suffix() {
assert!("sip_h_"
.parse::<SipPassthroughHeader>()
.is_err());
assert!("sip_i_"
.parse::<SipPassthroughHeader>()
.is_err());
}
#[test]
fn from_str_round_trip_all_prefixes() {
let headers = [
SipPassthroughHeader::invite(SipHeader::CallInfo),
SipPassthroughHeader::request(SipHeader::CallInfo),
SipPassthroughHeader::response(SipHeader::CallInfo),
SipPassthroughHeader::provisional(SipHeader::AlertInfo),
SipPassthroughHeader::bye(SipHeader::Reason),
SipPassthroughHeader::no_bye(SipHeader::Reason),
];
for h in &headers {
let parsed: SipPassthroughHeader = h
.as_str()
.parse()
.unwrap();
assert_eq!(&parsed, h, "round-trip failed for {}", h.as_str());
}
}
#[test]
fn prefix_accessor() {
assert_eq!(
SipPassthroughHeader::invite(SipHeader::Via).prefix(),
SipHeaderPrefix::Invite
);
assert_eq!(
SipPassthroughHeader::request(SipHeader::Via).prefix(),
SipHeaderPrefix::Request
);
}
#[test]
fn canonical_name_accessor() {
assert_eq!(
SipPassthroughHeader::invite(SipHeader::CallInfo).canonical_name(),
"Call-Info"
);
assert_eq!(
SipPassthroughHeader::request_raw("X-Tenant")
.unwrap()
.canonical_name(),
"X-Tenant"
);
}
#[test]
fn is_array_header_invite_multi_valued() {
assert!(SipPassthroughHeader::invite(SipHeader::Via).is_array_header());
assert!(SipPassthroughHeader::invite(SipHeader::CallInfo).is_array_header());
assert!(SipPassthroughHeader::invite(SipHeader::PAssertedIdentity).is_array_header());
assert!(SipPassthroughHeader::invite(SipHeader::RecordRoute).is_array_header());
}
#[test]
fn is_array_header_invite_single_valued() {
assert!(!SipPassthroughHeader::invite(SipHeader::From).is_array_header());
assert!(!SipPassthroughHeader::invite(SipHeader::CallId).is_array_header());
assert!(!SipPassthroughHeader::invite(SipHeader::ContentType).is_array_header());
}
#[test]
fn is_array_header_non_invite_always_false() {
assert!(!SipPassthroughHeader::request(SipHeader::Via).is_array_header());
assert!(!SipPassthroughHeader::response(SipHeader::CallInfo).is_array_header());
}
#[test]
fn is_array_header_raw_unknown() {
assert!(!SipPassthroughHeader::invite_raw("X-Custom")
.unwrap()
.is_array_header());
}
#[test]
fn extract_from_sip_message() {
let msg = "INVITE sip:bob@example.com SIP/2.0\r\n\
Call-Info: <sip:example.com>;answer-after=0\r\n\
\r\n";
let h = SipPassthroughHeader::invite(SipHeader::CallInfo);
assert_eq!(
h.extract_from(msg),
vec!["<sip:example.com>;answer-after=0"]
);
}
#[test]
fn extract_from_missing() {
let msg = "INVITE sip:bob@example.com SIP/2.0\r\n\
From: Alice <sip:alice@example.com>\r\n\
\r\n";
let h = SipPassthroughHeader::invite(SipHeader::CallInfo);
assert!(h
.extract_from(msg)
.is_empty());
}
#[test]
fn variable_name_trait() {
use crate::variables::VariableName;
let h = SipPassthroughHeader::request(SipHeader::CallInfo);
let name: &str = VariableName::as_str(&h);
assert_eq!(name, "sip_h_Call-Info");
}
}