1use crate::header::SipHeader;
9
10const COMPACT_FORMS: &[(u8, &str)] = &[
15 (b'a', "Accept-Contact"),
16 (b'b', "Referred-By"),
17 (b'c', "Content-Type"),
18 (b'd', "Request-Disposition"),
19 (b'e', "Content-Encoding"),
20 (b'f', "From"),
21 (b'i', "Call-ID"),
22 (b'j', "Reject-Contact"),
23 (b'k', "Supported"),
24 (b'l', "Content-Length"),
25 (b'm', "Contact"),
26 (b'n', "Identity-Info"),
27 (b'o', "Event"),
28 (b'r', "Refer-To"),
29 (b's', "Subject"),
30 (b't', "To"),
31 (b'u', "Allow-Events"),
32 (b'v', "Via"),
33 (b'x', "Session-Expires"),
34 (b'y', "Identity"),
35];
36
37fn matches_header_name(wire_name: &str, target: &str) -> bool {
40 if wire_name.eq_ignore_ascii_case(target) {
41 return true;
42 }
43 let equiv = if target.len() == 1 {
45 let ch = target.as_bytes()[0].to_ascii_lowercase();
46 COMPACT_FORMS
47 .iter()
48 .find(|(c, _)| *c == ch)
49 } else {
50 COMPACT_FORMS
51 .iter()
52 .find(|(_, full)| full.eq_ignore_ascii_case(target))
53 };
54 if let Some(&(compact, full)) = equiv {
55 if wire_name.len() == 1 {
56 wire_name.as_bytes()[0].to_ascii_lowercase() == compact
57 } else {
58 wire_name.eq_ignore_ascii_case(full)
59 }
60 } else {
61 false
62 }
63}
64
65pub fn extract_header(message: &str, name: &str) -> Option<String> {
78 let mut values: Vec<String> = Vec::new();
79 let mut current_match = false;
80
81 for line in message.split('\n') {
82 let line = line
83 .strip_suffix('\r')
84 .unwrap_or(line);
85
86 if line.is_empty() {
87 break;
88 }
89
90 if line.starts_with(' ') || line.starts_with('\t') {
91 if current_match {
92 if let Some(last) = values.last_mut() {
93 last.push(' ');
94 last.push_str(line.trim_start());
95 }
96 }
97 continue;
98 }
99
100 current_match = false;
101
102 if let Some((hdr_name, hdr_value)) = line.split_once(':') {
103 let hdr_name = hdr_name.trim_end();
104 if !hdr_name.contains(' ') && matches_header_name(hdr_name, name) {
108 current_match = true;
109 values.push(
110 hdr_value
111 .trim_start()
112 .to_string(),
113 );
114 }
115 }
116 }
117
118 if values.is_empty() {
119 None
120 } else {
121 Some(values.join(", "))
122 }
123}
124
125impl SipHeader {
126 pub fn extract_from(&self, message: &str) -> Option<String> {
132 extract_header(message, self.as_str())
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 const SAMPLE_INVITE: &str = "\
141INVITE sip:bob@biloxi.example.com SIP/2.0\r\n\
142Via: SIP/2.0/UDP pc33.atlanta.example.com;branch=z9hG4bK776asdhds\r\n\
143Via: SIP/2.0/UDP bigbox3.site3.atlanta.example.com;branch=z9hG4bKnashds8\r\n\
144Max-Forwards: 70\r\n\
145To: Bob <sip:bob@biloxi.example.com>\r\n\
146From: Alice <sip:alice@atlanta.example.com>;tag=1928301774\r\n\
147Call-ID: a84b4c76e66710@pc33.atlanta.example.com\r\n\
148CSeq: 314159 INVITE\r\n\
149Contact: <sip:alice@pc33.atlanta.example.com>\r\n\
150Content-Type: application/sdp\r\n\
151Content-Length: 142\r\n\
152\r\n\
153v=0\r\n\
154o=alice 2890844526 2890844526 IN IP4 pc33.atlanta.example.com\r\n";
155
156 #[test]
157 fn basic_extraction() {
158 assert_eq!(
159 extract_header(SAMPLE_INVITE, "From"),
160 Some("Alice <sip:alice@atlanta.example.com>;tag=1928301774".into())
161 );
162 assert_eq!(
163 extract_header(SAMPLE_INVITE, "Call-ID"),
164 Some("a84b4c76e66710@pc33.atlanta.example.com".into())
165 );
166 assert_eq!(
167 extract_header(SAMPLE_INVITE, "CSeq"),
168 Some("314159 INVITE".into())
169 );
170 }
171
172 #[test]
173 fn case_insensitive_name() {
174 let expected = Some("Alice <sip:alice@atlanta.example.com>;tag=1928301774".into());
175 assert_eq!(extract_header(SAMPLE_INVITE, "from"), expected);
176 assert_eq!(extract_header(SAMPLE_INVITE, "FROM"), expected);
177 assert_eq!(extract_header(SAMPLE_INVITE, "From"), expected);
178 }
179
180 #[test]
181 fn header_folding() {
182 let msg = concat!(
183 "SIP/2.0 200 OK\r\n",
184 "Subject: I know you're there,\r\n",
185 " pick up the phone\r\n",
186 " and talk to me!\r\n",
187 "\r\n",
188 );
189 assert_eq!(
190 extract_header(msg, "Subject"),
191 Some("I know you're there, pick up the phone and talk to me!".into())
192 );
193 }
194
195 #[test]
196 fn multiple_occurrences_concatenated() {
197 assert_eq!(
198 extract_header(SAMPLE_INVITE, "Via"),
199 Some(
200 "SIP/2.0/UDP pc33.atlanta.example.com;branch=z9hG4bK776asdhds, \
201 SIP/2.0/UDP bigbox3.site3.atlanta.example.com;branch=z9hG4bKnashds8"
202 .into()
203 )
204 );
205 }
206
207 #[test]
208 fn stops_at_blank_line() {
209 assert_eq!(extract_header(SAMPLE_INVITE, "o"), None);
211 }
212
213 #[test]
214 fn bare_lf_line_endings() {
215 let msg = "SIP/2.0 200 OK\n\
216 From: Alice <sip:alice@host>\n\
217 To: Bob <sip:bob@host>\n\
218 \n\
219 body\n";
220 assert_eq!(
221 extract_header(msg, "From"),
222 Some("Alice <sip:alice@host>".into())
223 );
224 }
225
226 #[test]
227 fn missing_header_returns_none() {
228 assert_eq!(extract_header(SAMPLE_INVITE, "X-Custom"), None);
229 }
230
231 #[test]
232 fn empty_message() {
233 assert_eq!(extract_header("", "From"), None);
234 }
235
236 #[test]
237 fn request_line_not_matched() {
238 assert_eq!(extract_header(SAMPLE_INVITE, "INVITE sip"), None);
240 }
241
242 #[test]
243 fn value_leading_whitespace_trimmed() {
244 let msg = "SIP/2.0 200 OK\r\n\
245 From: Alice <sip:alice@host>\r\n\
246 \r\n";
247 assert_eq!(
248 extract_header(msg, "From"),
249 Some("Alice <sip:alice@host>".into())
250 );
251 }
252
253 #[test]
254 fn folding_on_multiple_occurrence() {
255 let msg = concat!(
256 "SIP/2.0 200 OK\r\n",
257 "Via: SIP/2.0/UDP first.example.com\r\n",
258 " ;branch=z9hG4bKaaa\r\n",
259 "Via: SIP/2.0/UDP second.example.com;branch=z9hG4bKbbb\r\n",
260 "\r\n",
261 );
262 assert_eq!(
263 extract_header(msg, "Via"),
264 Some(
265 "SIP/2.0/UDP first.example.com ;branch=z9hG4bKaaa, \
266 SIP/2.0/UDP second.example.com;branch=z9hG4bKbbb"
267 .into()
268 )
269 );
270 }
271
272 #[test]
273 fn empty_header_value() {
274 let msg = "SIP/2.0 200 OK\r\n\
275 Subject:\r\n\
276 From: Alice <sip:alice@host>\r\n\
277 \r\n";
278 assert_eq!(extract_header(msg, "Subject"), Some(String::new()));
279 }
280
281 #[test]
282 fn tab_folding() {
283 let msg = concat!(
284 "SIP/2.0 200 OK\r\n",
285 "Subject: hello\r\n",
286 "\tworld\r\n",
287 "\r\n",
288 );
289 assert_eq!(extract_header(msg, "Subject"), Some("hello world".into()));
290 }
291
292 #[test]
295 fn compact_form_from() {
296 let msg = "SIP/2.0 200 OK\r\nf: Alice <sip:alice@host>\r\n\r\n";
297 assert_eq!(
298 extract_header(msg, "From"),
299 Some("Alice <sip:alice@host>".into())
300 );
301 assert_eq!(
302 extract_header(msg, "f"),
303 Some("Alice <sip:alice@host>".into())
304 );
305 }
306
307 #[test]
308 fn compact_form_via() {
309 let msg = "SIP/2.0 200 OK\r\nv: SIP/2.0/UDP host\r\n\r\n";
310 assert_eq!(extract_header(msg, "Via"), Some("SIP/2.0/UDP host".into()));
311 assert_eq!(extract_header(msg, "v"), Some("SIP/2.0/UDP host".into()));
312 }
313
314 #[test]
315 fn compact_form_mixed_with_full() {
316 let msg = concat!(
317 "SIP/2.0 200 OK\r\n",
318 "f: Alice <sip:alice@host>;tag=a\r\n",
319 "t: Bob <sip:bob@host>;tag=b\r\n",
320 "i: call-1@host\r\n",
321 "m: <sip:alice@192.0.2.1>\r\n",
322 "Content-Type: application/sdp\r\n",
323 "\r\n",
324 );
325 assert_eq!(
326 extract_header(msg, "From"),
327 Some("Alice <sip:alice@host>;tag=a".into())
328 );
329 assert_eq!(
330 extract_header(msg, "To"),
331 Some("Bob <sip:bob@host>;tag=b".into())
332 );
333 assert_eq!(extract_header(msg, "Call-ID"), Some("call-1@host".into()));
334 assert_eq!(
335 extract_header(msg, "Contact"),
336 Some("<sip:alice@192.0.2.1>".into())
337 );
338 assert_eq!(
339 extract_header(msg, "Content-Type"),
340 Some("application/sdp".into())
341 );
342 assert_eq!(extract_header(msg, "c"), Some("application/sdp".into()));
343 }
344
345 #[test]
346 fn compact_form_case_insensitive() {
347 let msg = "SIP/2.0 200 OK\r\nF: Alice <sip:alice@host>\r\n\r\n";
348 assert_eq!(
349 extract_header(msg, "From"),
350 Some("Alice <sip:alice@host>".into())
351 );
352 }
353
354 #[test]
355 fn compact_form_unknown_single_char() {
356 let msg = "SIP/2.0 200 OK\r\nz: something\r\n\r\n";
357 assert_eq!(extract_header(msg, "z"), Some("something".into()));
358 assert_eq!(extract_header(msg, "From"), None);
359 }
360
361 const NG911_INVITE: &str = concat!(
364 "INVITE sip:urn:service:sos@bcf.example.com SIP/2.0\r\n",
365 "Via: SIP/2.0/TLS proxy.example.com;branch=z9hG4bK776\r\n",
366 "From: \"Caller Name\" <sip:+15551234567@orig.example.com>;tag=abc123\r\n",
367 "To: <sip:urn:service:sos@bcf.example.com>\r\n",
368 "Call-ID: ng911-call-42@orig.example.com\r\n",
369 "P-Asserted-Identity: \"EXAMPLE CO\" <sip:+15551234567@198.51.100.1>\r\n",
370 "Call-Info: <urn:emergency:uid:callid:abc:bcf.example.com>;purpose=emergency-CallId,",
371 "<https://adr.example.com/serviceInfo?t=x>;purpose=EmergencyCallData.ServiceInfo\r\n",
372 "Geolocation: <cid:loc-id-1234>, <https://lis.example.com/held/test>\r\n",
373 "Content-Type: application/sdp\r\n",
374 "\r\n",
375 "v=0\r\n",
376 );
377
378 #[test]
379 fn extract_and_parse_call_info() {
380 use crate::call_info::SipCallInfo;
381
382 let raw = extract_header(NG911_INVITE, "Call-Info").unwrap();
383 let ci = SipCallInfo::parse(&raw).unwrap();
384 assert_eq!(ci.len(), 2);
385 assert_eq!(ci.entries()[0].purpose(), Some("emergency-CallId"));
386 assert!(ci
387 .entries()
388 .iter()
389 .any(|e| e.purpose() == Some("EmergencyCallData.ServiceInfo")));
390 }
391
392 #[test]
393 fn extract_and_parse_p_asserted_identity() {
394 use crate::header_addr::SipHeaderAddr;
395
396 let raw = extract_header(NG911_INVITE, "P-Asserted-Identity").unwrap();
397 let pai: SipHeaderAddr = raw
398 .parse()
399 .unwrap();
400 assert_eq!(pai.display_name(), Some("EXAMPLE CO"));
401 assert!(pai
402 .uri()
403 .to_string()
404 .contains("+15551234567"));
405 }
406
407 #[test]
408 fn extract_and_parse_geolocation() {
409 use crate::geolocation::SipGeolocation;
410
411 let raw = extract_header(NG911_INVITE, "Geolocation").unwrap();
412 let geo = SipGeolocation::parse(&raw);
413 assert_eq!(geo.len(), 2);
414 assert_eq!(geo.cid(), Some("loc-id-1234"));
415 assert!(geo
416 .url()
417 .unwrap()
418 .contains("lis.example.com"));
419 }
420
421 #[test]
422 fn extract_and_parse_from_to() {
423 use crate::header_addr::SipHeaderAddr;
424
425 let from_raw = extract_header(NG911_INVITE, "From").unwrap();
426 let from: SipHeaderAddr = from_raw
427 .parse()
428 .unwrap();
429 assert_eq!(from.display_name(), Some("Caller Name"));
430 assert_eq!(from.tag(), Some("abc123"));
431
432 let to_raw = extract_header(NG911_INVITE, "To").unwrap();
433 let to: SipHeaderAddr = to_raw
434 .parse()
435 .unwrap();
436 assert!(to
437 .uri()
438 .to_string()
439 .contains("urn:service:sos"));
440 }
441}