-
-
Notifications
You must be signed in to change notification settings - Fork 21
/
cookie.gleam
128 lines (119 loc) · 3.41 KB
/
cookie.gleam
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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)
}
}