src/cookie.bend
emerging-ezhttp@0.21.0.0 · 542 lines · 71 definitions
1
2
3
4
5import Base
6import emerging-ezhttp@0.21.0.0/src/http.bend as Http
7
8
9type Cookie is Data:
10 Cookie{
11 name: String
12 value: String
13 expires: String
14 max_age: String
15 domain: String
16 path: String
17 secure: Bool
18 http_only: Bool
19 same_site: String
20 host_only: Bool
21 }
22
23
24def cookie.new(name: String, value: String) -> Cookie:
25 Cookie{name, value, "", "", "", "", False{}, False{}, "", True{}}
26
27
28def cookie.expires(c: Cookie, when: String) -> Cookie:
29 match c:
30 case Cookie{name, value, expires, max_age, domain, path, secure, http_only,
31 same_site, host_only}:
32 Cookie{name, value, when, max_age, domain, path, secure, http_only,
33 same_site, host_only}
34
35
36def cookie.age(c: Cookie, age: String) -> Cookie:
37 match c:
38 case Cookie{name, value, expires, max_age, domain, path, secure, http_only,
39 same_site, host_only}:
40 Cookie{name, value, expires, age, domain, path, secure, http_only,
41 same_site, host_only}
42
43
44def cookie.domain(c: Cookie, host: String) -> Cookie:
45 match c:
46 case Cookie{name, value, expires, max_age, domain, path, secure, http_only,
47 same_site, host_only}:
48 Cookie{name, value, expires, max_age, host, path, secure, http_only,
49 same_site, False{}}
50
51
52def cookie.dir(c: Cookie, dir: String) -> Cookie:
53 match c:
54 case Cookie{name, value, expires, max_age, domain, path, secure, http_only,
55 same_site, host_only}:
56 Cookie{name, value, expires, max_age, domain, dir, secure, http_only,
57 same_site, host_only}
58
59
60def cookie.secure_on(c: Cookie) -> Cookie:
61 match c:
62 case Cookie{name, value, expires, max_age, domain, path, secure, http_only,
63 same_site, host_only}:
64 Cookie{name, value, expires, max_age, domain, path, True{}, http_only,
65 same_site, host_only}
66
67
68def cookie.http_on(c: Cookie) -> Cookie:
69 match c:
70 case Cookie{name, value, expires, max_age, domain, path, secure, http_only,
71 same_site, host_only}:
72 Cookie{name, value, expires, max_age, domain, path, secure, True{},
73 same_site, host_only}
74
75
76def cookie.site_set(c: Cookie, site: String) -> Cookie:
77 match c:
78 case Cookie{name, value, expires, max_age, domain, path, secure, http_only,
79 same_site, host_only}:
80 Cookie{name, value, expires, max_age, domain, path, secure, http_only,
81 site, host_only}
82
83
84def cookie.site.none(_low: String, hit: Bool) -> String:
85 match hit:
86 case True{}:
87 "None"
88 case False{}:
89 ""
90
91
92def cookie.site.strict(+low: String, hit: Bool) -> String:
93 match hit:
94 case True{}:
95 "Strict"
96 case False{}:
97 cookie.site.none(low, String.eq(low, "none"))
98
99
100def cookie.site.lax(+low: String, hit: Bool) -> String:
101 match hit:
102 case True{}:
103 "Lax"
104 case False{}:
105 cookie.site.strict(low, String.eq(low, "strict"))
106
107
108def cookie.site(+raw: String) -> String:
109 +low = String.to_lower(raw)
110 cookie.site.lax(low, String.eq(low, "lax"))
111
112
113def cookie.age_ok(c: Cookie, +raw: String, m: Maybe<&2, Nat>) -> Cookie:
114 match m:
115 case None{}:
116 c
117 case Some{n}:
118 cookie.age(c, raw)
119
120
121def cookie.flag.http(c: Cookie, _name: String, hit: Bool) -> Cookie:
122 match hit:
123 case True{}:
124 cookie.http_on(c)
125 case False{}:
126 c
127
128
129def cookie.flag.secure(c: Cookie, +name: String, hit: Bool) -> Cookie:
130 match hit:
131 case True{}:
132 cookie.secure_on(c)
133 case False{}:
134 cookie.flag.http(c, name, String.eq(name, "httponly"))
135
136
137def cookie.flag(c: Cookie, +name: String) -> Cookie:
138 cookie.flag.secure(c, name, String.eq(name, "secure"))
139
140
141def cookie.named.site(c: Cookie, _name: String, value: String, hit: Bool)
142 -> Cookie:
143 match hit:
144 case True{}:
145 cookie.site_set(c, cookie.site(value))
146 case False{}:
147 c
148
149
150def cookie.named.path(c: Cookie, +name: String, +value: String, hit: Bool)
151 -> Cookie:
152 match hit:
153 case True{}:
154 cookie.dir(c, value)
155 case False{}:
156 cookie.named.site(c, name, value, String.eq(name, "samesite"))
157
158
159def cookie.named.domain(c: Cookie, +name: String, +value: String, hit: Bool)
160 -> Cookie:
161 match hit:
162 case True{}:
163 cookie.domain(c, String.to_lower(value))
164 case False{}:
165 cookie.named.path(c, name, value, String.eq(name, "path"))
166
167
168def cookie.named.age(c: Cookie, +name: String, +value: String, hit: Bool)
169 -> Cookie:
170 match hit:
171 case True{}:
172 cookie.age_ok(c, value, Http.digits.read(value))
173 case False{}:
174 cookie.named.domain(c, name, value, String.eq(name, "domain"))
175
176
177def cookie.named.expires(c: Cookie, +name: String, +value: String, hit: Bool)
178 -> Cookie:
179 match hit:
180 case True{}:
181 cookie.expires(c, value)
182 case False{}:
183 cookie.named.age(c, name, value, String.eq(name, "max-age"))
184
185
186def cookie.named(c: Cookie, +name: String, +value: String) -> Cookie:
187 cookie.named.expires(c, name, value, String.eq(name, "expires"))
188
189
190def cookie.apply.cut(c: Cookie, +piece: String, cut: Http.Cut) -> Cookie:
191 match cut:
192 case Http.NoCut{}:
193 cookie.flag(c, String.to_lower(String.trim(piece)))
194 case Http.Cut{before, after}:
195 cookie.named(c, String.to_lower(String.trim(before)), String.trim(after))
196
197
198def cookie.apply(c: Cookie, +piece: String) -> Cookie:
199 cookie.apply.cut(c, piece, Http.divide(piece, "="))
200
201
202def cookie.fold(ps: List<&2, String>, c: Cookie) -> Cookie:
203 match ps:
204 case []:
205 c
206 case h <> t:
207 cookie.fold(t, cookie.apply(c, String.trim(h)))
208
209
210def cookie.from.name(+name: String, value: String, attrs: List<&2, String>,
211 empty: Bool) -> Maybe<&2, Cookie>:
212 match empty:
213 case True{}:
214 None{}
215 case False{}:
216 Some{cookie.fold(attrs, cookie.new(name, value))}
217
218
219def cookie.from(_pair: String, attrs: List<&2, String>, cut: Http.Cut)
220 -> Maybe<&2, Cookie>:
221 match cut:
222 case Http.NoCut{}:
223 None{}
224 case Http.Cut{before, after}:
225 +name = String.trim(before)
226 cookie.from.name(name, String.trim(after), attrs, String.eq(name, ""))
227
228
229def cookie.parse.parts(ps: List<&2, String>) -> Maybe<&2, Cookie>:
230 match ps:
231 case []:
232 None{}
233 case h <> t:
234 +pair = String.trim(h)
235 cookie.from(pair, t, Http.divide(pair, "="))
236
237
238def cookie.parse(line: String) -> Maybe<&2, Cookie>:
239 cookie.parse.parts(String.split(line, ';'))
240
241
242def cookie.opt.of(prefix: String, value: String, empty: Bool) -> String:
243 match empty:
244 case True{}:
245 ""
246 case False{}:
247 prefix ++ value
248
249
250def cookie.opt(prefix: String, +value: String) -> String:
251 cookie.opt.of(prefix, value, String.eq(value, ""))
252
253
254def cookie.domain_text(domain: String, host_only: Bool) -> String:
255 match host_only:
256 case True{}:
257 ""
258 case False{}:
259 cookie.opt("; Domain=", domain)
260
261
262def cookie.mark(label: String, on: Bool) -> String:
263 match on:
264 case False{}:
265 ""
266 case True{}:
267 "; " ++ label
268
269
270def cookie.line(c: Cookie) -> String:
271 match c:
272 case Cookie{name, value, expires, max_age, domain, path, secure, http_only,
273 same_site, host_only}:
274 name ++ "=" ++ value
275 ++ cookie.opt("; Expires=", expires)
276 ++ cookie.opt("; Max-Age=", max_age)
277 ++ cookie.domain_text(domain, host_only)
278 ++ cookie.opt("; Path=", path)
279 ++ cookie.mark("Secure", secure)
280 ++ cookie.mark("HttpOnly", http_only)
281 ++ cookie.opt("; SameSite=", same_site)
282
283
284def cookie.set(c: Cookie) -> Http.Header:
285 Http.H{"Set-Cookie", cookie.line(c)}
286
287
288def cookie.pairs.keep(name: String, value: String, empty: Bool,
289 rest: Unit -> List<&2, String>) -> List<&2, String>:
290 match empty:
291 case True{}:
292 rest(Unit{})
293 case False{}:
294 (name ++ "=" ++ value) <> rest(Unit{})
295
296
297def cookie.pairs.one(c: Cookie, rest: Unit -> List<&2, String>)
298 -> List<&2, String>:
299 match c:
300 case Cookie{+name, value, expires, max_age, domain, path, secure, http_only,
301 same_site, host_only}:
302 cookie.pairs.keep(name, value, String.eq(name, ""), rest)
303
304
305def cookie.pairs(cs: List<&2, Cookie>) -> List<&2, String>:
306 match cs:
307 case []:
308 []
309 case h <> t:
310 cookie.pairs.one(h, _u => cookie.pairs(t))
311
312
313def cookie.join(cs: List<&2, Cookie>) -> String:
314 String.join(cookie.pairs(cs), "; ")
315
316
317def cookie.request(cs: List<&2, Cookie>) -> Http.Header:
318 Http.H{"Cookie", cookie.join(cs)}
319
320
321def cookie.slashes.ch(slash: Bool, rest: Unit -> Nat) -> Nat:
322 match slash:
323 case False{}:
324 rest(Unit{})
325 case True{}:
326 Nat.add(1n, rest(Unit{}))
327
328
329def cookie.slashes(p: String) -> Nat:
330 match p:
331 case SNil{}:
332 0n
333 case SCon{h, t}:
334 cookie.slashes.ch(Char.is_eq(h, '/'), _u => cookie.slashes(t))
335
336
337def cookie.skip.ch(hit: Bool, +tail: String, rest: Unit -> String) -> String:
338 match hit:
339 case True{}:
340 tail
341 case False{}:
342 rest(Unit{})
343
344
345def cookie.skip(p: String) -> String:
346 match p:
347 case SNil{}:
348 ""
349 case SCon{h, +t}:
350 cookie.skip.ch(Char.is_eq(h, '/'), t, _u => cookie.skip(t))
351
352
353def cookie.before(p: String) -> String:
354 String.reverse(cookie.skip(String.reverse(p)))
355
356
357def cookie.default_of(+p: String, few: Bool) -> String:
358 match few:
359 case True{}:
360 "/"
361 case False{}:
362 cookie.before(p)
363
364
365def cookie.default_path(+p: String) -> String:
366 cookie.default_of(p, Nat.is_lt(cookie.slashes(p), 2n))
367
368
369def cookie.fill_domain(domain: String, host_only: Bool, host: String) -> String:
370 match host_only:
371 case True{}:
372 host
373 case False{}:
374 domain
375
376
377def cookie.fill_path.of(path: String, req: String, empty: Bool) -> String:
378 match empty:
379 case True{}:
380 cookie.default_path(req)
381 case False{}:
382 path
383
384
385def cookie.fill_path(+path: String, req: String) -> String:
386 cookie.fill_path.of(path, req, String.eq(path, ""))
387
388
389def cookie.receive(host: String, req_path: String, c: Cookie) -> Cookie:
390 match c:
391 case Cookie{name, value, expires, max_age, domain, path, secure, http_only,
392 same_site, +host_only}:
393 Cookie{name, value, expires, max_age,
394 cookie.fill_domain(domain, host_only, String.to_lower(host)),
395 cookie.fill_path(path, req_path), secure, http_only, same_site,
396 host_only}
397
398
399def cookie.ipv4.step(dot: Bool, digit: Bool, if_dot: Unit -> Bool,
400 if_digit: Unit -> Bool) -> Bool:
401 match dot digit:
402 case True{} _:
403 if_dot(Unit{})
404 case False{} True{}:
405 if_digit(Unit{})
406 case False{} False{}:
407 False{}
408
409
410def cookie.ipv4.go(host: String, seen: Bool) -> Bool:
411 match host:
412 case SNil{}:
413 seen
414 case SCon{h, +t}:
415 +u = Char.to_u32(h)
416 cookie.ipv4.step(U32.is_eq(u, 46),
417 Bool.and(U32.is_ge(u, 48), U32.is_le(u, 57)),
418 _u => cookie.ipv4.go(t, True{}), _d => cookie.ipv4.go(t, seen))
419
420
421def cookie.ipv4(host: String) -> Bool:
422 cookie.ipv4.go(host, False{})
423
424
425def cookie.domain_suffix(host: String, domain: String, ip: Bool) -> Bool:
426 match ip:
427 case True{}:
428 False{}
429 case False{}:
430 String.ends_with(host, "." ++ domain)
431
432
433def cookie.domain_eq(+host: String, +domain: String, same: Bool) -> Bool:
434 match same:
435 case True{}:
436 True{}
437 case False{}:
438 cookie.domain_suffix(host, domain, cookie.ipv4(host))
439
440
441def cookie.domain_match(+host: String, +domain: String) -> Bool:
442 cookie.domain_eq(host, domain, String.eq(host, domain))
443
444
445def cookie.domain_ok(host: String, domain: String, host_only: Bool) -> Bool:
446 match host_only:
447 case True{}:
448 String.eq(String.to_lower(host), String.to_lower(domain))
449 case False{}:
450 cookie.domain_match(String.to_lower(host), String.to_lower(domain))
451
452
453def cookie.path_slash(+req: String, +cpath: String, slash: Bool) -> Bool:
454 match slash:
455 case True{}:
456 True{}
457 case False{}:
458 String.starts_with(String.drop(req, String.length(cpath)), "/")
459
460
461def cookie.path_rest(+req: String, +cpath: String, same: Bool) -> Bool:
462 match same:
463 case True{}:
464 True{}
465 case False{}:
466 cookie.path_slash(req, cpath, String.ends_with(cpath, "/"))
467
468
469def cookie.path_prefix(+req: String, +cpath: String, hit: Bool) -> Bool:
470 match hit:
471 case False{}:
472 False{}
473 case True{}:
474 cookie.path_rest(req, cpath, String.eq(req, cpath))
475
476
477def cookie.path_ok(+req: String, +cpath: String) -> Bool:
478 cookie.path_prefix(req, cpath, String.starts_with(req, cpath))
479
480
481def cookie.send.secure(tls: Bool, secure: Bool) -> Bool:
482 match secure:
483 case False{}:
484 True{}
485 case True{}:
486 tls
487
488
489def cookie.send.path(tls: Bool, secure: Bool, ok: Bool) -> Bool:
490 match ok:
491 case False{}:
492 False{}
493 case True{}:
494 cookie.send.secure(tls, secure)
495
496
497def cookie.send.domain(req: String, tls: Bool, secure: Bool, cpath: String,
498 ok: Bool) -> Bool:
499 match ok:
500 case False{}:
501 False{}
502 case True{}:
503 cookie.send.path(tls, secure, cookie.path_ok(req, cpath))
504
505
506def cookie.send(host: String, req_path: String, tls: Bool, c: Cookie) -> Bool:
507 match c:
508 case Cookie{name, value, expires, max_age, domain, path, secure, http_only,
509 same_site, host_only}:
510 cookie.send.domain(req_path, tls, secure, path,
511 cookie.domain_ok(host, domain, host_only))
512
513
514def cookie.alive.of(age: Nat, m: Maybe<&2, Nat>) -> Bool:
515 match m:
516 case None{}:
517 True{}
518 case Some{n}:
519 Nat.is_lt(age, n)
520
521
522def cookie.alive(max_age: String, age: Nat) -> Bool:
523 cookie.alive.of(age, Http.digits.read(max_age))
524
525
526def cookie.select.cons(ok: Bool, c: Cookie, rest: Unit -> List<&2, Cookie>)
527 -> List<&2, Cookie>:
528 match ok:
529 case False{}:
530 rest(Unit{})
531 case True{}:
532 c <> rest(Unit{})
533
534
535def cookie.select(cs: List<&2, Cookie>, +host: String, +req_path: String,
536 +tls: Bool) -> List<&2, Cookie>:
537 match cs:
538 case []:
539 []
540 case +h <> t:
541 cookie.select.cons(cookie.send(host, req_path, tls, h), h,
542 _u => cookie.select(t, host, req_path, tls))