use std::sync::Arc;
use std::time::Duration;
pub use tower_cookies::cookie::SameSite;
const DEFAULT_COOKIE_NAME: &str = "axess.sid";
const DEFAULT_TTL: Duration = Duration::from_secs(24 * 60 * 60); const DEFAULT_MAX_CUSTOM_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone)]
pub struct SessionConfig {
pub ttl: Duration,
pub cookie_name: Arc<str>,
pub secure: bool,
pub same_site: SameSite,
pub http_only: bool,
pub path: Arc<str>,
pub max_custom_bytes: usize,
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
ttl: DEFAULT_TTL,
cookie_name: DEFAULT_COOKIE_NAME.into(),
secure: true,
same_site: SameSite::Lax,
http_only: true,
path: "/".into(),
max_custom_bytes: DEFAULT_MAX_CUSTOM_BYTES,
}
}
}
impl SessionConfig {
pub fn builder() -> SessionConfigBuilder {
SessionConfigBuilder(SessionConfig::default())
}
}
fn should_warn_insecure_cookie(secure: bool) -> bool {
!secure
}
pub struct SessionConfigBuilder(SessionConfig);
impl SessionConfigBuilder {
pub fn ttl(mut self, ttl: Duration) -> Self {
self.0.ttl = ttl;
self
}
pub fn cookie_name(mut self, name: impl Into<Arc<str>>) -> Self {
self.0.cookie_name = name.into();
self
}
pub fn secure(mut self, secure: bool) -> Self {
if should_warn_insecure_cookie(secure) {
tracing::warn!(
"SessionConfig: Secure cookie flag disabled; session cookies \
will be sent over plain HTTP. Do not use in production."
);
}
self.0.secure = secure;
self
}
pub fn same_site(mut self, same_site: SameSite) -> Self {
self.0.same_site = same_site;
self
}
pub fn http_only(mut self, http_only: bool) -> Self {
self.0.http_only = http_only;
self
}
pub fn path(mut self, path: impl Into<Arc<str>>) -> Self {
self.0.path = path.into();
self
}
pub fn max_custom_bytes(mut self, max: usize) -> Self {
self.0.max_custom_bytes = max;
self
}
pub fn build(self) -> SessionConfig {
assert!(
!self.0.ttl.is_zero(),
"SessionConfig: ttl must be > 0 (sessions would expire immediately)"
);
assert!(
!self.0.cookie_name.is_empty(),
"SessionConfig: cookie_name must not be empty"
);
let name: &str = &self.0.cookie_name;
if name.starts_with("__Host-") {
assert!(
self.0.secure,
"SessionConfig: cookie name `{name}` requires `secure = true`"
);
assert!(
self.0.path.as_ref() == "/",
"SessionConfig: cookie name `{name}` requires `path = \"/\"`"
);
} else if name.starts_with("__Secure-") {
assert!(
self.0.secure,
"SessionConfig: cookie name `{name}` requires `secure = true`"
);
}
self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_has_production_safe_values() {
let config = SessionConfig::default();
assert_eq!(config.ttl, Duration::from_secs(86400));
assert_eq!(config.cookie_name.as_ref(), "axess.sid");
assert!(config.secure);
assert_eq!(config.same_site, SameSite::Lax);
assert!(config.http_only);
assert_eq!(config.path.as_ref(), "/");
assert_eq!(
config.max_custom_bytes, 65536,
"DEFAULT_MAX_CUSTOM_BYTES must be 64 * 1024 = 65536"
);
}
#[test]
fn builder_overrides_defaults() {
let config = SessionConfig::builder()
.ttl(Duration::from_secs(7200))
.cookie_name("custom.sid")
.secure(false)
.same_site(SameSite::Strict)
.http_only(false)
.path("/app")
.build();
assert_eq!(config.ttl, Duration::from_secs(7200));
assert_eq!(config.cookie_name.as_ref(), "custom.sid");
assert!(!config.secure);
assert_eq!(config.same_site, SameSite::Strict);
assert!(!config.http_only);
assert_eq!(config.path.as_ref(), "/app");
}
#[test]
fn should_warn_insecure_cookie_only_for_disabled_flag() {
assert!(should_warn_insecure_cookie(false));
assert!(!should_warn_insecure_cookie(true));
}
#[test]
fn builder_partial_override_keeps_defaults() {
let config = SessionConfig::builder()
.ttl(Duration::from_secs(3600))
.build();
assert_eq!(config.ttl, Duration::from_secs(3600));
assert!(config.secure);
assert_eq!(config.same_site, SameSite::Lax);
assert!(config.http_only);
}
}