Source file vif_cookie.ml
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
let src = Logs.Src.create "vif.cookie"
module Log = (val Logs.src_log src : Logs.LOG)
let is_cookie key = String.lowercase_ascii key = "cookie"
type cookie = {
key: string
; value: string
; attributes: (string * string option) list
}
let cookie str =
match String.split_on_char '=' str with
| [ k; v ] -> Some (String.trim k, String.trim v)
| _ -> None
let attribute str =
match String.split_on_char '=' str with
| [ a ] -> Some (String.trim a, None)
| [ k; v ] -> Some (String.trim k, Some (String.trim v))
| _ -> None
let cookie_of_string str =
let ( let* ) = Option.bind in
match String.split_on_char ';' str with
| [ c ] ->
let* key, value = cookie c in
Some { key; value; attributes= [] }
| c :: vs ->
let* key, value = cookie c in
let attributes = List.map attribute vs in
let attributes = List.filter_map Fun.id attributes in
Some { key; value; attributes }
| _ -> None
let all_cookies hdrs =
let cookies = List.filter (fun (k, _) -> is_cookie k) hdrs in
let cookies = List.map snd cookies in
let cookies = List.map cookie_of_string cookies in
List.filter_map Fun.id cookies
let without_prefix (is_host, is_secure) cookie =
match (is_host, is_secure) with
| true, true | false, false -> cookie
| true, false ->
let key = cookie.key in
let key = String.sub key 7 (String.length key - 7) in
{ key; value= cookie.value; attributes= cookie.attributes }
| false, true ->
let key = cookie.key in
let key = String.sub key 9 (String.length key - 9) in
{ key; value= cookie.value; attributes= cookie.attributes }
let filter_secure req0 cookies =
let is_secure =
Option.is_some (Vif_request0.tls req0) || Vif_request0.on_localhost req0
in
let fn ({ key; _ } as cookie) =
let is_prefix_host = String.starts_with ~prefix:"__Host-" key in
let is_prefix_secure = String.starts_with ~prefix:"__Secure-" key in
let prefix = (is_prefix_host, is_prefix_secure) in
let result =
match (is_secure, is_prefix_host, is_prefix_secure) with
| true, true, false -> Some cookie
| true, false, true -> Some cookie
| false, false, false -> Some cookie
| _ -> None
in
Option.map (without_prefix prefix) result
in
List.filter_map fn cookies
let to_key_values { key; value; attributes } = (key, (value, attributes))
let guard error fn = if fn () then Ok () else Error error
let err_cookie = `Invalid_encrypted_cookie
type error = [ `Invalid_encrypted_cookie | `Msg of string | `Not_found ]
let pp_error ppf = function
| `Invalid_encrypted_cookie -> Fmt.string ppf "Invalid encrypted cookie"
| `Not_found -> Fmt.string ppf "Cookie not found"
| `Msg str -> Fmt.string ppf str
let get ?(encrypted = true) ~name server req0 =
let hdrs = Vif_request0.headers req0 in
let cookies = all_cookies hdrs in
let cookies = filter_secure req0 cookies in
let cookies = List.map to_key_values cookies in
match List.assoc_opt name cookies with
| None -> Error `Not_found
| Some (value, _) when encrypted ->
let ( let* ) = Result.bind in
let alphabet = Base64.uri_safe_alphabet in
let* value = Base64.decode ~pad:false ~alphabet value in
let err = `Invalid_encrypted_cookie in
let* () = guard err @@ fun () -> String.length value >= 14 in
let* () = guard err @@ fun () -> value.[0] == '\x00' in
let nonce = String.sub value 1 12 in
let adata = "vif.cookie-" ^ name in
let vdata = String.sub value 13 (String.length value - 13) in
let key = Vif_server.cookie_key server in
let value =
Mirage_crypto.AES.GCM.authenticate_decrypt ~key ~nonce ~adata vdata
in
let* () = guard err @@ fun () -> Option.is_some value in
Ok (Option.get value)
| Some (value, _) -> Ok value
type config = {
expires: float option
; max_age: float option
; domain: [ `host ] Domain_name.t option
; path: bool
; secure: bool
; http_only: bool
; same_site: [ `Strict | `Lax | `None ]
}
let default_config =
{
expires= None
; max_age= None
; domain= None
; path= true
; secure= true
; http_only= true
; same_site= `Lax
}
let config ?expires ?max_age ?domain ?(path = true) ?(secure = true)
?(http_only = true) ?(same_site = `Lax) () =
{ expires; max_age; domain; path; secure; http_only; same_site }
let set_cookie cfg ~path name value =
let expires = None in
let max_age = None in
let domain = Option.map (Fmt.str "Domain=%a" Domain_name.pp) cfg.domain in
let path = if cfg.path then Some (Fmt.str "Path=%s" path) else None in
let secure = if cfg.secure then Some "Secure" else None in
let http_only = if cfg.http_only then Some "HttpOnly" else None in
let same_site =
match cfg.same_site with
| `Lax -> Some "SameSite=Lax"
| `Strict -> Some "SameSite=Strict"
| `None -> Some "SameSite=None"
in
let attributes =
List.filter_map Fun.id
[ expires; max_age; domain; path; secure; http_only; same_site ]
in
Fmt.str "%s=%s; %a" name value Fmt.(list ~sep:(any "; ") string) attributes
let random len = Mirage_crypto_rng.generate len
let set ?(encrypt = true) ?(cfg = default_config) ?(path = "/") ~name server
req0 value =
let secure =
Option.is_some (Vif_request0.tls req0) || Vif_request0.on_localhost req0
in
let prefix =
match (cfg.secure, cfg.domain, cfg.path, secure, path) with
| true, None, true, true, "/" -> "__Host-"
| true, _, _, true, _ -> "__Secure-"
| _ -> ""
in
if encrypt then
let key = Vif_server.cookie_key server in
let nonce = random 12 in
let adata = "vif.cookie-" ^ name in
let value =
Mirage_crypto.AES.GCM.authenticate_encrypt ~key ~nonce ~adata value
in
let alphabet = Base64.uri_safe_alphabet in
let value = "\x00" ^ nonce ^ value in
let value = Base64.encode_exn ~pad:false ~alphabet value in
let value = set_cookie cfg ~path (prefix ^ name) value in
Vif_response.add ~field:"set-cookie" value
else
let value = set_cookie cfg ~path name value in
Vif_response.add ~field:"set-cookie" value
let set ?encrypt ?cfg ?path ~name server req value =
set ?encrypt ?cfg ?path ~name server req.Vif_request.request value