1use bytes::Bytes;
19use sipx_sip::headers::{From as FromHeader, To};
20use sipx_sip::{HeaderName, Request, Response, Uri};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Role {
25 Caller,
27 Callee,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33pub struct DialogId {
34 pub call_id: Vec<u8>,
36 pub local_tag: Vec<u8>,
38 pub remote_tag: Vec<u8>,
40}
41
42#[derive(Debug, Clone)]
44pub struct Dialog {
45 pub role: Role,
47 pub id: DialogId,
49 pub local_uri: String,
51 pub remote_uri: String,
53 pub remote_target: Uri,
55 pub local_cseq: u32,
57 pub remote_cseq: Option<u32>,
59 pub route_set: Vec<String>,
61}
62
63impl Dialog {
64 #[must_use]
69 pub fn from_response(request: &Request, response: &Response) -> Option<Self> {
70 let call_id = response.headers.value(&HeaderName::CallId)?.into_owned();
71 let local_tag = tag_of::<FromHeader>(&response.headers)?;
72 let remote_tag = tag_of::<To>(&response.headers)?;
73 let remote_target = contact_uri(&response.headers)?;
74
75 let mut route_set = record_routes(&response.headers);
79 route_set.reverse();
80
81 Some(Self {
82 role: Role::Caller,
83 id: DialogId {
84 call_id,
85 local_tag,
86 remote_tag,
87 },
88 local_uri: header_string(&request.headers, &HeaderName::From),
89 remote_uri: header_string(&request.headers, &HeaderName::To),
90 remote_target,
91 local_cseq: cseq_number(&request.headers).unwrap_or(1),
92 remote_cseq: None,
93 route_set,
94 })
95 }
96
97 #[must_use]
99 pub fn from_request(request: &Request, local_tag: &str) -> Option<Self> {
100 let call_id = request.headers.value(&HeaderName::CallId)?.into_owned();
101 let remote_tag = tag_of::<FromHeader>(&request.headers)?;
102 let remote_target = contact_uri(&request.headers)?;
103
104 let route_set = record_routes(&request.headers);
107
108 Some(Self {
109 role: Role::Callee,
110 id: DialogId {
111 call_id,
112 local_tag: local_tag.as_bytes().to_vec(),
113 remote_tag,
114 },
115 local_uri: header_string(&request.headers, &HeaderName::To),
116 remote_uri: header_string(&request.headers, &HeaderName::From),
117 remote_target,
118 local_cseq: 0,
120 remote_cseq: cseq_number(&request.headers),
121 route_set,
122 })
123 }
124
125 #[must_use]
130 pub fn local_and_remote(&self) -> (String, String) {
131 let local = format!(
132 "{};tag={}",
133 strip_header_params(&self.local_uri),
134 String::from_utf8_lossy(&self.id.local_tag)
135 );
136 let remote = format!(
137 "{};tag={}",
138 strip_header_params(&self.remote_uri),
139 String::from_utf8_lossy(&self.id.remote_tag)
140 );
141 (local, remote)
142 }
143
144 pub fn next_cseq(&mut self) -> u32 {
146 self.local_cseq = self.local_cseq.saturating_add(1);
147 self.local_cseq
148 }
149
150 #[must_use]
152 pub fn first_route(&self) -> Option<Uri> {
153 uri_in(self.route_set.first()?)
154 }
155
156 #[must_use]
165 pub fn hop(&self) -> Uri {
166 self.first_route()
167 .unwrap_or_else(|| self.remote_target.clone())
168 }
169
170 #[must_use]
179 pub fn request_target(&self) -> (Uri, Vec<String>) {
180 let Some(first) = self.first_route() else {
181 return (self.remote_target.clone(), Vec::new());
182 };
183 if first.params().is_some_and(|params| params.contains("lr")) {
184 return (self.remote_target.clone(), self.route_set.clone());
185 }
186
187 let mut routes: Vec<String> = self.route_set.iter().skip(1).cloned().collect();
188 routes.push(format!(
189 "<{}>",
190 String::from_utf8_lossy(&self.remote_target.to_bytes())
191 ));
192 (as_request_uri(&first), routes)
193 }
194
195 pub fn refresh_target(&mut self, headers: &sipx_sip::Headers) {
202 if let Some(contact) = contact_uri(headers) {
203 self.remote_target = contact;
204 }
205 }
206
207 #[must_use]
220 pub fn is_out_of_order(&self, request: &Request) -> bool {
221 let Some(sequence) = cseq_number(&request.headers) else {
222 return false;
226 };
227 self.remote_cseq.is_some_and(|last| sequence <= last)
228 }
229
230 pub fn record_remote_cseq(&mut self, request: &Request) {
236 if let Some(sequence) = cseq_number(&request.headers) {
237 self.remote_cseq = Some(self.remote_cseq.map_or(sequence, |last| last.max(sequence)));
238 }
239 }
240
241 #[must_use]
243 pub fn matches(&self, request: &Request) -> bool {
244 let Some(call_id) = request.headers.value(&HeaderName::CallId) else {
245 return false;
246 };
247 if call_id.as_ref() != self.id.call_id.as_slice() {
248 return false;
249 }
250 let their_tag = tag_of::<FromHeader>(&request.headers);
252 let our_tag = tag_of::<To>(&request.headers);
253 their_tag.as_deref() == Some(self.id.remote_tag.as_slice())
254 && our_tag.as_deref() == Some(self.id.local_tag.as_slice())
255 }
256}
257
258pub(crate) fn strip_header_params(value: &str) -> String {
266 if let Some(end) = value.rfind('>') {
267 return value.get(..=end).unwrap_or(value).trim().to_owned();
268 }
269 value.split(';').next().unwrap_or(value).trim().to_owned()
272}
273
274fn header_string(headers: &sipx_sip::Headers, name: &HeaderName) -> String {
275 headers
276 .value(name)
277 .map(|value| String::from_utf8_lossy(&value).into_owned())
278 .unwrap_or_default()
279}
280
281pub(crate) fn from_tag(headers: &sipx_sip::Headers) -> Option<Vec<u8>> {
287 tag_of::<FromHeader>(headers)
288}
289
290pub(crate) fn to_tag(headers: &sipx_sip::Headers) -> Option<Vec<u8>> {
293 tag_of::<To>(headers)
294}
295
296fn tag_of<T>(headers: &sipx_sip::Headers) -> Option<Vec<u8>>
297where
298 T: sipx_sip::TypedHeader,
299 T: HasTag,
300{
301 headers
302 .typed::<T>()
303 .and_then(Result::ok)
304 .and_then(|header| header.tag_bytes())
305}
306
307pub trait HasTag {
309 fn tag_bytes(&self) -> Option<Vec<u8>>;
311}
312
313impl HasTag for To {
314 fn tag_bytes(&self) -> Option<Vec<u8>> {
315 self.tag().map(<[u8]>::to_vec)
316 }
317}
318
319impl HasTag for FromHeader {
320 fn tag_bytes(&self) -> Option<Vec<u8>> {
321 self.tag().map(<[u8]>::to_vec)
322 }
323}
324
325fn contact_uri(headers: &sipx_sip::Headers) -> Option<Uri> {
326 let value = headers.value(&HeaderName::Contact)?;
327 uri_in(&String::from_utf8_lossy(&value))
328}
329
330fn as_request_uri(uri: &Uri) -> Uri {
341 let raw = uri.to_bytes();
342 let text = String::from_utf8_lossy(&raw);
343 let without_headers = text.split('?').next().unwrap_or(&text);
344
345 let mut parts = without_headers.split(';');
346 let Some(head) = parts.next() else {
347 return uri.clone();
348 };
349 let mut rebuilt = head.to_owned();
350 for param in parts {
351 let name = param.split('=').next().unwrap_or(param);
352 if name.eq_ignore_ascii_case("method") {
353 continue;
354 }
355 rebuilt.push(';');
356 rebuilt.push_str(param);
357 }
358
359 Uri::parse(Bytes::from(rebuilt)).unwrap_or_else(|_| uri.clone())
362}
363
364fn uri_in(text: &str) -> Option<Uri> {
370 let inner = text
371 .split_once('<')
372 .and_then(|(_, rest)| rest.split_once('>'))
373 .map_or_else(
374 || text.split(';').next().unwrap_or(text).trim().to_owned(),
375 |(uri, _)| uri.to_owned(),
376 );
377 Uri::parse(Bytes::from(inner)).ok()
378}
379
380fn record_routes(headers: &sipx_sip::Headers) -> Vec<String> {
387 let mut routes = Vec::new();
388 for header in headers.get_all(&HeaderName::RecordRoute) {
389 routes.extend(split_routes(&header.value()));
390 }
391 routes
392}
393
394fn split_routes(value: &[u8]) -> Vec<String> {
399 let mut routes = Vec::new();
400 let mut depth = 0usize;
401 let mut quoted = false;
402 let mut start = 0usize;
403
404 for (index, &byte) in value.iter().enumerate() {
405 match byte {
406 b'"' => quoted = !quoted,
407 b'<' if !quoted => depth += 1,
408 b'>' if !quoted => depth = depth.saturating_sub(1),
409 b',' if !quoted && depth == 0 => {
410 push_route(&mut routes, value.get(start..index));
411 start = index + 1;
412 }
413 _ => {}
414 }
415 }
416 push_route(&mut routes, value.get(start..));
417 routes
418}
419
420fn push_route(routes: &mut Vec<String>, slice: Option<&[u8]>) {
421 let Some(slice) = slice else {
422 return;
423 };
424 let text = String::from_utf8_lossy(slice).trim().to_owned();
425 if !text.is_empty() {
426 routes.push(text);
427 }
428}
429
430pub(crate) fn cseq_number(headers: &sipx_sip::Headers) -> Option<u32> {
435 headers
436 .typed::<sipx_sip::headers::CSeq>()
437 .and_then(Result::ok)
438 .map(|cseq| cseq.sequence)
439}
440
441#[cfg(test)]
442#[allow(
443 clippy::unwrap_used,
444 clippy::expect_used,
445 clippy::panic,
446 clippy::indexing_slicing
447)]
448mod tests {
449 use super::*;
450 use sipx_sip::{Limits, Message, parse_datagram};
451
452 fn request(text: &str) -> Request {
453 match parse_datagram(Bytes::from(text.to_owned()), &Limits::datagram()).expect("parses") {
454 Message::Request(r) => r,
455 Message::Response(_) => panic!("a request"),
456 }
457 }
458
459 fn response(text: &str) -> Response {
460 match parse_datagram(Bytes::from(text.to_owned()), &Limits::datagram()).expect("parses") {
461 Message::Response(r) => r,
462 Message::Request(_) => panic!("a response"),
463 }
464 }
465
466 fn invite() -> Request {
467 request(
468 "INVITE sip:bob@example.com SIP/2.0\r\n\
469 Via: SIP/2.0/UDP 192.0.2.1:5060;branch=z9hG4bKcall\r\n\
470 To: <sip:bob@example.com>\r\n\
471 From: <sip:alice@example.net>;tag=alicetag\r\n\
472 Call-ID: thecall@example.net\r\n\
473 CSeq: 1 INVITE\r\n\
474 Contact: <sip:alice@192.0.2.1:5060>\r\n\
475 Max-Forwards: 70\r\n\
476 Content-Length: 0\r\n\r\n",
477 )
478 }
479
480 fn ok() -> Response {
481 response(
482 "SIP/2.0 200 OK\r\n\
483 Via: SIP/2.0/UDP 192.0.2.1:5060;branch=z9hG4bKcall\r\n\
484 To: <sip:bob@example.com>;tag=bobtag\r\n\
485 From: <sip:alice@example.net>;tag=alicetag\r\n\
486 Call-ID: thecall@example.net\r\n\
487 CSeq: 1 INVITE\r\n\
488 Contact: <sip:bob@192.0.2.9:5060>\r\n\
489 Content-Length: 0\r\n\r\n",
490 )
491 }
492
493 #[test]
494 fn the_callers_dialog_takes_its_tags_from_the_right_places() {
495 let dialog = Dialog::from_response(&invite(), &ok()).expect("a dialog");
496 assert_eq!(dialog.role, Role::Caller);
497 assert_eq!(dialog.id.call_id, b"thecall@example.net");
498 assert_eq!(dialog.id.local_tag, b"alicetag", "our tag is in From");
499 assert_eq!(dialog.id.remote_tag, b"bobtag", "theirs is in To");
500 assert_eq!(
501 dialog.remote_target.to_bytes().as_ref(),
502 b"sip:bob@192.0.2.9:5060"
503 );
504 }
505
506 #[test]
509 fn the_callees_dialog_is_the_mirror_of_the_callers() {
510 let dialog = Dialog::from_request(&invite(), "bobtag").expect("a dialog");
511 assert_eq!(dialog.role, Role::Callee);
512 assert_eq!(dialog.id.local_tag, b"bobtag", "the tag we chose");
513 assert_eq!(dialog.id.remote_tag, b"alicetag", "theirs is in From");
514 assert_eq!(
515 dialog.remote_target.to_bytes().as_ref(),
516 b"sip:alice@192.0.2.1:5060"
517 );
518 }
519
520 #[test]
522 fn both_halves_describe_the_same_dialog() {
523 let uac = Dialog::from_response(&invite(), &ok()).expect("a dialog");
524 let uas = Dialog::from_request(&invite(), "bobtag").expect("a dialog");
525
526 assert_eq!(uac.id.call_id, uas.id.call_id);
527 assert_eq!(uac.id.local_tag, uas.id.remote_tag);
528 assert_eq!(uac.id.remote_tag, uas.id.local_tag);
529 }
530
531 #[test]
534 fn the_two_sides_number_their_requests_independently() {
535 let mut uac = Dialog::from_response(&invite(), &ok()).expect("a dialog");
536 let mut uas = Dialog::from_request(&invite(), "bobtag").expect("a dialog");
537
538 assert_eq!(uac.next_cseq(), 2, "the INVITE was 1");
539 assert_eq!(uas.next_cseq(), 1, "the callee starts its own count");
540 assert_eq!(uac.next_cseq(), 3);
541 assert_eq!(uas.next_cseq(), 2);
542 }
543
544 #[test]
548 fn a_request_from_the_callee_puts_the_callee_in_from() {
549 let callee = Dialog::from_request(&invite(), "bobtag").expect("a dialog");
550 let (local, remote) = callee.local_and_remote();
551 assert!(local.contains("bob@example.com"), "{local}");
552 assert!(local.contains("tag=bobtag"), "{local}");
553 assert!(remote.contains("alice@example.net"), "{remote}");
554 assert!(remote.contains("tag=alicetag"), "{remote}");
555 }
556
557 #[test]
558 fn a_request_from_the_caller_puts_the_caller_in_from() {
559 let caller = Dialog::from_response(&invite(), &ok()).expect("a dialog");
560 let (local, remote) = caller.local_and_remote();
561 assert!(local.contains("alice@example.net"), "{local}");
562 assert!(local.contains("tag=alicetag"), "{local}");
563 assert!(remote.contains("bob@example.com"), "{remote}");
564 }
565
566 #[test]
568 fn an_in_dialog_request_matches_its_dialog() {
569 let caller = Dialog::from_response(&invite(), &ok()).expect("a dialog");
570 let bye = request(
571 "BYE sip:alice@192.0.2.1:5060 SIP/2.0\r\n\
572 Via: SIP/2.0/UDP 192.0.2.9:5060;branch=z9hG4bKbye\r\n\
573 To: <sip:alice@example.net>;tag=alicetag\r\n\
574 From: <sip:bob@example.com>;tag=bobtag\r\n\
575 Call-ID: thecall@example.net\r\n\
576 CSeq: 1 BYE\r\n\
577 Max-Forwards: 70\r\n\
578 Content-Length: 0\r\n\r\n",
579 );
580 assert!(caller.matches(&bye));
581 }
582
583 #[test]
584 fn a_request_from_another_call_does_not_match() {
585 let caller = Dialog::from_response(&invite(), &ok()).expect("a dialog");
586 let other = request(
587 "BYE sip:alice@192.0.2.1:5060 SIP/2.0\r\n\
588 Via: SIP/2.0/UDP 192.0.2.9:5060;branch=z9hG4bKbye\r\n\
589 To: <sip:alice@example.net>;tag=alicetag\r\n\
590 From: <sip:bob@example.com>;tag=bobtag\r\n\
591 Call-ID: a-different-call@example.net\r\n\
592 CSeq: 1 BYE\r\n\
593 Max-Forwards: 70\r\n\
594 Content-Length: 0\r\n\r\n",
595 );
596 assert!(!caller.matches(&other));
597 }
598
599 #[test]
601 fn a_response_without_a_to_tag_creates_no_dialog() {
602 let trying = response(
603 "SIP/2.0 100 Trying\r\n\
604 Via: SIP/2.0/UDP 192.0.2.1:5060;branch=z9hG4bKcall\r\n\
605 To: <sip:bob@example.com>\r\n\
606 From: <sip:alice@example.net>;tag=alicetag\r\n\
607 Call-ID: thecall@example.net\r\n\
608 CSeq: 1 INVITE\r\n\
609 Content-Length: 0\r\n\r\n",
610 );
611 assert!(Dialog::from_response(&invite(), &trying).is_none());
612 }
613
614 #[test]
617 fn the_caller_reverses_the_route_set_and_the_callee_does_not() {
618 let routed_ok = response(
619 "SIP/2.0 200 OK\r\n\
620 Via: SIP/2.0/UDP 192.0.2.1:5060;branch=z9hG4bKcall\r\n\
621 Record-Route: <sip:proxy1.example.com;lr>\r\n\
622 Record-Route: <sip:proxy2.example.com;lr>\r\n\
623 To: <sip:bob@example.com>;tag=bobtag\r\n\
624 From: <sip:alice@example.net>;tag=alicetag\r\n\
625 Call-ID: thecall@example.net\r\n\
626 CSeq: 1 INVITE\r\n\
627 Contact: <sip:bob@192.0.2.9:5060>\r\n\
628 Content-Length: 0\r\n\r\n",
629 );
630 let caller = Dialog::from_response(&invite(), &routed_ok).expect("a dialog");
631 assert_eq!(caller.route_set.len(), 2);
632 assert!(
633 caller.route_set[0].contains("proxy2"),
634 "reversed for the caller: {:?}",
635 caller.route_set
636 );
637
638 let routed_invite = request(
639 "INVITE sip:bob@example.com SIP/2.0\r\n\
640 Via: SIP/2.0/UDP 192.0.2.1:5060;branch=z9hG4bKcall\r\n\
641 Record-Route: <sip:proxy1.example.com;lr>\r\n\
642 Record-Route: <sip:proxy2.example.com;lr>\r\n\
643 To: <sip:bob@example.com>\r\n\
644 From: <sip:alice@example.net>;tag=alicetag\r\n\
645 Call-ID: thecall@example.net\r\n\
646 CSeq: 1 INVITE\r\n\
647 Contact: <sip:alice@192.0.2.1:5060>\r\n\
648 Max-Forwards: 70\r\n\
649 Content-Length: 0\r\n\r\n",
650 );
651 let uas = Dialog::from_request(&routed_invite, "bobtag").expect("a dialog");
652 assert!(
653 uas.route_set[0].contains("proxy1"),
654 "as received for the callee: {:?}",
655 uas.route_set
656 );
657 }
658
659 #[test]
663 fn several_routes_on_one_line_are_separate_routes() {
664 let routed = response(
665 "SIP/2.0 200 OK\r\n\
666 Via: SIP/2.0/UDP 192.0.2.1:5060;branch=z9hG4bKcall\r\n\
667 Record-Route: <sip:proxy1.example.com;lr>, <sip:proxy2.example.com;lr>\r\n\
668 Record-Route: <sip:proxy3.example.com;lr>\r\n\
669 To: <sip:bob@example.com>;tag=bobtag\r\n\
670 From: <sip:alice@example.net>;tag=alicetag\r\n\
671 Call-ID: thecall@example.net\r\n\
672 CSeq: 1 INVITE\r\n\
673 Contact: <sip:bob@192.0.2.9:5060>\r\n\
674 Content-Length: 0\r\n\r\n",
675 );
676 let caller = Dialog::from_response(&invite(), &routed).expect("a dialog");
677 assert_eq!(
678 caller.route_set.len(),
679 3,
680 "three routes: {:?}",
681 caller.route_set
682 );
683 assert!(
684 caller.route_set[0].contains("proxy3"),
685 "{:?}",
686 caller.route_set
687 );
688 assert!(
689 caller.route_set[1].contains("proxy2"),
690 "{:?}",
691 caller.route_set
692 );
693 assert!(
694 caller.route_set[2].contains("proxy1"),
695 "{:?}",
696 caller.route_set
697 );
698 }
699
700 #[test]
702 fn a_comma_inside_a_route_does_not_split_it() {
703 assert_eq!(
704 split_routes(br#""Proxy, Inc" <sip:p1.example.com;lr>, <sip:p2.example.com;lr>"#),
705 vec![
706 r#""Proxy, Inc" <sip:p1.example.com;lr>"#.to_owned(),
707 "<sip:p2.example.com;lr>".to_owned()
708 ]
709 );
710 }
711
712 #[test]
716 fn a_uri_with_parameters_survives_having_its_tag_stripped() {
717 assert_eq!(
718 strip_header_params("<sip:alice@example.com;transport=tcp>;tag=abc"),
719 "<sip:alice@example.com;transport=tcp>"
720 );
721 assert_eq!(
722 strip_header_params("<sip:bob@example.com>;tag=x"),
723 "<sip:bob@example.com>"
724 );
725 assert_eq!(
726 strip_header_params("sip:carol@example.com;tag=y"),
727 "sip:carol@example.com"
728 );
729 assert_eq!(
730 strip_header_params(r#""Alice" <sip:alice@example.com;user=phone>;tag=z"#),
731 r#""Alice" <sip:alice@example.com;user=phone>"#
732 );
733 }
734
735 #[test]
737 fn a_request_from_a_dialog_with_uri_parameters_is_well_formed() {
738 let with_params = request(
739 "INVITE sip:bob@example.com SIP/2.0\r\n\
740 Via: SIP/2.0/UDP 192.0.2.1:5060;branch=z9hG4bKcall\r\n\
741 To: <sip:bob@example.com;user=phone>\r\n\
742 From: <sip:alice@example.net;transport=tcp>;tag=alicetag\r\n\
743 Call-ID: thecall@example.net\r\n\
744 CSeq: 1 INVITE\r\n\
745 Contact: <sip:alice@192.0.2.1:5060>\r\n\
746 Max-Forwards: 70\r\n\
747 Content-Length: 0\r\n\r\n",
748 );
749 let callee = Dialog::from_request(&with_params, "bobtag").expect("a dialog");
750 let (local, remote) = callee.local_and_remote();
751 assert_eq!(local, "<sip:bob@example.com;user=phone>;tag=bobtag");
752 assert_eq!(remote, "<sip:alice@example.net;transport=tcp>;tag=alicetag");
753 assert_eq!(local.matches('<').count(), local.matches('>').count());
754 assert_eq!(remote.matches('<').count(), remote.matches('>').count());
755 }
756
757 #[test]
759 fn a_contact_with_parameters_yields_only_the_uri() {
760 let with_params = response(
761 "SIP/2.0 200 OK\r\n\
762 Via: SIP/2.0/UDP 192.0.2.1:5060;branch=z9hG4bKcall\r\n\
763 To: <sip:bob@example.com>;tag=bobtag\r\n\
764 From: <sip:alice@example.net>;tag=alicetag\r\n\
765 Call-ID: thecall@example.net\r\n\
766 CSeq: 1 INVITE\r\n\
767 Contact: \"Bob\" <sip:bob@192.0.2.9:5060>;expires=300\r\n\
768 Content-Length: 0\r\n\r\n",
769 );
770 let dialog = Dialog::from_response(&invite(), &with_params).expect("a dialog");
771 assert_eq!(
772 dialog.remote_target.to_bytes().as_ref(),
773 b"sip:bob@192.0.2.9:5060"
774 );
775 }
776}