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) -> Option<String> {
82 let mut values: Vec<String> = Vec::new();
83 let mut current_match = false;
84
85 for line in message.split('\n') {
86 let line = line
87 .strip_suffix('\r')
88 .unwrap_or(line);
89
90 if line.is_empty() {
91 break;
92 }
93
94 if line.starts_with(' ') || line.starts_with('\t') {
95 if current_match {
96 if let Some(last) = values.last_mut() {
97 last.push(' ');
98 last.push_str(line.trim_start());
99 }
100 }
101 continue;
102 }
103
104 current_match = false;
105
106 if let Some((hdr_name, hdr_value)) = line.split_once(':') {
107 let hdr_name = hdr_name.trim_end();
108 if !hdr_name.contains(' ') && matches_header_name(hdr_name, name) {
112 current_match = true;
113 values.push(
114 hdr_value
115 .trim_start()
116 .to_string(),
117 );
118 }
119 }
120 }
121
122 if values.is_empty() {
123 None
124 } else {
125 Some(values.join(", "))
126 }
127}
128
129pub fn extract_request_uri(message: &str) -> Option<String> {
137 let first_line = message
138 .lines()
139 .next()?;
140 let first_line = first_line
141 .strip_suffix('\r')
142 .unwrap_or(first_line);
143 let mut parts = first_line.split_whitespace();
144 let method = parts.next()?;
145 if method.starts_with("SIP/") {
146 return None;
147 }
148 let uri = parts.next()?;
149 let version = parts.next()?;
150 if parts
151 .next()
152 .is_some()
153 {
154 return None;
155 }
156 if !version.starts_with("SIP/") {
157 return None;
158 }
159 Some(uri.to_string())
160}
161
162impl SipHeader {
163 pub fn extract_from(&self, message: &str) -> Option<String> {
169 extract_header(message, self.as_str())
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 const SAMPLE_INVITE: &str = "\
178INVITE sip:bob@biloxi.example.com SIP/2.0\r\n\
179Via: SIP/2.0/UDP pc33.atlanta.example.com;branch=z9hG4bK776asdhds\r\n\
180Via: SIP/2.0/UDP bigbox3.site3.atlanta.example.com;branch=z9hG4bKnashds8\r\n\
181Max-Forwards: 70\r\n\
182To: Bob <sip:bob@biloxi.example.com>\r\n\
183From: Alice <sip:alice@atlanta.example.com>;tag=1928301774\r\n\
184Call-ID: a84b4c76e66710@pc33.atlanta.example.com\r\n\
185CSeq: 314159 INVITE\r\n\
186Contact: <sip:alice@pc33.atlanta.example.com>\r\n\
187Content-Type: application/sdp\r\n\
188Content-Length: 142\r\n\
189\r\n\
190v=0\r\n\
191o=alice 2890844526 2890844526 IN IP4 pc33.atlanta.example.com\r\n";
192
193 #[test]
194 fn basic_extraction() {
195 assert_eq!(
196 extract_header(SAMPLE_INVITE, "From"),
197 Some("Alice <sip:alice@atlanta.example.com>;tag=1928301774".into())
198 );
199 assert_eq!(
200 extract_header(SAMPLE_INVITE, "Call-ID"),
201 Some("a84b4c76e66710@pc33.atlanta.example.com".into())
202 );
203 assert_eq!(
204 extract_header(SAMPLE_INVITE, "CSeq"),
205 Some("314159 INVITE".into())
206 );
207 }
208
209 #[test]
210 fn case_insensitive_name() {
211 let expected = Some("Alice <sip:alice@atlanta.example.com>;tag=1928301774".into());
212 assert_eq!(extract_header(SAMPLE_INVITE, "from"), expected);
213 assert_eq!(extract_header(SAMPLE_INVITE, "FROM"), expected);
214 assert_eq!(extract_header(SAMPLE_INVITE, "From"), 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 assert_eq!(
227 extract_header(msg, "Subject"),
228 Some("I know you're there, pick up the phone and talk to me!".into())
229 );
230 }
231
232 #[test]
233 fn multiple_occurrences_concatenated() {
234 assert_eq!(
235 extract_header(SAMPLE_INVITE, "Via"),
236 Some(
237 "SIP/2.0/UDP pc33.atlanta.example.com;branch=z9hG4bK776asdhds, \
238 SIP/2.0/UDP bigbox3.site3.atlanta.example.com;branch=z9hG4bKnashds8"
239 .into()
240 )
241 );
242 }
243
244 #[test]
245 fn stops_at_blank_line() {
246 assert_eq!(extract_header(SAMPLE_INVITE, "o"), None);
248 }
249
250 #[test]
251 fn bare_lf_line_endings() {
252 let msg = "SIP/2.0 200 OK\n\
253 From: Alice <sip:alice@host>\n\
254 To: Bob <sip:bob@host>\n\
255 \n\
256 body\n";
257 assert_eq!(
258 extract_header(msg, "From"),
259 Some("Alice <sip:alice@host>".into())
260 );
261 }
262
263 #[test]
264 fn missing_header_returns_none() {
265 assert_eq!(extract_header(SAMPLE_INVITE, "X-Custom"), None);
266 }
267
268 #[test]
269 fn empty_message() {
270 assert_eq!(extract_header("", "From"), None);
271 }
272
273 #[test]
274 fn request_line_not_matched() {
275 assert_eq!(extract_header(SAMPLE_INVITE, "INVITE sip"), None);
277 }
278
279 #[test]
280 fn value_leading_whitespace_trimmed() {
281 let msg = "SIP/2.0 200 OK\r\n\
282 From: Alice <sip:alice@host>\r\n\
283 \r\n";
284 assert_eq!(
285 extract_header(msg, "From"),
286 Some("Alice <sip:alice@host>".into())
287 );
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 assert_eq!(
300 extract_header(msg, "Via"),
301 Some(
302 "SIP/2.0/UDP first.example.com ;branch=z9hG4bKaaa, \
303 SIP/2.0/UDP second.example.com;branch=z9hG4bKbbb"
304 .into()
305 )
306 );
307 }
308
309 #[test]
310 fn empty_header_value() {
311 let msg = "SIP/2.0 200 OK\r\n\
312 Subject:\r\n\
313 From: Alice <sip:alice@host>\r\n\
314 \r\n";
315 assert_eq!(extract_header(msg, "Subject"), Some(String::new()));
316 }
317
318 #[test]
319 fn tab_folding() {
320 let msg = concat!(
321 "SIP/2.0 200 OK\r\n",
322 "Subject: hello\r\n",
323 "\tworld\r\n",
324 "\r\n",
325 );
326 assert_eq!(extract_header(msg, "Subject"), Some("hello world".into()));
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!(
335 extract_header(msg, "From"),
336 Some("Alice <sip:alice@host>".into())
337 );
338 assert_eq!(
339 extract_header(msg, "f"),
340 Some("Alice <sip:alice@host>".into())
341 );
342 }
343
344 #[test]
345 fn compact_form_via() {
346 let msg = "SIP/2.0 200 OK\r\nv: SIP/2.0/UDP host\r\n\r\n";
347 assert_eq!(extract_header(msg, "Via"), Some("SIP/2.0/UDP host".into()));
348 assert_eq!(extract_header(msg, "v"), Some("SIP/2.0/UDP host".into()));
349 }
350
351 #[test]
352 fn compact_form_mixed_with_full() {
353 let msg = concat!(
354 "SIP/2.0 200 OK\r\n",
355 "f: Alice <sip:alice@host>;tag=a\r\n",
356 "t: Bob <sip:bob@host>;tag=b\r\n",
357 "i: call-1@host\r\n",
358 "m: <sip:alice@192.0.2.1>\r\n",
359 "Content-Type: application/sdp\r\n",
360 "\r\n",
361 );
362 assert_eq!(
363 extract_header(msg, "From"),
364 Some("Alice <sip:alice@host>;tag=a".into())
365 );
366 assert_eq!(
367 extract_header(msg, "To"),
368 Some("Bob <sip:bob@host>;tag=b".into())
369 );
370 assert_eq!(extract_header(msg, "Call-ID"), Some("call-1@host".into()));
371 assert_eq!(
372 extract_header(msg, "Contact"),
373 Some("<sip:alice@192.0.2.1>".into())
374 );
375 assert_eq!(
376 extract_header(msg, "Content-Type"),
377 Some("application/sdp".into())
378 );
379 assert_eq!(extract_header(msg, "c"), Some("application/sdp".into()));
380 }
381
382 #[test]
383 fn compact_form_case_insensitive() {
384 let msg = "SIP/2.0 200 OK\r\nF: Alice <sip:alice@host>\r\n\r\n";
385 assert_eq!(
386 extract_header(msg, "From"),
387 Some("Alice <sip:alice@host>".into())
388 );
389 }
390
391 #[test]
392 fn compact_form_unknown_single_char() {
393 let msg = "SIP/2.0 200 OK\r\nz: something\r\n\r\n";
394 assert_eq!(extract_header(msg, "z"), Some("something".into()));
395 assert_eq!(extract_header(msg, "From"), None);
396 }
397
398 const NG911_INVITE: &str = concat!(
401 "INVITE sip:urn:service:sos@bcf.example.com SIP/2.0\r\n",
402 "Via: SIP/2.0/TLS proxy.example.com;branch=z9hG4bK776\r\n",
403 "From: \"Caller Name\" <sip:+15551234567@orig.example.com>;tag=abc123\r\n",
404 "To: <sip:urn:service:sos@bcf.example.com>\r\n",
405 "Call-ID: ng911-call-42@orig.example.com\r\n",
406 "P-Asserted-Identity: \"EXAMPLE CO\" <sip:+15551234567@198.51.100.1>\r\n",
407 "Call-Info: <urn:emergency:uid:callid:abc:bcf.example.com>;purpose=emergency-CallId,",
408 "<https://adr.example.com/serviceInfo?t=x>;purpose=EmergencyCallData.ServiceInfo\r\n",
409 "Geolocation: <cid:loc-id-1234>, <https://lis.example.com/held/test>\r\n",
410 "Content-Type: application/sdp\r\n",
411 "\r\n",
412 "v=0\r\n",
413 );
414
415 #[test]
416 fn extract_and_parse_call_info() {
417 use crate::call_info::SipCallInfo;
418
419 let raw = extract_header(NG911_INVITE, "Call-Info").unwrap();
420 let ci = SipCallInfo::parse(&raw).unwrap();
421 assert_eq!(ci.len(), 2);
422 assert_eq!(ci.entries()[0].purpose(), Some("emergency-CallId"));
423 assert!(ci
424 .entries()
425 .iter()
426 .any(|e| e.purpose() == Some("EmergencyCallData.ServiceInfo")));
427 }
428
429 #[test]
430 fn extract_and_parse_p_asserted_identity() {
431 use crate::header_addr::SipHeaderAddr;
432
433 let raw = extract_header(NG911_INVITE, "P-Asserted-Identity").unwrap();
434 let pai: SipHeaderAddr = raw
435 .parse()
436 .unwrap();
437 assert_eq!(pai.display_name(), Some("EXAMPLE CO"));
438 assert!(pai
439 .uri()
440 .to_string()
441 .contains("+15551234567"));
442 }
443
444 #[test]
445 fn extract_and_parse_geolocation() {
446 use crate::geolocation::SipGeolocation;
447
448 let raw = extract_header(NG911_INVITE, "Geolocation").unwrap();
449 let geo = SipGeolocation::parse(&raw);
450 assert_eq!(geo.len(), 2);
451 assert_eq!(geo.cid(), Some("loc-id-1234"));
452 assert!(geo
453 .url()
454 .unwrap()
455 .contains("lis.example.com"));
456 }
457
458 #[test]
459 fn extract_and_parse_from_to() {
460 use crate::header_addr::SipHeaderAddr;
461
462 let from_raw = extract_header(NG911_INVITE, "From").unwrap();
463 let from: SipHeaderAddr = from_raw
464 .parse()
465 .unwrap();
466 assert_eq!(from.display_name(), Some("Caller Name"));
467 assert_eq!(from.tag(), Some("abc123"));
468
469 let to_raw = extract_header(NG911_INVITE, "To").unwrap();
470 let to: SipHeaderAddr = to_raw
471 .parse()
472 .unwrap();
473 assert!(to
474 .uri()
475 .to_string()
476 .contains("urn:service:sos"));
477 }
478
479 #[test]
482 fn extract_request_uri_invite() {
483 let msg = "INVITE urn:service:sos SIP/2.0\r\nTo: <urn:service:sos>\r\n\r\n";
484 assert_eq!(extract_request_uri(msg), Some("urn:service:sos".into()));
485 }
486
487 #[test]
488 fn extract_request_uri_sip() {
489 let msg = "INVITE sip:+15550001234@198.51.100.1:5060 SIP/2.0\r\n\r\n";
490 assert_eq!(
491 extract_request_uri(msg),
492 Some("sip:+15550001234@198.51.100.1:5060".into()),
493 );
494 }
495
496 #[test]
497 fn extract_request_uri_status_line() {
498 let msg = "SIP/2.0 200 OK\r\n\r\n";
499 assert_eq!(extract_request_uri(msg), None);
500 }
501
502 #[test]
503 fn extract_request_uri_empty() {
504 assert_eq!(extract_request_uri(""), None);
505 }
506}