use std::time::Duration;
use bytes::Bytes;
use crate::error::{BuildError, HeaderError};
use crate::headers::grammar::{self, HeaderParam, trim};
use crate::message::{StatusCode, TypedHeader};
use crate::name::HeaderName;
use crate::params::Param;
use crate::uri::Uri;
pub const PN_PROVIDER: &str = "pn-provider";
pub const PN_PARAM: &str = "pn-param";
pub const PN_PRID: &str = "pn-prid";
pub const PN_PURR: &str = "pn-purr";
pub const SIP_PNS: &str = "+sip.pns";
pub const SIP_PNSREG: &str = "+sip.pnsreg";
pub const SIP_PNSPURR: &str = "+sip.pnspurr";
pub const NOT_SUPPORTED: u16 = 555;
pub const NOT_SUPPORTED_REASON: &str = "Push Notification Service Not Supported";
#[must_use]
pub fn is_not_supported(status: StatusCode) -> bool {
status.code() == NOT_SUPPORTED
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Device {
provider: String,
param: Option<String>,
prid: String,
}
impl Device {
pub fn new(provider: &str, prid: &str) -> Result<Self, BuildError> {
Ok(Self {
provider: pvalue(provider, PN_PROVIDER)?,
param: None,
prid: pvalue(prid, PN_PRID)?,
})
}
pub fn with_param(mut self, param: &str) -> Result<Self, BuildError> {
self.param = Some(pvalue(param, PN_PARAM)?);
Ok(self)
}
#[must_use]
pub fn provider(&self) -> &str {
&self.provider
}
#[must_use]
pub fn param(&self) -> Option<&str> {
self.param.as_deref()
}
#[must_use]
pub fn prid(&self) -> &str {
&self.prid
}
pub fn set_on(&self, uri: &mut Uri) {
for name in [PN_PROVIDER, PN_PARAM, PN_PRID] {
uri.remove_param(name);
}
uri.push_param(Param::new(
Bytes::from_static(PN_PROVIDER.as_bytes()),
Bytes::from(self.provider.clone()),
));
if let Some(param) = &self.param {
uri.push_param(Param::new(
Bytes::from_static(PN_PARAM.as_bytes()),
Bytes::from(param.clone()),
));
}
uri.push_param(Param::new(
Bytes::from_static(PN_PRID.as_bytes()),
Bytes::from(self.prid.clone()),
));
}
#[must_use]
pub fn from_uri(uri: &Uri) -> Option<Self> {
let params = uri.params()?;
let text = |name: &str| {
params
.value(name)
.and_then(|raw| std::str::from_utf8(raw).ok())
};
Some(Self {
provider: pvalue(text(PN_PROVIDER)?, PN_PROVIDER).ok()?,
param: text(PN_PARAM).and_then(|value| pvalue(value, PN_PARAM).ok()),
prid: pvalue(text(PN_PRID)?, PN_PRID).ok()?,
})
}
}
#[must_use]
pub fn purr(uri: &Uri) -> Option<&[u8]> {
uri.params().and_then(|params| params.value(PN_PURR))
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Indicators {
pns: Option<Vec<u8>>,
pnsreg: Option<Vec<u8>>,
pnspurr: Option<Vec<u8>>,
}
impl Indicators {
#[must_use]
pub fn pns(&self) -> Option<&[u8]> {
self.pns.as_deref()
}
#[must_use]
pub fn refreshes_required(&self) -> bool {
self.pnsreg.is_some()
}
#[must_use]
pub fn refresh_interval(&self) -> Option<Duration> {
let raw = self.pnsreg.as_deref()?;
std::str::from_utf8(raw)
.ok()?
.trim()
.parse::<u64>()
.ok()
.map(Duration::from_secs)
}
#[must_use]
pub fn purr(&self) -> Option<&[u8]> {
self.pnspurr.as_deref()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.pns.is_none() && self.pnsreg.is_none() && self.pnspurr.is_none()
}
fn from_params(params: &[HeaderParam]) -> Self {
let named = |name: &str| {
grammar::param(params, name)
.and_then(|found| found.value.clone())
.filter(|value| !value.is_empty())
};
Self {
pns: named(SIP_PNS),
pnsreg: grammar::param(params, SIP_PNSREG)
.map(|found| found.value.clone().unwrap_or_default()),
pnspurr: named(SIP_PNSPURR),
}
}
}
impl TypedHeader for Indicators {
const NAME: HeaderName = HeaderName::FeatureCaps;
fn decode(value: &[u8]) -> Result<Self, HeaderError> {
let parts = grammar::split_list(value, "Feature-Caps")?;
decode_one(parts.first().copied().unwrap_or(&[]))
}
fn decode_list(value: &[u8]) -> Result<Vec<Self>, HeaderError> {
grammar::split_list(value, "Feature-Caps")?
.into_iter()
.map(decode_one)
.collect()
}
}
fn decode_one(value: &[u8]) -> Result<Indicators, HeaderError> {
let value = trim(value);
if value.first() != Some(&b'*') {
return Err(HeaderError::Syntax {
header: "Feature-Caps",
});
}
let params = grammar::parse_params(trim(value.get(1..).unwrap_or(&[])), "Feature-Caps")?;
Ok(Indicators::from_params(¶ms))
}
fn pvalue(value: &str, field: &'static str) -> Result<String, BuildError> {
let bytes = value.as_bytes();
if bytes.is_empty() {
return Err(BuildError::NotAToken { field });
}
let mut at = 0usize;
while let Some(&byte) = bytes.get(at) {
if byte == b'%' {
if !bytes.get(at + 1).is_some_and(u8::is_ascii_hexdigit)
|| !bytes.get(at + 2).is_some_and(u8::is_ascii_hexdigit)
{
return Err(BuildError::NotAToken { field });
}
at += 3;
continue;
}
if !is_paramchar(byte) {
return Err(BuildError::NotAToken { field });
}
at += 1;
}
Ok(value.to_owned())
}
#[must_use]
fn is_paramchar(byte: u8) -> bool {
byte.is_ascii_alphanumeric()
|| matches!(
byte,
b'-' | b'_' | b'.' | b'!' | b'~' | b'*' | b'\'' | b'(' | b')'
| b'[' | b']' | b'/' | b':' | b'&' | b'+' | b'$'
)
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use super::*;
use crate::message::Headers;
use crate::parser::{Limits, parse_datagram};
use crate::{Message, Response};
fn uri(text: &str) -> Uri {
Uri::parse(Bytes::from(text.to_owned())).unwrap_or_else(|e| panic!("{text:?}: {e}"))
}
fn device() -> Device {
Device::new("webpush", "c1a5b3e7d9f2")
.expect("valid")
.with_param("7f3ad0")
.expect("valid")
}
#[test]
fn the_push_parameters_go_into_the_uri_grammar() {
let mut contact = uri("sip:alice@192.0.2.5:5060");
device().set_on(&mut contact);
assert_eq!(
contact.to_bytes(),
Bytes::from_static(
b"sip:alice@192.0.2.5:5060;pn-provider=webpush;pn-param=7f3ad0;pn-prid=c1a5b3e7d9f2"
)
);
assert_eq!(Device::from_uri(&contact), Some(device()));
}
#[test]
fn a_service_that_needs_no_pn_param_sends_none() {
let mut contact = uri("sip:alice@192.0.2.5:5060");
Device::new("webpush", "c1a5b3e7d9f2")
.expect("valid")
.set_on(&mut contact);
assert_eq!(
contact.to_bytes(),
Bytes::from_static(
b"sip:alice@192.0.2.5:5060;pn-provider=webpush;pn-prid=c1a5b3e7d9f2"
)
);
assert!(Device::from_uri(&contact).is_some_and(|d| d.param().is_none()));
}
#[test]
fn setting_the_parameters_twice_replaces_them_rather_than_repeating_them() {
let mut contact = uri("sip:alice@192.0.2.5:5060");
device().set_on(&mut contact);
Device::new("webpush", "0000deadbeef")
.expect("valid")
.set_on(&mut contact);
assert_eq!(
contact.to_bytes(),
Bytes::from_static(
b"sip:alice@192.0.2.5:5060;pn-provider=webpush;pn-prid=0000deadbeef"
)
);
assert!(Uri::parse(contact.to_bytes()).is_ok());
}
#[test]
fn the_other_uri_parameters_are_left_alone() {
let mut contact = uri("sip:alice@192.0.2.5:5060;transport=tcp;ob");
device().set_on(&mut contact);
let text = String::from_utf8_lossy(&contact.to_bytes()).into_owned();
assert!(
text.starts_with("sip:alice@192.0.2.5:5060;transport=tcp;ob;"),
"{text}"
);
assert!(text.contains(";pn-prid=c1a5b3e7d9f2"), "{text}");
}
#[test]
fn a_value_outside_pvalue_is_refused_rather_than_pasted_in() {
for bad in [
"tok=en", "tok;en", "tok en", "tok@en", "tok?en", "", "tok%zz", "tok%4",
] {
assert!(
Device::new("webpush", bad).is_err(),
"{bad:?} was accepted into a URI parameter"
);
}
assert!(Device::new("webpush", "tok%3Den").is_ok());
assert!(Device::new("webpush", "a-b_c.d~e+f/g").is_ok());
}
#[test]
fn half_a_binding_is_not_one() {
assert!(Device::from_uri(&uri("sip:alice@192.0.2.5;pn-provider=webpush")).is_none());
assert!(Device::from_uri(&uri("sip:alice@192.0.2.5;pn-prid=c1a5b3e7d9f2")).is_none());
assert!(Device::from_uri(&uri("sip:alice@192.0.2.5")).is_none());
}
#[test]
fn the_purr_is_read_off_a_uri_and_not_interpreted() {
assert_eq!(
purr(&uri("sip:alice@192.0.2.5;pn-purr=opaque-purr-1")),
Some(&b"opaque-purr-1"[..])
);
assert_eq!(purr(&uri("sip:alice@192.0.2.5")), None);
}
fn response(caps: &str) -> Response {
let text = format!(
"SIP/2.0 200 OK\r\n\
Via: SIP/2.0/UDP 192.0.2.5:5060;branch=z9hG4bKx\r\n\
To: <sip:alice@example.com>;tag=r\r\n\
From: <sip:alice@example.com>;tag=1\r\n\
Call-ID: reg-1@192.0.2.5\r\n\
CSeq: 1 REGISTER\r\n\
{caps}\
Content-Length: 0\r\n\r\n"
);
match parse_datagram(Bytes::from(text), &Limits::datagram()).expect("parses") {
Message::Response(r) => r,
Message::Request(_) => panic!("a response"),
}
}
fn read(caps: &str) -> Vec<Indicators> {
response(caps)
.headers
.typed_all::<Indicators>()
.collect::<Result<Vec<_>, _>>()
.expect("parses")
}
#[test]
fn the_push_indicators_are_read_out_of_feature_caps() {
let read = read(
"Feature-Caps: *;+sip.pns=\"webpush\";+sip.pnsreg=\"120\"\
;+sip.pnspurr=\"opaque-purr-1\"\r\n",
);
let one = read.first().expect("one value");
assert_eq!(one.pns(), Some(&b"webpush"[..]));
assert!(one.refreshes_required());
assert_eq!(one.refresh_interval(), Some(Duration::from_secs(120)));
assert_eq!(one.purr(), Some(&b"opaque-purr-1"[..]));
}
#[test]
fn a_comma_joined_row_is_the_same_as_separate_rows() {
let joined = read("Feature-Caps: *;+sip.pns=\"webpush\", *;+sip.pnspurr=\"p1\"\r\n");
let separate = read(
"Feature-Caps: *;+sip.pns=\"webpush\"\r\n\
Feature-Caps: *;+sip.pnspurr=\"p1\"\r\n",
);
assert_eq!(joined.len(), 2, "the comma-joined row was not split");
assert_eq!(joined, separate);
}
#[test]
fn indicators_this_side_does_not_know_are_ignored() {
let read = read("Feature-Caps: *;+sip.something.else=\"x\";+sip.pns=\"webpush\"\r\n");
assert_eq!(
read.first().and_then(Indicators::pns),
Some(&b"webpush"[..])
);
}
#[test]
fn a_value_with_no_push_indicators_is_empty_rather_than_negative() {
assert!(read("Feature-Caps: *;+sip.other=\"x\"\r\n")[0].is_empty());
assert!(
response("")
.headers
.typed_all::<Indicators>()
.next()
.is_none()
);
}
#[test]
fn a_valueless_indicator_names_nothing_rather_than_naming_the_empty_string() {
let values = read("Feature-Caps: *;+sip.pns;+sip.pnspurr;+sip.pnsreg\r\n");
let one = values.first().expect("one value");
assert_eq!(one.pns(), None, "an empty service name became a service");
assert_eq!(one.purr(), None, "an empty PURR named a binding");
assert!(
one.refreshes_required(),
"sip.pnsreg asks for refreshes by being there at all"
);
assert_eq!(one.refresh_interval(), None);
assert_eq!(
read("Feature-Caps: *;+sip.pns=\"\"\r\n")
.first()
.and_then(Indicators::pns),
None
);
}
#[test]
fn an_unreadable_refresh_interval_is_still_a_demand_for_refreshes() {
let read = read("Feature-Caps: *;+sip.pnsreg=\"soon\"\r\n");
let one = read.first().expect("one value");
assert!(one.refreshes_required());
assert_eq!(one.refresh_interval(), None);
}
#[test]
fn a_value_that_is_not_a_feature_caps_value_is_a_parse_error() {
assert!(Indicators::decode(b"+sip.pns=\"webpush\"").is_err());
assert!(Indicators::decode(b"").is_err());
assert!(Indicators::decode(b"*").expect("parses").is_empty());
}
#[test]
fn the_push_notification_status_code_is_555() {
assert_eq!(NOT_SUPPORTED, 555);
assert!(is_not_supported(StatusCode::new(555).expect("valid")));
assert!(!is_not_supported(StatusCode::new(500).expect("valid")));
assert!(!is_not_supported(StatusCode::new(403).expect("valid")));
}
#[test]
fn feature_caps_resolves_to_the_header_this_reads() {
let headers = response("Feature-Caps: *;+sip.pns=\"webpush\"\r\n").headers;
assert!(matches!(
Headers::typed::<Indicators>(&headers),
Some(Ok(_))
));
}
}