Skip to main content

sipx_ua/
gruu.rs

1//! GRUUs: obtaining them from a registrar, and choosing which one to use (RFC 5627).
2//!
3//! A registration says "this user is reachable here". A GRUU says "*this device* is reachable
4//! here", which is a different claim and the one a transfer, a conference invitation or a
5//! callback needs: an address of record resolves to every phone the user has registered, and a
6//! request routed to all of them is not a request routed back to the one that was talking.
7//!
8//! sipx **obtains and uses** GRUUs. Minting them is §5's registrar behaviour and is not here —
9//! sipx is not a registrar, and the two halves share nothing but the wire format.
10//!
11//! Three things are worth knowing before reading on:
12//!
13//! - **The instance ID is the same one Outbound registers with.** §4.1 identifies the instance
14//!   with the `+sip.instance` media feature tag that RFC 5626 §4.1 also defines. Two mechanisms,
15//!   one identity — [`Registration`](crate::registrar::Registration) holds it in a single field
16//!   so that they cannot come to hold two, which is a fault that only appears under a registrar
17//!   that correlates them.
18//! - **The two GRUUs are not interchangeable.** See [`Kind`]. Substituting one for the other
19//!   silently is the failure this module goes out of its way not to have.
20//! - **Recognising one needs more than URI equality.** §5.4: "A public GRUU will always be
21//!   equivalent to the AOR based on URI equality rules." The comparison that does not make that
22//!   mistake is [`sipx_sip::gruu::addressed_to`].
23
24use bytes::Bytes;
25use sipx_sip::headers::ContactValue;
26use sipx_sip::{Address, Response, Uri};
27
28use crate::outbound::InstanceId;
29
30/// The option tag a UA offers to ask for a GRUU.
31///
32/// §4.1: a compliant UA "MUST include the Supported header field" in every REGISTER and "the
33/// value of that header field MUST include 'gruu' as one of the option tags". A registrar that
34/// does not see it has been told nothing was asked for, and §5.2 has it attach nothing.
35pub const OPTION_TAG: &str = "gruu";
36
37/// The `Contact` header field parameter carrying the public GRUU (§7).
38const PUB_GRUU_PARAM: &str = "pub-gruu";
39
40/// The `Contact` header field parameter carrying the temporary GRUU (§7).
41const TEMP_GRUU_PARAM: &str = "temp-gruu";
42
43/// The `Contact` header field parameter naming the instance a binding belongs to.
44///
45/// RFC 5626 §4.1's media feature tag, which §4.1 of this RFC reuses rather than inventing a
46/// second name for the same fact.
47const INSTANCE_PARAM: &str = "+sip.instance";
48
49/// Which of the two GRUUs a UA puts in a `Contact` (§4.4).
50///
51/// # Why the default is [`Kind::Public`]
52///
53/// The two differ in exactly one property each way, and the trade is not sipx's to make.
54///
55/// A **public** GRUU is a stable identifier for the instance. It survives re-registration, and
56/// §5.2 has a registrar keep treating it as valid even after the binding lapses — answering 480
57/// until the device comes back — so an address handed out under one still names this device
58/// tomorrow. What it does not offer is privacy: two public GRUUs for one instance are the same
59/// URI, so anyone holding two of them can see they are the same device.
60///
61/// A **temporary** GRUU buys precisely that privacy. §5.4: "Given a pair of GRUUs, it MUST be
62/// computationally infeasible to determine whether they were issued for the same AOR or instance
63/// ID or for different AORs and instance IDs." It pays for it in lifetime — §4.2 requires a UA
64/// to discard every temporary GRUU it learned whenever its `Call-ID` changes, so an address
65/// handed out under one stops resolving as soon as the UA registers afresh.
66///
67/// So the default is the public one, because it is the choice that keeps working, and because
68/// only the application knows whether the address it is about to put in a `Contact` has to
69/// outlive this registration. Unlinkability is a property that must be *asked* for — and, having
70/// been asked for, is never quietly downgraded: see [`Gruus::preferred`].
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
72pub enum Kind {
73    /// One stable URI for this instance, usable for as long as the instance exists.
74    #[default]
75    Public,
76    /// An address that cannot be correlated with any other GRUU, and that lapses with the
77    /// registration that produced it.
78    Temporary,
79}
80
81/// The GRUUs a registrar issued for one instance's binding (§4.2, §5.2).
82///
83/// Held with the registration rather than beside it, and replaced wholesale on every 2xx. That
84/// is not tidiness: §4.2 requires a UA to "discard all temporary GRUUs learned through prior
85/// REGISTER responses" whenever the `Call-ID` changes, and a set that is replaced rather than
86/// merged cannot carry a stale one across.
87#[derive(Debug, Clone, Default)]
88pub struct Gruus {
89    public: Option<Uri>,
90    temporary: Option<Uri>,
91}
92
93impl Gruus {
94    /// Read the GRUUs a REGISTER 2xx issued for `instance` (§4.2).
95    ///
96    /// §4.2 pairs the two parameters with the `Contact` carrying the `+sip.instance` they were
97    /// minted for — and a 2xx lists *every* current binding for the address of record, other
98    /// devices' included (RFC 3261 §10.3). Selecting the row by instance rather than by position
99    /// is what stops this adopting another phone's GRUU and then answering to it.
100    ///
101    /// A value that is not a GRUU is dropped rather than kept. §7 gives a GRUU the `gr`
102    /// parameter, and a URI without one is the address of record: using it as though it named
103    /// this instance would route every one of the user's devices at a request meant for one.
104    #[must_use]
105    pub fn from_response(response: &Response, instance: &InstanceId) -> Self {
106        for value in response.headers.typed_all::<ContactValue>() {
107            let Ok(ContactValue::Address(address)) = value else {
108                continue;
109            };
110            if !names_instance(&address, instance) {
111                continue;
112            }
113            return Self {
114                public: gruu_param(&address, PUB_GRUU_PARAM),
115                temporary: gruu_param(&address, TEMP_GRUU_PARAM),
116            };
117        }
118        Self::default()
119    }
120
121    /// The public GRUU, if the registrar issued one.
122    #[must_use]
123    pub fn public(&self) -> Option<&Uri> {
124        self.public.as_ref()
125    }
126
127    /// The temporary GRUU, if the registrar issued one.
128    #[must_use]
129    pub fn temporary(&self) -> Option<&Uri> {
130        self.temporary.as_ref()
131    }
132
133    /// Whether the registrar issued neither.
134    ///
135    /// §4.2: "A UA must be prepared for a Contact to contain just one, both, or neither" — a
136    /// registrar that does not implement RFC 5627 answers a REGISTER perfectly well and attaches
137    /// nothing, and that is not an error.
138    #[must_use]
139    pub fn is_empty(&self) -> bool {
140        self.public.is_none() && self.temporary.is_none()
141    }
142
143    /// The GRUU to use for a caller that asked for `kind` (§4.4).
144    ///
145    /// **One never stands in for the other.** Asking for a temporary GRUU and being handed the
146    /// public one would tell the caller the opposite of the truth about what it just put in a
147    /// `Contact`: it believes it published an address nobody can correlate, and it published the
148    /// device's permanent name. `None` — falling back to the ordinary contact — is the honest
149    /// answer, and it is the one that leaks least.
150    #[must_use]
151    pub fn preferred(&self, kind: Kind) -> Option<&Uri> {
152        match kind {
153            Kind::Public => self.public(),
154            Kind::Temporary => self.temporary(),
155        }
156    }
157
158    /// Whether a request whose Request-URI is `request_uri` was sent to one of these (§4.5).
159    #[must_use]
160    pub fn sent_to(&self, request_uri: &Uri) -> bool {
161        [self.public.as_ref(), self.temporary.as_ref()]
162            .into_iter()
163            .flatten()
164            .any(|ours| sipx_sip::gruu::addressed_to(request_uri, ours))
165    }
166
167    /// Each GRUU rendered back to text, for logging and for comparison.
168    #[must_use]
169    fn rendered(&self) -> Vec<Option<String>> {
170        vec![
171            self.public.as_ref().map(Uri::to_string),
172            self.temporary.as_ref().map(Uri::to_string),
173        ]
174    }
175}
176
177/// Compared by what the two URIs say, because [`Uri`] deliberately has no `PartialEq`: RFC 3261
178/// §19.1.4 equivalence is not transitive, and it is the wrong relation here anyway — two
179/// registrations that returned *different spellings* of one GRUU did return different values.
180impl PartialEq for Gruus {
181    fn eq(&self, other: &Self) -> bool {
182        self.rendered() == other.rendered()
183    }
184}
185
186impl Eq for Gruus {}
187
188/// Whether this `Contact` is the binding for `instance` (§4.2).
189///
190/// The angle brackets are part of the wire form — RFC 5626 §4.1 quotes the URN inside them — and
191/// are stripped from both sides so that a registrar echoing the parameter in either spelling is
192/// still recognised.
193#[must_use]
194fn names_instance(address: &Address, instance: &InstanceId) -> bool {
195    address
196        .param(INSTANCE_PARAM)
197        .is_some_and(|value| trim_brackets(value).eq_ignore_ascii_case(instance.urn().as_bytes()))
198}
199
200/// One of the two GRUU `Contact` parameters, parsed (§7).
201///
202/// §7 makes both a `quoted-string`; the parser has already removed the quotes and resolved the
203/// escapes. Angle brackets are stripped as well, because they are not in the grammar and are
204/// exactly the sort of thing an implementation adds anyway — accepting them costs nothing and
205/// rejecting the GRUU costs reachability.
206#[must_use]
207fn gruu_param(address: &Address, name: &str) -> Option<Uri> {
208    let raw = trim_brackets(address.param(name)?);
209    let uri = Uri::parse(Bytes::copy_from_slice(raw)).ok()?;
210    sipx_sip::gruu::is_gruu(&uri).then_some(uri)
211}
212
213/// Strip one layer of `<`/`>`, and the whitespace around it.
214#[must_use]
215fn trim_brackets(value: &[u8]) -> &[u8] {
216    let value = value.trim_ascii();
217    match (value.first(), value.last()) {
218        (Some(b'<'), Some(b'>')) if value.len() >= 2 => {
219            value.get(1..value.len() - 1).unwrap_or_default()
220        }
221        _ => value,
222    }
223}
224
225#[cfg(test)]
226#[allow(
227    clippy::unwrap_used,
228    clippy::expect_used,
229    clippy::panic,
230    clippy::indexing_slicing
231)]
232mod tests {
233    use super::*;
234    use sipx_sip::{Limits, Message, parse_datagram};
235
236    const INSTANCE: &str = "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6";
237    const OTHER_INSTANCE: &str = "urn:uuid:00000000-0000-4000-8000-000000000000";
238
239    fn instance() -> InstanceId {
240        InstanceId::parse(INSTANCE).expect("a urn")
241    }
242
243    fn ok_with(contacts: &str) -> Response {
244        let text = format!(
245            "SIP/2.0 200 OK\r\n\
246             Via: SIP/2.0/UDP 192.0.2.5:5060;branch=z9hG4bKx\r\n\
247             To: <sip:alice@example.com>;tag=r\r\n\
248             From: <sip:alice@example.com>;tag=1\r\n\
249             Call-ID: reg-1@192.0.2.5\r\n\
250             CSeq: 1 REGISTER\r\n\
251             {contacts}\
252             Content-Length: 0\r\n\r\n"
253        );
254        match parse_datagram(Bytes::from(text), &Limits::datagram()).expect("parses") {
255            Message::Response(r) => r,
256            Message::Request(_) => panic!("a response"),
257        }
258    }
259
260    fn issued(contacts: &str) -> Gruus {
261        Gruus::from_response(&ok_with(contacts), &instance())
262    }
263
264    #[test]
265    fn both_gruus_are_read_from_the_binding_that_names_this_instance() {
266        let gruus = issued(&format!(
267            "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
268             ;pub-gruu=\"sip:alice@example.com;gr={INSTANCE}\"\
269             ;temp-gruu=\"sip:t7k2xq9f4m@example.com;gr\";expires=3600\r\n"
270        ));
271        assert_eq!(
272            gruus.public().map(Uri::to_string),
273            Some(format!("sip:alice@example.com;gr={INSTANCE}"))
274        );
275        assert_eq!(
276            gruus.temporary().map(Uri::to_string),
277            Some("sip:t7k2xq9f4m@example.com;gr".to_owned())
278        );
279    }
280
281    /// §4.2: "A UA must be prepared for a Contact to contain just one, both, or neither."
282    #[test]
283    fn one_both_or_neither_are_all_ordinary_answers() {
284        assert!(
285            issued(&format!(
286                "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\r\n"
287            ))
288            .is_empty()
289        );
290        let public_only = issued(&format!(
291            "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
292             ;pub-gruu=\"sip:alice@example.com;gr={INSTANCE}\"\r\n"
293        ));
294        assert!(public_only.public().is_some() && public_only.temporary().is_none());
295        let temp_only = issued(&format!(
296            "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
297             ;temp-gruu=\"sip:t7k2xq9f4m@example.com;gr\"\r\n"
298        ));
299        assert!(temp_only.public().is_none() && temp_only.temporary().is_some());
300    }
301
302    /// RFC 3261 §10.3 has a 2xx list *every* binding for the address of record. Reading the GRUUs
303    /// off the first row would adopt whichever device happened to register first and answer to a
304    /// URI that routes somewhere else entirely.
305    #[test]
306    fn another_devices_gruu_is_not_ours() {
307        let gruus = issued(&format!(
308            "Contact: <sip:alice@198.51.100.9:5060>;+sip.instance=\"<{OTHER_INSTANCE}>\"\
309             ;pub-gruu=\"sip:alice@example.com;gr={OTHER_INSTANCE}\"\r\n\
310             Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
311             ;pub-gruu=\"sip:alice@example.com;gr={INSTANCE}\"\r\n"
312        ));
313        assert_eq!(
314            gruus.public().map(Uri::to_string),
315            Some(format!("sip:alice@example.com;gr={INSTANCE}")),
316            "the row for another instance was taken for ours"
317        );
318    }
319
320    /// A binding with no `+sip.instance` is not this instance's, whatever it carries.
321    #[test]
322    fn a_binding_that_names_no_instance_yields_nothing() {
323        assert!(
324            issued(
325                "Contact: <sip:alice@192.0.2.5:5060>;pub-gruu=\"sip:alice@example.com;gr=x\"\r\n"
326            )
327            .is_empty()
328        );
329        assert!(issued("").is_empty());
330    }
331
332    /// §7 gives a GRUU the `gr` parameter. Without it the value is the address of record, and
333    /// putting *that* in a `Contact` as though it named this instance would fan a mid-dialog
334    /// request out to every device the user has.
335    #[test]
336    fn a_pub_gruu_without_the_gr_parameter_is_not_a_gruu() {
337        assert!(
338            issued(&format!(
339                "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
340                 ;pub-gruu=\"sip:alice@example.com\"\r\n"
341            ))
342            .is_empty()
343        );
344    }
345
346    /// §7 spells both parameters as quoted strings and neither in angle brackets, but a value
347    /// that arrives bracketed is still the URI it names.
348    #[test]
349    fn a_bracketed_value_is_accepted_the_same_as_a_bare_one() {
350        let gruus = issued(&format!(
351            "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
352             ;pub-gruu=\"<sip:alice@example.com;gr={INSTANCE}>\"\r\n"
353        ));
354        assert_eq!(
355            gruus.public().map(Uri::to_string),
356            Some(format!("sip:alice@example.com;gr={INSTANCE}"))
357        );
358    }
359
360    /// The point of the type: a caller that asked for privacy is told it did not get it, rather
361    /// than being handed the stable identifier and left believing otherwise.
362    #[test]
363    fn a_missing_temporary_gruu_is_never_answered_with_the_public_one() {
364        let gruus = issued(&format!(
365            "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
366             ;pub-gruu=\"sip:alice@example.com;gr={INSTANCE}\"\r\n"
367        ));
368        assert!(gruus.preferred(Kind::Public).is_some());
369        assert!(
370            gruus.preferred(Kind::Temporary).is_none(),
371            "§5.4's unlinkability is not something a public GRUU can stand in for"
372        );
373    }
374
375    #[test]
376    fn the_default_choice_is_the_public_gruu() {
377        assert_eq!(Kind::default(), Kind::Public);
378    }
379
380    #[test]
381    fn a_request_is_recognised_only_when_it_names_one_of_our_gruus() {
382        let gruus = issued(&format!(
383            "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
384             ;pub-gruu=\"sip:alice@example.com;gr={INSTANCE}\"\
385             ;temp-gruu=\"sip:t7k2xq9f4m@example.com;gr\"\r\n"
386        ));
387        let uri = |text: &str| Uri::parse(Bytes::from(text.to_owned())).expect("a URI");
388        assert!(gruus.sent_to(&uri(&format!("sip:alice@example.com;gr={INSTANCE}"))));
389        assert!(gruus.sent_to(&uri("sip:t7k2xq9f4m@example.com;gr")));
390        // §5.4: the address of record is URI-equivalent to the public GRUU and must still not
391        // count, because it names every device rather than this one.
392        assert!(!gruus.sent_to(&uri("sip:alice@example.com")));
393        assert!(!gruus.sent_to(&uri(&format!("sip:alice@example.com;gr={OTHER_INSTANCE}"))));
394        assert!(!Gruus::default().sent_to(&uri(&format!("sip:alice@example.com;gr={INSTANCE}"))));
395    }
396}