sipx_ua/push.rs
1//! Being reachable through a push notification (RFC 8599), from the user agent's side.
2//!
3//! The client this is for holds no connection at all: no socket, no keep-alive, and quite possibly
4//! not running. Every other mechanism in this crate assumes there is *something* the registrar can
5//! route down — Outbound keeps a flow open, GRUU names the instance at the end of one. RFC 8599 is
6//! what is left when there is nothing: the proxy leaves SIP for one hop, asks the client's push
7//! notification service to wake it, and the client goes and gets a flow.
8//!
9//! Three things are worth knowing before reading on.
10//!
11//! - **The push is not the call.** §4.1.3: "When a UA receives a push notification, the UA MUST
12//! send a binding-refresh REGISTER request." The notification is permission to go and get a
13//! flow; the request it was sent for arrives down that flow afterwards. A client that waits for
14//! the INVITE instead of refreshing is waiting on a path that does not exist yet — which is why
15//! the ordering is a type here ([`Pending`]) rather than a comment.
16//! - **sipx ships no push service.** [`PushService`] is a trait and this repository implements it
17//! nowhere. sipx is a stack, not a client of anybody's push transport, and the non-goals in
18//! `docs/vision.md` rule out the alternative. What sipx owes is the SIP half: the parameters,
19//! the option negotiation, 555, and the refresh ordering.
20//! - **The proxy half is not here.** §5.6 has a proxy hold the request in a bucket while the
21//! client wakes, and §4.2's registrar behaviour mints the PURR. Both are roles sipx does not
22//! play, and neither shares anything with this but the wire format.
23
24use std::time::Duration;
25
26use sipx_sip::Response;
27use sipx_sip::error::BuildError;
28use sipx_sip::push::{Device, Indicators};
29
30pub use sipx_sip::push::{NOT_SUPPORTED, NOT_SUPPORTED_REASON};
31
32/// The push notification service a device can be woken through (§3).
33///
34/// **sipx implements this nowhere, and that is deliberate.** Waking a device means speaking some
35/// vendor's HTTP API over some vendor's credentials, on a schedule that vendor sets — none of
36/// which is SIP, and all of which would date faster than the rest of this crate. What sipx needs
37/// from a push service is three strings, and this is them.
38///
39/// An implementation is an adapter the application writes over whatever it already uses to reach
40/// its push service. The tests use a stub for the same reason: there is nothing here that a real
41/// implementation would exercise differently.
42pub trait PushService {
43 /// The `pn-provider` value naming this service (§8.7).
44 ///
45 /// A value from the registry §8.8 creates. It is the name the *registrar* has to recognise, so
46 /// inventing one produces a binding nothing will ever wake — see [`Support::supports`].
47 fn provider(&self) -> &str;
48
49 /// The `pn-prid` value: the identifier this service knows the device by (§8.7).
50 fn prid(&self) -> &str;
51
52 /// The `pn-param` value, when the service needs one (§8.7).
53 ///
54 /// Service-specific and not SIP's business, which is why the default is `None`.
55 fn param(&self) -> Option<&str> {
56 None
57 }
58
59 /// The parameters a REGISTER's `Contact` URI must carry to name this service (§4.1.2).
60 ///
61 /// Fails when one of the three values is not something a URI parameter can hold; see
62 /// [`Device`] for why that is checked here rather than discovered at the registrar.
63 fn device(&self) -> Result<Device, BuildError> {
64 let device = Device::new(self.provider(), self.prid())?;
65 match self.param() {
66 Some(param) => device.with_param(param),
67 None => Ok(device),
68 }
69 }
70}
71
72/// What a registrar said about push, read from the `Feature-Caps` of a REGISTER response (§8.2).
73///
74/// The interesting question this answers is not "did the registration succeed" — it did, or there
75/// would be no response to read. It is **"can this registrar actually wake me"**, and the two come
76/// apart: a registrar that supports some other push service answers 200 and records a perfectly
77/// good binding that nothing will ever ring. That failure looks exactly like success from every
78/// angle except this one.
79#[derive(Debug, Clone, Default, PartialEq, Eq)]
80pub struct Support {
81 services: Vec<String>,
82 refreshes_required: bool,
83 refresh_interval: Option<Duration>,
84 purr: Option<String>,
85}
86
87impl Support {
88 /// Read what a REGISTER response said about push (§8.2).
89 ///
90 /// Every `Feature-Caps` row is read, because a registrar offering several push services says
91 /// so with several indicators and a client that stopped at the first would miss its own.
92 #[must_use]
93 pub fn from_response(response: &Response) -> Self {
94 let mut support = Self::default();
95 for value in response.headers.typed_all::<Indicators>() {
96 let Ok(indicators) = value else {
97 continue;
98 };
99 if let Some(pns) = indicators.pns() {
100 support
101 .services
102 .push(String::from_utf8_lossy(pns).into_owned());
103 }
104 if indicators.refreshes_required() {
105 support.refreshes_required = true;
106 support.refresh_interval = support
107 .refresh_interval
108 .or_else(|| indicators.refresh_interval());
109 }
110 if let Some(purr) = indicators.purr() {
111 support.purr = Some(String::from_utf8_lossy(purr).into_owned());
112 }
113 }
114 support
115 }
116
117 /// The push notification services the registrar named (§8.2's `sip.pns`).
118 #[must_use]
119 pub fn services(&self) -> &[String] {
120 &self.services
121 }
122
123 /// Whether the registrar named this push service.
124 ///
125 /// The question a client has to ask after every registration, and the reason `sip.pns` exists.
126 /// `false` does not mean the registration failed — it means the binding is one nothing will
127 /// wake, and a client that never asks will sit there believing it is reachable.
128 ///
129 /// Compared case-insensitively: §8.8's registry values are tokens.
130 #[must_use]
131 pub fn supports(&self, provider: &str) -> bool {
132 self.services
133 .iter()
134 .any(|named| named.eq_ignore_ascii_case(provider))
135 }
136
137 /// Whether the registrar asked for binding refreshes even without a push (§8.2's
138 /// `sip.pnsreg`).
139 #[must_use]
140 pub fn refreshes_required(&self) -> bool {
141 self.refreshes_required
142 }
143
144 /// How long to leave between those refreshes, when the registrar said a readable number.
145 ///
146 /// `None` alongside a true [`Support::refreshes_required`] means the registrar asked for
147 /// refreshes without naming an interval this side could read; the lease's own refresh point
148 /// still applies.
149 #[must_use]
150 pub fn refresh_interval(&self) -> Option<Duration> {
151 self.refresh_interval
152 }
153
154 /// The PURR the registrar assigned this binding (§8.2's `sip.pnspurr`).
155 ///
156 /// Carried, not acted on. A PURR exists so that a request can be matched to a *stored*
157 /// binding without re-deriving it from the `pn-*` values, and the party that stores bindings
158 /// is the registrar or the proxy of §5.6 — not a user agent, which has exactly one. sipx
159 /// therefore reads it, keeps it, and hands it to the application, and does no matching with
160 /// it; that half arrives with the proxy role or not at all.
161 #[must_use]
162 pub fn purr(&self) -> Option<&str> {
163 self.purr.as_deref()
164 }
165
166 /// Whether the registrar said nothing about push at all — every registrar that does not
167 /// implement RFC 8599, which is not an error and not a refusal.
168 #[must_use]
169 pub fn is_empty(&self) -> bool {
170 self.services.is_empty() && !self.refreshes_required && self.purr.is_none()
171 }
172}
173
174/// Permission to expect the request a push notification was sent for (§4.1.3).
175///
176/// **The only way to get one is [`crate::UserAgent::woken`], and only after the binding-refresh
177/// REGISTER has succeeded.** That is the whole point of the type. §4.1.3 fixes an order — push,
178/// then REGISTER, then the request — and it is the easiest thing in this RFC to run backwards,
179/// because waiting for the INVITE is what a woken client *feels* like it should do. A client that
180/// waits without refreshing is waiting on a flow that does not exist, and the call it was woken
181/// for times out somewhere it cannot see.
182#[derive(Debug, Clone, PartialEq, Eq)]
183#[non_exhaustive]
184pub struct Pending {
185 /// The lease the binding-refresh REGISTER renewed.
186 pub lease: crate::registrar::Lease,
187 /// The PURR the registrar assigned this binding, if it assigned one (§8.2's `sip.pnspurr`).
188 pub purr: Option<String>,
189}
190
191/// Put the push parameters into a contact's URI (§4.1.2).
192///
193/// The parameters are **URI** parameters, and the difference is the whole of why this is not a
194/// `format!`. Inside the angle brackets a `;` starts a `uri-parameter`; outside them it starts a
195/// header parameter, which RFC 3261 §20 makes a different field of a different grammar. A
196/// registrar reading `Contact` URIs would never see one pasted on the outside — and would answer
197/// 200 to the registration, so nothing would say it had gone wrong.
198///
199/// A contact arriving without angle brackets gains them, because that form has nowhere to put a
200/// URI parameter at all; any header parameters after it stay outside, where they were.
201///
202/// **A contact that cannot carry the parameters is returned byte-for-byte unchanged, and warned
203/// about.** There are two such contacts and they fail for different reasons:
204///
205/// - one whose URI does not parse — including a display name whose own quoted `<` or `>` defeats
206/// the split above, since a bracket sipx picked wrongly yields text that is not a URI;
207/// - one whose URI parses under a scheme sipx does not model — `tel:`, `http:`, `urn:` — which
208/// has no `uri-parameter` list to put them in, so setting them is a no-op.
209///
210/// Returning the contact unchanged is failure-closed, and the warning is the point: the second
211/// case is otherwise indistinguishable from success. The registration would go out looking
212/// ordinary, the registrar would answer 200, and the device would be unreachable by exactly the
213/// mechanism it was configured for. This is the application's own configuration rather than
214/// network input, so it is a fault an operator can fix once told about it.
215#[must_use]
216pub fn in_contact(contact: &str, device: &Device) -> String {
217 use bytes::Bytes;
218
219 let trimmed = contact.trim();
220 let (prefix, uri_text, tail) = match (trimmed.find('<'), trimmed.rfind('>')) {
221 (Some(open), Some(close)) if open < close => (
222 trimmed.get(..=open).unwrap_or_default(),
223 trimmed.get(open + 1..close).unwrap_or_default(),
224 trimmed.get(close + 1..).unwrap_or_default(),
225 ),
226 // A bare addr-spec: the URI runs to the first semicolon, and everything from there is a
227 // header parameter list that must stay one.
228 _ => {
229 let end = trimmed.find(';').unwrap_or(trimmed.len());
230 (
231 "<",
232 trimmed.get(..end).unwrap_or_default(),
233 trimmed.get(end..).unwrap_or_default(),
234 )
235 }
236 };
237
238 let Ok(mut uri) = sipx_sip::Uri::parse(Bytes::from(uri_text.to_owned())) else {
239 tracing::warn!(
240 contact,
241 "the contact is not a URI sipx can parse, so RFC 8599 §4.1.2's push parameters were \
242 left off the registration"
243 );
244 return contact.to_owned();
245 };
246 device.set_on(&mut uri);
247 // Reading them back is the only honest check that they went in. `Uri::push_param` is a no-op
248 // on a scheme sipx does not model, because such a URI has no `uri-parameter` list at all — so
249 // without this the contact comes back looking registered for push and carrying none, which is
250 // the one failure mode this whole function exists to prevent, arrived at by another road.
251 if Device::from_uri(&uri).is_none() {
252 tracing::warn!(
253 contact,
254 "the contact's URI cannot carry a uri-parameter, so RFC 8599 §4.1.2's push \
255 parameters were left off the registration and no push notification will reach this \
256 device"
257 );
258 return contact.to_owned();
259 }
260 format!(
261 "{prefix}{}>{tail}",
262 String::from_utf8_lossy(&uri.to_bytes())
263 )
264}
265
266#[cfg(test)]
267#[allow(
268 clippy::unwrap_used,
269 clippy::expect_used,
270 clippy::panic,
271 clippy::indexing_slicing
272)]
273mod tests {
274 use super::*;
275 use bytes::Bytes;
276 use sipx_sip::{Limits, Message, parse_datagram};
277
278 /// A stub, and the only implementation of [`PushService`] anywhere near this repository.
279 struct Stub {
280 param: Option<&'static str>,
281 }
282
283 impl PushService for Stub {
284 fn provider(&self) -> &'static str {
285 "webpush"
286 }
287
288 fn prid(&self) -> &'static str {
289 "c1a5b3e7d9f2"
290 }
291
292 fn param(&self) -> Option<&str> {
293 self.param
294 }
295 }
296
297 fn device() -> Device {
298 Stub {
299 param: Some("7f3ad0"),
300 }
301 .device()
302 .expect("valid")
303 }
304
305 /// The trait exists to turn three service-specific strings into §4.1.2's parameters, and
306 /// `pn-param` is the one that is optional.
307 #[test]
308 fn a_service_becomes_the_parameters_a_contact_carries() {
309 assert_eq!(device().provider(), "webpush");
310 assert_eq!(device().param(), Some("7f3ad0"));
311 assert_eq!(device().prid(), "c1a5b3e7d9f2");
312 assert_eq!(Stub { param: None }.device().expect("valid").param(), None);
313 }
314
315 /// The failure this function exists to prevent: parameters after the `>` are header
316 /// parameters, and a registrar reading the `Contact` URI never sees them (RFC 3261 §20).
317 #[test]
318 fn the_parameters_land_inside_the_angle_brackets() {
319 assert_eq!(
320 in_contact("<sip:alice@192.0.2.5:5060>", &device()),
321 "<sip:alice@192.0.2.5:5060;pn-provider=webpush;pn-param=7f3ad0\
322 ;pn-prid=c1a5b3e7d9f2>"
323 );
324 }
325
326 /// A display name and the header parameters after the brackets belong to the header, not the
327 /// URI, and must come through untouched.
328 #[test]
329 fn a_display_name_and_header_parameters_survive() {
330 assert_eq!(
331 in_contact("\"Alice\" <sip:alice@192.0.2.5>;expires=600", &device()),
332 "\"Alice\" <sip:alice@192.0.2.5;pn-provider=webpush;pn-param=7f3ad0\
333 ;pn-prid=c1a5b3e7d9f2>;expires=600"
334 );
335 }
336
337 /// A bare addr-spec has nowhere to put a URI parameter, so it gains the brackets — and its
338 /// header parameters stay outside them, which is the half that is easy to lose.
339 #[test]
340 fn a_bare_contact_gains_the_brackets_a_uri_parameter_needs() {
341 assert_eq!(
342 in_contact("sip:alice@192.0.2.5", &device()),
343 "<sip:alice@192.0.2.5;pn-provider=webpush;pn-param=7f3ad0;pn-prid=c1a5b3e7d9f2>"
344 );
345 assert_eq!(
346 in_contact("sip:alice@192.0.2.5;expires=600", &device()),
347 "<sip:alice@192.0.2.5;pn-provider=webpush;pn-param=7f3ad0\
348 ;pn-prid=c1a5b3e7d9f2>;expires=600"
349 );
350 }
351
352 /// Configuration this side cannot parse is left alone rather than rewritten into something
353 /// else. The REGISTER will fail on its own terms, which is a fault an operator can see.
354 #[test]
355 fn a_contact_that_is_not_a_uri_is_left_as_it_was() {
356 assert_eq!(in_contact("<not a uri>", &device()), "<not a uri>");
357 }
358
359 /// A scheme sipx does not model parses perfectly well and then has nowhere to put a
360 /// `uri-parameter`. Without a check that the parameters went in, this is the one failure that
361 /// looks exactly like success: an ordinary-looking REGISTER, a 200, and a device that no push
362 /// notification can reach. Unchanged is the answer, so nothing downstream can mistake it.
363 #[test]
364 fn a_contact_whose_scheme_cannot_carry_a_uri_parameter_is_left_as_it_was() {
365 for contact in [
366 "<tel:+15551234>",
367 "tel:+15551234",
368 "<urn:service:sos>",
369 "<http://example.com/alice>",
370 ] {
371 assert_eq!(
372 in_contact(contact, &device()),
373 contact,
374 "the push parameters were silently dropped and the contact still rewritten"
375 );
376 }
377 }
378
379 /// A display name carrying its own brackets defeats the split, which yields text that is not
380 /// a URI — so it takes the failure-closed path rather than registering a contact assembled
381 /// around the wrong bracket.
382 #[test]
383 fn a_display_name_with_its_own_brackets_is_left_as_it_was() {
384 let contact = "\"Alice <at home>\" <sip:alice@192.0.2.5>";
385 assert_eq!(in_contact(contact, &device()), contact);
386 }
387
388 fn response(caps: &str) -> Response {
389 let text = format!(
390 "SIP/2.0 200 OK\r\n\
391 Via: SIP/2.0/UDP 192.0.2.5:5060;branch=z9hG4bKx\r\n\
392 To: <sip:alice@example.com>;tag=r\r\n\
393 From: <sip:alice@example.com>;tag=1\r\n\
394 Call-ID: reg-1@192.0.2.5\r\n\
395 CSeq: 1 REGISTER\r\n\
396 {caps}\
397 Content-Length: 0\r\n\r\n"
398 );
399 match parse_datagram(Bytes::from(text), &Limits::datagram()).expect("parses") {
400 Message::Response(r) => r,
401 Message::Request(_) => panic!("a response"),
402 }
403 }
404
405 #[test]
406 fn the_registrars_answer_says_which_service_it_can_use() {
407 let support = Support::from_response(&response(
408 "Feature-Caps: *;+sip.pns=\"webpush\";+sip.pnsreg=\"120\"\
409 ;+sip.pnspurr=\"opaque-purr-1\"\r\n",
410 ));
411 assert!(support.supports("webpush"));
412 // §8.8's values are tokens, and a token compares case-insensitively.
413 assert!(support.supports("WebPush"));
414 assert!(support.refreshes_required());
415 assert_eq!(support.refresh_interval(), Some(Duration::from_secs(120)));
416 assert_eq!(support.purr(), Some("opaque-purr-1"));
417 }
418
419 /// The failure that looks like success: a 200, a good binding, and a push service nobody here
420 /// can reach.
421 #[test]
422 fn a_registrar_naming_another_service_does_not_support_ours() {
423 let support = Support::from_response(&response("Feature-Caps: *;+sip.pns=\"other\"\r\n"));
424 assert!(!support.supports("webpush"));
425 assert!(
426 !support.is_empty(),
427 "it said something, just not our service"
428 );
429 assert_eq!(support.purr(), None);
430 }
431
432 /// A registrar offering several services says so with several indicators, and a client that
433 /// stopped at the first would miss its own.
434 #[test]
435 fn every_named_service_is_read_not_only_the_first() {
436 let support = Support::from_response(&response(
437 "Feature-Caps: *;+sip.pns=\"other\"\r\n\
438 Feature-Caps: *;+sip.pns=\"webpush\"\r\n",
439 ));
440 assert_eq!(
441 support.services(),
442 ["other".to_owned(), "webpush".to_owned()]
443 );
444 assert!(support.supports("webpush"));
445 }
446
447 /// Every registrar that does not implement RFC 8599 answers a REGISTER perfectly well and
448 /// says nothing. That is not a refusal, and it is not an error.
449 #[test]
450 fn a_registrar_that_says_nothing_about_push_is_empty_rather_than_negative() {
451 let support = Support::from_response(&response(""));
452 assert!(support.is_empty());
453 assert!(!support.supports("webpush"));
454 assert!(!support.refreshes_required());
455 }
456}