use crate::config::SessionTimerMode;
use anyhow::{Result, anyhow};
use std::str::FromStr;
use std::time::Duration;
use std::time::Instant;
pub const HEADER_SESSION_EXPIRES: &str = "Session-Expires";
pub const HEADER_MIN_SE: &str = "Min-SE";
pub const HEADER_SUPPORTED: &str = "Supported";
#[cfg(test)]
pub const HEADER_REQUIRE: &str = "Require";
pub const TIMER_TAG: &str = "timer";
pub const DEFAULT_SESSION_EXPIRES: u64 = 1800;
pub const MIN_MIN_SE: u64 = 90;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionRefresher {
Uac,
Uas,
}
impl std::fmt::Display for SessionRefresher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SessionRefresher::Uac => write!(f, "uac"),
SessionRefresher::Uas => write!(f, "uas"),
}
}
}
impl FromStr for SessionRefresher {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"uac" => Ok(SessionRefresher::Uac),
"uas" => Ok(SessionRefresher::Uas),
_ => Err(()),
}
}
}
impl SessionRefresher {
pub fn for_local_role(we_are_uac: bool) -> Self {
if we_are_uac {
SessionRefresher::Uac
} else {
SessionRefresher::Uas
}
}
pub fn is_our_role(&self, we_are_uac: bool) -> bool {
matches!(
(self, we_are_uac),
(SessionRefresher::Uac, true) | (SessionRefresher::Uas, false)
)
}
}
#[derive(Debug, Clone)]
pub struct SessionTimerState {
pub mode: SessionTimerMode,
pub enabled: bool,
pub session_interval: Duration,
pub min_se: Duration,
pub refresher: SessionRefresher,
pub active: bool,
pub refreshing: bool,
pub last_refresh: Instant,
#[cfg(test)]
pub session_start: Instant,
pub refresh_count: u32,
pub failed_refreshes: u32,
}
impl Default for SessionTimerState {
fn default() -> Self {
Self {
mode: SessionTimerMode::Off,
enabled: false,
session_interval: Duration::from_secs(DEFAULT_SESSION_EXPIRES),
min_se: Duration::from_secs(MIN_MIN_SE),
refresher: SessionRefresher::Uac,
active: false,
refreshing: false,
last_refresh: Instant::now(),
#[cfg(test)]
session_start: Instant::now(),
refresh_count: 0,
failed_refreshes: 0,
}
}
}
impl SessionTimerState {
#[cfg(test)]
pub fn new(session_interval: Duration, min_se: Duration, refresher: SessionRefresher) -> Self {
Self {
mode: SessionTimerMode::Supported,
enabled: true,
session_interval,
min_se,
refresher,
active: true,
refreshing: false,
last_refresh: Instant::now(),
session_start: Instant::now(),
refresh_count: 0,
failed_refreshes: 0,
}
}
pub fn should_refresh(&self) -> bool {
if !self.active || !self.enabled || self.refreshing {
return false;
}
self.last_refresh.elapsed() >= self.session_interval / 2
}
pub fn is_expired(&self) -> bool {
if !self.active || !self.enabled {
return false;
}
self.last_refresh.elapsed() >= self.session_interval
}
pub fn next_refresh_time(&self) -> Option<Instant> {
if !self.active || !self.enabled {
return None;
}
Some(self.last_refresh + self.session_interval / 2)
}
pub fn expiration_time(&self) -> Option<Instant> {
if !self.active || !self.enabled {
return None;
}
Some(self.last_refresh + self.session_interval)
}
pub fn time_until_expiration(&self) -> Option<Duration> {
self.expiration_time().map(|exp| {
let now = Instant::now();
if exp > now { exp - now } else { Duration::ZERO }
})
}
pub fn time_until_refresh(&self) -> Option<Duration> {
self.next_refresh_time().map(|next| {
let now = Instant::now();
if next > now {
next - now
} else {
Duration::ZERO
}
})
}
pub fn should_we_refresh(&self, we_are_uac: bool) -> bool {
self.refresher.is_our_role(we_are_uac)
}
pub fn next_timeout_for_role(&self, we_are_uac: bool) -> Option<Duration> {
if !self.active || !self.enabled {
return None;
}
if !self.refreshing && self.should_we_refresh(we_are_uac) {
self.time_until_refresh()
} else {
self.time_until_expiration()
}
}
pub fn start_refresh(&mut self) -> bool {
if self.refreshing {
return false;
}
self.refreshing = true;
true
}
pub fn complete_refresh(&mut self) {
self.last_refresh = Instant::now();
self.refreshing = false;
self.refresh_count += 1;
}
pub fn fail_refresh(&mut self) {
self.refreshing = false;
self.failed_refreshes += 1;
}
pub fn update_refresh(&mut self) {
self.last_refresh = Instant::now();
self.refresh_count += 1;
}
pub fn get_session_expires_value(&self) -> String {
format!(
"{};refresher={}",
self.session_interval.as_secs(),
self.refresher
)
}
pub fn get_min_se_value(&self) -> String {
self.min_se.as_secs().to_string()
}
#[cfg(test)]
pub fn deactivate(&mut self) {
self.active = false;
}
#[cfg(test)]
pub fn reset(&mut self, interval: Duration, refresher: SessionRefresher) {
self.session_interval = interval;
self.refresher = refresher;
self.last_refresh = Instant::now();
self.refreshing = false;
}
#[cfg(test)]
pub fn require_timer(&self) -> bool {
self.enabled && self.active
}
#[cfg(test)]
pub fn session_duration(&self) -> Duration {
self.session_start.elapsed()
}
#[cfg(test)]
pub fn stats(&self) -> TimerStats {
TimerStats {
enabled: self.enabled,
active: self.active,
refreshing: self.refreshing,
session_interval_secs: self.session_interval.as_secs(),
min_se_secs: self.min_se.as_secs(),
refresher: format!("{}", self.refresher),
refresh_count: self.refresh_count,
failed_refreshes: self.failed_refreshes,
session_duration_secs: self.session_duration().as_secs(),
time_until_refresh_secs: self.time_until_refresh().map(|d| d.as_secs()),
time_until_expiration_secs: self.time_until_expiration().map(|d| d.as_secs()),
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
#[cfg(test)]
pub struct TimerStats {
pub enabled: bool,
pub active: bool,
pub refreshing: bool,
pub session_interval_secs: u64,
pub min_se_secs: u64,
pub refresher: String,
pub refresh_count: u32,
pub failed_refreshes: u32,
pub session_duration_secs: u64,
pub time_until_refresh_secs: Option<u64>,
pub time_until_expiration_secs: Option<u64>,
}
pub fn get_header_value(headers: &rsipstack::sip::Headers, name: &str) -> Option<String> {
headers
.iter()
.find(|header| header.name().eq_ignore_ascii_case(name))
.map(|header| header.value().to_string())
}
pub fn parse_session_expires(value: &str) -> Option<(Duration, Option<SessionRefresher>)> {
let parts: Vec<&str> = value.split(';').collect();
if parts.is_empty() {
return None;
}
let seconds = parts[0].trim().parse::<u64>().ok()?;
let mut refresher = None;
for part in parts.iter().skip(1) {
let part = part.trim();
if part.starts_with("refresher=") {
let val = part.trim_start_matches("refresher=");
refresher = SessionRefresher::from_str(val).ok();
}
}
Some((Duration::from_secs(seconds), refresher))
}
pub fn has_timer_support(headers: &rsipstack::sip::Headers) -> bool {
headers.iter().any(|h| match h {
rsipstack::sip::Header::Supported(s) => s.value().split(',').any(|v| v.trim() == TIMER_TAG),
rsipstack::sip::Header::Other(n, v) if n.eq_ignore_ascii_case(HEADER_SUPPORTED) => {
v.split(',').any(|v| v.trim() == TIMER_TAG)
}
_ => false,
})
}
pub fn parse_min_se(value: &str) -> Option<Duration> {
let seconds = value.trim().parse::<u64>().ok()?;
Some(Duration::from_secs(seconds))
}
pub fn select_server_timer_refresher(
peer_supports_timer: bool,
session_expires_present: bool,
requested_refresher: Option<SessionRefresher>,
) -> SessionRefresher {
if let Some(refresher) = requested_refresher {
refresher
} else if peer_supports_timer && session_expires_present {
SessionRefresher::Uac
} else {
SessionRefresher::Uas
}
}
pub fn select_client_timer_refresher(
requested_refresher: Option<SessionRefresher>,
) -> SessionRefresher {
requested_refresher.unwrap_or(SessionRefresher::Uac)
}
pub fn apply_session_timer_headers(
timer: &mut SessionTimerState,
headers: &rsipstack::sip::Headers,
) -> Result<()> {
if let Some(se_value) = get_header_value(headers, HEADER_SESSION_EXPIRES)
&& let Some((interval, refresher)) = parse_session_expires(&se_value)
{
if interval < timer.min_se {
return Err(anyhow!(
"Session-Expires too small: {} < {}",
interval.as_secs(),
timer.min_se.as_secs()
));
}
timer.session_interval = interval;
if let Some(new_refresher) = refresher {
timer.refresher = new_refresher;
}
}
Ok(())
}
pub fn apply_refresh_response(
timer: &mut SessionTimerState,
headers: &rsipstack::sip::Headers,
we_are_uac: bool,
) -> Result<()> {
if get_header_value(headers, HEADER_SESSION_EXPIRES).is_none() {
timer.complete_refresh();
if timer.mode.is_always() {
timer.refresher = SessionRefresher::for_local_role(we_are_uac);
} else {
timer.enabled = false;
timer.active = false;
}
return Ok(());
}
if let Err(e) = apply_session_timer_headers(timer, headers) {
timer.fail_refresh();
return Err(e);
}
timer.complete_refresh();
Ok(())
}
fn build_timer_headers(
session_expires: String,
min_se: String,
include_content_type: bool,
) -> Vec<rsipstack::sip::Header> {
let mut headers = Vec::new();
if include_content_type {
headers.push(rsipstack::sip::Header::ContentType(
"application/sdp".into(),
));
}
headers.push(rsipstack::sip::Header::Other(
HEADER_SESSION_EXPIRES.to_string(),
session_expires,
));
headers.push(rsipstack::sip::Header::Other(
HEADER_MIN_SE.to_string(),
min_se,
));
headers.push(rsipstack::sip::Header::Supported(
rsipstack::sip::headers::Supported::from(TIMER_TAG),
));
headers
}
pub fn build_default_session_timer_headers(
session_expires: u64,
min_se: u64,
) -> Vec<rsipstack::sip::Header> {
build_timer_headers(session_expires.to_string(), min_se.to_string(), false)
}
pub fn build_session_timer_headers(
timer: &SessionTimerState,
include_content_type: bool,
) -> Vec<rsipstack::sip::Header> {
build_timer_headers(
timer.get_session_expires_value(),
timer.get_min_se_value(),
include_content_type,
)
}
pub fn build_session_timer_response_headers(
timer: &SessionTimerState,
) -> Vec<rsipstack::sip::Header> {
vec![rsipstack::sip::Header::Other(
HEADER_SESSION_EXPIRES.to_string(),
timer.get_session_expires_value(),
)]
}
#[cfg(test)]
pub fn is_timer_required(headers: &rsipstack::sip::Headers) -> bool {
headers.iter().any(|h| match h {
rsipstack::sip::Header::Require(s) => s.value().split(',').any(|v| v.trim() == TIMER_TAG),
rsipstack::sip::Header::Other(n, v) if n.eq_ignore_ascii_case(HEADER_REQUIRE) => {
v.split(',').any(|v| v.trim() == TIMER_TAG)
}
_ => false,
})
}
#[cfg(test)]
pub fn build_session_expires_header(
interval: Duration,
refresher: SessionRefresher,
) -> rsipstack::sip::Header {
let value = format!("{};refresher={}", interval.as_secs(), refresher);
rsipstack::sip::Header::Other(HEADER_SESSION_EXPIRES.to_string(), value)
}
#[cfg(test)]
pub fn build_min_se_header(min_se: Duration) -> rsipstack::sip::Header {
rsipstack::sip::Header::Other(HEADER_MIN_SE.to_string(), min_se.as_secs().to_string())
}
#[cfg(test)]
pub fn negotiate_session_interval(
requested: Duration,
local_min_se: Duration,
) -> Result<Duration, Duration> {
if requested < local_min_se {
Err(local_min_se)
} else {
Ok(requested)
}
}
#[cfg(test)]
#[path = "session_timer_tests.rs"]
mod session_timer_tests;