Current section
Files
Jump to
Current section
Files
src/gleam/http/cookie.gleam
import gleam/result
import gleam/int
import gleam/list
import gleam/regex
import gleam/string
import gleam/option.{Option, Some}
import gleam/http.{Scheme}
/// Policy options for the SameSite cookie attribute
///
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
pub type SameSitePolicy {
Lax
Strict
None
}
fn same_site_to_string(policy) {
case policy {
Lax -> "Lax"
Strict -> "Strict"
None -> "None"
}
}
/// Attributes of a cookie when sent to a client in the `set-cookie` header.
pub type Attributes {
Attributes(
max_age: Option(Int),
domain: Option(String),
path: Option(String),
secure: Bool,
http_only: Bool,
same_site: Option(SameSitePolicy),
)
}
/// Helper to create sensible default attributes for a set cookie.
///
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Attributes
pub fn defaults(scheme: Scheme) {
Attributes(
max_age: option.None,
domain: option.None,
path: option.Some("/"),
secure: scheme == http.Https,
http_only: True,
same_site: Some(Lax),
)
}
const epoch = "Expires=Thu, 01 Jan 1970 00:00:00 GMT"
fn cookie_attributes_to_list(attributes) {
let Attributes(
max_age: max_age,
domain: domain,
path: path,
secure: secure,
http_only: http_only,
same_site: same_site,
) = attributes
[
// Expires is a deprecated attribute for cookies, it has been replaced with MaxAge
// MaxAge is widely supported and so Expires values are not set.
// Only when deleting cookies is the exception made to use the old format,
// to ensure complete clearup of cookies if required by an application.
case max_age {
option.Some(0) -> option.Some([epoch])
_ -> option.None
},
option.map(max_age, fn(max_age) { ["Max-Age=", int.to_string(max_age)] }),
option.map(domain, fn(domain) { ["Domain=", domain] }),
option.map(path, fn(path) { ["Path=", path] }),
case secure {
True -> option.Some(["Secure"])
False -> option.None
},
case http_only {
True -> option.Some(["HttpOnly"])
False -> option.None
},
option.map(
same_site,
fn(same_site) { ["SameSite=", same_site_to_string(same_site)] },
),
]
|> list.filter_map(option.to_result(_, Nil))
}
pub fn set_header(name: String, value: String, attributes: Attributes) -> String {
[[name, "=", value], ..cookie_attributes_to_list(attributes)]
|> list.map(string.join(_, ""))
|> string.join("; ")
}
/// Parse a list of cookies from a header string. Any malformed cookies will be
/// discarded.
///
pub fn parse(cookie_string: String) -> List(#(String, String)) {
let assert Ok(re) = regex.from_string("[,;]")
regex.split(re, cookie_string)
|> list.filter_map(fn(pair) {
case string.split_once(string.trim(pair), "=") {
Ok(#("", _)) -> Error(Nil)
Ok(#(key, value)) -> {
let key = string.trim(key)
let value = string.trim(value)
use _ <- result.then(check_token(key))
use _ <- result.then(check_token(value))
Ok(#(key, value))
}
Error(Nil) -> Error(Nil)
}
})
}
fn check_token(token: String) -> Result(Nil, Nil) {
case string.pop_grapheme(token) {
Error(Nil) -> Ok(Nil)
Ok(#(" ", _)) -> Error(Nil)
Ok(#("\t", _)) -> Error(Nil)
Ok(#("\r", _)) -> Error(Nil)
Ok(#("\n", _)) -> Error(Nil)
Ok(#("\f", _)) -> Error(Nil)
Ok(#(_, rest)) -> check_token(rest)
}
}