sipx_sip/push.rs
1//! Push notifications (RFC 8599), at the URI and header level.
2//!
3//! The problem: a mobile client is not running, or is running with every socket torn down by the
4//! operating system. There is no flow to route a call down and no keep-alive that could hold one
5//! open, so the registrar's binding names an address that reaches nothing. RFC 8599's answer is to
6//! route *around* SIP for one hop — the proxy asks the client's push notification service to wake
7//! it, and the client, once awake, goes and gets a flow.
8//!
9//! What lives here is the part that is pure syntax, and it is three separate things that are easy
10//! to conflate:
11//!
12//! - **The `pn-*` parameters** (§8.7) a `Contact` URI carries, which tell the registrar which push
13//! service to ask and how that service names this device. They are *URI* parameters, so they go
14//! inside the angle brackets of a `Contact`; outside them a `;` starts a header parameter and a
15//! registrar reading the URI would never see them (RFC 3261 §20).
16//! - **The feature-capability indicators** (§8.2), which travel in `Feature-Caps` (RFC 6809) and
17//! are how the registrar answers back: which push service it actually supports, whether it wants
18//! refreshes anyway, and what it will call this binding.
19//! - **555** (§8.1), the status code that says the client's whole reachability plan is wrong.
20//!
21//! Nothing here sends or receives a push notification. sipx implements the SIP half of RFC 8599
22//! and nothing else — the push service is behind a trait in `sipx-ua` and this repository ships no
23//! implementation of one. Deciding what a registration should say, and reading what came back,
24//! belongs in `sipx-ua` for the same reason [`crate::gruu`]'s registration half does.
25
26use std::time::Duration;
27
28use bytes::Bytes;
29
30use crate::error::{BuildError, HeaderError};
31use crate::headers::grammar::{self, HeaderParam, trim};
32use crate::message::{StatusCode, TypedHeader};
33use crate::name::HeaderName;
34use crate::params::Param;
35use crate::uri::Uri;
36
37/// The `Contact` URI parameter naming the push notification service (§8.7).
38pub const PN_PROVIDER: &str = "pn-provider";
39
40/// The `Contact` URI parameter carrying whatever else the named service needs (§8.7).
41///
42/// Its meaning is the service's, not SIP's, which is why nothing here interprets it.
43pub const PN_PARAM: &str = "pn-param";
44
45/// The `Contact` URI parameter carrying the identifier the service knows this device by (§8.7).
46pub const PN_PRID: &str = "pn-prid";
47
48/// The `Contact` URI parameter carrying the PURR — the Push Resource Reachability Reference the
49/// proxy assigned this binding (§8.7).
50///
51/// Read, never minted here: a UA does not choose its own PURR. See [`purr`].
52pub const PN_PURR: &str = "pn-purr";
53
54/// The feature-capability indicator naming a push notification service (§8.2).
55pub const SIP_PNS: &str = "+sip.pns";
56
57/// The feature-capability indicator asking for binding refreshes even without a push (§8.2).
58pub const SIP_PNSREG: &str = "+sip.pnsreg";
59
60/// The feature-capability indicator carrying the PURR assigned to a binding (§8.2).
61pub const SIP_PNSPURR: &str = "+sip.pnspurr";
62
63/// 555 (Push Notification Service Not Supported), registered in §8.1.
64pub const NOT_SUPPORTED: u16 = 555;
65
66/// The reason phrase §8.1 registers alongside [`NOT_SUPPORTED`].
67pub const NOT_SUPPORTED_REASON: &str = "Push Notification Service Not Supported";
68
69/// Whether a status is §8.1's 555.
70///
71/// Worth a name because of what it is not: 555 is not "the server said no" in the way a 403 is. It
72/// says the push service the request named cannot be used here, so every retry against this
73/// registrar with these parameters will fail the same way. A client that folds it into a generic
74/// 5xx retries forever against a plan that cannot work.
75#[must_use]
76pub fn is_not_supported(status: StatusCode) -> bool {
77 status.code() == NOT_SUPPORTED
78}
79
80/// How a push notification service names one device (§4.1.2, §8.7).
81///
82/// The three values a `Contact` URI carries so that a proxy can wake this client: which service to
83/// ask, what that service calls this device, and whatever else the service needs in between.
84///
85/// # Why constructing one can fail
86///
87/// A `pn-prid` is an opaque token minted by somebody else, and RFC 3261 §25.1's `pvalue` does not
88/// admit every octet — `=`, `;`, `?` and `@` among them. A value carrying one of those, pasted into
89/// a URI unchecked, does not produce a rejected registration; it produces a *different* URI, with
90/// the tail of the token read as another parameter. So the values are checked once, here, and a
91/// caller holding a token that needs octets outside `pvalue` percent-escapes it — `escaped` is part
92/// of the grammar and survives [`Uri`] round-tripping unchanged.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct Device {
95 provider: String,
96 param: Option<String>,
97 prid: String,
98}
99
100impl Device {
101 /// The service to ask, and the identifier it knows this device by (§4.1.2).
102 ///
103 /// `provider` is a value from the registry §8.8 creates; `prid` is the token the service
104 /// issued for this device.
105 pub fn new(provider: &str, prid: &str) -> Result<Self, BuildError> {
106 Ok(Self {
107 provider: pvalue(provider, PN_PROVIDER)?,
108 param: None,
109 prid: pvalue(prid, PN_PRID)?,
110 })
111 }
112
113 /// Add the `pn-param` the named service needs (§4.1.2).
114 ///
115 /// Optional because it is service-specific: some need nothing beyond the identifier.
116 pub fn with_param(mut self, param: &str) -> Result<Self, BuildError> {
117 self.param = Some(pvalue(param, PN_PARAM)?);
118 Ok(self)
119 }
120
121 /// The `pn-provider` value.
122 #[must_use]
123 pub fn provider(&self) -> &str {
124 &self.provider
125 }
126
127 /// The `pn-param` value, if the service needs one.
128 #[must_use]
129 pub fn param(&self) -> Option<&str> {
130 self.param.as_deref()
131 }
132
133 /// The `pn-prid` value.
134 #[must_use]
135 pub fn prid(&self) -> &str {
136 &self.prid
137 }
138
139 /// Put these parameters on a URI, replacing any already there (§4.1.2).
140 ///
141 /// Replacing rather than appending is not tidiness: RFC 3261 §19.1.1 says "any given
142 /// parameter-name MUST NOT appear more than once" in a URI, and a duplicate makes the whole
143 /// URI unparseable at the far end. A stale `pn-prid` left beside a fresh one would take the
144 /// registration down rather than merely be ignored.
145 pub fn set_on(&self, uri: &mut Uri) {
146 for name in [PN_PROVIDER, PN_PARAM, PN_PRID] {
147 uri.remove_param(name);
148 }
149 uri.push_param(Param::new(
150 Bytes::from_static(PN_PROVIDER.as_bytes()),
151 Bytes::from(self.provider.clone()),
152 ));
153 if let Some(param) = &self.param {
154 uri.push_param(Param::new(
155 Bytes::from_static(PN_PARAM.as_bytes()),
156 Bytes::from(param.clone()),
157 ));
158 }
159 uri.push_param(Param::new(
160 Bytes::from_static(PN_PRID.as_bytes()),
161 Bytes::from(self.prid.clone()),
162 ));
163 }
164
165 /// Read the push parameters off a URI (§8.7).
166 ///
167 /// `None` unless both `pn-provider` and `pn-prid` are there: §4.1.2 has a UA insert them
168 /// together, and either alone names no service that could be asked to wake anything.
169 #[must_use]
170 pub fn from_uri(uri: &Uri) -> Option<Self> {
171 let params = uri.params()?;
172 let text = |name: &str| {
173 params
174 .value(name)
175 .and_then(|raw| std::str::from_utf8(raw).ok())
176 };
177 Some(Self {
178 provider: pvalue(text(PN_PROVIDER)?, PN_PROVIDER).ok()?,
179 param: text(PN_PARAM).and_then(|value| pvalue(value, PN_PARAM).ok()),
180 prid: pvalue(text(PN_PRID)?, PN_PRID).ok()?,
181 })
182 }
183}
184
185/// The PURR a URI carries, if it carries one (§8.7).
186///
187/// Returned raw and not interpreted. The PURR is the proxy's name for a binding, so that a
188/// mid-dialog request can be matched to the binding it belongs to without re-deriving it from the
189/// `pn-*` values — which means it is only useful to a party that *stores* bindings. sipx stores
190/// none: it is a user agent, not a registrar or a proxy, so it reads the PURR its registrar
191/// assigned (see [`Indicators::purr`]), carries it, and does no matching with it. That half of
192/// §5.6 belongs where the other proxy roles do.
193#[must_use]
194pub fn purr(uri: &Uri) -> Option<&[u8]> {
195 uri.params().and_then(|params| params.value(PN_PURR))
196}
197
198/// The push feature-capability indicators one `Feature-Caps` value carries (§8.2).
199///
200/// RFC 6809 §4 gives the header the shape `*` followed by `;`-separated indicators, and RFC 8599
201/// §8.2 registers three of them that a client cares about. Indicators belonging to other
202/// mechanisms are ignored rather than rejected — that is what an extensible list is for, and a
203/// parser that failed on an unknown one would break against every proxy that grows a feature.
204///
205/// This models one value. A registrar may send several, on one row or on several; read them with
206/// [`crate::message::Headers::typed_all`], which treats those two spellings as the same message.
207#[derive(Debug, Clone, Default, PartialEq, Eq)]
208pub struct Indicators {
209 pns: Option<Vec<u8>>,
210 pnsreg: Option<Vec<u8>>,
211 pnspurr: Option<Vec<u8>>,
212}
213
214impl Indicators {
215 /// The push notification service this value names (§8.2's `sip.pns`).
216 ///
217 /// This is the answer to "does this registrar support the service I asked for". A registrar
218 /// naming a different one has not refused the registration — it has accepted a binding that
219 /// nothing will ever wake, which is worse, because it looks like success.
220 #[must_use]
221 pub fn pns(&self) -> Option<&[u8]> {
222 self.pns.as_deref()
223 }
224
225 /// Whether the registrar asked for binding refreshes even in the absence of a push (§8.2's
226 /// `sip.pnsreg`).
227 ///
228 /// Presence is a fact of its own, separate from the interval: an indicator sent with a value
229 /// this side cannot read still says the registrar wants refreshes, and treating it as absent
230 /// would let the binding lapse.
231 #[must_use]
232 pub fn refreshes_required(&self) -> bool {
233 self.pnsreg.is_some()
234 }
235
236 /// How long the registrar said to leave between those refreshes (§8.2's `sip.pnsreg`).
237 ///
238 /// `None` when the indicator is absent *and* when its value is not a number of seconds; see
239 /// [`Indicators::refreshes_required`] for the difference.
240 #[must_use]
241 pub fn refresh_interval(&self) -> Option<Duration> {
242 let raw = self.pnsreg.as_deref()?;
243 std::str::from_utf8(raw)
244 .ok()?
245 .trim()
246 .parse::<u64>()
247 .ok()
248 .map(Duration::from_secs)
249 }
250
251 /// The PURR the proxy assigned this binding (§8.2's `sip.pnspurr`).
252 #[must_use]
253 pub fn purr(&self) -> Option<&[u8]> {
254 self.pnspurr.as_deref()
255 }
256
257 /// Whether this value said nothing about push at all.
258 #[must_use]
259 pub fn is_empty(&self) -> bool {
260 self.pns.is_none() && self.pnsreg.is_none() && self.pnspurr.is_none()
261 }
262
263 /// Pick the three push indicators out of a parsed indicator list.
264 ///
265 /// Two readings of a valueless indicator, because the three do not mean the same kind of
266 /// thing. `sip.pns` and `sip.pnspurr` **name** something — a push service, a binding — and a
267 /// name with no characters in it names nothing; kept, it would answer
268 /// [`crate::push::Indicators::pns`] with a service that compares equal to the empty string,
269 /// which is a service no client asked for and every client with an empty provider matches.
270 /// `sip.pnsreg` asks for something, and the asking is the whole of it: present without a
271 /// readable interval it still says the registrar wants refreshes, and dropping it would let
272 /// the binding lapse.
273 fn from_params(params: &[HeaderParam]) -> Self {
274 let named = |name: &str| {
275 grammar::param(params, name)
276 .and_then(|found| found.value.clone())
277 .filter(|value| !value.is_empty())
278 };
279 Self {
280 pns: named(SIP_PNS),
281 pnsreg: grammar::param(params, SIP_PNSREG)
282 .map(|found| found.value.clone().unwrap_or_default()),
283 pnspurr: named(SIP_PNSPURR),
284 }
285 }
286}
287
288impl TypedHeader for Indicators {
289 const NAME: HeaderName = HeaderName::FeatureCaps;
290
291 /// Decodes the **first** value in the row; use
292 /// [`crate::message::Headers::typed_all`] when every one is needed.
293 fn decode(value: &[u8]) -> Result<Self, HeaderError> {
294 let parts = grammar::split_list(value, "Feature-Caps")?;
295 decode_one(parts.first().copied().unwrap_or(&[]))
296 }
297
298 fn decode_list(value: &[u8]) -> Result<Vec<Self>, HeaderError> {
299 grammar::split_list(value, "Feature-Caps")?
300 .into_iter()
301 .map(decode_one)
302 .collect()
303 }
304}
305
306/// One `fc-value` (RFC 6809 §4): `"*" *( SEMI feature-cap )`.
307fn decode_one(value: &[u8]) -> Result<Indicators, HeaderError> {
308 let value = trim(value);
309 // The `*` is not decoration: RFC 6809 §4 makes it the whole of the value's non-parameter
310 // part, and a row that opens with anything else is not a `Feature-Caps` value.
311 if value.first() != Some(&b'*') {
312 return Err(HeaderError::Syntax {
313 header: "Feature-Caps",
314 });
315 }
316 let params = grammar::parse_params(trim(value.get(1..).unwrap_or(&[])), "Feature-Caps")?;
317 Ok(Indicators::from_params(¶ms))
318}
319
320/// Check a value against RFC 3261 §25.1's `pvalue`, which is what a URI parameter may hold.
321///
322/// ```abnf
323/// pvalue = 1*paramchar
324/// paramchar = param-unreserved / unreserved / escaped
325/// param-unreserved = "[" / "]" / "/" / ":" / "&" / "+" / "$"
326/// ```
327fn pvalue(value: &str, field: &'static str) -> Result<String, BuildError> {
328 let bytes = value.as_bytes();
329 if bytes.is_empty() {
330 return Err(BuildError::NotAToken { field });
331 }
332 let mut at = 0usize;
333 while let Some(&byte) = bytes.get(at) {
334 if byte == b'%' {
335 // `escaped = "%" HEXDIG HEXDIG`, and a lone `%` is not one.
336 if !bytes.get(at + 1).is_some_and(u8::is_ascii_hexdigit)
337 || !bytes.get(at + 2).is_some_and(u8::is_ascii_hexdigit)
338 {
339 return Err(BuildError::NotAToken { field });
340 }
341 at += 3;
342 continue;
343 }
344 if !is_paramchar(byte) {
345 return Err(BuildError::NotAToken { field });
346 }
347 at += 1;
348 }
349 Ok(value.to_owned())
350}
351
352/// `paramchar` less `escaped`, which [`pvalue`] handles separately.
353#[must_use]
354fn is_paramchar(byte: u8) -> bool {
355 byte.is_ascii_alphanumeric()
356 || matches!(
357 byte,
358 // unreserved: mark
359 b'-' | b'_' | b'.' | b'!' | b'~' | b'*' | b'\'' | b'(' | b')'
360 // param-unreserved
361 | b'[' | b']' | b'/' | b':' | b'&' | b'+' | b'$'
362 )
363}
364
365#[cfg(test)]
366#[allow(
367 clippy::unwrap_used,
368 clippy::expect_used,
369 clippy::panic,
370 clippy::indexing_slicing
371)]
372mod tests {
373 use super::*;
374 use crate::message::Headers;
375 use crate::parser::{Limits, parse_datagram};
376 use crate::{Message, Response};
377
378 fn uri(text: &str) -> Uri {
379 Uri::parse(Bytes::from(text.to_owned())).unwrap_or_else(|e| panic!("{text:?}: {e}"))
380 }
381
382 fn device() -> Device {
383 Device::new("webpush", "c1a5b3e7d9f2")
384 .expect("valid")
385 .with_param("7f3ad0")
386 .expect("valid")
387 }
388
389 /// §4.1.2's three parameters, in a URI, in the order the section lists them.
390 #[test]
391 fn the_push_parameters_go_into_the_uri_grammar() {
392 let mut contact = uri("sip:alice@192.0.2.5:5060");
393 device().set_on(&mut contact);
394 assert_eq!(
395 contact.to_bytes(),
396 Bytes::from_static(
397 b"sip:alice@192.0.2.5:5060;pn-provider=webpush;pn-param=7f3ad0;pn-prid=c1a5b3e7d9f2"
398 )
399 );
400 // And back out again: what a registrar reads is what the UA meant to say.
401 assert_eq!(Device::from_uri(&contact), Some(device()));
402 }
403
404 /// §4.1.2 makes `pn-param` service-specific, so a service that needs nothing beyond the
405 /// identifier sends two parameters, not three.
406 #[test]
407 fn a_service_that_needs_no_pn_param_sends_none() {
408 let mut contact = uri("sip:alice@192.0.2.5:5060");
409 Device::new("webpush", "c1a5b3e7d9f2")
410 .expect("valid")
411 .set_on(&mut contact);
412 assert_eq!(
413 contact.to_bytes(),
414 Bytes::from_static(
415 b"sip:alice@192.0.2.5:5060;pn-provider=webpush;pn-prid=c1a5b3e7d9f2"
416 )
417 );
418 assert!(Device::from_uri(&contact).is_some_and(|d| d.param().is_none()));
419 }
420
421 /// RFC 3261 §19.1.1: "any given parameter-name MUST NOT appear more than once". Appending a
422 /// second `pn-prid` beside a stale one produces a URI the registrar cannot parse at all —
423 /// which takes the registration down rather than merely losing the push parameters.
424 #[test]
425 fn setting_the_parameters_twice_replaces_them_rather_than_repeating_them() {
426 let mut contact = uri("sip:alice@192.0.2.5:5060");
427 device().set_on(&mut contact);
428 Device::new("webpush", "0000deadbeef")
429 .expect("valid")
430 .set_on(&mut contact);
431 assert_eq!(
432 contact.to_bytes(),
433 Bytes::from_static(
434 b"sip:alice@192.0.2.5:5060;pn-provider=webpush;pn-prid=0000deadbeef"
435 )
436 );
437 // The proof that matters: it still parses.
438 assert!(Uri::parse(contact.to_bytes()).is_ok());
439 }
440
441 /// Other URI parameters are none of this mechanism's business and must survive it.
442 #[test]
443 fn the_other_uri_parameters_are_left_alone() {
444 let mut contact = uri("sip:alice@192.0.2.5:5060;transport=tcp;ob");
445 device().set_on(&mut contact);
446 let text = String::from_utf8_lossy(&contact.to_bytes()).into_owned();
447 assert!(
448 text.starts_with("sip:alice@192.0.2.5:5060;transport=tcp;ob;"),
449 "{text}"
450 );
451 assert!(text.contains(";pn-prid=c1a5b3e7d9f2"), "{text}");
452 }
453
454 /// A token carrying an octet `pvalue` does not admit would not be rejected by the registrar —
455 /// it would silently become a *different* URI, its tail read as further parameters. So it is
456 /// refused here, where the caller can still do something about it.
457 #[test]
458 fn a_value_outside_pvalue_is_refused_rather_than_pasted_in() {
459 for bad in [
460 "tok=en", "tok;en", "tok en", "tok@en", "tok?en", "", "tok%zz", "tok%4",
461 ] {
462 assert!(
463 Device::new("webpush", bad).is_err(),
464 "{bad:?} was accepted into a URI parameter"
465 );
466 }
467 // `escaped` is in the grammar, which is how a caller carries the rest.
468 assert!(Device::new("webpush", "tok%3Den").is_ok());
469 // As are the characters `param-unreserved` names, which base64url tokens use.
470 assert!(Device::new("webpush", "a-b_c.d~e+f/g").is_ok());
471 }
472
473 /// A URI with only half the pair names no service that anything could be asked to wake.
474 #[test]
475 fn half_a_binding_is_not_one() {
476 assert!(Device::from_uri(&uri("sip:alice@192.0.2.5;pn-provider=webpush")).is_none());
477 assert!(Device::from_uri(&uri("sip:alice@192.0.2.5;pn-prid=c1a5b3e7d9f2")).is_none());
478 assert!(Device::from_uri(&uri("sip:alice@192.0.2.5")).is_none());
479 }
480
481 /// §8.7 registers `pn-purr` as a URI parameter, and it is read rather than minted.
482 #[test]
483 fn the_purr_is_read_off_a_uri_and_not_interpreted() {
484 assert_eq!(
485 purr(&uri("sip:alice@192.0.2.5;pn-purr=opaque-purr-1")),
486 Some(&b"opaque-purr-1"[..])
487 );
488 assert_eq!(purr(&uri("sip:alice@192.0.2.5")), None);
489 }
490
491 fn response(caps: &str) -> Response {
492 let text = format!(
493 "SIP/2.0 200 OK\r\n\
494 Via: SIP/2.0/UDP 192.0.2.5:5060;branch=z9hG4bKx\r\n\
495 To: <sip:alice@example.com>;tag=r\r\n\
496 From: <sip:alice@example.com>;tag=1\r\n\
497 Call-ID: reg-1@192.0.2.5\r\n\
498 CSeq: 1 REGISTER\r\n\
499 {caps}\
500 Content-Length: 0\r\n\r\n"
501 );
502 match parse_datagram(Bytes::from(text), &Limits::datagram()).expect("parses") {
503 Message::Response(r) => r,
504 Message::Request(_) => panic!("a response"),
505 }
506 }
507
508 fn read(caps: &str) -> Vec<Indicators> {
509 response(caps)
510 .headers
511 .typed_all::<Indicators>()
512 .collect::<Result<Vec<_>, _>>()
513 .expect("parses")
514 }
515
516 /// §8.2's three indicators, as RFC 6809 §4 spells them.
517 #[test]
518 fn the_push_indicators_are_read_out_of_feature_caps() {
519 let read = read(
520 "Feature-Caps: *;+sip.pns=\"webpush\";+sip.pnsreg=\"120\"\
521 ;+sip.pnspurr=\"opaque-purr-1\"\r\n",
522 );
523 let one = read.first().expect("one value");
524 assert_eq!(one.pns(), Some(&b"webpush"[..]));
525 assert!(one.refreshes_required());
526 assert_eq!(one.refresh_interval(), Some(Duration::from_secs(120)));
527 assert_eq!(one.purr(), Some(&b"opaque-purr-1"[..]));
528 }
529
530 /// RFC 6809 §4 makes the header a comma-separated list, so one row of two values and two rows
531 /// of one are the same message (RFC 3261 §7.3).
532 #[test]
533 fn a_comma_joined_row_is_the_same_as_separate_rows() {
534 let joined = read("Feature-Caps: *;+sip.pns=\"webpush\", *;+sip.pnspurr=\"p1\"\r\n");
535 let separate = read(
536 "Feature-Caps: *;+sip.pns=\"webpush\"\r\n\
537 Feature-Caps: *;+sip.pnspurr=\"p1\"\r\n",
538 );
539 assert_eq!(joined.len(), 2, "the comma-joined row was not split");
540 assert_eq!(joined, separate);
541 }
542
543 /// An indicator belonging to some other mechanism is not an error. A parser that rejected one
544 /// would break against every proxy that ever grows a feature.
545 #[test]
546 fn indicators_this_side_does_not_know_are_ignored() {
547 let read = read("Feature-Caps: *;+sip.something.else=\"x\";+sip.pns=\"webpush\"\r\n");
548 assert_eq!(
549 read.first().and_then(Indicators::pns),
550 Some(&b"webpush"[..])
551 );
552 }
553
554 /// A registrar that says nothing about push has said nothing about push — not "no".
555 #[test]
556 fn a_value_with_no_push_indicators_is_empty_rather_than_negative() {
557 assert!(read("Feature-Caps: *;+sip.other=\"x\"\r\n")[0].is_empty());
558 assert!(
559 response("")
560 .headers
561 .typed_all::<Indicators>()
562 .next()
563 .is_none()
564 );
565 }
566
567 /// A valueless `sip.pns` names no service, and a name of no characters must not become one:
568 /// kept, it would answer [`Indicators::pns`] with a service equal to the empty string, which
569 /// no client asked for and any client with an empty provider would match. Same for the PURR,
570 /// which names a binding. `sip.pnsreg` is the exception because its meaning is the asking.
571 #[test]
572 fn a_valueless_indicator_names_nothing_rather_than_naming_the_empty_string() {
573 let values = read("Feature-Caps: *;+sip.pns;+sip.pnspurr;+sip.pnsreg\r\n");
574 let one = values.first().expect("one value");
575 assert_eq!(one.pns(), None, "an empty service name became a service");
576 assert_eq!(one.purr(), None, "an empty PURR named a binding");
577 assert!(
578 one.refreshes_required(),
579 "sip.pnsreg asks for refreshes by being there at all"
580 );
581 assert_eq!(one.refresh_interval(), None);
582 // And an explicitly empty value is the same claim written differently.
583 assert_eq!(
584 read("Feature-Caps: *;+sip.pns=\"\"\r\n")
585 .first()
586 .and_then(Indicators::pns),
587 None
588 );
589 }
590
591 /// `sip.pnsreg` present with a value this side cannot read still says the registrar wants
592 /// refreshes. Reading it as absent would let the binding lapse.
593 #[test]
594 fn an_unreadable_refresh_interval_is_still_a_demand_for_refreshes() {
595 let read = read("Feature-Caps: *;+sip.pnsreg=\"soon\"\r\n");
596 let one = read.first().expect("one value");
597 assert!(one.refreshes_required());
598 assert_eq!(one.refresh_interval(), None);
599 }
600
601 /// RFC 6809 §4's `fc-value` opens with `*`. A row that does not is not one of these.
602 #[test]
603 fn a_value_that_is_not_a_feature_caps_value_is_a_parse_error() {
604 assert!(Indicators::decode(b"+sip.pns=\"webpush\"").is_err());
605 assert!(Indicators::decode(b"").is_err());
606 // A bare `*` is legal and says nothing.
607 assert!(Indicators::decode(b"*").expect("parses").is_empty());
608 }
609
610 /// §8.1's code, and the reason it is worth telling apart from every other refusal.
611 #[test]
612 fn the_push_notification_status_code_is_555() {
613 assert_eq!(NOT_SUPPORTED, 555);
614 assert!(is_not_supported(StatusCode::new(555).expect("valid")));
615 assert!(!is_not_supported(StatusCode::new(500).expect("valid")));
616 assert!(!is_not_supported(StatusCode::new(403).expect("valid")));
617 }
618
619 /// The name table has to know the header, or `typed_all` looks for a variant nothing carries.
620 #[test]
621 fn feature_caps_resolves_to_the_header_this_reads() {
622 let headers = response("Feature-Caps: *;+sip.pns=\"webpush\"\r\n").headers;
623 assert!(matches!(
624 Headers::typed::<Indicators>(&headers),
625 Some(Ok(_))
626 ));
627 }
628}