1use crate::header::SipHeader;
13
14const COMPACT_FORMS: &[(u8, &str)] = &[
19 (b'a', "Accept-Contact"),
20 (b'b', "Referred-By"),
21 (b'c', "Content-Type"),
22 (b'd', "Request-Disposition"),
23 (b'e', "Content-Encoding"),
24 (b'f', "From"),
25 (b'i', "Call-ID"),
26 (b'j', "Reject-Contact"),
27 (b'k', "Supported"),
28 (b'l', "Content-Length"),
29 (b'm', "Contact"),
30 (b'n', "Identity-Info"),
31 (b'o', "Event"),
32 (b'r', "Refer-To"),
33 (b's', "Subject"),
34 (b't', "To"),
35 (b'u', "Allow-Events"),
36 (b'v', "Via"),
37 (b'x', "Session-Expires"),
38 (b'y', "Identity"),
39];
40
41fn matches_header_name(wire_name: &str, target: &str) -> bool {
44 if wire_name.eq_ignore_ascii_case(target) {
45 return true;
46 }
47 let equiv = if target.len() == 1 {
49 let ch = target.as_bytes()[0].to_ascii_lowercase();
50 COMPACT_FORMS
51 .iter()
52 .find(|(c, _)| *c == ch)
53 } else {
54 COMPACT_FORMS
55 .iter()
56 .find(|(_, full)| full.eq_ignore_ascii_case(target))
57 };
58 if let Some(&(compact, full)) = equiv {
59 if wire_name.len() == 1 {
60 wire_name.as_bytes()[0].to_ascii_lowercase() == compact
61 } else {
62 wire_name.eq_ignore_ascii_case(full)
63 }
64 } else {
65 false
66 }
67}
68
69pub fn extract_header(message: &str, name: &str) -> Vec<String> {
84 let mut values: Vec<String> = Vec::new();
85 let mut current_match = false;
86
87 for line in message.split('\n') {
88 let line = line
89 .strip_suffix('\r')
90 .unwrap_or(line);
91
92 if line.is_empty() {
93 break;
94 }
95
96 if line.starts_with(' ') || line.starts_with('\t') {
97 if current_match {
98 if let Some(last) = values.last_mut() {
99 last.push(' ');
100 last.push_str(line.trim_start());
101 }
102 }
103 continue;
104 }
105
106 current_match = false;
107
108 if let Some((hdr_name, hdr_value)) = line.split_once(':') {
109 let hdr_name = hdr_name.trim_end();
110 if !hdr_name.contains(' ') && matches_header_name(hdr_name, name) {
114 current_match = true;
115 values.push(
116 hdr_value
117 .trim_start()
118 .to_string(),
119 );
120 }
121 }
122 }
123
124 values
125}
126
127pub fn extract_all_headers(message: &str) -> Vec<(String, String)> {
136 let mut headers: Vec<(String, String)> = Vec::new();
137
138 for line in message.split('\n') {
139 let line = line
140 .strip_suffix('\r')
141 .unwrap_or(line);
142
143 if line.is_empty() {
144 break;
145 }
146
147 if line.starts_with(' ') || line.starts_with('\t') {
148 if let Some((_, value)) = headers.last_mut() {
149 value.push(' ');
150 value.push_str(line.trim_start());
151 }
152 continue;
153 }
154
155 if let Some((hdr_name, hdr_value)) = line.split_once(':') {
156 let hdr_name = hdr_name.trim_end();
157 if !hdr_name.contains(' ') {
161 headers.push((
162 hdr_name.to_string(),
163 hdr_value
164 .trim_start()
165 .to_string(),
166 ));
167 }
168 }
169 }
170
171 headers
172}
173
174pub fn extract_request_uri(message: &str) -> Option<String> {
182 let first_line = message
183 .lines()
184 .next()?;
185 let first_line = first_line
186 .strip_suffix('\r')
187 .unwrap_or(first_line);
188 let mut parts = first_line.split_whitespace();
189 let method = parts.next()?;
190 if method.starts_with("SIP/") {
191 return None;
192 }
193 let uri = parts.next()?;
194 let version = parts.next()?;
195 if parts
196 .next()
197 .is_some()
198 {
199 return None;
200 }
201 if !version.starts_with("SIP/") {
202 return None;
203 }
204 Some(uri.to_string())
205}
206
207impl SipHeader {
208 pub fn extract_from(&self, message: &str) -> Vec<String> {
214 extract_header(message, self.as_str())
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 const SAMPLE_INVITE: &str = "\
223INVITE sip:bob@biloxi.example.com SIP/2.0\r\n\
224Via: SIP/2.0/UDP pc33.atlanta.example.com;branch=z9hG4bK776asdhds\r\n\
225Via: SIP/2.0/UDP bigbox3.site3.atlanta.example.com;branch=z9hG4bKnashds8\r\n\
226Max-Forwards: 70\r\n\
227To: Bob <sip:bob@biloxi.example.com>\r\n\
228From: Alice <sip:alice@atlanta.example.com>;tag=1928301774\r\n\
229Call-ID: a84b4c76e66710@pc33.atlanta.example.com\r\n\
230CSeq: 314159 INVITE\r\n\
231Contact: <sip:alice@pc33.atlanta.example.com>\r\n\
232Content-Type: application/sdp\r\n\
233Content-Length: 142\r\n\
234\r\n\
235v=0\r\n\
236o=alice 2890844526 2890844526 IN IP4 pc33.atlanta.example.com\r\n";
237
238 #[test]
239 fn basic_extraction() {
240 let from = extract_header(SAMPLE_INVITE, "From");
241 assert_eq!(from.len(), 1);
242 assert_eq!(
243 from[0],
244 "Alice <sip:alice@atlanta.example.com>;tag=1928301774"
245 );
246
247 let call_id = extract_header(SAMPLE_INVITE, "Call-ID");
248 assert_eq!(call_id.len(), 1);
249 assert_eq!(call_id[0], "a84b4c76e66710@pc33.atlanta.example.com");
250
251 let cseq = extract_header(SAMPLE_INVITE, "CSeq");
252 assert_eq!(cseq.len(), 1);
253 assert_eq!(cseq[0], "314159 INVITE");
254 }
255
256 #[test]
257 fn case_insensitive_name() {
258 let expected = "Alice <sip:alice@atlanta.example.com>;tag=1928301774";
259 assert_eq!(extract_header(SAMPLE_INVITE, "from")[0], expected);
260 assert_eq!(extract_header(SAMPLE_INVITE, "FROM")[0], expected);
261 assert_eq!(extract_header(SAMPLE_INVITE, "From")[0], expected);
262 }
263
264 #[test]
265 fn header_folding() {
266 let msg = concat!(
267 "SIP/2.0 200 OK\r\n",
268 "Subject: I know you're there,\r\n",
269 " pick up the phone\r\n",
270 " and talk to me!\r\n",
271 "\r\n",
272 );
273 let result = extract_header(msg, "Subject");
274 assert_eq!(result.len(), 1);
275 assert_eq!(
276 result[0],
277 "I know you're there, pick up the phone and talk to me!"
278 );
279 }
280
281 #[test]
282 fn multiple_occurrences_separate() {
283 let via = extract_header(SAMPLE_INVITE, "Via");
284 assert_eq!(via.len(), 2);
285 assert_eq!(
286 via[0],
287 "SIP/2.0/UDP pc33.atlanta.example.com;branch=z9hG4bK776asdhds"
288 );
289 assert_eq!(
290 via[1],
291 "SIP/2.0/UDP bigbox3.site3.atlanta.example.com;branch=z9hG4bKnashds8"
292 );
293 }
294
295 #[test]
296 fn stops_at_blank_line() {
297 assert!(extract_header(SAMPLE_INVITE, "o").is_empty());
298 }
299
300 #[test]
301 fn bare_lf_line_endings() {
302 let msg = "SIP/2.0 200 OK\n\
303 From: Alice <sip:alice@host>\n\
304 To: Bob <sip:bob@host>\n\
305 \n\
306 body\n";
307 let from = extract_header(msg, "From");
308 assert_eq!(from.len(), 1);
309 assert_eq!(from[0], "Alice <sip:alice@host>");
310 }
311
312 #[test]
313 fn missing_header_returns_empty() {
314 assert!(extract_header(SAMPLE_INVITE, "X-Custom").is_empty());
315 }
316
317 #[test]
318 fn empty_message() {
319 assert!(extract_header("", "From").is_empty());
320 }
321
322 #[test]
323 fn request_line_not_matched() {
324 assert!(extract_header(SAMPLE_INVITE, "INVITE sip").is_empty());
325 }
326
327 #[test]
328 fn value_leading_whitespace_trimmed() {
329 let msg = "SIP/2.0 200 OK\r\n\
330 From: Alice <sip:alice@host>\r\n\
331 \r\n";
332 let from = extract_header(msg, "From");
333 assert_eq!(from.len(), 1);
334 assert_eq!(from[0], "Alice <sip:alice@host>");
335 }
336
337 #[test]
338 fn folding_on_multiple_occurrence() {
339 let msg = concat!(
340 "SIP/2.0 200 OK\r\n",
341 "Via: SIP/2.0/UDP first.example.com\r\n",
342 " ;branch=z9hG4bKaaa\r\n",
343 "Via: SIP/2.0/UDP second.example.com;branch=z9hG4bKbbb\r\n",
344 "\r\n",
345 );
346 let via = extract_header(msg, "Via");
347 assert_eq!(via.len(), 2);
348 assert_eq!(via[0], "SIP/2.0/UDP first.example.com ;branch=z9hG4bKaaa");
349 assert_eq!(via[1], "SIP/2.0/UDP second.example.com;branch=z9hG4bKbbb");
350 }
351
352 #[test]
353 fn empty_header_value() {
354 let msg = "SIP/2.0 200 OK\r\n\
355 Subject:\r\n\
356 From: Alice <sip:alice@host>\r\n\
357 \r\n";
358 let subject = extract_header(msg, "Subject");
359 assert_eq!(subject.len(), 1);
360 assert_eq!(subject[0], "");
361 }
362
363 #[test]
364 fn tab_folding() {
365 let msg = concat!(
366 "SIP/2.0 200 OK\r\n",
367 "Subject: hello\r\n",
368 "\tworld\r\n",
369 "\r\n",
370 );
371 let subject = extract_header(msg, "Subject");
372 assert_eq!(subject.len(), 1);
373 assert_eq!(subject[0], "hello world");
374 }
375
376 #[test]
379 fn compact_form_from() {
380 let msg = "SIP/2.0 200 OK\r\nf: Alice <sip:alice@host>\r\n\r\n";
381 assert_eq!(extract_header(msg, "From")[0], "Alice <sip:alice@host>");
382 assert_eq!(extract_header(msg, "f")[0], "Alice <sip:alice@host>");
383 }
384
385 #[test]
386 fn compact_form_via() {
387 let msg = "SIP/2.0 200 OK\r\nv: SIP/2.0/UDP host\r\n\r\n";
388 assert_eq!(extract_header(msg, "Via")[0], "SIP/2.0/UDP host");
389 assert_eq!(extract_header(msg, "v")[0], "SIP/2.0/UDP host");
390 }
391
392 #[test]
393 fn compact_form_mixed_with_full() {
394 let msg = concat!(
395 "SIP/2.0 200 OK\r\n",
396 "f: Alice <sip:alice@host>;tag=a\r\n",
397 "t: Bob <sip:bob@host>;tag=b\r\n",
398 "i: call-1@host\r\n",
399 "m: <sip:alice@192.0.2.1>\r\n",
400 "Content-Type: application/sdp\r\n",
401 "\r\n",
402 );
403 assert_eq!(
404 extract_header(msg, "From")[0],
405 "Alice <sip:alice@host>;tag=a"
406 );
407 assert_eq!(extract_header(msg, "To")[0], "Bob <sip:bob@host>;tag=b");
408 assert_eq!(extract_header(msg, "Call-ID")[0], "call-1@host");
409 assert_eq!(extract_header(msg, "Contact")[0], "<sip:alice@192.0.2.1>");
410 assert_eq!(extract_header(msg, "Content-Type")[0], "application/sdp");
411 assert_eq!(extract_header(msg, "c")[0], "application/sdp");
412 }
413
414 #[test]
415 fn compact_form_case_insensitive() {
416 let msg = "SIP/2.0 200 OK\r\nF: Alice <sip:alice@host>\r\n\r\n";
417 assert_eq!(extract_header(msg, "From")[0], "Alice <sip:alice@host>");
418 }
419
420 #[test]
421 fn compact_form_unknown_single_char() {
422 let msg = "SIP/2.0 200 OK\r\nz: something\r\n\r\n";
423 assert_eq!(extract_header(msg, "z")[0], "something");
424 assert!(extract_header(msg, "From").is_empty());
425 }
426
427 const NG911_INVITE: &str = concat!(
430 "INVITE sip:urn:service:sos@bcf.example.com SIP/2.0\r\n",
431 "Via: SIP/2.0/TLS proxy.example.com;branch=z9hG4bK776\r\n",
432 "From: \"Caller Name\" <sip:+15551234567@orig.example.com>;tag=abc123\r\n",
433 "To: <sip:urn:service:sos@bcf.example.com>\r\n",
434 "Call-ID: ng911-call-42@orig.example.com\r\n",
435 "P-Asserted-Identity: \"EXAMPLE CO\" <sip:+15551234567@198.51.100.1>\r\n",
436 "Call-Info: <urn:emergency:uid:callid:abc:bcf.example.com>;purpose=emergency-CallId,",
437 "<https://adr.example.com/serviceInfo?t=x>;purpose=EmergencyCallData.ServiceInfo\r\n",
438 "Geolocation: <cid:loc-id-1234>, <https://lis.example.com/held/test>\r\n",
439 "Content-Type: application/sdp\r\n",
440 "\r\n",
441 "v=0\r\n",
442 );
443
444 #[test]
445 fn extract_and_parse_call_info() {
446 use crate::uri_info::UriInfo;
447
448 let raw = extract_header(NG911_INVITE, "Call-Info");
449 assert_eq!(raw.len(), 1);
450 let ci = UriInfo::parse(&raw[0]).unwrap();
451 assert_eq!(ci.len(), 2);
452 assert_eq!(ci.entries()[0].purpose(), Some("emergency-CallId"));
453 assert!(ci
454 .entries()
455 .iter()
456 .any(|e| e.purpose() == Some("EmergencyCallData.ServiceInfo")));
457 }
458
459 #[test]
460 fn extract_and_parse_p_asserted_identity() {
461 use crate::header_addr::SipHeaderAddr;
462
463 let raw = extract_header(NG911_INVITE, "P-Asserted-Identity");
464 assert_eq!(raw.len(), 1);
465 let pai: SipHeaderAddr = raw[0]
466 .parse()
467 .unwrap();
468 assert_eq!(pai.display_name(), Some("EXAMPLE CO"));
469 assert!(pai
470 .uri()
471 .to_string()
472 .contains("+15551234567"));
473 }
474
475 #[test]
476 fn extract_and_parse_multi_pai() {
477 use crate::header_addr::SipHeaderAddr;
478
479 let msg = concat!(
480 "INVITE sip:sos@psap.example.com SIP/2.0\r\n",
481 "P-Asserted-Identity: \"EXAMPLE CO\" <sip:+15551234567@198.51.100.1>\r\n",
482 "P-Asserted-Identity: <tel:+15551234567>\r\n",
483 "\r\n",
484 );
485 let raw = extract_header(msg, "P-Asserted-Identity");
486 assert_eq!(raw.len(), 2);
487 let pai0: SipHeaderAddr = raw[0]
488 .parse()
489 .unwrap();
490 assert_eq!(pai0.display_name(), Some("EXAMPLE CO"));
491 let pai1: SipHeaderAddr = raw[1]
492 .parse()
493 .unwrap();
494 assert!(pai1
495 .uri()
496 .to_string()
497 .contains("+15551234567"));
498 }
499
500 #[test]
501 fn extract_and_parse_geolocation() {
502 use crate::geolocation::SipGeolocation;
503
504 let raw = extract_header(NG911_INVITE, "Geolocation");
505 assert_eq!(raw.len(), 1);
506 let geo = SipGeolocation::parse(&raw[0]);
507 assert_eq!(geo.len(), 2);
508 assert_eq!(geo.cid(), Some("loc-id-1234"));
509 assert!(geo
510 .url()
511 .unwrap()
512 .contains("lis.example.com"));
513 }
514
515 #[test]
516 fn extract_and_parse_from_to() {
517 use crate::header_addr::SipHeaderAddr;
518
519 let from_raw = extract_header(NG911_INVITE, "From");
520 assert_eq!(from_raw.len(), 1);
521 let from: SipHeaderAddr = from_raw[0]
522 .parse()
523 .unwrap();
524 assert_eq!(from.display_name(), Some("Caller Name"));
525 assert_eq!(from.tag(), Some("abc123"));
526
527 let to_raw = extract_header(NG911_INVITE, "To");
528 assert_eq!(to_raw.len(), 1);
529 let to: SipHeaderAddr = to_raw[0]
530 .parse()
531 .unwrap();
532 assert!(to
533 .uri()
534 .to_string()
535 .contains("urn:service:sos"));
536 }
537
538 #[test]
541 fn extract_request_uri_invite() {
542 let msg = "INVITE urn:service:sos SIP/2.0\r\nTo: <urn:service:sos>\r\n\r\n";
543 assert_eq!(extract_request_uri(msg), Some("urn:service:sos".into()));
544 }
545
546 #[test]
547 fn extract_request_uri_sip() {
548 let msg = "INVITE sip:+15550001234@198.51.100.1:5060 SIP/2.0\r\n\r\n";
549 assert_eq!(
550 extract_request_uri(msg),
551 Some("sip:+15550001234@198.51.100.1:5060".into()),
552 );
553 }
554
555 #[test]
556 fn extract_request_uri_status_line() {
557 let msg = "SIP/2.0 200 OK\r\n\r\n";
558 assert_eq!(extract_request_uri(msg), None);
559 }
560
561 #[test]
562 fn extract_request_uri_empty() {
563 assert_eq!(extract_request_uri(""), None);
564 }
565
566 #[test]
569 fn extract_all_headers_basic() {
570 let msg = concat!(
571 "SIP/2.0 200 OK\r\n",
572 "Via: SIP/2.0/UDP host\r\n",
573 "From: Alice <sip:alice@example.com>\r\n",
574 "To: Bob <sip:bob@example.com>\r\n",
575 "\r\n",
576 );
577 let headers = extract_all_headers(msg);
578 assert_eq!(headers.len(), 3);
579 assert_eq!(headers[0], ("Via".into(), "SIP/2.0/UDP host".into()));
580 assert_eq!(
581 headers[1],
582 ("From".into(), "Alice <sip:alice@example.com>".into())
583 );
584 assert_eq!(
585 headers[2],
586 ("To".into(), "Bob <sip:bob@example.com>".into())
587 );
588 }
589
590 #[test]
591 fn extract_all_headers_folding() {
592 let msg = concat!(
593 "SIP/2.0 200 OK\r\n",
594 "Subject: I know you're there,\r\n",
595 " pick up the phone\r\n",
596 " and talk to me!\r\n",
597 "From: Alice <sip:alice@example.com>\r\n",
598 "\r\n",
599 );
600 let headers = extract_all_headers(msg);
601 assert_eq!(headers.len(), 2);
602 assert_eq!(
603 headers[0].1,
604 "I know you're there, pick up the phone and talk to me!"
605 );
606 }
607
608 #[test]
609 fn extract_all_headers_compact_forms_verbatim() {
610 let msg = concat!(
611 "SIP/2.0 200 OK\r\n",
612 "f: Alice <sip:alice@example.com>\r\n",
613 "t: Bob <sip:bob@example.com>\r\n",
614 "i: call-1@host\r\n",
615 "\r\n",
616 );
617 let headers = extract_all_headers(msg);
618 assert_eq!(headers.len(), 3);
619 assert_eq!(headers[0].0, "f");
620 assert_eq!(headers[1].0, "t");
621 assert_eq!(headers[2].0, "i");
622 }
623
624 #[test]
625 fn extract_all_headers_stops_at_blank_line() {
626 let msg = concat!(
627 "INVITE sip:bob@example.com SIP/2.0\r\n",
628 "From: Alice <sip:alice@example.com>\r\n",
629 "\r\n",
630 "v=0\r\n",
631 "o=alice 123 456 IN IP4 198.51.100.1\r\n",
632 );
633 let headers = extract_all_headers(msg);
634 assert_eq!(headers.len(), 1);
635 assert_eq!(headers[0].0, "From");
636 }
637
638 #[test]
639 fn extract_all_headers_multiple_same_name() {
640 let msg = concat!(
641 "SIP/2.0 200 OK\r\n",
642 "Via: SIP/2.0/UDP first.example.com\r\n",
643 "Via: SIP/2.0/UDP second.example.com\r\n",
644 "\r\n",
645 );
646 let headers = extract_all_headers(msg);
647 assert_eq!(headers.len(), 2);
648 assert_eq!(
649 headers[0],
650 ("Via".into(), "SIP/2.0/UDP first.example.com".into())
651 );
652 assert_eq!(
653 headers[1],
654 ("Via".into(), "SIP/2.0/UDP second.example.com".into())
655 );
656 }
657
658 #[test]
659 fn extract_all_headers_empty_message() {
660 assert!(extract_all_headers("").is_empty());
661 }
662
663 #[test]
664 fn extract_all_headers_skips_request_line() {
665 let msg = concat!(
666 "INVITE sip:bob@example.com SIP/2.0\r\n",
667 "From: Alice <sip:alice@example.com>\r\n",
668 "\r\n",
669 );
670 let headers = extract_all_headers(msg);
671 assert_eq!(headers.len(), 1);
672 assert_eq!(headers[0].0, "From");
673 }
674
675 #[test]
676 fn extract_all_headers_skips_status_line() {
677 let msg = concat!(
678 "SIP/2.0 200 OK\r\n",
679 "From: Alice <sip:alice@example.com>\r\n",
680 "\r\n",
681 );
682 let headers = extract_all_headers(msg);
683 assert_eq!(headers.len(), 1);
684 assert_eq!(headers[0].0, "From");
685 }
686
687 #[test]
688 fn extract_all_headers_tab_folding() {
689 let msg = concat!(
690 "SIP/2.0 200 OK\r\n",
691 "Subject: hello\r\n",
692 "\tworld\r\n",
693 "\r\n",
694 );
695 let headers = extract_all_headers(msg);
696 assert_eq!(headers.len(), 1);
697 assert_eq!(headers[0].1, "hello world");
698 }
699
700 #[test]
701 fn extract_all_headers_empty_value() {
702 let msg = concat!(
703 "SIP/2.0 200 OK\r\n",
704 "Subject:\r\n",
705 "From: Alice <sip:alice@example.com>\r\n",
706 "\r\n",
707 );
708 let headers = extract_all_headers(msg);
709 assert_eq!(headers.len(), 2);
710 assert_eq!(headers[0], ("Subject".into(), "".into()));
711 }
712
713 #[test]
714 fn extract_all_headers_bare_lf() {
715 let msg = "SIP/2.0 200 OK\n\
716 From: Alice <sip:alice@example.com>\n\
717 \n\
718 body\n";
719 let headers = extract_all_headers(msg);
720 assert_eq!(headers.len(), 1);
721 assert_eq!(
722 headers[0],
723 ("From".into(), "Alice <sip:alice@example.com>".into())
724 );
725 }
726}