use std::borrow::Cow;
use std::fmt;
use std::net::SocketAddr;
fn parse_socket_addr(address: &str) -> Option<SocketAddr> {
if let Ok(addr) = address.parse() {
return Some(addr);
}
let (ip, port) = address.strip_prefix('[')?.split_once("]:")?;
Some(SocketAddr::new(ip.parse().ok()?, port.parse().ok()?))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SkipReason {
PartialFirstFrame,
OversizedFrame,
MidStreamSkip,
ReplayedFrame,
IncompleteFrame,
InvalidHeader,
}
impl fmt::Display for SkipReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SkipReason::PartialFirstFrame => f.write_str("partial first frame"),
SkipReason::OversizedFrame => f.write_str("oversized frame"),
SkipReason::MidStreamSkip => f.write_str("mid-stream skip"),
SkipReason::ReplayedFrame => f.write_str("replayed frame (logrotate)"),
SkipReason::IncompleteFrame => f.write_str("incomplete frame"),
SkipReason::InvalidHeader => f.write_str("invalid header"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SkipTracking {
CountOnly,
TrackRegions,
CaptureData,
}
#[derive(Debug, Clone)]
pub struct UnparsedRegion {
pub offset: u64,
pub length: u64,
pub reason: SkipReason,
pub data: Option<Vec<u8>>,
}
#[derive(Debug, Default, Clone)]
pub struct ParseStats {
pub bytes_read: u64,
pub bytes_skipped: u64,
pub unparsed_regions: Vec<UnparsedRegion>,
}
impl ParseStats {
pub fn drain_regions(&mut self) -> Vec<UnparsedRegion> {
std::mem::take(&mut self.unparsed_regions)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Direction {
Recv,
Sent,
}
impl fmt::Display for Direction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Direction::Recv => f.write_str("recv"),
Direction::Sent => f.write_str("sent"),
}
}
}
impl Direction {
pub fn preposition(&self) -> &'static str {
match self {
Direction::Recv => "from",
Direction::Sent => "to",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Transport {
Tcp,
Udp,
Tls,
Wss,
}
impl fmt::Display for Transport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Transport::Tcp => f.write_str("tcp"),
Transport::Udp => f.write_str("udp"),
Transport::Tls => f.write_str("tls"),
Transport::Wss => f.write_str("wss"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Timestamp {
TimeOnly {
hour: u8,
min: u8,
sec: u8,
usec: u32,
},
DateTime {
year: u16,
month: u8,
day: u8,
hour: u8,
min: u8,
sec: u8,
usec: u32,
},
}
impl Timestamp {
pub fn time_of_day_secs(&self) -> u32 {
let (h, m, s) = match self {
Timestamp::TimeOnly { hour, min, sec, .. } => (*hour, *min, *sec),
Timestamp::DateTime { hour, min, sec, .. } => (*hour, *min, *sec),
};
h as u32 * 3600 + m as u32 * 60 + s as u32
}
pub fn sort_key(&self) -> (u16, u8, u8, u8, u8, u8, u32) {
match self {
Timestamp::TimeOnly {
hour,
min,
sec,
usec,
} => (0, 0, 0, *hour, *min, *sec, *usec),
Timestamp::DateTime {
year,
month,
day,
hour,
min,
sec,
usec,
} => (*year, *month, *day, *hour, *min, *sec, *usec),
}
}
}
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Timestamp::TimeOnly {
hour,
min,
sec,
usec,
} => write!(f, "{hour:02}:{min:02}:{sec:02}.{usec:06}"),
Timestamp::DateTime {
year,
month,
day,
hour,
min,
sec,
usec,
} => write!(
f,
"{year:04}-{month:02}-{day:02} {hour:02}:{min:02}:{sec:02}.{usec:06}"
),
}
}
}
#[derive(Debug, Clone)]
pub struct Frame {
pub direction: Direction,
pub byte_count: usize,
pub transport: Transport,
pub address: String,
pub timestamp: Timestamp,
pub content: Vec<u8>,
}
impl Frame {
pub fn socket_addr(&self) -> Option<SocketAddr> {
parse_socket_addr(&self.address)
}
}
#[derive(Debug, Clone)]
pub struct SipMessage {
pub direction: Direction,
pub transport: Transport,
pub address: String,
pub timestamp: Timestamp,
pub content: Vec<u8>,
pub frame_count: usize,
}
impl SipMessage {
pub fn socket_addr(&self) -> Option<SocketAddr> {
parse_socket_addr(&self.address)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SipMessageType {
Request {
method: String,
uri: String,
},
Response {
code: u16,
reason: String,
},
}
impl fmt::Display for SipMessageType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SipMessageType::Request { method, uri } => write!(f, "{method} {uri}"),
SipMessageType::Response { code, reason } => write!(f, "{code} {reason}"),
}
}
}
impl SipMessageType {
pub fn summary(&self) -> Cow<'_, str> {
match self {
SipMessageType::Request { method, .. } => Cow::Borrowed(method),
SipMessageType::Response { code, reason } => Cow::Owned(format!("{code} {reason}")),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Headers(pub Vec<(String, String)>);
impl Headers {
pub fn value(&self, name: &str) -> Option<&str> {
self.0
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_str())
}
}
impl std::ops::Deref for Headers {
type Target = [(String, String)];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for Headers {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<Vec<(String, String)>> for Headers {
fn from(headers: Vec<(String, String)>) -> Self {
Headers(headers)
}
}
impl FromIterator<(String, String)> for Headers {
fn from_iter<I: IntoIterator<Item = (String, String)>>(iter: I) -> Self {
Headers(iter.into_iter().collect())
}
}
impl<'a> IntoIterator for &'a Headers {
type Item = &'a (String, String);
type IntoIter = std::slice::Iter<'a, (String, String)>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
#[derive(Debug, Clone)]
pub struct ParsedSipMessage {
pub direction: Direction,
pub transport: Transport,
pub address: String,
pub timestamp: Timestamp,
pub message_type: SipMessageType,
pub headers: Headers,
pub body: Vec<u8>,
pub frame_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SipFragment {
pub message_type: Option<SipMessageType>,
pub headers: Headers,
pub body: Vec<u8>,
}
impl SipFragment {
pub fn header_value(&self, name: &str) -> Option<&str> {
self.headers.value(name)
}
pub fn content_type(&self) -> Option<&str> {
self.header_value("Content-Type")
.or_else(|| self.header_value("c"))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MimePart {
pub headers: Headers,
pub body: Vec<u8>,
}
impl MimePart {
pub fn content_type(&self) -> Option<&str> {
self.headers.value("Content-Type")
}
pub fn header_value(&self, name: &str) -> Option<&str> {
self.headers.value(name)
}
pub fn content_id(&self) -> Option<&str> {
self.header_value("Content-ID")
}
pub fn content_disposition(&self) -> Option<&str> {
self.header_value("Content-Disposition")
}
pub fn content_transfer_encoding(&self) -> Option<&str> {
self.header_value("Content-Transfer-Encoding")
}
}
impl ParsedSipMessage {
pub fn socket_addr(&self) -> Option<SocketAddr> {
parse_socket_addr(&self.address)
}
pub fn call_id(&self) -> Option<&str> {
self.header_value("Call-ID")
.or_else(|| self.header_value("i"))
}
pub fn content_type(&self) -> Option<&str> {
self.header_value("Content-Type")
.or_else(|| self.header_value("c"))
}
pub fn content_length(&self) -> Option<usize> {
self.header_value("Content-Length")
.or_else(|| self.header_value("l"))
.and_then(|v| v.trim().parse().ok())
}
pub fn cseq(&self) -> Option<&str> {
self.header_value("CSeq")
}
pub fn method(&self) -> Option<&str> {
match &self.message_type {
SipMessageType::Request { method, .. } => Some(method),
SipMessageType::Response { .. } => {
self.cseq().and_then(|cs| cs.split_whitespace().nth(1))
}
}
}
pub fn body_data(&self) -> Cow<'_, str> {
String::from_utf8_lossy(&self.body)
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::new();
match &self.message_type {
SipMessageType::Request { method, uri } => {
out.extend_from_slice(format!("{method} {uri} SIP/2.0\r\n").as_bytes());
}
SipMessageType::Response { code, reason } => {
out.extend_from_slice(format!("SIP/2.0 {code} {reason}\r\n").as_bytes());
}
}
for (name, value) in &self.headers {
out.extend_from_slice(format!("{name}: {value}\r\n").as_bytes());
}
out.extend_from_slice(b"\r\n");
if !self.body.is_empty() {
out.extend_from_slice(&self.body);
}
out
}
pub fn header_value(&self, name: &str) -> Option<&str> {
self.headers.value(name)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_parsed(
msg_type: SipMessageType,
headers: Vec<(&str, &str)>,
body: &[u8],
) -> ParsedSipMessage {
ParsedSipMessage {
direction: Direction::Recv,
transport: Transport::Tcp,
address: "10.0.0.1:5060".into(),
timestamp: Timestamp::TimeOnly {
hour: 12,
min: 0,
sec: 0,
usec: 0,
},
message_type: msg_type,
headers: Headers(
headers
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
),
body: body.to_vec(),
frame_count: 1,
}
}
fn make_frame(address: &str) -> Frame {
Frame {
direction: Direction::Recv,
byte_count: 0,
transport: Transport::Tcp,
address: address.into(),
timestamp: Timestamp::TimeOnly {
hour: 0,
min: 0,
sec: 0,
usec: 0,
},
content: Vec::new(),
}
}
fn make_message(address: &str) -> SipMessage {
SipMessage {
direction: Direction::Recv,
transport: Transport::Tcp,
address: address.into(),
timestamp: Timestamp::TimeOnly {
hour: 0,
min: 0,
sec: 0,
usec: 0,
},
content: Vec::new(),
frame_count: 1,
}
}
fn parsed_with_address(address: &str) -> ParsedSipMessage {
let mut msg = make_parsed(
SipMessageType::Request {
method: "OPTIONS".into(),
uri: "sip:host".into(),
},
vec![],
b"",
);
msg.address = address.into();
msg
}
#[test]
fn socket_addr_ipv4() {
let addr = make_frame("10.0.0.1:5060").socket_addr().unwrap();
assert!(addr.is_ipv4());
assert_eq!(addr.port(), 5060);
assert_eq!(addr.ip().to_string(), "10.0.0.1");
}
#[test]
fn socket_addr_ipv6_bracketed() {
let addr = make_message("[2001:db8::1]:5061").socket_addr().unwrap();
assert!(addr.is_ipv6());
assert_eq!(addr.port(), 5061);
assert_eq!(addr.ip().to_string(), "2001:db8::1");
}
#[test]
fn socket_addr_ipv4_bracketed() {
let addr = make_frame("[198.51.100.7]:5060").socket_addr().unwrap();
assert!(addr.is_ipv4());
assert_eq!(addr.port(), 5060);
assert_eq!(addr.ip().to_string(), "198.51.100.7");
}
#[test]
fn socket_addr_on_parsed_message() {
let addr = parsed_with_address("192.0.2.4:5080").socket_addr().unwrap();
assert_eq!(addr.port(), 5080);
}
#[test]
fn socket_addr_rejects_non_addresses() {
for bad in [
"345.678.987.654:5060",
"10.0.0.1",
"host.example.test:5060",
"2001:db8::1:5060",
"",
] {
assert!(
make_frame(bad).socket_addr().is_none(),
"should not parse: {bad}"
);
assert!(make_message(bad).socket_addr().is_none());
assert!(parsed_with_address(bad).socket_addr().is_none());
}
}
#[test]
fn to_bytes_request_no_body() {
let msg = make_parsed(
SipMessageType::Request {
method: "OPTIONS".into(),
uri: "sip:host".into(),
},
vec![("Call-ID", "test")],
b"",
);
let bytes = msg.to_bytes();
let text = String::from_utf8(bytes).unwrap();
assert!(text.starts_with("OPTIONS sip:host SIP/2.0\r\n"));
assert!(text.contains("Call-ID: test\r\n"));
assert!(text.ends_with("\r\n\r\n"));
}
#[test]
fn to_bytes_request_with_body() {
let body = b"v=0\r\ns=-\r\n";
let msg = make_parsed(
SipMessageType::Request {
method: "INVITE".into(),
uri: "sip:host".into(),
},
vec![("Call-ID", "test")],
body,
);
let bytes = msg.to_bytes();
assert!(bytes.ends_with(body));
}
#[test]
fn to_bytes_response() {
let msg = make_parsed(
SipMessageType::Response {
code: 200,
reason: "OK".into(),
},
vec![("Call-ID", "resp-test")],
b"",
);
let bytes = msg.to_bytes();
let text = String::from_utf8(bytes).unwrap();
assert!(text.starts_with("SIP/2.0 200 OK\r\n"));
}
#[test]
fn body_data_valid_utf8() {
let msg = make_parsed(
SipMessageType::Request {
method: "MESSAGE".into(),
uri: "sip:host".into(),
},
vec![],
b"hello world",
);
assert_eq!(&*msg.body_data(), "hello world");
}
#[test]
fn body_data_empty() {
let msg = make_parsed(
SipMessageType::Request {
method: "OPTIONS".into(),
uri: "sip:host".into(),
},
vec![],
b"",
);
assert_eq!(&*msg.body_data(), "");
}
#[test]
fn body_data_binary() {
let msg = make_parsed(
SipMessageType::Request {
method: "MESSAGE".into(),
uri: "sip:host".into(),
},
vec![],
&[0xFF, 0xFE],
);
assert!(msg.body_data().contains('\u{FFFD}'));
}
#[test]
fn body_text_non_json_passthrough() {
let msg = make_parsed(
SipMessageType::Request {
method: "INVITE".into(),
uri: "sip:host".into(),
},
vec![("Content-Type", "application/sdp")],
b"v=0\r\ns=-\r\n",
);
assert_eq!(msg.body_text().as_ref(), msg.body_data().as_ref());
}
#[test]
fn body_text_json_unescapes_newlines() {
let msg = make_parsed(
SipMessageType::Request {
method: "NOTIFY".into(),
uri: "sip:host".into(),
},
vec![("Content-Type", "application/json")],
br#"{"invite":"INVITE sip:host SIP/2.0\r\nTo: <sip:host>\r\n"}"#,
);
let text = msg.body_text();
assert!(
text.contains("INVITE sip:host SIP/2.0\r\nTo: <sip:host>\r\n"),
"JSON \\r\\n should be unescaped to actual CRLF, got: {text:?}"
);
}
#[test]
fn body_text_plus_json_content_type() {
let msg = make_parsed(
SipMessageType::Request {
method: "NOTIFY".into(),
uri: "sip:host".into(),
},
vec![(
"Content-Type",
"application/emergencyCallData.AbandonedCall+json",
)],
br#"{"invite":"line1\nline2"}"#,
);
let text = msg.body_text();
assert!(
text.contains("line1\nline2"),
"application/*+json should trigger unescaping, got: {text:?}"
);
}
#[test]
fn body_data_preserves_json_escapes() {
let raw = br#"{"key":"value\nwith\\escapes"}"#;
let msg = make_parsed(
SipMessageType::Request {
method: "NOTIFY".into(),
uri: "sip:host".into(),
},
vec![("Content-Type", "application/json")],
raw,
);
assert_eq!(
msg.body_data().as_ref(),
r#"{"key":"value\nwith\\escapes"}"#,
"body_data() must preserve raw escapes"
);
}
}