1use std::net::{IpAddr, SocketAddr};
17use std::time::Duration;
18
19use bytes::Bytes;
20use sipx_sip::build::RequestBuilder;
21use sipx_sip::headers::{ContactValue, Via, first_hop_end};
22use sipx_sip::{Address, HeaderName, Method, Request, Response, Uri};
23
24use crate::auth::{Challenge, Credentials, new_cnonce, respond, strongest};
25use crate::gruu::{self, Gruus};
26use crate::outbound::{InstanceId, RegId};
27use crate::push::{self, Support};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct Lease {
32 pub granted: Duration,
34 pub refresh_after: Duration,
36}
37
38impl Lease {
39 #[must_use]
45 pub fn from_granted(granted: Duration) -> Self {
46 let seconds = granted.as_secs();
47 let refresh = if seconds <= 20 {
48 seconds / 2
51 } else {
52 seconds * 9 / 10
53 };
54 Self {
55 granted,
56 refresh_after: Duration::from_secs(refresh.max(1)),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
68#[non_exhaustive]
69pub enum RegistrationObservation {
70 #[default]
72 NotRegistered,
73 Absent,
75 Observed(SocketAddr),
77 Invalid(RegistrationObservationError),
79}
80
81impl RegistrationObservation {
82 #[must_use]
84 pub const fn address(self) -> Option<SocketAddr> {
85 match self {
86 Self::Observed(address) => Some(address),
87 Self::NotRegistered | Self::Absent | Self::Invalid(_) => None,
88 }
89 }
90
91 #[must_use]
93 pub fn from_response(response: &Response) -> Self {
94 let Some(header) = response.headers.get(&HeaderName::Via) else {
95 return Self::Invalid(RegistrationObservationError::MissingVia);
96 };
97 let value = header.value();
98 let Some(top_hop) = value.get(..first_hop_end(&value)) else {
99 return Self::Invalid(RegistrationObservationError::MalformedVia);
100 };
101 let Ok(via) = Via::parse_one(top_hop) else {
102 return Self::Invalid(RegistrationObservationError::MalformedVia);
103 };
104
105 let received: Vec<_> = via
106 .params
107 .iter()
108 .filter(|parameter| parameter.is("received"))
109 .collect();
110 if received.len() > 1 {
111 return Self::Invalid(RegistrationObservationError::ContradictoryReceived);
112 }
113 let rport: Vec<_> = via
114 .params
115 .iter()
116 .filter(|parameter| parameter.is("rport"))
117 .collect();
118 if rport.len() > 1 {
119 return Self::Invalid(RegistrationObservationError::ContradictoryRport);
120 }
121
122 let received = received.first().map(|parameter| parameter.value.as_deref());
123 let rport = rport.first().map(|parameter| parameter.value.as_deref());
124 match (received, rport) {
125 (None, None) => return Self::Absent,
126 (None, Some(_)) => {
127 return Self::Invalid(RegistrationObservationError::MissingReceived);
128 }
129 (Some(_), None | Some(None)) => {
130 return Self::Invalid(RegistrationObservationError::MissingRport);
131 }
132 (Some(_), Some(Some(_))) => {}
133 }
134
135 let Some(received) = received.flatten().and_then(parse_observed_ip) else {
136 return Self::Invalid(RegistrationObservationError::NonIpReceived);
137 };
138 let Some(rport) = rport
139 .flatten()
140 .filter(|value| value.iter().all(u8::is_ascii_digit))
141 .and_then(|value| std::str::from_utf8(value).ok())
142 .and_then(|value| value.parse::<u16>().ok())
143 .filter(|port| *port != 0)
144 else {
145 return Self::Invalid(RegistrationObservationError::InvalidRport);
146 };
147 Self::Observed(SocketAddr::new(received, rport))
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153#[non_exhaustive]
154pub enum RegistrationObservationError {
155 MissingVia,
157 MalformedVia,
159 ContradictoryReceived,
161 ContradictoryRport,
163 MissingReceived,
165 MissingRport,
167 NonIpReceived,
169 InvalidRport,
171}
172
173fn parse_observed_ip(raw: &[u8]) -> Option<IpAddr> {
174 let text = std::str::from_utf8(raw).ok()?;
175 let unbracketed = match text
176 .strip_prefix('[')
177 .and_then(|without_open| without_open.strip_suffix(']'))
178 {
179 Some(address) => address,
180 None if text.starts_with('[') || text.ends_with(']') => return None,
181 None => text,
182 };
183 unbracketed.parse().ok()
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct Registered {
194 pub lease: Lease,
196 pub observation: RegistrationObservation,
201 pub path: PathSet,
203 pub service_route: ServiceRoute,
205 pub flow_accepted: bool,
211 pub flow_timer: Option<Duration>,
216 pub gruus: Gruus,
223 pub push: Support,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
234pub enum Outcome {
235 Registered(Box<Registered>),
237 Challenged(Box<Challenge>),
239 PushNotSupported {
249 reason: String,
251 },
252 Rejected {
254 status: u16,
256 reason: String,
258 },
259}
260
261#[derive(Debug, Clone)]
263pub struct Registration {
264 pub registrar: Uri,
266 pub aor: String,
268 pub contact: String,
270 pub expires: Duration,
272 pub call_id: String,
274 pub cseq: u32,
276 pub instance: Option<InstanceId>,
286 pub reg_id: Option<RegId>,
292 pub gruu: Option<gruu::Kind>,
297 pub push: Option<sipx_sip::push::Device>,
306 pub headers: Vec<sipx_sip::Header>,
308}
309
310#[derive(Debug, Clone, Default)]
323pub struct PathSet(pub Vec<Address>);
324
325impl PathSet {
326 #[must_use]
329 pub fn hops(&self) -> &[Address] {
330 &self.0
331 }
332
333 #[must_use]
335 pub fn is_empty(&self) -> bool {
336 self.0.is_empty()
337 }
338
339 #[must_use]
341 pub fn rendered(&self) -> Vec<String> {
342 render_hops(&self.0)
343 }
344
345 #[must_use]
352 pub fn hops_outside(&self, expected: &[&str]) -> Vec<String> {
353 self.rendered()
354 .into_iter()
355 .filter(|hop| !expected.iter().any(|allowed| hop.contains(allowed)))
356 .collect()
357 }
358
359 #[must_use]
366 pub fn from_response(response: &Response) -> Self {
367 Self(
368 response
369 .headers
370 .typed_all::<sipx_sip::headers::address::Path>()
371 .filter_map(std::result::Result::ok)
372 .map(|path| path.0)
373 .collect(),
374 )
375 }
376}
377
378impl PartialEq for PathSet {
379 fn eq(&self, other: &Self) -> bool {
380 self.rendered() == other.rendered()
381 }
382}
383
384impl Eq for PathSet {}
385
386#[derive(Debug, Clone, Default)]
397pub struct ServiceRoute(pub Vec<Address>);
398
399impl ServiceRoute {
400 #[must_use]
402 pub fn hops(&self) -> &[Address] {
403 &self.0
404 }
405
406 #[must_use]
408 pub fn is_empty(&self) -> bool {
409 self.0.is_empty()
410 }
411
412 #[must_use]
416 pub fn rendered(&self) -> Vec<String> {
417 render_hops(&self.0)
418 }
419
420 #[must_use]
429 pub fn from_response(response: &Response) -> Self {
430 Self(
431 response
432 .headers
433 .typed_all::<sipx_sip::headers::address::ServiceRoute>()
434 .filter_map(std::result::Result::ok)
435 .map(|route| route.0)
436 .collect(),
437 )
438 }
439
440 #[must_use]
453 pub fn hops_without_loose_routing(&self) -> Vec<String> {
454 self.0
455 .iter()
456 .filter(|hop| {
457 hop.uri.params().is_none_or(|params| !params.contains("lr"))
460 })
461 .map(|hop| format!("<{}>", String::from_utf8_lossy(&hop.uri.to_bytes())))
462 .collect()
463 }
464}
465
466impl PartialEq for ServiceRoute {
467 fn eq(&self, other: &Self) -> bool {
468 self.rendered() == other.rendered()
469 }
470}
471
472impl Eq for ServiceRoute {}
473
474fn render_hops(hops: &[Address]) -> Vec<String> {
480 hops.iter()
481 .map(|hop| {
482 let mut text = format!("<{}>", String::from_utf8_lossy(&hop.uri.to_bytes()));
483 for param in &hop.params {
484 text.push(';');
485 text.push_str(&String::from_utf8_lossy(¶m.name));
486 if let Some(value) = ¶m.value {
487 text.push('=');
488 text.push_str(&String::from_utf8_lossy(value));
489 }
490 }
491 text
492 })
493 .collect()
494}
495
496impl Registration {
497 pub fn request(&self) -> Result<Request, sipx_sip::error::BuildError> {
502 let mut builder = RequestBuilder::new(Method::Register, self.registrar.clone())
503 .header(HeaderName::To, Bytes::from(self.aor.clone()))?
504 .header(
505 HeaderName::From,
506 Bytes::from(format!("{};tag={}", self.aor, new_cnonce())),
507 )?
508 .header(HeaderName::CallId, Bytes::from(self.call_id.clone()))?
509 .cseq(self.cseq, &Method::Register)?
510 .header(HeaderName::Contact, Bytes::from(self.contact()))?
511 .header(HeaderName::Supported, Bytes::from(self.supported()))?
516 .header(
517 HeaderName::Expires,
518 Bytes::from(self.expires.as_secs().to_string()),
519 )?
520 .max_forwards(70);
521 for header in &self.headers {
522 builder = builder.header(
523 header.name().clone(),
524 Bytes::copy_from_slice(header.raw_value()),
525 )?;
526 }
527 Ok(builder.build())
528 }
529
530 #[must_use]
540 pub fn contact(&self) -> String {
541 let base = match &self.push {
542 Some(device) => push::in_contact(&self.contact, device),
543 None => self.contact.clone(),
544 };
545 match (&self.instance, self.reg_id) {
546 (Some(instance), Some(reg_id)) => crate::outbound::contact(&base, instance, reg_id),
548 (Some(instance), None) => format!("{};{}", base, instance.contact_param()),
551 (None, _) => base,
552 }
553 }
554
555 #[must_use]
566 fn supported(&self) -> String {
567 let mut tags = vec!["path"];
568 if self.instance.is_some() {
569 if self.reg_id.is_some() {
570 tags.push(crate::outbound::OPTION_TAG);
571 }
572 if self.gruu.is_some() {
573 tags.push(gruu::OPTION_TAG);
574 }
575 }
576 tags.join(", ")
577 }
578
579 pub fn advance(&mut self) {
584 self.cseq = self.cseq.saturating_add(1);
585 }
586}
587
588#[must_use]
596pub fn interpret(response: &Response, registration: &Registration) -> Outcome {
597 let status = response.status.code();
598 let contact = registration.contact();
599
600 if (200..300).contains(&status) {
601 let granted = granted_expiry(response, &contact).unwrap_or(registration.expires);
604 return Outcome::Registered(Box::new(Registered {
608 lease: Lease::from_granted(granted),
609 observation: RegistrationObservation::from_response(response),
610 path: PathSet::from_response(response),
611 service_route: ServiceRoute::from_response(response),
612 flow_accepted: crate::outbound::accepted(response),
613 flow_timer: crate::outbound::flow_timer(response),
614 gruus: registration
618 .instance
619 .as_ref()
620 .map_or_else(Gruus::default, |instance| {
621 Gruus::from_response(response, instance)
622 }),
623 push: Support::from_response(response),
627 }));
628 }
629
630 if status == sipx_sip::push::NOT_SUPPORTED && registration.push.is_some() {
640 return Outcome::PushNotSupported {
641 reason: String::from_utf8_lossy(&response.reason).into_owned(),
642 };
643 }
644
645 if status == 401 || status == 407 {
646 let from_proxy = status == 407;
647 let header = if from_proxy {
648 HeaderName::ProxyAuthenticate
649 } else {
650 HeaderName::WwwAuthenticate
651 };
652 let challenges: Vec<Challenge> = response
653 .headers
654 .get_all(&header)
655 .filter_map(|h| Challenge::parse(&h.value(), from_proxy))
656 .collect();
657 if let Some(challenge) = strongest(challenges) {
658 return Outcome::Challenged(Box::new(challenge));
659 }
660 }
661
662 Outcome::Rejected {
663 status,
664 reason: String::from_utf8_lossy(&response.reason).into_owned(),
665 }
666}
667
668fn granted_expiry(response: &Response, contact: &str) -> Option<Duration> {
676 if let Ok(own) = Address::parse(contact.as_bytes(), "Contact") {
677 for value in response.headers.typed_all::<ContactValue>() {
678 let Ok(ContactValue::Address(address)) = value else {
679 continue;
680 };
681 if !address.uri.equivalent(&own.uri) {
682 continue;
683 }
684 if let Some(seconds) = contact_expires(&address) {
685 return Some(Duration::from_secs(seconds));
686 }
687 break;
689 }
690 }
691 response
692 .headers
693 .value(&HeaderName::Expires)
694 .and_then(|value| {
695 std::str::from_utf8(&value)
696 .ok()
697 .and_then(|text| text.trim().parse::<u64>().ok())
698 })
699 .map(Duration::from_secs)
700}
701
702fn contact_expires(address: &Address) -> Option<u64> {
703 let value = address.param("expires")?;
704 std::str::from_utf8(value).ok()?.trim().parse().ok()
705}
706
707pub fn authorize(
709 request: &mut Request,
710 challenge: &Challenge,
711 credentials: &Credentials,
712 nonce_count: u32,
713) -> Result<(), sipx_sip::error::BuildError> {
714 let uri = String::from_utf8_lossy(&request.uri.to_bytes()).into_owned();
715 let method = String::from_utf8_lossy(request.method.as_bytes()).into_owned();
716 let value = respond(
717 challenge,
718 credentials,
719 &method,
720 &uri,
721 nonce_count,
722 &new_cnonce(),
723 );
724 let header = sipx_sip::Header::build(challenge.response_header(), Bytes::from(value))?;
725 request.headers.push(header);
726 Ok(())
727}
728
729#[cfg(test)]
730#[allow(
731 clippy::unwrap_used,
732 clippy::expect_used,
733 clippy::panic,
734 clippy::indexing_slicing
735)]
736mod tests {
737 use super::*;
738 use sipx_sip::{Host, HostName, Limits, Message, parse_datagram};
739
740 const CONTACT: &str = "<sip:alice@192.0.2.5:5060>";
742
743 fn registration() -> Registration {
744 Registration {
745 registrar: Uri::sip(Host::Name(HostName::new("example.com").expect("valid"))),
746 aor: "<sip:alice@example.com>".to_owned(),
747 contact: CONTACT.to_owned(),
748 expires: Duration::from_secs(3600),
749 call_id: "reg-1@192.0.2.5".to_owned(),
750 cseq: 1,
751 instance: None,
752 reg_id: None,
753 gruu: None,
754 push: None,
755 headers: Vec::new(),
756 }
757 }
758
759 fn with_gruu() -> Registration {
761 Registration {
762 instance: Some(instance()),
763 gruu: Some(gruu::Kind::Public),
764 ..registration()
765 }
766 }
767
768 fn instance() -> InstanceId {
769 InstanceId::parse("urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6").expect("a urn")
770 }
771
772 fn response(text: &str) -> Response {
773 match parse_datagram(Bytes::from(text.to_owned()), &Limits::datagram()).expect("parses") {
774 Message::Response(r) => r,
775 Message::Request(_) => panic!("a response"),
776 }
777 }
778
779 fn ok_with(extra: &str) -> Response {
780 response(&format!(
781 "SIP/2.0 200 OK\r\n\
782 Via: SIP/2.0/UDP 192.0.2.5:5060;branch=z9hG4bKx\r\n\
783 To: <sip:alice@example.com>;tag=r\r\n\
784 From: <sip:alice@example.com>;tag=1\r\n\
785 Call-ID: reg-1@192.0.2.5\r\n\
786 CSeq: 1 REGISTER\r\n\
787 {extra}\
788 Content-Length: 0\r\n\r\n"
789 ))
790 }
791
792 fn observation_from(via: Option<&str>) -> RegistrationObservation {
793 let via = via.map_or_else(String::new, |value| format!("Via: {value}\r\n"));
794 let outcome = interpret(
795 &response(&format!(
796 "SIP/2.0 200 OK\r\n\
797 {via}\
798 To: <sip:alice@example.com>;tag=r\r\n\
799 From: <sip:alice@example.com>;tag=1\r\n\
800 Call-ID: reg-1@192.0.2.5\r\n\
801 CSeq: 1 REGISTER\r\n\
802 Content-Length: 0\r\n\r\n"
803 )),
804 ®istration(),
805 );
806 let Outcome::Registered(registered) = outcome else {
807 panic!("expected successful registration, got {outcome:?}");
808 };
809 registered.observation
810 }
811
812 #[test]
813 fn registration_observation_vectors_are_typed_and_non_authoritative() {
814 assert_eq!(
815 observation_from(Some("SIP/2.0/UDP private.example:5060;branch=z9hG4bKx")),
816 RegistrationObservation::Absent
817 );
818 assert_eq!(
819 observation_from(Some(
820 "SIP/2.0/UDP private.example:5060;received=203.0.113.9;rport=41234;branch=z9hG4bKx"
821 )),
822 RegistrationObservation::Observed("203.0.113.9:41234".parse().expect("address"))
823 );
824 assert_eq!(
825 observation_from(Some(
826 "SIP/2.0/TCP private.example:5060;received=[2001:db8::9];rport=5060;branch=z9hG4bKx"
827 )),
828 RegistrationObservation::Observed("[2001:db8::9]:5060".parse().expect("address"))
829 );
830 for (via, error) in [
831 (None, RegistrationObservationError::MissingVia),
832 (
833 Some("not-a-via"),
834 RegistrationObservationError::MalformedVia,
835 ),
836 (
837 Some("SIP/2.0/UDP h;rport=41234;branch=z9hG4bKx"),
838 RegistrationObservationError::MissingReceived,
839 ),
840 (
841 Some("SIP/2.0/UDP h;received=203.0.113.9;branch=z9hG4bKx"),
842 RegistrationObservationError::MissingRport,
843 ),
844 (
845 Some("SIP/2.0/UDP h;received=203.0.113.9;rport;branch=z9hG4bKx"),
846 RegistrationObservationError::MissingRport,
847 ),
848 (
849 Some("SIP/2.0/UDP h;received=registrar.example;rport=5060;branch=z9hG4bKx"),
850 RegistrationObservationError::NonIpReceived,
851 ),
852 (
853 Some("SIP/2.0/UDP h;received;rport=5060;branch=z9hG4bKx"),
854 RegistrationObservationError::NonIpReceived,
855 ),
856 (
857 Some("SIP/2.0/UDP h;received=203.0.113.9;rport=nope;branch=z9hG4bKx"),
858 RegistrationObservationError::InvalidRport,
859 ),
860 (
861 Some("SIP/2.0/UDP h;received=203.0.113.9;rport=+5060;branch=z9hG4bKx"),
862 RegistrationObservationError::InvalidRport,
863 ),
864 (
865 Some("SIP/2.0/UDP h;received=203.0.113.9;rport=0;branch=z9hG4bKx"),
866 RegistrationObservationError::InvalidRport,
867 ),
868 (
869 Some(
870 "SIP/2.0/UDP h;received=203.0.113.9;Received=203.0.113.10;rport=5060;branch=z9hG4bKx",
871 ),
872 RegistrationObservationError::ContradictoryReceived,
873 ),
874 (
875 Some("SIP/2.0/UDP h;received=203.0.113.9;rport=5060;RPORT=5061;branch=z9hG4bKx"),
876 RegistrationObservationError::ContradictoryRport,
877 ),
878 ] {
879 assert_eq!(
880 observation_from(via),
881 RegistrationObservation::Invalid(error)
882 );
883 }
884 }
885
886 #[test]
887 fn only_the_top_via_hop_contributes_the_observation() {
888 assert_eq!(
889 observation_from(Some(
890 "SIP/2.0/UDP top.example:5060;received=203.0.113.9;rport=41234;branch=z9hG4bKtop, \
891 SIP/2.0/UDP lower.example:5060;received=not-an-ip;rport=0;branch=z9hG4bKlower"
892 )),
893 RegistrationObservation::Observed("203.0.113.9:41234".parse().expect("address")),
894 "a malformed lower hop cannot contaminate the top-hop observation"
895 );
896 assert_eq!(
897 observation_from(Some(
898 "SIP/2.0/UDP top.example:5060;received=203.0.113.9;rport=41234;branch=z9hG4bKtop, \
899 SIP/2.0/UDP lower.example:5060;opaque=\"unterminated"
900 )),
901 RegistrationObservation::Observed("203.0.113.9:41234".parse().expect("address")),
902 "even invalid quoted syntax in a lower hop is outside this observation"
903 );
904
905 let outcome = interpret(
906 &ok_with(
907 "Via: SIP/2.0/UDP lower.example:5060;received=198.51.100.7;rport=50999;branch=z9hG4bKlower\r\n",
908 ),
909 ®istration(),
910 );
911 let Outcome::Registered(registered) = outcome else {
912 panic!("expected successful registration, got {outcome:?}");
913 };
914 assert_eq!(
915 registered.observation,
916 RegistrationObservation::Absent,
917 "a lower Via row cannot supply parameters missing from the top row"
918 );
919 }
920
921 #[test]
922 fn invalid_observation_does_not_replace_registration_results() {
923 let instance = instance();
924 let urn = instance.urn().to_owned();
925 let registration = Registration {
926 instance: Some(instance),
927 reg_id: RegId::new(1),
928 gruu: Some(gruu::Kind::Public),
929 push: Some(
930 sipx_sip::push::Device::new("webpush", "c1a5b3e7d9f2").expect("valid push device"),
931 ),
932 ..registration()
933 };
934 let returned_contact = format!(
935 "{};pub-gruu=\"sip:alice@example.com;gr={urn}\"\
936 ;temp-gruu=\"sip:t7k2xq9f4m@example.com;gr\";expires=600",
937 registration.contact()
938 );
939 let outcome = interpret(
940 &response(&format!(
941 "SIP/2.0 200 OK\r\n\
942 Via: SIP/2.0/UDP private.example:5060;received=not-an-ip;rport=41234;branch=z9hG4bKx\r\n\
943 To: <sip:alice@example.com>;tag=r\r\n\
944 From: <sip:alice@example.com>;tag=1\r\n\
945 Call-ID: reg-1@192.0.2.5\r\n\
946 CSeq: 1 REGISTER\r\n\
947 Contact: {returned_contact}\r\n\
948 Path: <sip:path.example;lr>\r\n\
949 Service-Route: <sip:route.example;lr>\r\n\
950 Require: outbound\r\n\
951 Flow-Timer: 25\r\n\
952 Feature-Caps: *;+sip.pns=\"webpush\";+sip.pnsreg=\"120\"\
953 ;+sip.pnspurr=\"opaque-purr-1\"\r\n\
954 Content-Length: 0\r\n\r\n"
955 )),
956 ®istration,
957 );
958 let Outcome::Registered(registered) = outcome else {
959 panic!("an invalid observation must not reject the registration");
960 };
961 assert_eq!(
962 registered.observation,
963 RegistrationObservation::Invalid(RegistrationObservationError::NonIpReceived)
964 );
965 assert_eq!(registered.lease.granted, Duration::from_secs(600));
966 assert_eq!(
967 registered.path.rendered(),
968 vec!["<sip:path.example;lr>".to_owned()]
969 );
970 assert_eq!(
971 registered.service_route.rendered(),
972 vec!["<sip:route.example;lr>".to_owned()]
973 );
974 assert!(registered.flow_accepted);
975 assert_eq!(registered.flow_timer, Some(Duration::from_secs(25)));
976 assert_eq!(
977 registered.gruus.public().map(sipx_sip::Uri::to_string),
978 Some(format!("sip:alice@example.com;gr={urn}"))
979 );
980 assert_eq!(
981 registered.gruus.temporary().map(sipx_sip::Uri::to_string),
982 Some("sip:t7k2xq9f4m@example.com;gr".to_owned())
983 );
984 assert!(registered.push.supports("webpush"));
985 assert!(registered.push.refreshes_required());
986 assert_eq!(
987 registered.push.refresh_interval(),
988 Some(Duration::from_secs(120))
989 );
990 assert_eq!(registered.push.purr(), Some("opaque-purr-1"));
991 }
992
993 #[test]
995 fn a_registration_preserves_the_path_it_was_returned() {
996 let outcome = interpret(
999 &ok_with(
1000 "Path: <sip:edge.example.com;lr>\r\nPath: <sip:core.example.net;lr>\r\nContact: <sip:alice@192.0.2.5:5060>;expires=600\r\n",
1001 ),
1002 ®istration(),
1003 );
1004 let Outcome::Registered(registered) = outcome else {
1005 panic!("expected a registration, got {outcome:?}");
1006 };
1007 assert_eq!(registered.lease.granted, Duration::from_secs(600));
1008 assert_eq!(
1009 registered.path.rendered(),
1010 vec![
1011 "<sip:edge.example.com;lr>".to_owned(),
1012 "<sip:core.example.net;lr>".to_owned()
1013 ],
1014 "the path vector was lost, reordered, or flattened"
1015 );
1016 }
1017
1018 #[test]
1019 fn a_comma_joined_path_is_the_same_as_separate_rows() {
1020 let joined = interpret(
1024 &ok_with("Path: <sip:edge.example.com;lr>, <sip:core.example.net;lr>\r\n"),
1025 ®istration(),
1026 );
1027 let separate = interpret(
1028 &ok_with("Path: <sip:edge.example.com;lr>\r\nPath: <sip:core.example.net;lr>\r\n"),
1029 ®istration(),
1030 );
1031 match (joined, separate) {
1032 (Outcome::Registered(one), Outcome::Registered(other)) => {
1033 assert_eq!(
1034 one.path.rendered().len(),
1035 2,
1036 "the comma-joined row was not split"
1037 );
1038 assert_eq!(one.path, other.path);
1039 }
1040 other => panic!("expected two registrations, got {other:?}"),
1041 }
1042 }
1043
1044 #[test]
1045 fn a_path_parameter_survives_because_outbound_will_need_it() {
1046 let outcome = interpret(
1049 &ok_with("Path: <sip:edge.example.com;lr;ob>\r\n"),
1050 ®istration(),
1051 );
1052 let Outcome::Registered(registered) = outcome else {
1053 panic!("expected a registration");
1054 };
1055 assert!(
1056 registered
1057 .path
1058 .hops()
1059 .first()
1060 .expect("one hop")
1061 .uri
1062 .params()
1063 .is_some_and(|params| params.contains("ob")),
1064 "the ob parameter was dropped: {:?}",
1065 registered.path.rendered()
1066 );
1067 }
1068
1069 #[test]
1070 fn a_register_offers_the_path_option_tag() {
1071 let request = registration().request().expect("builds");
1075 let supported = request
1076 .headers
1077 .value(&HeaderName::Supported)
1078 .expect("Supported is present");
1079 assert!(String::from_utf8_lossy(&supported).contains("path"));
1080 }
1081
1082 #[test]
1086 fn a_register_asking_for_a_gruu_offers_the_tag_and_names_the_instance() {
1087 let request = with_gruu().request().expect("builds");
1088 let supported = request
1089 .headers
1090 .value(&HeaderName::Supported)
1091 .expect("Supported is present");
1092 assert!(String::from_utf8_lossy(&supported).contains("gruu"));
1093 let contact = request
1094 .headers
1095 .value(&HeaderName::Contact)
1096 .expect("a Contact");
1097 assert_eq!(
1098 String::from_utf8_lossy(&contact),
1099 format!("{CONTACT};+sip.instance=\"<{}>\"", instance().urn())
1100 );
1101 }
1102
1103 #[test]
1110 fn outbound_and_gruu_present_one_instance_identity_between_them() {
1111 let both = Registration {
1112 reg_id: Some(RegId::new(2).expect("valid")),
1113 ..with_gruu()
1114 };
1115 let request = both.request().expect("builds");
1116 let contact = request
1117 .headers
1118 .value(&HeaderName::Contact)
1119 .expect("a Contact");
1120 let contact = String::from_utf8_lossy(&contact);
1121 assert_eq!(
1122 contact.matches("+sip.instance").count(),
1123 1,
1124 "the instance identity appeared more than once: {contact}"
1125 );
1126 assert!(contact.contains(&format!("+sip.instance=\"<{}>\"", instance().urn())));
1127 assert!(contact.contains(";reg-id=2"), "{contact}");
1128
1129 let supported = request
1130 .headers
1131 .value(&HeaderName::Supported)
1132 .expect("a Supported");
1133 let supported = String::from_utf8_lossy(&supported);
1134 assert_eq!(supported, "path, outbound, gruu");
1135 }
1136
1137 #[test]
1141 fn neither_mechanism_is_offered_without_an_instance_to_name() {
1142 let confused = Registration {
1143 instance: None,
1144 reg_id: RegId::new(1),
1145 gruu: Some(gruu::Kind::Public),
1146 ..registration()
1147 };
1148 let request = confused.request().expect("builds");
1149 let supported = request
1150 .headers
1151 .value(&HeaderName::Supported)
1152 .expect("a Supported");
1153 assert_eq!(String::from_utf8_lossy(&supported), "path");
1154 assert_eq!(
1155 request
1156 .headers
1157 .value(&HeaderName::Contact)
1158 .expect("a Contact")
1159 .as_ref(),
1160 CONTACT.as_bytes()
1161 );
1162 }
1163
1164 #[test]
1166 fn a_registration_keeps_the_gruus_the_registrar_issued() {
1167 let urn = instance().urn().to_owned();
1168 let outcome = interpret(
1169 &ok_with(&format!(
1170 "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{urn}>\"\
1171 ;pub-gruu=\"sip:alice@example.com;gr={urn}\"\
1172 ;temp-gruu=\"sip:t7k2xq9f4m@example.com;gr\";expires=600\r\n"
1173 )),
1174 &with_gruu(),
1175 );
1176 let Outcome::Registered(registered) = outcome else {
1177 panic!("expected a registration");
1178 };
1179 assert_eq!(
1180 registered.gruus.public().map(sipx_sip::Uri::to_string),
1181 Some(format!("sip:alice@example.com;gr={urn}"))
1182 );
1183 assert_eq!(
1184 registered.gruus.temporary().map(sipx_sip::Uri::to_string),
1185 Some("sip:t7k2xq9f4m@example.com;gr".to_owned())
1186 );
1187 }
1188
1189 #[test]
1192 fn a_registration_without_an_instance_adopts_no_gruus() {
1193 let outcome = interpret(
1194 &ok_with(
1195 "Contact: <sip:alice@198.51.100.9:5060>\
1196 ;+sip.instance=\"<urn:uuid:00000000-0000-4000-8000-000000000000>\"\
1197 ;pub-gruu=\"sip:alice@example.com;gr=urn:uuid:00000000-0000-4000-8000-000000000000\"\r\n",
1198 ),
1199 ®istration(),
1200 );
1201 let Outcome::Registered(registered) = outcome else {
1202 panic!("expected a registration");
1203 };
1204 assert!(registered.gruus.is_empty());
1205 }
1206
1207 #[test]
1208 fn a_path_returned_unasked_is_still_reported() {
1209 let outcome = interpret(
1213 &ok_with("Path: <sip:stranger.example.org;lr>\r\n"),
1214 ®istration(),
1215 );
1216 let Outcome::Registered(registered) = outcome else {
1217 panic!("expected a registration");
1218 };
1219 assert_eq!(
1220 registered.path.hops_outside(&["edge.example.com"]),
1221 vec!["<sip:stranger.example.org;lr>".to_owned()]
1222 );
1223 }
1224
1225 #[test]
1226 fn no_path_is_an_empty_set_rather_than_an_absent_one() {
1227 let outcome = interpret(&ok_with(""), ®istration());
1228 let Outcome::Registered(registered) = outcome else {
1229 panic!("expected a registration");
1230 };
1231 assert!(registered.path.is_empty());
1232 }
1233
1234 fn service_route_of(extra: &str) -> ServiceRoute {
1235 let outcome = interpret(&ok_with(extra), ®istration());
1236 match outcome {
1237 Outcome::Registered(registered) => registered.service_route,
1238 other => panic!("expected a registration, got {other:?}"),
1239 }
1240 }
1241
1242 #[test]
1244 fn a_service_route_keeps_the_order_the_registrar_listed() {
1245 let route = service_route_of(
1246 "Service-Route: <sip:edge.example.com;lr>\r\n\
1247 Service-Route: <sip:core.example.net;lr>\r\n",
1248 );
1249 assert_eq!(
1250 route.rendered(),
1251 vec![
1252 "<sip:edge.example.com;lr>".to_owned(),
1253 "<sip:core.example.net;lr>".to_owned(),
1254 ],
1255 "the outbound route set is not in the order it arrived in"
1256 );
1257 }
1258
1259 #[test]
1261 fn a_comma_joined_service_route_is_the_same_as_separate_rows() {
1262 let joined = service_route_of(
1263 "Service-Route: <sip:edge.example.com;lr>, <sip:core.example.net;lr>\r\n",
1264 );
1265 let separate = service_route_of(
1266 "Service-Route: <sip:edge.example.com;lr>\r\n\
1267 Service-Route: <sip:core.example.net;lr>\r\n",
1268 );
1269 assert_eq!(joined.hops().len(), 2, "the comma-joined row was not split");
1270 assert_eq!(joined, separate);
1271 }
1272
1273 #[test]
1279 fn a_response_without_a_service_route_says_clear_it_rather_than_keep_it() {
1280 assert!(
1281 service_route_of("").is_empty(),
1282 "an absent Service-Route must read as empty, so that storing it clears the old one"
1283 );
1284 }
1285
1286 #[test]
1291 fn a_hop_without_lr_is_reported_rather_than_dropped() {
1292 let route = service_route_of(
1293 "Service-Route: <sip:edge.example.com;lr>\r\n\
1294 Service-Route: <sip:strict.example.net>\r\n",
1295 );
1296 assert_eq!(route.hops().len(), 2, "the offending hop was dropped");
1297 assert_eq!(
1298 route.hops_without_loose_routing(),
1299 vec!["<sip:strict.example.net>".to_owned()],
1300 "the hop missing ;lr was not reported"
1301 );
1302 }
1303
1304 #[test]
1307 fn a_path_is_not_a_service_route() {
1308 let outcome = interpret(
1309 &ok_with("Path: <sip:edge.example.com;lr>\r\n"),
1310 ®istration(),
1311 );
1312 let Outcome::Registered(registered) = outcome else {
1313 panic!("expected a registration");
1314 };
1315 assert!(!registered.path.is_empty(), "the Path was lost");
1316 assert!(
1317 registered.service_route.is_empty(),
1318 "a Path was read as a Service-Route; the UA would route its own requests through \
1319 proxies that only asked to be on the inbound path"
1320 );
1321 }
1322
1323 #[test]
1324 fn the_request_uri_names_the_registrar_and_the_to_names_the_user() {
1325 let request = registration().request().expect("builds");
1326 assert_eq!(request.uri.to_bytes().as_ref(), b"sip:example.com");
1327 assert_eq!(
1328 request
1329 .headers
1330 .value(&HeaderName::To)
1331 .expect("a To")
1332 .as_ref(),
1333 b"<sip:alice@example.com>"
1334 );
1335 }
1336
1337 #[test]
1340 fn the_granted_expiry_overrides_what_was_asked_for() {
1341 let outcome = interpret(
1342 &ok_with("Contact: <sip:alice@192.0.2.5:5060>;expires=60\r\n"),
1343 ®istration(),
1344 );
1345 match outcome {
1346 Outcome::Registered(r) => assert_eq!(r.lease.granted, Duration::from_secs(60)),
1347 other => panic!("expected a lease, got {other:?}"),
1348 }
1349 }
1350
1351 #[test]
1353 fn a_contact_expiry_beats_the_expires_header() {
1354 let outcome = interpret(
1355 &ok_with("Expires: 3600\r\nContact: <sip:alice@192.0.2.5:5060>;expires=120\r\n"),
1356 ®istration(),
1357 );
1358 match outcome {
1359 Outcome::Registered(r) => assert_eq!(r.lease.granted, Duration::from_secs(120)),
1360 other => panic!("expected a lease, got {other:?}"),
1361 }
1362 }
1363
1364 #[test]
1369 fn the_expiry_comes_from_our_own_binding_not_the_first_listed() {
1370 let outcome = interpret(
1371 &ok_with(
1372 "Contact: <sip:alice@198.51.100.9:5060>;expires=3600\r\n\
1373 Contact: <sip:alice@192.0.2.5:5060>;expires=60\r\n",
1374 ),
1375 ®istration(),
1376 );
1377 match outcome {
1378 Outcome::Registered(r) => assert_eq!(r.lease.granted, Duration::from_secs(60)),
1379 other => panic!("expected a lease, got {other:?}"),
1380 }
1381 }
1382
1383 #[test]
1384 fn the_expires_header_is_used_when_the_contact_has_none() {
1385 let outcome = interpret(&ok_with("Expires: 300\r\n"), ®istration());
1386 match outcome {
1387 Outcome::Registered(r) => assert_eq!(r.lease.granted, Duration::from_secs(300)),
1388 other => panic!("expected a lease, got {other:?}"),
1389 }
1390 }
1391
1392 #[test]
1395 fn the_refresh_leaves_margin_before_the_lease_ends() {
1396 let lease = Lease::from_granted(Duration::from_secs(3600));
1397 assert_eq!(lease.refresh_after, Duration::from_secs(3240));
1398 assert!(lease.refresh_after < lease.granted);
1399
1400 let short = Lease::from_granted(Duration::from_secs(15));
1402 assert!(short.refresh_after < short.granted);
1403 assert!(short.refresh_after >= Duration::from_secs(1));
1404
1405 let degenerate = Lease::from_granted(Duration::from_secs(1));
1408 assert_eq!(degenerate.refresh_after, Duration::from_secs(1));
1409 }
1410
1411 #[test]
1412 fn a_401_is_a_challenge_rather_than_a_failure() {
1413 let challenged = response(
1414 "SIP/2.0 401 Unauthorized\r\n\
1415 Via: SIP/2.0/UDP 192.0.2.5:5060;branch=z9hG4bKx\r\n\
1416 To: <sip:alice@example.com>;tag=r\r\n\
1417 From: <sip:alice@example.com>;tag=1\r\n\
1418 Call-ID: reg-1@192.0.2.5\r\n\
1419 CSeq: 1 REGISTER\r\n\
1420 WWW-Authenticate: Digest realm=\"example.com\", nonce=\"abc\", qop=\"auth\"\r\n\
1421 Content-Length: 0\r\n\r\n",
1422 );
1423 match interpret(&challenged, ®istration()) {
1424 Outcome::Challenged(challenge) => {
1425 assert_eq!(challenge.realm, "example.com");
1426 assert!(challenge.qop_auth);
1427 assert!(!challenge.from_proxy);
1428 }
1429 other => panic!("expected a challenge, got {other:?}"),
1430 }
1431 }
1432
1433 #[test]
1434 fn a_407_is_a_proxy_challenge_and_answered_in_the_proxy_header() {
1435 let challenged = response(
1436 "SIP/2.0 407 Proxy Authentication Required\r\n\
1437 Via: SIP/2.0/UDP 192.0.2.5:5060;branch=z9hG4bKx\r\n\
1438 To: <sip:alice@example.com>;tag=r\r\n\
1439 From: <sip:alice@example.com>;tag=1\r\n\
1440 Call-ID: reg-1@192.0.2.5\r\n\
1441 CSeq: 1 REGISTER\r\n\
1442 Proxy-Authenticate: Digest realm=\"p\", nonce=\"n\"\r\n\
1443 Content-Length: 0\r\n\r\n",
1444 );
1445 match interpret(&challenged, ®istration()) {
1446 Outcome::Challenged(challenge) => {
1447 assert!(challenge.from_proxy);
1448 assert_eq!(challenge.response_header(), HeaderName::ProxyAuthorization);
1449 }
1450 other => panic!("expected a challenge, got {other:?}"),
1451 }
1452 }
1453
1454 #[test]
1455 fn a_403_is_a_rejection_not_a_challenge() {
1456 let refused = response(
1457 "SIP/2.0 403 Forbidden\r\n\
1458 Via: SIP/2.0/UDP 192.0.2.5:5060;branch=z9hG4bKx\r\n\
1459 To: <sip:alice@example.com>;tag=r\r\n\
1460 From: <sip:alice@example.com>;tag=1\r\n\
1461 Call-ID: reg-1@192.0.2.5\r\n\
1462 CSeq: 1 REGISTER\r\n\
1463 Content-Length: 0\r\n\r\n",
1464 );
1465 match interpret(&refused, ®istration()) {
1466 Outcome::Rejected { status, reason } => {
1467 assert_eq!(status, 403);
1468 assert_eq!(reason, "Forbidden");
1469 }
1470 other => panic!("expected a rejection, got {other:?}"),
1471 }
1472 }
1473
1474 fn refused_555() -> Response {
1475 response(
1476 "SIP/2.0 555 Push Notification Service Not Supported\r\n\
1477 Via: SIP/2.0/UDP 192.0.2.5:5060;branch=z9hG4bKx\r\n\
1478 To: <sip:alice@example.com>;tag=r\r\n\
1479 From: <sip:alice@example.com>;tag=1\r\n\
1480 Call-ID: reg-1@192.0.2.5\r\n\
1481 CSeq: 1 REGISTER\r\n\
1482 Content-Length: 0\r\n\r\n",
1483 )
1484 }
1485
1486 #[test]
1489 fn a_555_to_a_push_registration_is_its_own_outcome_rather_than_a_number() {
1490 let asking = Registration {
1491 push: Some(sipx_sip::push::Device::new("webpush", "c1a5b3e7d9f2").expect("valid")),
1492 ..registration()
1493 };
1494 match interpret(&refused_555(), &asking) {
1495 Outcome::PushNotSupported { reason } => {
1496 assert_eq!(reason, sipx_sip::push::NOT_SUPPORTED_REASON);
1497 }
1498 other => panic!("expected §8.1's own outcome, got {other:?}"),
1499 }
1500 }
1501
1502 #[test]
1506 fn a_555_to_a_registration_that_asked_for_no_push_is_an_ordinary_rejection() {
1507 match interpret(&refused_555(), ®istration()) {
1508 Outcome::Rejected { status, reason } => {
1509 assert_eq!(status, 555);
1510 assert_eq!(reason, sipx_sip::push::NOT_SUPPORTED_REASON);
1511 }
1512 other => panic!("expected an ordinary rejection, got {other:?}"),
1513 }
1514 }
1515
1516 #[test]
1519 fn a_401_with_an_unusable_challenge_is_a_rejection() {
1520 let bad = response(
1521 "SIP/2.0 401 Unauthorized\r\n\
1522 Via: SIP/2.0/UDP 192.0.2.5:5060;branch=z9hG4bKx\r\n\
1523 To: <sip:alice@example.com>;tag=r\r\n\
1524 From: <sip:alice@example.com>;tag=1\r\n\
1525 Call-ID: reg-1@192.0.2.5\r\n\
1526 CSeq: 1 REGISTER\r\n\
1527 WWW-Authenticate: Basic realm=\"example.com\"\r\n\
1528 Content-Length: 0\r\n\r\n",
1529 );
1530 assert!(matches!(
1531 interpret(&bad, ®istration()),
1532 Outcome::Rejected { status: 401, .. }
1533 ));
1534 }
1535
1536 #[test]
1539 fn a_refresh_keeps_the_call_id_and_advances_the_cseq() {
1540 let mut registration = registration();
1541 let first = registration.request().expect("builds");
1542 registration.advance();
1543 let second = registration.request().expect("builds");
1544
1545 assert_eq!(
1546 first.headers.value(&HeaderName::CallId),
1547 second.headers.value(&HeaderName::CallId),
1548 );
1549 assert_eq!(
1550 second
1551 .headers
1552 .value(&HeaderName::CSeq)
1553 .expect("a CSeq")
1554 .as_ref(),
1555 b"2 REGISTER"
1556 );
1557 }
1558
1559 #[test]
1560 fn application_owned_fields_are_preserved_on_register_refreshes() {
1561 let mut registration = registration();
1562 registration.headers.push(
1563 sipx_sip::Header::build(
1564 HeaderName::Supported,
1565 Bytes::from_static(b"deployment-feature"),
1566 )
1567 .expect("a validated header"),
1568 );
1569 let first = registration.request().expect("builds");
1570 registration.advance();
1571 let second = registration.request().expect("builds");
1572 for request in [first, second] {
1573 assert!(
1574 request
1575 .headers
1576 .get_all(&HeaderName::Supported)
1577 .any(|header| header.raw_value() == b"deployment-feature")
1578 );
1579 }
1580 }
1581
1582 #[test]
1584 fn authorization_covers_the_request_uri() {
1585 let mut request = registration().request().expect("builds");
1586 let challenge = Challenge::parse(
1587 br#"Digest realm="example.com", nonce="abc", qop="auth""#,
1588 false,
1589 )
1590 .expect("parses");
1591 authorize(
1592 &mut request,
1593 &challenge,
1594 &Credentials::new("alice", "secret"),
1595 1,
1596 )
1597 .expect("authorizes");
1598
1599 let header = request
1600 .headers
1601 .value(&HeaderName::Authorization)
1602 .expect("an Authorization");
1603 let text = String::from_utf8_lossy(&header);
1604 assert!(text.contains(r#"uri="sip:example.com""#), "{text}");
1605 assert!(text.contains(r#"username="alice""#), "{text}");
1606 assert!(text.contains("nc=00000001"), "{text}");
1607 }
1608}