use std::time::Duration;
use crate::error::HeaderError;
use crate::headers::grammar::{find_param_start, parse_params, parse_u64, trim};
use crate::message::TypedHeader;
use crate::name::HeaderName;
pub const OPTION_TAG: &str = "timer";
pub const ABSOLUTE_MIN_INTERVAL: Duration = Duration::from_secs(90);
pub const DEFAULT_INTERVAL: Duration = Duration::from_secs(1800);
const EARLY_BYE_CAP: Duration = Duration::from_secs(32);
fn split_at_params(value: &[u8]) -> (&[u8], &[u8]) {
let at = find_param_start(value).unwrap_or(value.len());
(
value.get(..at).unwrap_or(value),
value.get(at..).unwrap_or_default(),
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Refresher {
Uac,
Uas,
}
impl Refresher {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Uac => "uac",
Self::Uas => "uas",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SessionExpires {
pub interval: Duration,
pub refresher: Option<Refresher>,
}
impl TypedHeader for SessionExpires {
const NAME: HeaderName = HeaderName::SessionExpires;
fn decode(value: &[u8]) -> Result<Self, HeaderError> {
let (delta, tail) = split_at_params(value);
let seconds = parse_u64(trim(delta), "Session-Expires")?;
let params = parse_params(tail, "Session-Expires")?;
let refresher = crate::headers::grammar::param(¶ms, "refresher")
.and_then(|p| p.value.as_deref())
.map(|v| match v {
v if v.eq_ignore_ascii_case(b"uac") => Ok(Refresher::Uac),
v if v.eq_ignore_ascii_case(b"uas") => Ok(Refresher::Uas),
_ => Err(HeaderError::Syntax {
header: "Session-Expires",
}),
})
.transpose()?;
Ok(Self {
interval: Duration::from_secs(seconds),
refresher,
})
}
}
impl std::fmt::Display for SessionExpires {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.interval.as_secs())?;
if let Some(refresher) = self.refresher {
write!(f, ";refresher={}", refresher.as_str())?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MinSe(pub Duration);
impl TypedHeader for MinSe {
const NAME: HeaderName = HeaderName::MinSe;
fn decode(value: &[u8]) -> Result<Self, HeaderError> {
let (delta, _) = split_at_params(value);
parse_u64(trim(delta), "Min-SE").map(|s| Self(Duration::from_secs(s)))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Answer {
None,
Accept(Accepted),
TooBrief(Duration),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Accepted {
pub interval: Duration,
pub refresher: Refresher,
pub require: bool,
}
#[must_use]
pub fn answer(
peer_supports: bool,
requested: Option<SessionExpires>,
peer_min_se: Option<Duration>,
floor: Duration,
) -> Answer {
let floor = floor.max(ABSOLUTE_MIN_INTERVAL);
let Some(requested) = requested else {
if !peer_supports {
return Answer::None;
}
return Answer::Accept(Accepted {
interval: DEFAULT_INTERVAL.max(peer_min_se.unwrap_or(Duration::ZERO)),
refresher: Refresher::Uas,
require: true,
});
};
if requested.interval < floor {
return Answer::TooBrief(floor);
}
let refresher = match requested.refresher {
Some(chosen) => chosen,
None => Refresher::Uas,
};
Answer::Accept(Accepted {
interval: requested.interval,
require: refresher == Refresher::Uac || peer_supports,
refresher,
})
}
#[must_use]
pub fn adopt(response: Option<SessionExpires>, asked_for: Option<Duration>) -> Option<Session> {
match (response, asked_for) {
(Some(agreed), _) => Some(Session {
interval: agreed.interval.max(ABSOLUTE_MIN_INTERVAL),
we_refresh: agreed.refresher != Some(Refresher::Uas),
}),
(None, Some(interval)) => Some(Session {
interval: interval.max(ABSOLUTE_MIN_INTERVAL),
we_refresh: true,
}),
(None, None) => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Session {
pub interval: Duration,
pub we_refresh: bool,
}
impl Session {
#[must_use]
pub fn act_after(self) -> Duration {
if self.we_refresh {
self.interval / 2
} else {
self.interval
.saturating_sub(EARLY_BYE_CAP.min(self.interval / 3))
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn parse(value: &str) -> SessionExpires {
SessionExpires::decode(value.as_bytes()).expect("parses")
}
#[test]
fn a_session_expires_carries_its_interval_and_refresher() {
assert_eq!(
parse("1800;refresher=uas"),
SessionExpires {
interval: Duration::from_secs(1800),
refresher: Some(Refresher::Uas),
}
);
assert_eq!(parse("90").refresher, None);
assert_eq!(parse("1800;REFRESHER=UAC").refresher, Some(Refresher::Uac));
}
#[test]
fn a_refresher_that_is_neither_side_is_rejected() {
assert!(SessionExpires::decode(b"1800;refresher=proxy").is_err());
}
#[test]
fn a_session_expires_round_trips() {
for value in ["1800;refresher=uac", "90;refresher=uas", "600"] {
assert_eq!(parse(value).to_string(), value);
}
}
#[test]
fn a_min_se_survives_parameters_it_does_not_define() {
assert_eq!(
MinSe::decode(b"90").expect("parses").0,
Duration::from_secs(90)
);
assert_eq!(
MinSe::decode(b"120;ext=1").expect("parses").0,
Duration::from_secs(120)
);
}
#[test]
fn an_interval_under_the_floor_is_refused_with_the_floor() {
let asked = SessionExpires {
interval: Duration::from_secs(60),
refresher: None,
};
assert_eq!(
answer(true, Some(asked), None, Duration::from_secs(120)),
Answer::TooBrief(Duration::from_secs(120))
);
}
#[test]
fn a_floor_below_the_rfc_minimum_is_raised_to_it() {
let asked = SessionExpires {
interval: Duration::from_secs(30),
refresher: None,
};
assert_eq!(
answer(true, Some(asked), None, Duration::from_secs(10)),
Answer::TooBrief(ABSOLUTE_MIN_INTERVAL)
);
}
#[test]
fn table_2_governs_who_refreshes() {
let with = |refresher| {
let asked = SessionExpires {
interval: Duration::from_secs(600),
refresher,
};
match answer(true, Some(asked), None, ABSOLUTE_MIN_INTERVAL) {
Answer::Accept(accepted) => accepted,
other => panic!("expected acceptance, got {other:?}"),
}
};
assert_eq!(with(Some(Refresher::Uac)).refresher, Refresher::Uac);
assert_eq!(with(Some(Refresher::Uas)).refresher, Refresher::Uas);
assert_eq!(with(None).refresher, Refresher::Uas);
assert!(with(Some(Refresher::Uac)).require);
}
#[test]
fn a_peer_that_never_mentioned_timers_gets_none() {
assert_eq!(
answer(false, None, None, ABSOLUTE_MIN_INTERVAL),
Answer::None
);
}
#[test]
fn support_without_a_request_lets_the_uas_ask() {
let Answer::Accept(accepted) = answer(true, None, None, ABSOLUTE_MIN_INTERVAL) else {
panic!("expected the uas to be able to ask for a timer");
};
assert_eq!(accepted.interval, DEFAULT_INTERVAL);
assert_eq!(accepted.refresher, Refresher::Uas);
}
#[test]
fn a_2xx_without_a_session_expires_leaves_the_asker_refreshing() {
let session = adopt(None, Some(Duration::from_secs(600))).expect("a timer");
assert!(session.we_refresh);
assert_eq!(session.interval, Duration::from_secs(600));
assert_eq!(adopt(None, None), None);
}
#[test]
fn a_2xx_cannot_drive_us_faster_than_the_floor() {
let agreed = SessionExpires {
interval: Duration::from_secs(5),
refresher: Some(Refresher::Uac),
};
let session = adopt(Some(agreed), Some(DEFAULT_INTERVAL)).expect("a timer");
assert_eq!(session.interval, ABSOLUTE_MIN_INTERVAL);
assert_eq!(session.act_after(), Duration::from_secs(45));
}
#[test]
fn the_two_roles_act_at_different_times() {
let refreshing = Session {
interval: Duration::from_secs(1800),
we_refresh: true,
};
assert_eq!(refreshing.act_after(), Duration::from_secs(900));
let waiting = Session {
we_refresh: false,
..refreshing
};
assert_eq!(waiting.act_after(), Duration::from_secs(1768));
let short = Session {
interval: Duration::from_secs(90),
we_refresh: false,
};
assert_eq!(short.act_after(), Duration::from_secs(60));
}
}