1use std::borrow::Cow;
2
3use sip_header::extract_all_headers;
4
5use crate::finders::CRLF;
6use crate::frame::ParseError;
7use crate::message::MessageIterator;
8use crate::sip::content_type::{extract_boundary, normalize_media_type};
9use crate::sip::json::unescape_json_body;
10use crate::sip::multipart::{is_multipart_type, split_multipart};
11use crate::startline::{bytes_to_str, parse_first_line, parse_first_line_ref, StartLineRef};
12use crate::types::{
13 value_or_compact, Headers, MimePart, ParseStats, ParsedSipMessage, SipFragment, SipMessage,
14 SkipTracking, UnparsedRegion,
15};
16
17pub(crate) mod content_type;
18mod fragment;
19mod json;
20mod multipart;
21#[cfg(test)]
22pub(crate) mod test_support;
23
24pub use content_type::is_json_content_type;
25pub use fragment::parse_sipfrag;
26
27pub(crate) trait HasHeaders {
31 fn headers(&self) -> &Headers;
32
33 fn body(&self) -> &[u8];
34
35 fn content_type(&self) -> Option<&str> {
36 value_or_compact(self.headers(), "Content-Type")
37 }
38
39 fn media_type(&self) -> Option<Cow<'_, str>> {
40 self.content_type().map(normalize_media_type)
41 }
42
43 fn is_multipart(&self) -> bool {
44 is_multipart_type(self.content_type())
45 }
46
47 fn multipart_boundary(&self) -> Option<&str> {
48 extract_boundary(self.content_type()?)
49 }
50
51 fn body_parts(&self) -> Option<Vec<MimePart>> {
52 split_multipart(self.content_type(), self.body())
53 }
54
55 fn body_text(&self) -> Cow<'_, str> {
56 match self.content_type() {
57 Some(ct) if is_json_content_type(ct) => Cow::Owned(unescape_json_body(self.body())),
58 _ => String::from_utf8_lossy(self.body()),
59 }
60 }
61
62 fn json_field(&self, key: &str) -> Option<String> {
63 let ct = self.content_type()?;
64 if !is_json_content_type(ct) {
65 return None;
66 }
67 let value: serde_json::Value = serde_json::from_slice(self.body()).ok()?;
68 let obj = value.as_object()?;
69 obj.get(key)?.as_str().map(|s| s.to_string())
70 }
71}
72
73impl HasHeaders for ParsedSipMessage {
74 fn headers(&self) -> &Headers {
75 &self.headers
76 }
77
78 fn body(&self) -> &[u8] {
79 &self.body
80 }
81}
82
83impl HasHeaders for MimePart {
84 fn headers(&self) -> &Headers {
85 &self.headers
86 }
87
88 fn body(&self) -> &[u8] {
89 &self.body
90 }
91}
92
93impl HasHeaders for SipFragment {
94 fn headers(&self) -> &Headers {
95 &self.headers
96 }
97
98 fn body(&self) -> &[u8] {
99 &self.body
100 }
101}
102
103impl SipMessage {
104 pub fn parse(&self) -> Result<ParsedSipMessage, ParseError> {
107 parse_sip_message(self)
108 }
109
110 pub fn method(&self) -> Option<&str> {
119 let first_line_end = CRLF.find(&self.content)?;
120 match parse_first_line_ref(&self.content[..first_line_end]).ok()? {
121 StartLineRef::Request { method, .. } => std::str::from_utf8(method).ok(),
122 StartLineRef::Response { .. } => {
123 let (headers, _) = split_headers_body(&self.content, first_line_end + 2);
124 cseq_method(headers)
125 }
126 }
127 }
128}
129
130fn cseq_method(headers: &[u8]) -> Option<&str> {
135 let mut lines = headers.split(|&b| b == b'\n').peekable();
136
137 while let Some(line) = lines.next() {
138 let line = match line {
139 [rest @ .., b'\r'] => rest,
140 rest => rest,
141 };
142 if line.is_empty() {
143 return None;
144 }
145 if matches!(line.first(), Some(b' ' | b'\t')) {
146 continue;
147 }
148 let Some(colon) = memchr::memchr(b':', line) else {
149 continue;
150 };
151 let mut name = &line[..colon];
152 while let [rest @ .., b' ' | b'\t'] = name {
153 name = rest;
154 }
155 if name.contains(&b' ') || !name.eq_ignore_ascii_case(b"CSeq") {
156 continue;
157 }
158
159 if matches!(lines.peek(), Some([b' ' | b'\t', ..])) {
160 return None;
161 }
162 let value = &line[colon + 1..];
163 if !value.is_ascii() {
164 return None;
165 }
166 return std::str::from_utf8(value)
167 .ok()?
168 .split_ascii_whitespace()
169 .nth(1);
170 }
171
172 None
173}
174
175pub struct ParsedMessageIterator<R> {
196 inner: MessageIterator<R>,
197}
198
199impl<R: std::io::Read> ParsedMessageIterator<R> {
200 pub fn new(reader: R) -> Self {
202 ParsedMessageIterator {
203 inner: MessageIterator::new(reader),
204 }
205 }
206
207 pub fn capture_skipped(mut self, enable: bool) -> Self {
211 self.inner = self.inner.capture_skipped(enable);
212 self
213 }
214
215 pub fn skip_tracking(mut self, tracking: SkipTracking) -> Self {
217 self.inner = self.inner.skip_tracking(tracking);
218 self
219 }
220
221 pub fn parse_stats(&self) -> &ParseStats {
223 self.inner.parse_stats()
224 }
225
226 pub fn drain_unparsed(&mut self) -> Vec<UnparsedRegion> {
228 self.inner.drain_unparsed()
229 }
230}
231
232impl<R: std::io::Read> Iterator for ParsedMessageIterator<R> {
233 type Item = Result<ParsedSipMessage, ParseError>;
234
235 fn next(&mut self) -> Option<Self::Item> {
236 let msg = match self.inner.next()? {
237 Ok(m) => m,
238 Err(e) => return Some(Err(e)),
239 };
240 Some(msg.parse())
241 }
242}
243
244fn content_preview(content: &[u8], max_len: usize) -> String {
245 let len = content.len().min(max_len);
246 let s = String::from_utf8_lossy(&content[..len]);
247 let mut out = String::with_capacity(s.len());
248 for c in s.chars() {
249 match c {
250 '\r' => out.push_str("\\r"),
251 '\n' => out.push_str("\\n"),
252 '\t' => out.push_str("\\t"),
253 '\0' => out.push_str("\\0"),
254 c if c.is_control() => out.push_str(&format!("\\x{:02x}", c as u32)),
255 c => out.push(c),
256 }
257 }
258 if content.len() > max_len {
259 out.push_str("...");
260 }
261 out
262}
263
264fn parse_sip_message(msg: &SipMessage) -> Result<ParsedSipMessage, ParseError> {
265 let content = &msg.content;
266
267 if content
268 .iter()
269 .all(|&b| matches!(b, b'\r' | b'\n' | b' ' | b'\t'))
270 {
271 return Err(ParseError::TransportNoise {
272 bytes: content.len(),
273 transport: msg.transport,
274 address: msg.address.clone(),
275 });
276 }
277
278 parse_sip_content(msg, content).map_err(|e| {
279 let reason = match e {
280 ParseError::InvalidMessage(reason) => reason,
281 other => return other,
282 };
283 let preview = content_preview(content, 200);
284 ParseError::InvalidMessage(format!(
285 "{} {}/{} at {} ({} frames, {} bytes): {reason}\n {preview}",
286 msg.direction,
287 msg.transport,
288 msg.address,
289 msg.timestamp,
290 msg.frame_count,
291 content.len(),
292 ))
293 })
294}
295
296fn parse_sip_content(msg: &SipMessage, content: &[u8]) -> Result<ParsedSipMessage, ParseError> {
297 let first_line_end = CRLF
299 .find(content)
300 .ok_or_else(|| ParseError::InvalidMessage("no CRLF found".into()))?;
301 let first_line = &content[..first_line_end];
302
303 let message_type = parse_first_line(first_line)?;
304
305 let (header_bytes, body) = split_headers_body(content, first_line_end + 2);
306 let headers = parse_headers(header_bytes);
307
308 Ok(ParsedSipMessage {
309 direction: msg.direction,
310 transport: msg.transport,
311 address: msg.address.clone(),
312 timestamp: msg.timestamp,
313 message_type,
314 headers,
315 body: body.to_vec(),
316 frame_count: msg.frame_count,
317 })
318}
319
320pub(crate) fn parse_headers(data: &[u8]) -> Headers {
321 Headers::from(extract_all_headers(&bytes_to_str(data)))
322}
323
324pub(crate) fn split_headers_body(data: &[u8], headers_start: usize) -> (&[u8], &[u8]) {
328 let start = headers_start.min(data.len());
329 let mut pos = start;
330 while let Some(rel) = memchr::memchr(b'\n', &data[pos..]) {
331 let line = match &data[pos..pos + rel] {
332 [rest @ .., b'\r'] => rest,
333 rest => rest,
334 };
335 if line.is_empty() {
336 return (&data[start..pos], &data[pos + rel + 1..]);
337 }
338 pos += rel + 1;
339 }
340 (&data[start..], &[][..])
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use crate::sip::test_support::make_sip_message;
347 use crate::types::{Direction, SipMessage, SipMessageType, Timestamp, Transport};
348
349 fn assert_agrees_with_parse(content: &[u8]) {
352 let msg = make_sip_message(content);
353 if let Some(cheap) = msg.method() {
354 let parsed = msg.parse().expect("classified message must parse");
355 assert_eq!(Some(cheap), parsed.method());
356 }
357 }
358
359 #[test]
360 fn method_from_request_line() {
361 let msg = make_sip_message(b"INVITE sip:user@host SIP/2.0\r\nCSeq: 1 INVITE\r\n\r\n");
362 assert_eq!(msg.method(), Some("INVITE"));
363 }
364
365 #[test]
366 fn method_from_response_cseq() {
367 let msg = make_sip_message(b"SIP/2.0 200 OK\r\nVia: x\r\nCSeq: 42 OPTIONS\r\n\r\n");
368 assert_eq!(msg.method(), Some("OPTIONS"));
369 }
370
371 #[test]
372 fn method_from_response_cseq_name_variants() {
373 let lower = make_sip_message(b"SIP/2.0 200 OK\r\ncseq: 1 BYE\r\n\r\n");
374 assert_eq!(lower.method(), Some("BYE"));
375
376 let padded = make_sip_message(b"SIP/2.0 200 OK\r\nCSeq \t: 1 BYE\r\n\r\n");
377 assert_eq!(padded.method(), Some("BYE"));
378
379 let tabbed = make_sip_message(b"SIP/2.0 200 OK\r\nCSeq:\t1\tBYE\r\n\r\n");
380 assert_eq!(tabbed.method(), Some("BYE"));
381 }
382
383 #[test]
384 fn method_takes_first_cseq_in_wire_order() {
385 let msg = make_sip_message(b"SIP/2.0 200 OK\r\nCSeq: 1 BYE\r\nCSeq: 2 INVITE\r\n\r\n");
386 assert_eq!(msg.method(), Some("BYE"));
387 assert_agrees_with_parse(b"SIP/2.0 200 OK\r\nCSeq: 1 BYE\r\nCSeq: 2 INVITE\r\n\r\n");
388 }
389
390 #[test]
391 fn method_none_on_folded_cseq() {
392 let content = b"SIP/2.0 200 OK\r\nCSeq: 1\r\n INVITE\r\n\r\n";
393 assert_eq!(make_sip_message(content).method(), None);
394 assert_agrees_with_parse(content);
395 }
396
397 #[test]
398 fn method_none_without_cseq() {
399 let msg = make_sip_message(b"SIP/2.0 200 OK\r\nVia: x\r\n\r\nCSeq: 1 INVITE\r\n");
400 assert_eq!(msg.method(), None);
401 }
402
403 #[test]
404 fn method_none_on_malformed_start_line() {
405 assert_eq!(
406 make_sip_message(b"INVITE sip:user@host SIP/3.0\r\nCSeq: 1 INVITE\r\n\r\n").method(),
407 None
408 );
409 assert_eq!(
410 make_sip_message(b"garbage\r\nCSeq: 1 INVITE\r\n\r\n").method(),
411 None
412 );
413 assert_eq!(make_sip_message(b"\r\n\r\n").method(), None);
414 }
415
416 #[test]
419 fn method_none_when_cseq_follows_lf_blank_line() {
420 let content = b"SIP/2.0 200 OK\r\nVia: x\n\r\nCSeq: 1 OPTIONS\r\n\r\n";
421 let parsed = make_sip_message(content).parse().unwrap();
422 assert_eq!(
423 parsed.method(),
424 None,
425 "precondition: the parsed side cannot see this CSeq"
426 );
427 assert_eq!(make_sip_message(content).method(), None);
428
429 assert_eq!(parsed.headers.len(), 1);
430 assert_eq!(parsed.header_value("Via"), Some("x"));
431 assert_eq!(parsed.body, b"CSeq: 1 OPTIONS\r\n\r\n");
432
433 let start_line = b"SIP/2.0 200 OK\r\n".len();
434 let (headers, body) = split_headers_body(content, start_line);
435 assert_eq!(headers, b"Via: x\n");
436 assert_eq!(
437 start_line + headers.len() + b"\r\n".len() + body.len(),
438 content.len(),
439 "every byte lands in the start line, the headers, the blank line or the body"
440 );
441 }
442
443 #[test]
445 fn method_none_on_non_ascii_cseq_value() {
446 let content = "SIP/2.0 200 OK\r\nCSeq: 1\u{a0}2 OPTIONS\r\n\r\n".as_bytes();
447 assert_eq!(make_sip_message(content).method(), None);
448 assert_agrees_with_parse(content);
449 }
450
451 #[test]
452 fn method_none_on_transport_noise() {
453 assert_eq!(make_sip_message(b"\r\n\r\n\r\n").method(), None);
454 assert_eq!(make_sip_message(b"").method(), None);
455 }
456
457 #[test]
458 fn non_utf8_header_value_falls_back_to_lossy() {
459 let mut content = b"OPTIONS sip:host SIP/2.0\r\nSubject: caf".to_vec();
460 content.push(0xE9);
461 content.extend_from_slice(b"\r\nContent-Length: 0\r\n\r\n");
462
463 let parsed = make_sip_message(&content).parse().unwrap();
464 assert_eq!(parsed.header_value("Subject"), Some("caf\u{fffd}"));
465 }
466
467 #[test]
468 fn parse_stats_delegates() {
469 let content =
470 b"OPTIONS sip:host SIP/2.0\r\nCall-ID: stats-test\r\nContent-Length: 0\r\n\r\n";
471 let header = format!(
472 "recv {} bytes from udp/10.0.0.1:5060 at 00:00:00.000000:\n",
473 content.len()
474 );
475 let mut data = header.into_bytes();
476 data.extend_from_slice(content);
477 data.extend_from_slice(b"\x0B\n");
478
479 let mut iter = ParsedMessageIterator::new(&data[..]);
480 let parsed: Vec<_> = iter.by_ref().collect::<Result<Vec<_>, _>>().unwrap();
481 assert_eq!(parsed.len(), 1);
482 let stats = iter.parse_stats();
483 assert_eq!(stats.bytes_read, data.len() as u64);
484 assert_eq!(stats.bytes_skipped, 0);
485 }
486
487 #[test]
488 fn parse_options_request() {
489 let content = b"OPTIONS sip:user@host SIP/2.0\r\n\
490 Via: SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK-1\r\n\
491 From: <sip:user@host>;tag=abc\r\n\
492 To: <sip:user@host>\r\n\
493 Call-ID: test-call-id@host\r\n\
494 CSeq: 1 OPTIONS\r\n\
495 Content-Length: 0\r\n\
496 \r\n";
497 let msg = make_sip_message(content);
498 let parsed = msg.parse().unwrap();
499
500 assert_eq!(
501 parsed.message_type,
502 SipMessageType::Request {
503 method: "OPTIONS".into(),
504 uri: "sip:user@host".into()
505 }
506 );
507 assert_eq!(parsed.call_id(), Some("test-call-id@host"));
508 assert_eq!(parsed.cseq(), Some("1 OPTIONS"));
509 assert_eq!(parsed.content_length(), Some(0));
510 assert_eq!(parsed.method(), Some("OPTIONS"));
511 assert!(parsed.body.is_empty());
512 }
513
514 #[test]
515 fn parse_200_ok_response() {
516 let content = b"SIP/2.0 200 OK\r\n\
517 Via: SIP/2.0/UDP 10.0.0.1:5060\r\n\
518 Call-ID: resp-id@host\r\n\
519 CSeq: 1 INVITE\r\n\
520 Content-Length: 0\r\n\
521 \r\n";
522 let msg = make_sip_message(content);
523 let parsed = msg.parse().unwrap();
524
525 assert_eq!(
526 parsed.message_type,
527 SipMessageType::Response {
528 code: 200,
529 reason: "OK".into()
530 }
531 );
532 assert_eq!(parsed.method(), Some("INVITE"));
533 }
534
535 #[test]
536 fn parse_100_trying() {
537 let content = b"SIP/2.0 100 Trying\r\n\
538 Via: SIP/2.0/TCP 10.0.0.1:5060\r\n\
539 Call-ID: trying-id\r\n\
540 CSeq: 42 INVITE\r\n\
541 Content-Length: 0\r\n\
542 \r\n";
543 let msg = make_sip_message(content);
544 let parsed = msg.parse().unwrap();
545
546 assert_eq!(
547 parsed.message_type,
548 SipMessageType::Response {
549 code: 100,
550 reason: "Trying".into()
551 }
552 );
553 assert_eq!(parsed.method(), Some("INVITE"));
554 }
555
556 #[test]
557 fn parse_invite_with_sdp_body() {
558 let body = b"v=0\r\no=- 123 456 IN IP4 10.0.0.1\r\ns=-\r\n";
559 let mut content = Vec::new();
560 content.extend_from_slice(b"INVITE sip:user@host SIP/2.0\r\n");
561 content.extend_from_slice(b"Call-ID: invite-body@host\r\n");
562 content.extend_from_slice(b"CSeq: 1 INVITE\r\n");
563 content.extend_from_slice(b"Content-Type: application/sdp\r\n");
564 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
565 content.extend_from_slice(b"\r\n");
566 content.extend_from_slice(body);
567
568 let msg = make_sip_message(&content);
569 let parsed = msg.parse().unwrap();
570
571 assert_eq!(parsed.method(), Some("INVITE"));
572 assert_eq!(parsed.content_type(), Some("application/sdp"));
573 assert_eq!(parsed.content_length(), Some(body.len()));
574 assert_eq!(parsed.body, body);
575 }
576
577 #[test]
578 fn parse_notify_with_json_body() {
579 let body = br#"{"event":"AbandonedCall","id":"123"}"#;
580 let mut content = Vec::new();
581 content.extend_from_slice(b"NOTIFY sip:user@host SIP/2.0\r\n");
582 content.extend_from_slice(b"Call-ID: notify-json@host\r\n");
583 content.extend_from_slice(b"CSeq: 1 NOTIFY\r\n");
584 content.extend_from_slice(b"Content-Type: application/json\r\n");
585 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
586 content.extend_from_slice(b"\r\n");
587 content.extend_from_slice(body);
588
589 let msg = make_sip_message(&content);
590 let parsed = msg.parse().unwrap();
591
592 assert_eq!(parsed.method(), Some("NOTIFY"));
593 assert_eq!(parsed.content_type(), Some("application/json"));
594 assert_eq!(parsed.body, body);
595 }
596
597 #[test]
598 fn compact_headers() {
599 let content = b"NOTIFY sip:user@host SIP/2.0\r\n\
600 i: compact-call-id\r\n\
601 l: 0\r\n\
602 c: text/plain\r\n\
603 \r\n";
604 let msg = make_sip_message(content);
605 let parsed = msg.parse().unwrap();
606
607 assert_eq!(parsed.call_id(), Some("compact-call-id"));
608 assert_eq!(parsed.content_length(), Some(0));
609 assert_eq!(parsed.content_type(), Some("text/plain"));
610 }
611
612 #[test]
613 fn header_folding() {
614 let content = b"OPTIONS sip:host SIP/2.0\r\n\
615 Via: SIP/2.0/UDP 10.0.0.1:5060\r\n\
616 Subject: this is a long\r\n \
617 folded header value\r\n\
618 Call-ID: fold-test\r\n\
619 Content-Length: 0\r\n\
620 \r\n";
621 let msg = make_sip_message(content);
622 let parsed = msg.parse().unwrap();
623
624 let subject = parsed
625 .headers
626 .iter()
627 .find(|(k, _)| k == "Subject")
628 .map(|(_, v)| v.as_str());
629 assert_eq!(subject, Some("this is a long folded header value"));
630 assert_eq!(parsed.call_id(), Some("fold-test"));
631 }
632
633 #[test]
634 fn folded_header_no_crlf_leak() {
635 let content = b"OPTIONS sip:host SIP/2.0\r\n\
636 Subject: line1\r\n \
637 line2\r\n\
638 Content-Length: 0\r\n\
639 \r\n";
640 let msg = make_sip_message(content);
641 let parsed = msg.parse().unwrap();
642 let subject = parsed.headers.iter().find(|(k, _)| k == "Subject").unwrap();
643 assert!(!subject.1.contains('\r'), "CRLF leaked: {:?}", subject.1);
644 assert!(!subject.1.contains('\n'), "LF leaked: {:?}", subject.1);
645 assert_eq!(subject.1, "line1 line2");
646 }
647
648 #[test]
649 fn no_body() {
650 let content = b"OPTIONS sip:host SIP/2.0\r\n\
651 Call-ID: nobody\r\n\
652 Content-Length: 0\r\n\
653 \r\n";
654 let msg = make_sip_message(content);
655 let parsed = msg.parse().unwrap();
656 assert!(parsed.body.is_empty());
657 }
658
659 #[test]
660 fn no_blank_line_no_body() {
661 let content = b"OPTIONS sip:host SIP/2.0\r\n\
663 Call-ID: no-blank\r\n\
664 Content-Length: 0";
665 let msg = make_sip_message(content);
666 let parsed = msg.parse().unwrap();
667 assert!(parsed.body.is_empty());
668 assert_eq!(parsed.call_id(), Some("no-blank"));
669 }
670
671 #[test]
672 fn preserves_metadata() {
673 let content = b"REGISTER sip:host SIP/2.0\r\n\
674 Call-ID: meta-test\r\n\
675 \r\n";
676 let msg = SipMessage {
677 direction: Direction::Sent,
678 transport: Transport::Tls,
679 address: "[2001:db8::1]:5061".into(),
680 timestamp: Timestamp::DateTime {
681 year: 2026,
682 month: 2,
683 day: 12,
684 hour: 10,
685 min: 30,
686 sec: 0,
687 usec: 123456,
688 },
689 content: content.to_vec(),
690 frame_count: 3,
691 };
692 let parsed = msg.parse().unwrap();
693
694 assert_eq!(parsed.direction, Direction::Sent);
695 assert_eq!(parsed.transport, Transport::Tls);
696 assert_eq!(parsed.address, "[2001:db8::1]:5061");
697 assert_eq!(parsed.frame_count, 3);
698 assert_eq!(
699 parsed.timestamp,
700 Timestamp::DateTime {
701 year: 2026,
702 month: 2,
703 day: 12,
704 hour: 10,
705 min: 30,
706 sec: 0,
707 usec: 123456,
708 }
709 );
710 }
711
712 #[test]
713 fn multiple_same_name_headers() {
714 let content = b"INVITE sip:host SIP/2.0\r\n\
715 Via: SIP/2.0/UDP proxy1:5060\r\n\
716 Via: SIP/2.0/UDP proxy2:5060\r\n\
717 Record-Route: <sip:proxy1>\r\n\
718 Record-Route: <sip:proxy2>\r\n\
719 Call-ID: multi-hdr\r\n\
720 Content-Length: 0\r\n\
721 \r\n";
722 let msg = make_sip_message(content);
723 let parsed = msg.parse().unwrap();
724
725 let via_count = parsed.headers.iter().filter(|(k, _)| k == "Via").count();
726 assert_eq!(via_count, 2);
727
728 let rr_count = parsed
729 .headers
730 .iter()
731 .filter(|(k, _)| k == "Record-Route")
732 .count();
733 assert_eq!(rr_count, 2);
734 }
735
736 #[test]
737 fn header_ordering_preserved() {
738 let content = b"OPTIONS sip:host SIP/2.0\r\n\
739 Via: v1\r\n\
740 From: f1\r\n\
741 To: t1\r\n\
742 Call-ID: order-test\r\n\
743 CSeq: 1 OPTIONS\r\n\
744 \r\n";
745 let msg = make_sip_message(content);
746 let parsed = msg.parse().unwrap();
747
748 let names: Vec<&str> = parsed.headers.iter().map(|(k, _)| k.as_str()).collect();
749 assert_eq!(names, vec!["Via", "From", "To", "Call-ID", "CSeq"]);
750 }
751
752 #[test]
753 fn binary_body() {
754 let body: Vec<u8> = (0..256).map(|i| i as u8).collect();
755 let mut content = Vec::new();
756 content.extend_from_slice(b"MESSAGE sip:host SIP/2.0\r\n");
757 content.extend_from_slice(b"Call-ID: binary-body\r\n");
758 content.extend_from_slice(b"Content-Type: application/octet-stream\r\n");
759 content.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
760 content.extend_from_slice(b"\r\n");
761 content.extend_from_slice(&body);
762
763 let msg = make_sip_message(&content);
764 let parsed = msg.parse().unwrap();
765
766 assert_eq!(parsed.body, body);
767 }
768
769 #[test]
770 fn error_no_crlf() {
771 let content = b"garbage without any crlf";
772 let msg = make_sip_message(content);
773 let result = msg.parse();
774 assert!(result.is_err());
775 }
776
777 #[test]
778 fn header_value_with_colon() {
779 let content = b"INVITE sip:host SIP/2.0\r\n\
781 Contact: <sip:user@10.0.0.1:5060;transport=tcp>\r\n\
782 Call-ID: colon-val\r\n\
783 \r\n";
784 let msg = make_sip_message(content);
785 let parsed = msg.parse().unwrap();
786
787 let contact = parsed
788 .headers
789 .iter()
790 .find(|(k, _)| k == "Contact")
791 .map(|(_, v)| v.as_str());
792 assert_eq!(contact, Some("<sip:user@10.0.0.1:5060;transport=tcp>"));
793 }
794
795 #[test]
796 fn whitespace_around_header_value() {
797 let content = b"OPTIONS sip:host SIP/2.0\r\n\
798 Call-ID: spaces-around \r\n\
799 \r\n";
800 let msg = make_sip_message(content);
801 let parsed = msg.parse().unwrap();
802
803 assert_eq!(parsed.call_id(), Some("spaces-around "));
805 }
806
807 #[test]
808 fn parsed_message_iterator() {
809 let content =
810 b"OPTIONS sip:host SIP/2.0\r\nCall-ID: iter-test\r\nContent-Length: 0\r\n\r\n";
811 let header = format!(
812 "recv {} bytes from udp/10.0.0.1:5060 at 00:00:00.000000:\n",
813 content.len()
814 );
815 let mut data = header.into_bytes();
816 data.extend_from_slice(content);
817 data.extend_from_slice(b"\x0B\n");
818
819 let parsed: Vec<ParsedSipMessage> = ParsedMessageIterator::new(&data[..])
820 .collect::<Result<Vec<_>, _>>()
821 .unwrap();
822
823 assert_eq!(parsed.len(), 1);
824 assert_eq!(parsed[0].call_id(), Some("iter-test"));
825 assert_eq!(parsed[0].method(), Some("OPTIONS"));
826 }
827
828 #[test]
829 fn whitespace_only_returns_transport_noise() {
830 use crate::frame::ParseError;
831
832 for content in [b"\n".as_slice(), b"\r\n", b"\n\n\n", b" \t\r\n"] {
833 let msg = SipMessage {
834 direction: Direction::Recv,
835 transport: Transport::Tls,
836 address: "[10.0.0.1]:5061".into(),
837 timestamp: Timestamp::TimeOnly {
838 hour: 0,
839 min: 0,
840 sec: 0,
841 usec: 0,
842 },
843 content: content.to_vec(),
844 frame_count: 1,
845 };
846 let err = msg.parse().unwrap_err();
847 assert!(
848 matches!(err, ParseError::TransportNoise { .. }),
849 "whitespace-only content {:?} should produce TransportNoise, got: {err}",
850 content,
851 );
852 }
853 }
854}