///|
/// The SameSite attribute for cookies, controlling cross-site request behavior.
pub(all) enum SameSiteOption {
Lax
Strict
SameSiteNone
} derive(Debug, Eq)
///|
pub impl Show for SameSiteOption with fn output(self, logger) {
match self {
Lax => logger.write_string("Lax")
Strict => logger.write_string("Strict")
SameSiteNone => logger.write_string("SameSiteNone")
}
}
///|
pub impl ToJson for SameSiteOption with fn to_json(self : SameSiteOption) -> Json {
match self {
Lax => "lax"
Strict => "strict"
SameSiteNone => "none"
}
}
///|
/// Represents an HTTP cookie with its name, value, and optional attributes.
pub(all) struct CookieItem {
/// Cookie name (the key in `name=value`).
name : String
/// Cookie value.
value : String
/// Expiry in seconds from now; `0` deletes the cookie.
max_age : Int?
/// URL path scope for which the browser sends this cookie.
path : String?
/// Domain scope for which the browser sends this cookie.
domain : String?
/// If `true`, the cookie is only sent over HTTPS.
secure : Bool?
/// If `true`, JavaScript cannot access the cookie (`document.cookie`).
http_only : Bool?
/// Cross-site request policy (`Lax`, `Strict`, or `SameSiteNone`).
same_site : SameSiteOption?
} derive(Eq, Debug)
///|
/// Creates a new `CookieItem` with the given name, value, and optional attributes.
pub fn CookieItem::CookieItem(
name~ : String,
value~ : String,
max_age? : Int,
path? : String,
domain? : String,
secure? : Bool,
http_only? : Bool,
same_site? : SameSiteOption,
) -> CookieItem {
{ name, value, max_age, path, domain, secure, http_only, same_site }
}
///|
fn sanitize_cookie_value(s : String) -> String {
if s.contains("\r") || s.contains("\n") || s.contains(";") {
let buf = StringBuilder::new()
for c in s {
if c != '\r' && c != '\n' && c != ';' {
buf.write_char(c)
}
}
buf.to_string()
} else {
s
}
}
///|
pub impl Show for CookieItem with fn output(self, logger) -> Unit {
logger.write_string(sanitize_cookie_value(self.name))
logger.write_char('=')
logger.write_string(sanitize_cookie_value(self.value))
if self.max_age is Some(max_age) {
logger.write_string("; Max-Age=")
logger.write_string(max_age.to_string())
}
if self.path is Some(path) {
logger.write_string("; Path=")
logger.write_string(path)
}
if self.domain is Some(domain) {
logger.write_string("; Domain=")
logger.write_string(domain)
}
if self.secure == Some(true) {
logger.write_string("; Secure")
}
if self.http_only == Some(true) {
logger.write_string("; HttpOnly")
}
if self.same_site is Some(same_site) {
logger.write_string("; SameSite=")
logger.write_string(same_site.to_string())
}
}
///|
pub impl Show for CookieItem with fn to_string(self : CookieItem) -> String {
let buf = StringBuilder()
self.output(buf)
buf.to_string()
}
///|
/// Serializes an array of cookie items into a semicolon-separated string.
pub fn cookie_to_string(cookie : Array[CookieItem]) -> String {
cookie.map(x => x.to_string()).join(";")
}
///|
/// Parses a raw cookie header string into a map of cookie names to `CookieItem` values.
pub fn parse_cookie(cookie : StringView) -> Map[String, CookieItem] {
fn dequote(value : StringView) -> StringView {
if value is ['"', .. rest, '"'] {
rest
} else {
value
}
}
let res = Map([])
let mut last_cookie_item : (String, CookieItem)? = None
fn on_key_value(key : StringView, value : StringView) -> Unit {
let key = key.to_owned()
let value = dequote(value).to_owned()
if last_cookie_item is Some((name, item)) {
let new_item = match key.to_lower() {
"path" => { ..item, path: Some(value) }
"domain" => { ..item, domain: Some(value) }
"max-age" =>
{
..item,
max_age: try @string.parse_int(value) catch {
_ => None
} noraise {
x => Some(x)
},
}
"secure" => { ..item, secure: Some(true) }
"httponly" => { ..item, http_only: Some(true) }
"samesite" => {
let same_site = match value.to_lower() {
"lax" => Some(Lax)
"strict" => Some(Strict)
"none" => Some(SameSiteNone)
_ => None
}
{ ..item, same_site, }
}
_ => item
}
if !physical_equal(new_item, item) {
res.set(name, new_item)
last_cookie_item = Some((name, new_item))
return
}
}
let item = CookieItem(name=key, value~)
res.set(key, item)
last_cookie_item = Some((key, item))
}
for frag in cookie.split(";") {
let frag_trimmed = frag.trim()
if frag_trimmed.length() == 0 {
continue
}
let (key, value) = match frag_trimmed.find("=") {
Some(idx) => (frag_trimmed[0:idx].trim(), frag_trimmed[idx + 1:].trim())
None => (frag_trimmed, "")
}
on_key_value(key, value)
}
res
}