use std::time::Duration;
pub use cookie::SameSite;
fn is_safe_cookie_attribute_byte(s: &str) -> bool {
s.bytes().all(|b| b == b'\t' || (0x20..=0x7E).contains(&b))
}
#[derive(Debug, Clone)]
pub struct SessionConfig {
pub(crate) cookie_name: String,
pub(crate) path: String,
pub(crate) domain: Option<String>,
pub(crate) max_age: Duration,
pub(crate) secure: bool,
pub(crate) http_only: bool,
pub(crate) same_site: SameSite,
pub(crate) refresh_after: Option<Duration>,
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
cookie_name: "session".to_string(),
path: "/".to_string(),
domain: None,
max_age: Duration::from_secs(24 * 60 * 60),
secure: true,
http_only: true,
same_site: SameSite::Lax,
refresh_after: None,
}
}
}
impl SessionConfig {
#[must_use]
pub fn cookie_name(mut self, name: impl Into<String>) -> Self {
let name = name.into();
assert!(
is_safe_cookie_attribute_byte(&name),
"seshcookie: cookie_name must contain only printable ASCII or tab; got {name:?}"
);
self.cookie_name = name;
self
}
#[must_use]
pub fn path(mut self, path: impl Into<String>) -> Self {
let path = path.into();
assert!(
is_safe_cookie_attribute_byte(&path),
"seshcookie: path must contain only printable ASCII or tab; got {path:?}"
);
self.path = path;
self
}
#[must_use]
pub fn domain(mut self, domain: impl Into<String>) -> Self {
let domain = domain.into();
assert!(
is_safe_cookie_attribute_byte(&domain),
"seshcookie: domain must contain only printable ASCII or tab; got {domain:?}"
);
self.domain = Some(domain);
self
}
#[must_use]
pub fn no_domain(mut self) -> Self {
self.domain = None;
self
}
#[must_use]
pub fn max_age(mut self, max_age: Duration) -> Self {
self.max_age = max_age;
self
}
#[must_use]
pub fn secure(mut self, secure: bool) -> Self {
self.secure = secure;
self
}
#[must_use]
pub fn http_only(mut self, http_only: bool) -> Self {
self.http_only = http_only;
self
}
#[must_use]
pub fn same_site(mut self, same_site: SameSite) -> Self {
self.same_site = same_site;
self
}
#[must_use]
pub fn refresh_after(mut self, refresh_after: Option<Duration>) -> Self {
self.refresh_after = refresh_after;
self
}
pub fn cookie_name_ref(&self) -> &str {
&self.cookie_name
}
pub fn path_ref(&self) -> &str {
&self.path
}
pub fn domain_ref(&self) -> Option<&str> {
self.domain.as_deref()
}
pub fn max_age_ref(&self) -> Duration {
self.max_age
}
pub fn is_secure(&self) -> bool {
self.secure
}
pub fn is_http_only(&self) -> bool {
self.http_only
}
pub fn same_site_ref(&self) -> SameSite {
self.same_site
}
pub fn refresh_after_ref(&self) -> Option<Duration> {
self.refresh_after
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_cookie_name_is_session_ac7_1() {
let c = SessionConfig::default();
assert_eq!(c.cookie_name_ref(), "session");
}
#[test]
fn default_path_is_root_ac7_1() {
let c = SessionConfig::default();
assert_eq!(c.path_ref(), "/");
}
#[test]
fn default_domain_is_none_ac7_1() {
let c = SessionConfig::default();
assert_eq!(c.domain_ref(), None);
}
#[test]
fn default_max_age_is_24h_ac7_1() {
let c = SessionConfig::default();
assert_eq!(c.max_age_ref(), Duration::from_secs(24 * 60 * 60));
}
#[test]
fn default_secure_is_true_ac7_1() {
let c = SessionConfig::default();
assert!(c.is_secure());
}
#[test]
fn default_http_only_is_true_ac7_1() {
let c = SessionConfig::default();
assert!(c.is_http_only());
}
#[test]
fn default_same_site_is_lax_ac7_1() {
let c = SessionConfig::default();
assert_eq!(c.same_site_ref(), SameSite::Lax);
}
#[test]
fn default_refresh_after_is_none_ac7_1() {
let c = SessionConfig::default();
assert_eq!(c.refresh_after_ref(), None);
}
#[test]
fn builder_chain_sets_every_field_and_returns_self_ac7_2() {
let c = SessionConfig::default()
.cookie_name("s")
.path("/api")
.domain("x.test")
.max_age(Duration::from_secs(60))
.secure(false)
.http_only(false)
.same_site(SameSite::Strict)
.refresh_after(Some(Duration::from_secs(30)));
assert_eq!(c.cookie_name_ref(), "s");
assert_eq!(c.path_ref(), "/api");
assert_eq!(c.domain_ref(), Some("x.test"));
assert_eq!(c.max_age_ref(), Duration::from_secs(60));
assert!(!c.is_secure());
assert!(!c.is_http_only());
assert_eq!(c.same_site_ref(), SameSite::Strict);
assert_eq!(c.refresh_after_ref(), Some(Duration::from_secs(30)));
}
#[test]
fn cookie_name_accepts_str_and_string_ac7_2() {
let c1 = SessionConfig::default().cookie_name("from-str");
assert_eq!(c1.cookie_name_ref(), "from-str");
let c2 = SessionConfig::default().cookie_name(String::from("from-string"));
assert_eq!(c2.cookie_name_ref(), "from-string");
}
#[test]
fn path_accepts_str_and_string_ac7_2() {
let c1 = SessionConfig::default().path("/from-str");
assert_eq!(c1.path_ref(), "/from-str");
let c2 = SessionConfig::default().path(String::from("/from-string"));
assert_eq!(c2.path_ref(), "/from-string");
}
#[test]
fn domain_accepts_str_and_string_ac7_2() {
let c1 = SessionConfig::default().domain("a.test");
assert_eq!(c1.domain_ref(), Some("a.test"));
let c2 = SessionConfig::default().domain(String::from("b.test"));
assert_eq!(c2.domain_ref(), Some("b.test"));
}
#[test]
fn refresh_after_can_be_disabled_after_being_enabled_ac7_2() {
let c = SessionConfig::default()
.refresh_after(Some(Duration::from_secs(60)))
.refresh_after(None);
assert_eq!(c.refresh_after_ref(), None);
}
#[test]
fn no_domain_clears_previously_set_domain() {
let c = SessionConfig::default().domain("a.test").no_domain();
assert_eq!(c.domain_ref(), None);
}
#[test]
fn no_domain_is_noop_when_already_none() {
let c = SessionConfig::default().no_domain();
assert_eq!(c.domain_ref(), None);
}
#[test]
#[should_panic(expected = "seshcookie: cookie_name must contain only printable ASCII or tab")]
fn cookie_name_with_newline_panics() {
let _ = SessionConfig::default().cookie_name("bad\nname");
}
#[test]
#[should_panic(expected = "seshcookie: cookie_name must contain only printable ASCII or tab")]
fn cookie_name_with_nul_panics() {
let _ = SessionConfig::default().cookie_name("bad\x00name");
}
#[test]
#[should_panic(expected = "seshcookie: path must contain only printable ASCII or tab")]
fn path_with_crlf_panics() {
let _ = SessionConfig::default().path("/api\r\nattacker=evil");
}
#[test]
#[should_panic(expected = "seshcookie: path must contain only printable ASCII or tab")]
fn path_with_newline_panics() {
let _ = SessionConfig::default().path("/api\nattacker=evil");
}
#[test]
#[should_panic(expected = "seshcookie: domain must contain only printable ASCII or tab")]
fn domain_with_newline_panics() {
let _ = SessionConfig::default().domain("evil\ndomain");
}
#[test]
fn config_is_clone_and_debug() {
let original = SessionConfig::default()
.cookie_name("orig")
.domain("a.test");
let cloned = original.clone();
assert_eq!(cloned.cookie_name_ref(), "orig");
assert_eq!(cloned.domain_ref(), Some("a.test"));
let mutated = cloned.cookie_name("mutated");
assert_eq!(mutated.cookie_name_ref(), "mutated");
assert_eq!(original.cookie_name_ref(), "orig");
let dbg = format!("{original:?}");
assert!(dbg.contains("cookie_name"));
}
}