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_request_uri(message: &str) -> Option<String> {
135 let first_line = message
136 .lines()
137 .next()?;
138 let first_line = first_line
139 .strip_suffix('\r')
140 .unwrap_or(first_line);
141 let mut parts = first_line.split_whitespace();
142 let method = parts.next()?;
143 if method.starts_with("SIP/") {
144 return None;
145 }
146 let uri = parts.next()?;
147 let version = parts.next()?;
148 if parts
149 .next()
150 .is_some()
151 {
152 return None;
153 }
154 if !version.starts_with("SIP/") {
155 return None;
156 }
157 Some(uri.to_string())
158}
159
160impl SipHeader {
161 pub fn extract_from(&self, message: &str) -> Vec<String> {
167 extract_header(message, self.as_str())
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 const SAMPLE_INVITE: &str = "\
176INVITE sip:bob@biloxi.example.com SIP/2.0\r\n\
177Via: SIP/2.0/UDP pc33.atlanta.example.com;branch=z9hG4bK776asdhds\r\n\
178Via: SIP/2.0/UDP bigbox3.site3.atlanta.example.com;branch=z9hG4bKnashds8\r\n\
179Max-Forwards: 70\r\n\
180To: Bob <sip:bob@biloxi.example.com>\r\n\
181From: Alice <sip:alice@atlanta.example.com>;tag=1928301774\r\n\
182Call-ID: a84b4c76e66710@pc33.atlanta.example.com\r\n\
183CSeq: 314159 INVITE\r\n\
184Contact: <sip:alice@pc33.atlanta.example.com>\r\n\
185Content-Type: application/sdp\r\n\
186Content-Length: 142\r\n\
187\r\n\
188v=0\r\n\
189o=alice 2890844526 2890844526 IN IP4 pc33.atlanta.example.com\r\n";
190
191 #[test]
192 fn basic_extraction() {
193 let from = extract_header(SAMPLE_INVITE, "From");
194 assert_eq!(from.len(), 1);
195 assert_eq!(
196 from[0],
197 "Alice <sip:alice@atlanta.example.com>;tag=1928301774"
198 );
199
200 let call_id = extract_header(SAMPLE_INVITE, "Call-ID");
201 assert_eq!(call_id.len(), 1);
202 assert_eq!(call_id[0], "a84b4c76e66710@pc33.atlanta.example.com");
203
204 let cseq = extract_header(SAMPLE_INVITE, "CSeq");
205 assert_eq!(cseq.len(), 1);
206 assert_eq!(cseq[0], "314159 INVITE");
207 }
208
209 #[test]
210 fn case_insensitive_name() {
211 let expected = "Alice <sip:alice@atlanta.example.com>;tag=1928301774";
212 assert_eq!(extract_header(SAMPLE_INVITE, "from")[0], expected);
213 assert_eq!(extract_header(SAMPLE_INVITE, "FROM")[0], expected);
214 assert_eq!(extract_header(SAMPLE_INVITE, "From")[0], expected);
215 }
216
217 #[test]
218 fn header_folding() {
219 let msg = concat!(
220 "SIP/2.0 200 OK\r\n",
221 "Subject: I know you're there,\r\n",
222 " pick up the phone\r\n",
223 " and talk to me!\r\n",
224 "\r\n",
225 );
226 let result = extract_header(msg, "Subject");
227 assert_eq!(result.len(), 1);
228 assert_eq!(
229 result[0],
230 "I know you're there, pick up the phone and talk to me!"
231 );
232 }
233
234 #[test]
235 fn multiple_occurrences_separate() {
236 let via = extract_header(SAMPLE_INVITE, "Via");
237 assert_eq!(via.len(), 2);
238 assert_eq!(
239 via[0],
240 "SIP/2.0/UDP pc33.atlanta.example.com;branch=z9hG4bK776asdhds"
241 );
242 assert_eq!(
243 via[1],
244 "SIP/2.0/UDP bigbox3.site3.atlanta.example.com;branch=z9hG4bKnashds8"
245 );
246 }
247
248 #[test]
249 fn stops_at_blank_line() {
250 assert!(extract_header(SAMPLE_INVITE, "o").is_empty());
251 }
252
253 #[test]
254 fn bare_lf_line_endings() {
255 let msg = "SIP/2.0 200 OK\n\
256 From: Alice <sip:alice@host>\n\
257 To: Bob <sip:bob@host>\n\
258 \n\
259 body\n";
260 let from = extract_header(msg, "From");
261 assert_eq!(from.len(), 1);
262 assert_eq!(from[0], "Alice <sip:alice@host>");
263 }
264
265 #[test]
266 fn missing_header_returns_empty() {
267 assert!(extract_header(SAMPLE_INVITE, "X-Custom").is_empty());
268 }
269
270 #[test]
271 fn empty_message() {
272 assert!(extract_header("", "From").is_empty());
273 }
274
275 #[test]
276 fn request_line_not_matched() {
277 assert!(extract_header(SAMPLE_INVITE, "INVITE sip").is_empty());
278 }
279
280 #[test]
281 fn value_leading_whitespace_trimmed() {
282 let msg = "SIP/2.0 200 OK\r\n\
283 From: Alice <sip:alice@host>\r\n\
284 \r\n";
285 let from = extract_header(msg, "From");
286 assert_eq!(from.len(), 1);
287 assert_eq!(from[0], "Alice <sip:alice@host>");
288 }
289
290 #[test]
291 fn folding_on_multiple_occurrence() {
292 let msg = concat!(
293 "SIP/2.0 200 OK\r\n",
294 "Via: SIP/2.0/UDP first.example.com\r\n",
295 " ;branch=z9hG4bKaaa\r\n",
296 "Via: SIP/2.0/UDP second.example.com;branch=z9hG4bKbbb\r\n",
297 "\r\n",
298 );
299 let via = extract_header(msg, "Via");
300 assert_eq!(via.len(), 2);
301 assert_eq!(via[0], "SIP/2.0/UDP first.example.com ;branch=z9hG4bKaaa");
302 assert_eq!(via[1], "SIP/2.0/UDP second.example.com;branch=z9hG4bKbbb");
303 }
304
305 #[test]
306 fn empty_header_value() {
307 let msg = "SIP/2.0 200 OK\r\n\
308 Subject:\r\n\
309 From: Alice <sip:alice@host>\r\n\
310 \r\n";
311 let subject = extract_header(msg, "Subject");
312 assert_eq!(subject.len(), 1);
313 assert_eq!(subject[0], "");
314 }
315
316 #[test]
317 fn tab_folding() {
318 let msg = concat!(
319 "SIP/2.0 200 OK\r\n",
320 "Subject: hello\r\n",
321 "\tworld\r\n",
322 "\r\n",
323 );
324 let subject = extract_header(msg, "Subject");
325 assert_eq!(subject.len(), 1);
326 assert_eq!(subject[0], "hello world");
327 }
328
329 #[test]
332 fn compact_form_from() {
333 let msg = "SIP/2.0 200 OK\r\nf: Alice <sip:alice@host>\r\n\r\n";
334 assert_eq!(extract_header(msg, "From")[0], "Alice <sip:alice@host>");
335 assert_eq!(extract_header(msg, "f")[0], "Alice <sip:alice@host>");
336 }
337
338 #[test]
339 fn compact_form_via() {
340 let msg = "SIP/2.0 200 OK\r\nv: SIP/2.0/UDP host\r\n\r\n";
341 assert_eq!(extract_header(msg, "Via")[0], "SIP/2.0/UDP host");
342 assert_eq!(extract_header(msg, "v")[0], "SIP/2.0/UDP host");
343 }
344
345 #[test]
346 fn compact_form_mixed_with_full() {
347 let msg = concat!(
348 "SIP/2.0 200 OK\r\n",
349 "f: Alice <sip:alice@host>;tag=a\r\n",
350 "t: Bob <sip:bob@host>;tag=b\r\n",
351 "i: call-1@host\r\n",
352 "m: <sip:alice@192.0.2.1>\r\n",
353 "Content-Type: application/sdp\r\n",
354 "\r\n",
355 );
356 assert_eq!(
357 extract_header(msg, "From")[0],
358 "Alice <sip:alice@host>;tag=a"
359 );
360 assert_eq!(extract_header(msg, "To")[0], "Bob <sip:bob@host>;tag=b");
361 assert_eq!(extract_header(msg, "Call-ID")[0], "call-1@host");
362 assert_eq!(extract_header(msg, "Contact")[0], "<sip:alice@192.0.2.1>");
363 assert_eq!(extract_header(msg, "Content-Type")[0], "application/sdp");
364 assert_eq!(extract_header(msg, "c")[0], "application/sdp");
365 }
366
367 #[test]
368 fn compact_form_case_insensitive() {
369 let msg = "SIP/2.0 200 OK\r\nF: Alice <sip:alice@host>\r\n\r\n";
370 assert_eq!(extract_header(msg, "From")[0], "Alice <sip:alice@host>");
371 }
372
373 #[test]
374 fn compact_form_unknown_single_char() {
375 let msg = "SIP/2.0 200 OK\r\nz: something\r\n\r\n";
376 assert_eq!(extract_header(msg, "z")[0], "something");
377 assert!(extract_header(msg, "From").is_empty());
378 }
379
380 const NG911_INVITE: &str = concat!(
383 "INVITE sip:urn:service:sos@bcf.example.com SIP/2.0\r\n",
384 "Via: SIP/2.0/TLS proxy.example.com;branch=z9hG4bK776\r\n",
385 "From: \"Caller Name\" <sip:+15551234567@orig.example.com>;tag=abc123\r\n",
386 "To: <sip:urn:service:sos@bcf.example.com>\r\n",
387 "Call-ID: ng911-call-42@orig.example.com\r\n",
388 "P-Asserted-Identity: \"EXAMPLE CO\" <sip:+15551234567@198.51.100.1>\r\n",
389 "Call-Info: <urn:emergency:uid:callid:abc:bcf.example.com>;purpose=emergency-CallId,",
390 "<https://adr.example.com/serviceInfo?t=x>;purpose=EmergencyCallData.ServiceInfo\r\n",
391 "Geolocation: <cid:loc-id-1234>, <https://lis.example.com/held/test>\r\n",
392 "Content-Type: application/sdp\r\n",
393 "\r\n",
394 "v=0\r\n",
395 );
396
397 #[test]
398 fn extract_and_parse_call_info() {
399 use crate::uri_info::UriInfo;
400
401 let raw = extract_header(NG911_INVITE, "Call-Info");
402 assert_eq!(raw.len(), 1);
403 let ci = UriInfo::parse(&raw[0]).unwrap();
404 assert_eq!(ci.len(), 2);
405 assert_eq!(ci.entries()[0].purpose(), Some("emergency-CallId"));
406 assert!(ci
407 .entries()
408 .iter()
409 .any(|e| e.purpose() == Some("EmergencyCallData.ServiceInfo")));
410 }
411
412 #[test]
413 fn extract_and_parse_p_asserted_identity() {
414 use crate::header_addr::SipHeaderAddr;
415
416 let raw = extract_header(NG911_INVITE, "P-Asserted-Identity");
417 assert_eq!(raw.len(), 1);
418 let pai: SipHeaderAddr = raw[0]
419 .parse()
420 .unwrap();
421 assert_eq!(pai.display_name(), Some("EXAMPLE CO"));
422 assert!(pai
423 .uri()
424 .to_string()
425 .contains("+15551234567"));
426 }
427
428 #[test]
429 fn extract_and_parse_multi_pai() {
430 use crate::header_addr::SipHeaderAddr;
431
432 let msg = concat!(
433 "INVITE sip:sos@psap.example.com SIP/2.0\r\n",
434 "P-Asserted-Identity: \"EXAMPLE CO\" <sip:+15551234567@198.51.100.1>\r\n",
435 "P-Asserted-Identity: <tel:+15551234567>\r\n",
436 "\r\n",
437 );
438 let raw = extract_header(msg, "P-Asserted-Identity");
439 assert_eq!(raw.len(), 2);
440 let pai0: SipHeaderAddr = raw[0]
441 .parse()
442 .unwrap();
443 assert_eq!(pai0.display_name(), Some("EXAMPLE CO"));
444 let pai1: SipHeaderAddr = raw[1]
445 .parse()
446 .unwrap();
447 assert!(pai1
448 .uri()
449 .to_string()
450 .contains("+15551234567"));
451 }
452
453 #[test]
454 fn extract_and_parse_geolocation() {
455 use crate::geolocation::SipGeolocation;
456
457 let raw = extract_header(NG911_INVITE, "Geolocation");
458 assert_eq!(raw.len(), 1);
459 let geo = SipGeolocation::parse(&raw[0]);
460 assert_eq!(geo.len(), 2);
461 assert_eq!(geo.cid(), Some("loc-id-1234"));
462 assert!(geo
463 .url()
464 .unwrap()
465 .contains("lis.example.com"));
466 }
467
468 #[test]
469 fn extract_and_parse_from_to() {
470 use crate::header_addr::SipHeaderAddr;
471
472 let from_raw = extract_header(NG911_INVITE, "From");
473 assert_eq!(from_raw.len(), 1);
474 let from: SipHeaderAddr = from_raw[0]
475 .parse()
476 .unwrap();
477 assert_eq!(from.display_name(), Some("Caller Name"));
478 assert_eq!(from.tag(), Some("abc123"));
479
480 let to_raw = extract_header(NG911_INVITE, "To");
481 assert_eq!(to_raw.len(), 1);
482 let to: SipHeaderAddr = to_raw[0]
483 .parse()
484 .unwrap();
485 assert!(to
486 .uri()
487 .to_string()
488 .contains("urn:service:sos"));
489 }
490
491 #[test]
494 fn extract_request_uri_invite() {
495 let msg = "INVITE urn:service:sos SIP/2.0\r\nTo: <urn:service:sos>\r\n\r\n";
496 assert_eq!(extract_request_uri(msg), Some("urn:service:sos".into()));
497 }
498
499 #[test]
500 fn extract_request_uri_sip() {
501 let msg = "INVITE sip:+15550001234@198.51.100.1:5060 SIP/2.0\r\n\r\n";
502 assert_eq!(
503 extract_request_uri(msg),
504 Some("sip:+15550001234@198.51.100.1:5060".into()),
505 );
506 }
507
508 #[test]
509 fn extract_request_uri_status_line() {
510 let msg = "SIP/2.0 200 OK\r\n\r\n";
511 assert_eq!(extract_request_uri(msg), None);
512 }
513
514 #[test]
515 fn extract_request_uri_empty() {
516 assert_eq!(extract_request_uri(""), None);
517 }
518}