use cookie::time::Duration;
use cookie::{Cookie, CookieBuilder, SameSite};
use std::borrow::Cow;
pub const DEFAULT_COOKIE_NAME: &str = "webgates";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CookieTemplate {
name: Cow<'static, str>,
value: Cow<'static, str>,
path: Cow<'static, str>,
domain: Option<Cow<'static, str>>,
secure: bool,
http_only: bool,
same_site: SameSite,
max_age: Option<Duration>,
}
impl Default for CookieTemplate {
fn default() -> Self {
Self {
name: Cow::Borrowed(DEFAULT_COOKIE_NAME),
value: Cow::Borrowed(""),
path: Cow::Borrowed("/"),
domain: None,
secure: true,
http_only: true,
same_site: SameSite::Strict,
max_age: None, }
}
}
impl CookieTemplate {
#[must_use]
pub fn recommended() -> Self {
Self::default()
}
#[must_use]
pub fn name(mut self, name: impl Into<Cow<'static, str>>) -> Self {
self.name = name.into();
self
}
#[must_use]
pub fn value(mut self, value: impl Into<Cow<'static, str>>) -> Self {
self.value = value.into();
self
}
#[must_use]
pub fn path(mut self, path: impl Into<Cow<'static, str>>) -> Self {
self.path = path.into();
self
}
#[must_use]
pub fn domain(mut self, domain: impl Into<Cow<'static, str>>) -> Self {
self.domain = Some(domain.into());
self
}
#[must_use]
pub fn clear_domain(mut self) -> Self {
self.domain = None;
self
}
#[must_use]
pub fn secure(mut self, flag: bool) -> Self {
self.secure = flag;
self
}
#[must_use]
pub fn insecure_dev_only(mut self) -> Self {
self.secure = false;
self.same_site = SameSite::Lax;
self
}
#[must_use]
pub fn http_only(mut self, flag: bool) -> Self {
self.http_only = flag;
self
}
#[must_use]
pub fn same_site(mut self, same_site: SameSite) -> Self {
self.same_site = same_site;
self
}
#[must_use]
pub fn max_age(mut self, max_age: Duration) -> Self {
self.max_age = Some(max_age);
self
}
#[must_use]
pub fn clear_max_age(mut self) -> Self {
self.max_age = None;
self
}
#[must_use]
pub fn persistent(self, duration: Duration) -> Self {
self.max_age(duration)
}
#[must_use]
pub fn short_lived(self) -> Self {
self.max_age(Duration::minutes(15))
}
pub fn assert_production_secure(&self) {
assert!(
self.secure,
"CookieTemplate '{}' has Secure=false. \
This indicates an explicitly insecure cookie configuration. \
Do NOT use insecure cookies in production or staging environments. \
Remove `.insecure_dev_only()` or call `.secure(true)` after confirming \
the deployment context.",
self.name
);
}
pub fn validate(&self) -> Result<(), CookieTemplateBuilderError> {
if self.same_site == SameSite::None && !self.secure {
return Err(CookieTemplateBuilderError::InsecureNoneSameSite);
}
Ok(())
}
#[must_use]
#[inline]
pub fn builder(&self) -> CookieBuilder<'static> {
let mut builder = CookieBuilder::new(self.name.clone(), self.value.clone())
.secure(self.secure)
.http_only(self.http_only)
.same_site(self.same_site)
.path(self.path.clone());
if let Some(ref domain) = self.domain {
builder = builder.domain(domain.clone());
}
if let Some(max_age) = self.max_age {
builder = builder.max_age(max_age);
}
builder
}
pub fn validate_and_build(&self) -> Result<Cookie<'static>, CookieTemplateBuilderError> {
self.validate()?;
Ok(self.builder().build())
}
#[must_use]
#[inline]
pub fn build_with_name_value(&self, name: &str, value: &str) -> Cookie<'static> {
let mut builder = CookieBuilder::new(name.to_owned(), value.to_owned())
.secure(self.secure)
.http_only(self.http_only)
.same_site(self.same_site)
.path(self.path.clone());
if let Some(ref domain) = self.domain {
builder = builder.domain(domain.clone());
}
if let Some(max_age) = self.max_age {
builder = builder.max_age(max_age);
}
builder.build()
}
#[must_use]
#[inline]
pub fn build_with_value(&self, value: &str) -> Cookie<'static> {
self.build_with_name_value(self.name.as_ref(), value)
}
#[must_use]
#[inline]
pub fn build_with_name(&self, name: &str) -> Cookie<'static> {
self.build_with_name_value(name, self.value.as_ref())
}
#[must_use]
pub fn build_removal(&self) -> Cookie<'static> {
let mut cookie = self.builder().build();
cookie.make_removal();
cookie
}
#[must_use]
#[inline]
pub fn cookie_name_ref(&self) -> &str {
self.name.as_ref()
}
}
#[derive(Debug, thiserror::Error)]
pub enum CookieTemplateBuilderError {
#[error("SameSite=None requires Secure=true (browser enforcement & CSRF protection)")]
InsecureNoneSameSite,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recommended_defaults_are_secure_and_strict() {
let template = CookieTemplate::recommended();
let cookie = template.build_with_value("token");
assert_eq!(cookie.secure(), Some(true));
assert_eq!(cookie.http_only(), Some(true));
assert_eq!(cookie.same_site(), Some(SameSite::Strict));
assert_eq!(cookie.max_age(), None);
}
#[test]
fn insecure_dev_only_is_explicit_and_relaxes_transport_settings() {
let template = CookieTemplate::recommended().insecure_dev_only();
let cookie = template.build_with_value("token");
assert_eq!(cookie.secure(), Some(false));
assert_eq!(cookie.http_only(), Some(true));
assert_eq!(cookie.same_site(), Some(SameSite::Lax));
}
#[test]
fn same_site_none_still_requires_secure_even_with_dev_override() {
let Err(error) = CookieTemplate::recommended()
.insecure_dev_only()
.same_site(SameSite::None)
.validate()
else {
return;
};
assert!(matches!(
error,
CookieTemplateBuilderError::InsecureNoneSameSite
));
}
#[test]
fn production_assertion_rejects_explicit_insecure_configuration() {
let template = CookieTemplate::recommended().insecure_dev_only();
let panic = std::panic::catch_unwind(|| template.assert_production_secure());
assert!(panic.is_err());
}
}