use std::time::Duration;
use crate::error::{Error, Result};
use crate::rfc2326_lex as lex;
pub const DEFAULT_SESSION_TIMEOUT: Duration = Duration::from_secs(60);
const PARAM_TIMEOUT: &str = "timeout";
const DELTA_SECONDS_DIGITS: usize = 19;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SessionHeader {
pub id: String,
pub timeout: Option<Duration>,
pub extensions: Vec<(String, Option<String>)>,
}
impl SessionHeader {
pub fn new(id: impl Into<String>) -> Self {
SessionHeader {
id: id.into(),
timeout: None,
extensions: Vec::new(),
}
}
pub fn parse(value: &str) -> Result<Self> {
Self::parse_with_warnings(value).map(|(h, _)| h)
}
pub fn parse_with_warnings(value: &str) -> Result<(Self, Vec<String>)> {
let (id, segs) = lex::split_session_value(value);
if id.is_empty() {
return Err(Error::SessionParse("empty session id".into()));
}
if lex::has_ctl(id) {
return Err(Error::SessionParse(format!(
"control character in session id {id:?}"
)));
}
let mut out = SessionHeader::new(id);
let mut warnings = Vec::new();
for seg in segs {
if lex::trim_lws(seg).is_empty() {
continue;
}
let (name, raw) = lex::split_param(seg).map_err(|e| Error::SessionParse(e.0))?;
let value = match raw {
Some(v) => Some(lex::unquote(v).map_err(|e| Error::SessionParse(e.0))?.0),
None => None,
};
if name.eq_ignore_ascii_case(PARAM_TIMEOUT) {
let parsed = value
.as_deref()
.and_then(|v| lex::digits(v, DELTA_SECONDS_DIGITS, PARAM_TIMEOUT).ok());
match parsed {
Some(n) if n > 0 => out.timeout = Some(Duration::from_secs(n.into())),
_ => {
warnings.push(format!(
"malformed or zero timeout {value:?}; using the 60 s default"
));
out.timeout = Some(DEFAULT_SESSION_TIMEOUT);
}
}
} else {
out.extensions.push((name.to_string(), value));
}
}
Ok((out, warnings))
}
pub fn to_header_value(&self) -> Result<String> {
if self.id.is_empty() || lex::has_ctl(&self.id) || lex::trim_lws(&self.id) != self.id {
return Err(Error::HeaderSerialize(format!(
"invalid session id {:?}",
self.id
)));
}
let mut s = self.id.clone();
if let Some(t) = self.timeout {
s.push(lex::PARAM_SEP);
s.push_str(&format!("{PARAM_TIMEOUT}{}{}", lex::VALUE_SEP, t.as_secs()));
}
for (name, value) in &self.extensions {
if !lex::is_token(name) {
return Err(Error::HeaderSerialize(format!(
"extension name {name:?} is not a token"
)));
}
s.push(lex::PARAM_SEP);
s.push_str(name);
if let Some(v) = value {
s.push(lex::VALUE_SEP);
s.push_str(&lex::emit_value(v).map_err(|e| Error::HeaderSerialize(e.0))?);
}
}
let first = lex::split_outside_quotes(&s, lex::PARAM_SEP)[0];
if lex::trim_lws(first) != self.id {
return Err(Error::HeaderSerialize(format!(
"session id {:?} cannot be emitted unambiguously",
self.id
)));
}
Ok(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn p(v: &str) -> SessionHeader {
SessionHeader::parse(v).unwrap()
}
#[test]
fn the_interop_table() {
for v in [
"abc;timeout=30",
"abc; timeout=30",
"abc ; timeout=30",
"abc;TIMEOUT=30",
"abc;timeout = 30",
] {
let h = p(v);
assert_eq!(
(h.id.as_str(), h.timeout),
("abc", Some(Duration::from_secs(30))),
"{v}"
);
}
let (h, w) = SessionHeader::parse_with_warnings("abc;timeout=0").unwrap();
assert_eq!(
h.timeout,
Some(DEFAULT_SESSION_TIMEOUT),
"timeout=0 is invalid"
);
assert_eq!(w.len(), 1);
let (h, w) = SessionHeader::parse_with_warnings("abc;timeout=x").unwrap();
assert_eq!(h.timeout, Some(DEFAULT_SESSION_TIMEOUT));
assert_eq!(w.len(), 1);
let h = p("abc;foo=bar");
assert_eq!(h.extensions, vec![("foo".into(), Some("bar".into()))]);
assert!(matches!(
SessionHeader::parse(""),
Err(Error::SessionParse(_))
));
assert!(SessionHeader::parse(" ;timeout=5").is_err());
assert_eq!(p("12345678").timeout, None);
}
#[test]
fn an_unbalanced_quote_falls_back_to_the_first_semicolon() {
let (h, w) = SessionHeader::parse_with_warnings("ab\"c;timeout=30").unwrap();
assert_eq!(h.id, "ab\"c");
assert_eq!(h.timeout, Some(Duration::from_secs(30)));
assert!(w.is_empty(), "{w:?}");
let (h, w) = SessionHeader::parse_with_warnings("\"a;b\";timeout=30").unwrap();
assert_eq!(h.id, "\"a;b\"");
assert_eq!(h.timeout, Some(Duration::from_secs(30)));
assert!(w.is_empty(), "{w:?}");
assert_eq!(p("ab\"c").id, "ab\"c");
}
#[test]
fn canonical_output_round_trips_and_is_idempotent() {
for v in [
"abc",
"abc ; Timeout = 30 ; x=\"a b\"",
"abc;timeout=x;flag",
] {
let a = p(v);
let c = a.to_header_value().unwrap();
let b = p(&c);
assert_eq!(a.id, b.id);
assert_eq!(b.to_header_value().unwrap(), c, "idempotent {v}");
assert_eq!(p(&c), b);
}
assert_eq!(
p("abc ; Timeout = 30").to_header_value().unwrap(),
"abc;timeout=30"
);
}
#[test]
fn the_serializer_never_emits_control_characters() {
let mut h = SessionHeader::new("abc");
h.id = "ab\r\nSet-Cookie: x".into();
assert!(matches!(
h.to_header_value(),
Err(Error::HeaderSerialize(_))
));
let mut h = SessionHeader::new("abc");
h.extensions.push(("x\r\ny".into(), None));
assert!(matches!(
h.to_header_value(),
Err(Error::HeaderSerialize(_))
));
let mut h = SessionHeader::new("abc");
h.extensions.push(("x".into(), Some("a\r\nb".into())));
assert!(matches!(
h.to_header_value(),
Err(Error::HeaderSerialize(_))
));
let mut h = SessionHeader::new("abc");
h.id = String::new();
assert!(matches!(
h.to_header_value(),
Err(Error::HeaderSerialize(_))
));
let mut h = SessionHeader::new("ab\"c");
h.timeout = Some(Duration::from_secs(5));
assert!(matches!(
h.to_header_value(),
Err(Error::HeaderSerialize(_))
));
}
#[test]
fn a_received_lone_quote_id_parses_but_is_not_emittable() {
let (h, _) = SessionHeader::parse_with_warnings("\";;!").unwrap();
assert_eq!(h.id, "\"");
assert!(matches!(
h.to_header_value(),
Err(crate::Error::HeaderSerialize(_))
));
}
}