sipx_sip/gruu.rs
1//! GRUUs — Globally Routable User Agent URIs (RFC 5627), at the URI level.
2//!
3//! A GRUU is an ordinary SIP URI with one extra URI parameter. §4.5: "A GRUU is identified by
4//! the presence of the 'gr' URI parameter, and this URI parameter might or might not have a
5//! value." Nothing here takes one apart beyond that, because §4.2 requires a UA to treat the
6//! user and host parts as it received them.
7//!
8//! What is here is a **comparison**, and it exists because RFC 3261's is the wrong one for this
9//! job. §5.4 states it plainly: "A public GRUU will always be equivalent to the AOR based on
10//! URI equality rules", the reason being that §19.1.4 ignores a URI parameter that appears in
11//! only one of the two URIs. So a UA deciding "was this request sent to *my* GRUU?" with
12//! [`Uri::equivalent`] alone would say yes to a request addressed at the plain address of
13//! record — and the address of record names every device the user has registered, which is
14//! exactly the set a GRUU exists to narrow down to one.
15//!
16//! Obtaining and using GRUUs is a registration concern and lives in `sipx-ua`; minting them is
17//! a registrar's job (§5), and sipx is not a registrar.
18
19use crate::Uri;
20
21/// The URI parameter that makes a URI a GRUU.
22///
23/// §7's grammar: `gr-param = "gr" [ "=" pvalue ]`.
24pub const GR_PARAM: &str = "gr";
25
26/// Whether this URI is a GRUU (§4.5).
27///
28/// Presence is the entire test. A valueless `gr` is as much a GRUU as one carrying a value —
29/// §7 makes the value optional, and it is the form a registrar commonly mints temporary GRUUs
30/// in, where the whole URI is opaque and there is nothing for a value to add.
31#[must_use]
32pub fn is_gruu(uri: &Uri) -> bool {
33 uri.params().is_some_and(|params| params.contains(GR_PARAM))
34}
35
36/// The value of the `gr` parameter, when it has one.
37///
38/// Returned raw and deliberately not interpreted. A *public* GRUU's value happens to be the
39/// instance ID it was minted for, but a temporary one's must not be: §5.4 requires that "given
40/// a pair of GRUUs, it MUST be computationally infeasible to determine whether they were issued
41/// for the same AOR or instance ID or for different AORs and instance IDs". Code that read an
42/// instance out of this parameter would work against public GRUUs and quietly mis-attribute
43/// temporary ones, which is the failure mode nobody notices until it matters.
44#[must_use]
45pub fn gr_value(uri: &Uri) -> Option<&[u8]> {
46 uri.params().and_then(|params| params.value(GR_PARAM))
47}
48
49/// Whether a request whose Request-URI is `request_uri` was sent to `gruu` (§4.5).
50///
51/// Three conditions, and the first is the one RFC 3261 cannot express: **both** URIs must
52/// actually be GRUUs. §5.4 warns that a public GRUU and its address of record are equivalent
53/// under §19.1.4, so requiring `gr` on both sides is what keeps a request aimed at the address
54/// of record from being read as one aimed at a single instance.
55///
56/// The rest is §19.1.4 as written: `gr` is a parameter present in both URIs, so
57/// [`Uri::equivalent`] already requires the two values to agree — which is what separates one
58/// instance's GRUU from another's, and a valued `gr` from a valueless one.
59#[must_use]
60pub fn addressed_to(request_uri: &Uri, gruu: &Uri) -> bool {
61 is_gruu(request_uri) && is_gruu(gruu) && request_uri.equivalent(gruu)
62}
63
64#[cfg(test)]
65#[allow(
66 clippy::unwrap_used,
67 clippy::expect_used,
68 clippy::panic,
69 clippy::indexing_slicing
70)]
71mod tests {
72 use super::*;
73 use bytes::Bytes;
74
75 const INSTANCE: &str = "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6";
76
77 fn uri(text: &str) -> Uri {
78 Uri::parse(Bytes::from(text.to_owned())).unwrap_or_else(|e| panic!("{text:?}: {e}"))
79 }
80
81 #[test]
82 fn a_gruu_is_a_uri_carrying_the_gr_parameter() {
83 assert!(is_gruu(&uri(&format!(
84 "sip:alice@example.com;gr={INSTANCE}"
85 ))));
86 // §7 makes the value optional, and §4.5 says so again in prose.
87 assert!(is_gruu(&uri("sip:t7k2xq9f4m@example.com;gr")));
88 assert!(!is_gruu(&uri("sip:alice@example.com")));
89 // A URI parameter whose name merely starts with the letters is not the parameter.
90 assert!(!is_gruu(&uri("sip:alice@example.com;group=5")));
91 }
92
93 #[test]
94 fn the_gr_parameter_round_trips_through_the_parser_unchanged() {
95 for text in [
96 "sip:alice@example.com;gr=urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
97 "sip:t7k2xq9f4m@example.com;gr",
98 "sips:alice@example.com;transport=tls;gr=urn:uuid:abc",
99 ] {
100 assert_eq!(
101 uri(text).to_bytes(),
102 Bytes::from(text.to_owned()),
103 "§4.2 has a UA treat a GRUU as opaque, so it must go back out as it came in"
104 );
105 }
106 assert_eq!(
107 gr_value(&uri(&format!("sip:alice@example.com;gr={INSTANCE}"))),
108 Some(INSTANCE.as_bytes())
109 );
110 assert_eq!(gr_value(&uri("sip:t7k2xq9f4m@example.com;gr")), None);
111 }
112
113 /// §5.4: "A public GRUU will always be equivalent to the AOR based on URI equality rules."
114 ///
115 /// This test asserts the RFC's own observation *and* that this module does not inherit it.
116 /// If [`Uri::equivalent`] ever stopped agreeing, the first half would fail and someone would
117 /// find out here rather than from a UA answering calls meant for another device.
118 #[test]
119 fn an_address_of_record_is_rfc3261_equivalent_to_a_public_gruu_but_is_not_sent_to_it() {
120 let aor = uri("sip:alice@example.com");
121 let gruu = uri(&format!("sip:alice@example.com;gr={INSTANCE}"));
122 assert!(
123 aor.equivalent(&gruu),
124 "§5.4 says RFC 3261 §19.1.4 calls these the same URI, because it ignores a \
125 parameter present in only one of them"
126 );
127 assert!(
128 !addressed_to(&aor, &gruu),
129 "a request to the address of record names every device the user registered; \
130 answering it as though it named this instance is the whole bug §5.4 warns about"
131 );
132 assert!(addressed_to(&gruu, &gruu));
133 }
134
135 #[test]
136 fn one_instances_gruu_is_not_another_instances() {
137 let ours = uri(&format!("sip:alice@example.com;gr={INSTANCE}"));
138 let theirs = uri("sip:alice@example.com;gr=urn:uuid:00000000-0000-4000-8000-000000000000");
139 assert!(!addressed_to(&theirs, &ours));
140 assert!(!addressed_to(&ours, &theirs));
141 }
142
143 #[test]
144 fn a_valueless_temporary_gruu_matches_only_itself() {
145 let temp = uri("sip:t7k2xq9f4m@example.com;gr");
146 assert!(addressed_to(&temp, &temp));
147 // A valueless `gr` and a valued one are not the same parameter value, so §19.1.4 already
148 // separates them — but only once both sides are known to be GRUUs at all.
149 assert!(!addressed_to(
150 &uri(&format!("sip:t7k2xq9f4m@example.com;gr={INSTANCE}")),
151 &temp
152 ));
153 assert!(!addressed_to(&uri("sip:t7k2xq9f4m@example.com"), &temp));
154 // A different opaque user part is a different GRUU, which is the point of §5.4's
155 // unlinkability requirement: nothing about the two says they came from one instance.
156 assert!(!addressed_to(&uri("sip:h4d0s2p8v1@example.com;gr"), &temp));
157 }
158
159 /// The rest of §19.1.4 still applies: a GRUU is a SIP URI, and `sip:` is never `sips:`.
160 #[test]
161 fn the_ordinary_uri_rules_still_decide_everything_else() {
162 let plain = uri(&format!("sip:alice@example.com;gr={INSTANCE}"));
163 let secure = uri(&format!("sips:alice@example.com;gr={INSTANCE}"));
164 assert!(!addressed_to(&secure, &plain));
165 // Host case and an escaped parameter name are spellings, not differences.
166 assert!(addressed_to(
167 &uri(&format!("sip:alice@EXAMPLE.com;%67r={INSTANCE}")),
168 &plain
169 ));
170 }
171}