tangled
alpha
login
or
join now
anil.recoil.org
/
ocaml-cookeio
0
fork
atom
OCaml HTTP cookie handling library with support for Eio-based storage jars
0
fork
atom
overview
issues
pulls
pipelines
fmt
anil.recoil.org
3 months ago
2447ba92
7907b494
0/1
build.yml
failed
3m 49s
+389
-193
5 changed files
expand all
collapse all
unified
split
lib
core
cookeio.ml
cookeio.mli
jar
cookeio_jar.ml
test
dune
test_cookeio.ml
+16
-12
lib/core/cookeio.ml
reviewed
···
226
226
| "domain" -> attrs.domain <- Some (normalize_domain attr_value)
227
227
| "path" -> attrs.path <- Some attr_value
228
228
| "expires" -> (
229
229
-
(* Special case: Expires=0 means session cookie *)
230
230
-
if attr_value = "0" then attrs.expires <- Some `Session
229
229
+
if
230
230
+
(* Special case: Expires=0 means session cookie *)
231
231
+
attr_value = "0"
232
232
+
then attrs.expires <- Some `Session
231
233
else
232
234
match Ptime.of_rfc3339 attr_value with
233
235
| Ok (time, _, _) -> attrs.expires <- Some (`DateTime time)
···
248
250
(* Store the max-age as a Ptime.Span *)
249
251
attrs.max_age <- Some (Ptime.Span.of_int_s seconds);
250
252
(* Also compute and store expires as DateTime *)
251
251
-
let expires = Ptime.add_span current_time (Ptime.Span.of_int_s seconds) in
253
253
+
let expires =
254
254
+
Ptime.add_span current_time (Ptime.Span.of_int_s seconds)
255
255
+
in
252
256
(match expires with
253
257
| Some time -> attrs.expires <- Some (`DateTime time)
254
258
| None -> ());
···
288
292
if attrs.partitioned && not attrs.secure then (
289
293
Log.warn (fun m ->
290
294
m
291
291
-
"Cookie has Partitioned attribute but Secure flag is not set; \
292
292
-
this violates CHIPS requirements");
295
295
+
"Cookie has Partitioned attribute but Secure flag is not set; this \
296
296
+
violates CHIPS requirements");
293
297
false)
294
298
else true
295
299
in
···
303
307
let path = Option.value attrs.path ~default:request_path in
304
308
make ~domain ~path ~name ~value ~secure:attrs.secure
305
309
~http_only:attrs.http_only ?expires:attrs.expires ?max_age:attrs.max_age
306
306
-
?same_site:attrs.same_site ~partitioned:attrs.partitioned
307
307
-
~creation_time:now ~last_access:now ()
310
310
+
?same_site:attrs.same_site ~partitioned:attrs.partitioned ~creation_time:now
311
311
+
~last_access:now ()
308
312
309
313
(** {1 Pretty Printing} *)
310
314
311
315
let pp ppf cookie =
312
316
Format.fprintf ppf
313
317
"@[<hov 2>{ name=%S;@ value=%S;@ domain=%S;@ path=%S;@ secure=%b;@ \
314
314
-
http_only=%b;@ partitioned=%b;@ expires=%a;@ max_age=%a;@ same_site=%a }@]"
318
318
+
http_only=%b;@ partitioned=%b;@ expires=%a;@ max_age=%a;@ same_site=%a \
319
319
+
}@]"
315
320
(name cookie) (value cookie) (domain cookie) (path cookie) (secure cookie)
316
321
(http_only cookie) (partitioned cookie)
317
322
(Format.pp_print_option Expiration.pp)
···
392
397
Error (Printf.sprintf "Cookie missing '=' separator: %s" name_value)
393
398
| Some eq_pos ->
394
399
let cookie_name = String.sub name_value 0 eq_pos |> String.trim in
395
395
-
if String.length cookie_name = 0 then
396
396
-
Error "Cookie has empty name"
400
400
+
if String.length cookie_name = 0 then Error "Cookie has empty name"
397
401
else
398
402
let cookie_value =
399
403
String.sub name_value (eq_pos + 1)
···
404
408
(* Create cookie with defaults from Cookie header context *)
405
409
let cookie =
406
410
make ~domain ~path ~name:cookie_name ~value:cookie_value
407
407
-
~secure:false ~http_only:false ~partitioned:false ~creation_time:current_time
408
408
-
~last_access:current_time ()
411
411
+
~secure:false ~http_only:false ~partitioned:false
412
412
+
~creation_time:current_time ~last_access:current_time ()
409
413
in
410
414
Ok cookie)
411
415
parts
+6
-5
lib/core/cookeio.mli
reviewed
···
37
37
protection
38
38
- [`Lax]: Cookie sent for same-site requests and top-level navigation
39
39
(default for modern browsers)
40
40
-
- [`None]: Cookie sent for all cross-site requests (requires [secure] flag) *)
40
40
+
- [`None]: Cookie sent for all cross-site requests (requires [secure]
41
41
+
flag) *)
41
42
42
43
val equal : t -> t -> bool
43
44
(** Equality function for same-site values *)
···
179
180
- [Partitioned] requires the [Secure] flag to be set
180
181
181
182
Example:
182
182
-
[of_set_cookie_header ~now:(fun () -> Ptime_clock.now ()) ~domain:"example.com" ~path:"/" "session=abc123;
183
183
-
Secure; HttpOnly"] *)
183
183
+
[of_set_cookie_header ~now:(fun () -> Ptime_clock.now ())
184
184
+
~domain:"example.com" ~path:"/" "session=abc123; Secure; HttpOnly"] *)
184
185
185
186
val of_cookie_header :
186
187
now:(unit -> Ptime.t) ->
···
204
205
values and excess whitespace are ignored.
205
206
206
207
Example:
207
207
-
[of_cookie_header ~now:(fun () -> Ptime_clock.now ()) ~domain:"example.com" ~path:"/"
208
208
-
"session=abc; theme=dark"] *)
208
208
+
[of_cookie_header ~now:(fun () -> Ptime_clock.now ()) ~domain:"example.com"
209
209
+
~path:"/" "session=abc; theme=dark"] *)
209
210
210
211
val make_cookie_header : t list -> string
211
212
(** Create cookie header value from cookies.
+23
-44
lib/jar/cookeio_jar.ml
reviewed
···
44
44
let is_expired cookie clock =
45
45
match Cookeio.expires cookie with
46
46
| None -> false (* No expiration *)
47
47
-
| Some `Session -> false (* Session cookie - not expired until browser closes *)
47
47
+
| Some `Session ->
48
48
+
false (* Session cookie - not expired until browser closes *)
48
49
| Some (`DateTime exp_time) ->
49
50
let now =
50
51
Ptime.of_float_s (Eio.Time.now clock)
···
70
71
71
72
let add_cookie jar cookie =
72
73
Log.debug (fun m ->
73
73
-
m "Adding cookie to delta: %s=%s for domain %s"
74
74
-
(Cookeio.name cookie)
75
75
-
(Cookeio.value cookie)
76
76
-
(Cookeio.domain cookie));
74
74
+
m "Adding cookie to delta: %s=%s for domain %s" (Cookeio.name cookie)
75
75
+
(Cookeio.value cookie) (Cookeio.domain cookie));
77
76
78
77
Eio.Mutex.lock jar.mutex;
79
78
(* Remove existing cookie with same identity from delta *)
···
86
85
87
86
let add_original jar cookie =
88
87
Log.debug (fun m ->
89
89
-
m "Adding original cookie: %s=%s for domain %s"
90
90
-
(Cookeio.name cookie)
91
91
-
(Cookeio.value cookie)
92
92
-
(Cookeio.domain cookie));
88
88
+
m "Adding original cookie: %s=%s for domain %s" (Cookeio.name cookie)
89
89
+
(Cookeio.value cookie) (Cookeio.domain cookie));
93
90
94
91
Eio.Mutex.lock jar.mutex;
95
92
(* Remove existing cookie with same identity from original *)
···
116
113
Ptime.sub_span now (Ptime.Span.of_int_s (365 * 24 * 60 * 60))
117
114
|> Option.value ~default:Ptime.epoch
118
115
in
119
119
-
Cookeio.make
120
120
-
~domain:(Cookeio.domain cookie)
121
121
-
~path:(Cookeio.path cookie)
122
122
-
~name:(Cookeio.name cookie)
123
123
-
~value:""
124
124
-
~secure:(Cookeio.secure cookie)
125
125
-
~http_only:(Cookeio.http_only cookie)
126
126
-
~expires:(`DateTime past_expiry)
127
127
-
~max_age:(Ptime.Span.of_int_s 0)
128
128
-
?same_site:(Cookeio.same_site cookie)
116
116
+
Cookeio.make ~domain:(Cookeio.domain cookie) ~path:(Cookeio.path cookie)
117
117
+
~name:(Cookeio.name cookie) ~value:"" ~secure:(Cookeio.secure cookie)
118
118
+
~http_only:(Cookeio.http_only cookie) ~expires:(`DateTime past_expiry)
119
119
+
~max_age:(Ptime.Span.of_int_s 0) ?same_site:(Cookeio.same_site cookie)
129
120
~partitioned:(Cookeio.partitioned cookie)
130
121
~creation_time:now ~last_access:now ()
131
122
132
123
let remove jar ~clock cookie =
133
124
Log.debug (fun m ->
134
134
-
m "Removing cookie: %s=%s for domain %s"
135
135
-
(Cookeio.name cookie)
136
136
-
(Cookeio.value cookie)
137
137
-
(Cookeio.domain cookie));
125
125
+
m "Removing cookie: %s=%s for domain %s" (Cookeio.name cookie)
126
126
+
(Cookeio.value cookie) (Cookeio.domain cookie));
138
127
139
128
Eio.Mutex.lock jar.mutex;
140
129
(* Check if this cookie exists in original_cookies *)
···
204
193
List.map
205
194
(fun c ->
206
195
if List.exists (fun a -> cookie_identity_matches a c) applicable then
207
207
-
Cookeio.make
208
208
-
~domain:(Cookeio.domain c)
209
209
-
~path:(Cookeio.path c)
210
210
-
~name:(Cookeio.name c)
211
211
-
~value:(Cookeio.value c)
212
212
-
~secure:(Cookeio.secure c)
213
213
-
~http_only:(Cookeio.http_only c)
214
214
-
?expires:(Cookeio.expires c)
215
215
-
?max_age:(Cookeio.max_age c)
196
196
+
Cookeio.make ~domain:(Cookeio.domain c) ~path:(Cookeio.path c)
197
197
+
~name:(Cookeio.name c) ~value:(Cookeio.value c)
198
198
+
~secure:(Cookeio.secure c) ~http_only:(Cookeio.http_only c)
199
199
+
?expires:(Cookeio.expires c) ?max_age:(Cookeio.max_age c)
216
200
?same_site:(Cookeio.same_site c)
217
201
~partitioned:(Cookeio.partitioned c)
218
218
-
~creation_time:(Cookeio.creation_time c)
219
219
-
~last_access:now ()
202
202
+
~creation_time:(Cookeio.creation_time c) ~last_access:now ()
220
203
else c)
221
204
cookies
222
205
in
···
346
329
in
347
330
348
331
Buffer.add_string buffer
349
349
-
(Printf.sprintf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
350
350
-
(Cookeio.domain cookie)
351
351
-
include_subdomains
352
352
-
(Cookeio.path cookie)
353
353
-
secure_flag expires_str
354
354
-
(Cookeio.name cookie)
355
355
-
(Cookeio.value cookie)))
332
332
+
(Printf.sprintf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" (Cookeio.domain cookie)
333
333
+
include_subdomains (Cookeio.path cookie) secure_flag expires_str
334
334
+
(Cookeio.name cookie) (Cookeio.value cookie)))
356
335
unique;
357
336
358
337
Buffer.contents buffer
···
389
368
390
369
let cookie =
391
370
Cookeio.make ~domain:(normalize_domain domain) ~path ~name ~value
392
392
-
~secure:(secure = "TRUE") ~http_only:false ?expires ?max_age:None
393
393
-
?same_site:None ~partitioned:false ~creation_time:now
394
394
-
~last_access:now ()
371
371
+
~secure:(secure = "TRUE") ~http_only:false ?expires
372
372
+
?max_age:None ?same_site:None ~partitioned:false
373
373
+
~creation_time:now ~last_access:now ()
395
374
in
396
375
add_original jar cookie;
397
376
Log.debug (fun m -> m "Loaded cookie: %s=%s" name value)
+10
-1
test/dune
reviewed
···
1
1
(test
2
2
(name test_cookeio)
3
3
-
(libraries cookeio cookeio_jar alcotest eio eio.unix eio_main eio.mock ptime str)
3
3
+
(libraries
4
4
+
cookeio
5
5
+
cookeio_jar
6
6
+
alcotest
7
7
+
eio
8
8
+
eio.unix
9
9
+
eio_main
10
10
+
eio.mock
11
11
+
ptime
12
12
+
str)
4
13
(deps cookies.txt))
+334
-131
test/test_cookeio.ml
reviewed
···
19
19
partitioned=%b; expires=%a; max_age=%a; same_site=%a }"
20
20
(Cookeio.name c) (Cookeio.value c) (Cookeio.domain c) (Cookeio.path c)
21
21
(Cookeio.secure c) (Cookeio.http_only c) (Cookeio.partitioned c)
22
22
-
(Format.pp_print_option
23
23
-
(fun ppf e ->
22
22
+
(Format.pp_print_option (fun ppf e ->
24
23
match e with
25
24
| `Session -> Format.pp_print_string ppf "Session"
26
25
| `DateTime t -> Format.fprintf ppf "DateTime(%a)" Ptime.pp t))
27
26
(Cookeio.expires c)
28
27
(Format.pp_print_option Ptime.Span.pp)
29
28
(Cookeio.max_age c)
30
30
-
(Format.pp_print_option
31
31
-
(fun ppf -> function
32
32
-
| `Strict -> Format.pp_print_string ppf "Strict"
33
33
-
| `Lax -> Format.pp_print_string ppf "Lax"
34
34
-
| `None -> Format.pp_print_string ppf "None"))
29
29
+
(Format.pp_print_option (fun ppf -> function
30
30
+
| `Strict -> Format.pp_print_string ppf "Strict"
31
31
+
| `Lax -> Format.pp_print_string ppf "Lax"
32
32
+
| `None -> Format.pp_print_string ppf "None"))
35
33
(Cookeio.same_site c))
36
34
(fun c1 c2 ->
37
35
let expires_equal e1 e2 =
···
126
124
begin match expected_expiry with
127
125
| Some t ->
128
126
Alcotest.(check (option expiration_testable))
129
129
-
"cookie-3 expires" (Some (`DateTime t)) (Cookeio.expires cookie3)
127
127
+
"cookie-3 expires"
128
128
+
(Some (`DateTime t))
129
129
+
(Cookeio.expires cookie3)
130
130
| None -> Alcotest.fail "Expected expiry time for cookie-3"
131
131
end;
132
132
···
142
142
begin match expected_expiry with
143
143
| Some t ->
144
144
Alcotest.(check (option expiration_testable))
145
145
-
"cookie-4 expires" (Some (`DateTime t)) (Cookeio.expires cookie4)
145
145
+
"cookie-4 expires"
146
146
+
(Some (`DateTime t))
147
147
+
(Cookeio.expires cookie4)
146
148
| None -> Alcotest.fail "Expected expiry time for cookie-4"
147
149
end;
148
150
···
158
160
begin match expected_expiry with
159
161
| Some t ->
160
162
Alcotest.(check (option expiration_testable))
161
161
-
"cookie-5 expires" (Some (`DateTime t)) (Cookeio.expires cookie5)
163
163
+
"cookie-5 expires"
164
164
+
(Some (`DateTime t))
165
165
+
(Cookeio.expires cookie5)
162
166
| None -> Alcotest.fail "Expected expiry time for cookie-5"
163
167
end
164
168
···
191
195
begin match expected_expiry with
192
196
| Some t ->
193
197
Alcotest.(check (option expiration_testable))
194
194
-
"file cookie-5 expires" (Some (`DateTime t)) (Cookeio.expires cookie5)
198
198
+
"file cookie-5 expires"
199
199
+
(Some (`DateTime t))
200
200
+
(Cookeio.expires cookie5)
195
201
| None -> Alcotest.fail "Expected expiry time for cookie-5"
196
202
end;
197
203
···
292
298
begin match Ptime.of_float_s 1257894000.0 with
293
299
| Some t ->
294
300
Alcotest.(check (option expiration_testable))
295
295
-
"round trip expires" (Some (`DateTime t)) (Cookeio.expires cookie2)
301
301
+
"round trip expires"
302
302
+
(Some (`DateTime t))
303
303
+
(Cookeio.expires cookie2)
296
304
| None -> Alcotest.fail "Expected expiry time"
297
305
end
298
306
···
376
384
(* Parse a Set-Cookie header with Max-Age *)
377
385
let header = "session=abc123; Max-Age=3600; Secure; HttpOnly" in
378
386
let cookie_opt =
379
379
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
387
387
+
of_set_cookie_header
388
388
+
~now:(fun () ->
389
389
+
Ptime.of_float_s (Eio.Time.now clock)
390
390
+
|> Option.value ~default:Ptime.epoch)
391
391
+
~domain:"example.com" ~path:"/" header
380
392
in
381
393
382
394
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
···
392
404
begin match expected_expiry with
393
405
| Some t ->
394
406
Alcotest.(check (option expiration_testable))
395
395
-
"expires set from max-age" (Some (`DateTime t)) (Cookeio.expires cookie)
407
407
+
"expires set from max-age"
408
408
+
(Some (`DateTime t))
409
409
+
(Cookeio.expires cookie)
396
410
| None -> Alcotest.fail "Expected expiry time"
397
411
end;
398
412
···
455
469
"id=xyz789; Expires=2025-10-21T07:28:00Z; Path=/; Domain=.example.com"
456
470
in
457
471
let cookie_opt =
458
458
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
472
472
+
of_set_cookie_header
473
473
+
~now:(fun () ->
474
474
+
Ptime.of_float_s (Eio.Time.now clock)
475
475
+
|> Option.value ~default:Ptime.epoch)
476
476
+
~domain:"example.com" ~path:"/" header
459
477
in
460
478
461
479
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
···
476
494
match expected_expiry with
477
495
| Ok (time, _, _) ->
478
496
Alcotest.(check (option expiration_testable))
479
479
-
"expires matches parsed value" (Some (`DateTime time))
497
497
+
"expires matches parsed value"
498
498
+
(Some (`DateTime time))
480
499
(Cookeio.expires cookie)
481
500
| Error _ -> Alcotest.fail "Failed to parse expected expiry time"
482
501
···
490
509
(* This should be rejected: SameSite=None without Secure *)
491
510
let invalid_header = "token=abc; SameSite=None" in
492
511
let cookie_opt =
493
493
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" invalid_header
512
512
+
of_set_cookie_header
513
513
+
~now:(fun () ->
514
514
+
Ptime.of_float_s (Eio.Time.now clock)
515
515
+
|> Option.value ~default:Ptime.epoch)
516
516
+
~domain:"example.com" ~path:"/" invalid_header
494
517
in
495
518
496
519
Alcotest.(check bool)
···
500
523
(* This should be accepted: SameSite=None with Secure *)
501
524
let valid_header = "token=abc; SameSite=None; Secure" in
502
525
let cookie_opt2 =
503
503
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" valid_header
526
526
+
of_set_cookie_header
527
527
+
~now:(fun () ->
528
528
+
Ptime.of_float_s (Eio.Time.now clock)
529
529
+
|> Option.value ~default:Ptime.epoch)
530
530
+
~domain:"example.com" ~path:"/" valid_header
504
531
in
505
532
506
533
Alcotest.(check bool)
···
528
555
(* Test parsing ".example.com" stores as "example.com" *)
529
556
let header = "test=value; Domain=.example.com" in
530
557
let cookie_opt =
531
531
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
558
558
+
of_set_cookie_header
559
559
+
~now:(fun () ->
560
560
+
Ptime.of_float_s (Eio.Time.now clock)
561
561
+
|> Option.value ~default:Ptime.epoch)
562
562
+
~domain:"example.com" ~path:"/" header
532
563
in
533
564
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
534
565
let cookie = Option.get cookie_opt in
···
562
593
(* Parse a Set-Cookie header with Max-Age *)
563
594
let header = "session=abc123; Max-Age=3600" in
564
595
let cookie_opt =
565
565
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
596
596
+
of_set_cookie_header
597
597
+
~now:(fun () ->
598
598
+
Ptime.of_float_s (Eio.Time.now clock)
599
599
+
|> Option.value ~default:Ptime.epoch)
600
600
+
~domain:"example.com" ~path:"/" header
566
601
in
567
602
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
568
603
···
582
617
begin match expected_expiry with
583
618
| Some t ->
584
619
Alcotest.(check (option expiration_testable))
585
585
-
"expires computed from max-age" (Some (`DateTime t))
620
620
+
"expires computed from max-age"
621
621
+
(Some (`DateTime t))
586
622
(Cookeio.expires cookie)
587
623
| None -> Alcotest.fail "Expected expiry time"
588
624
end
···
595
631
(* Parse a Set-Cookie header with negative Max-Age *)
596
632
let header = "session=abc123; Max-Age=-100" in
597
633
let cookie_opt =
598
598
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
634
634
+
of_set_cookie_header
635
635
+
~now:(fun () ->
636
636
+
Ptime.of_float_s (Eio.Time.now clock)
637
637
+
|> Option.value ~default:Ptime.epoch)
638
638
+
~domain:"example.com" ~path:"/" header
599
639
in
600
640
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
601
641
···
615
655
begin match expected_expiry with
616
656
| Some t ->
617
657
Alcotest.(check (option expiration_testable))
618
618
-
"expires computed with 0 seconds" (Some (`DateTime t))
658
658
+
"expires computed with 0 seconds"
659
659
+
(Some (`DateTime t))
619
660
(Cookeio.expires cookie)
620
661
| None -> Alcotest.fail "Expected expiry time"
621
662
end
···
641
682
let expires_time = Ptime.of_float_s 8600.0 |> Option.get in
642
683
let cookie =
643
684
Cookeio.make ~domain:"example.com" ~path:"/" ~name:"session" ~value:"abc123"
644
644
-
~secure:true ~http_only:true ?expires:(Some (`DateTime expires_time))
685
685
+
~secure:true ~http_only:true
686
686
+
?expires:(Some (`DateTime expires_time))
645
687
?max_age:(Some max_age_span) ?same_site:(Some `Strict)
646
688
~creation_time:(Ptime.of_float_s 5000.0 |> Option.get)
647
689
~last_access:(Ptime.of_float_s 5000.0 |> Option.get)
···
679
721
(* Parse a cookie with Max-Age *)
680
722
let header = "session=xyz; Max-Age=7200; Secure; HttpOnly" in
681
723
let cookie_opt =
682
682
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
724
724
+
of_set_cookie_header
725
725
+
~now:(fun () ->
726
726
+
Ptime.of_float_s (Eio.Time.now clock)
727
727
+
|> Option.value ~default:Ptime.epoch)
728
728
+
~domain:"example.com" ~path:"/" header
683
729
in
684
730
Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
685
731
let cookie = Option.get cookie_opt in
···
691
737
Eio_mock.Clock.set_time clock 5000.0;
692
738
(* Reset clock to same time *)
693
739
let cookie2_opt =
694
694
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" set_cookie_header
740
740
+
of_set_cookie_header
741
741
+
~now:(fun () ->
742
742
+
Ptime.of_float_s (Eio.Time.now clock)
743
743
+
|> Option.value ~default:Ptime.epoch)
744
744
+
~domain:"example.com" ~path:"/" set_cookie_header
695
745
in
696
746
Alcotest.(check bool) "cookie re-parsed" true (Option.is_some cookie2_opt);
697
747
let cookie2 = Option.get cookie2_opt in
···
760
810
(* Test FMT1: "Wed, 21 Oct 2015 07:28:00 GMT" (RFC 1123) *)
761
811
let header = "session=abc; Expires=Wed, 21 Oct 2015 07:28:00 GMT" in
762
812
let cookie_opt =
763
763
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
813
813
+
of_set_cookie_header
814
814
+
~now:(fun () ->
815
815
+
Ptime.of_float_s (Eio.Time.now clock)
816
816
+
|> Option.value ~default:Ptime.epoch)
817
817
+
~domain:"example.com" ~path:"/" header
764
818
in
765
819
Alcotest.(check bool) "FMT1 cookie parsed" true (Option.is_some cookie_opt);
766
820
···
774
828
begin match expected with
775
829
| Some t ->
776
830
Alcotest.(check (option expiration_testable))
777
777
-
"FMT1 expiry correct" (Some (`DateTime t)) (Cookeio.expires cookie)
831
831
+
"FMT1 expiry correct"
832
832
+
(Some (`DateTime t))
833
833
+
(Cookeio.expires cookie)
778
834
| None -> Alcotest.fail "Expected expiry time for FMT1"
779
835
end
780
836
···
786
842
(* Test FMT2: "Wednesday, 21-Oct-15 07:28:00 GMT" (RFC 850 with abbreviated year) *)
787
843
let header = "session=abc; Expires=Wednesday, 21-Oct-15 07:28:00 GMT" in
788
844
let cookie_opt =
789
789
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
845
845
+
of_set_cookie_header
846
846
+
~now:(fun () ->
847
847
+
Ptime.of_float_s (Eio.Time.now clock)
848
848
+
|> Option.value ~default:Ptime.epoch)
849
849
+
~domain:"example.com" ~path:"/" header
790
850
in
791
851
Alcotest.(check bool) "FMT2 cookie parsed" true (Option.is_some cookie_opt);
792
852
···
800
860
begin match expected with
801
861
| Some t ->
802
862
Alcotest.(check (option expiration_testable))
803
803
-
"FMT2 expiry correct with year normalization" (Some (`DateTime t))
863
863
+
"FMT2 expiry correct with year normalization"
864
864
+
(Some (`DateTime t))
804
865
(Cookeio.expires cookie)
805
866
| None -> Alcotest.fail "Expected expiry time for FMT2"
806
867
end
···
813
874
(* Test FMT3: "Wed Oct 21 07:28:00 2015" (asctime) *)
814
875
let header = "session=abc; Expires=Wed Oct 21 07:28:00 2015" in
815
876
let cookie_opt =
816
816
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
877
877
+
of_set_cookie_header
878
878
+
~now:(fun () ->
879
879
+
Ptime.of_float_s (Eio.Time.now clock)
880
880
+
|> Option.value ~default:Ptime.epoch)
881
881
+
~domain:"example.com" ~path:"/" header
817
882
in
818
883
Alcotest.(check bool) "FMT3 cookie parsed" true (Option.is_some cookie_opt);
819
884
···
826
891
begin match expected with
827
892
| Some t ->
828
893
Alcotest.(check (option expiration_testable))
829
829
-
"FMT3 expiry correct" (Some (`DateTime t)) (Cookeio.expires cookie)
894
894
+
"FMT3 expiry correct"
895
895
+
(Some (`DateTime t))
896
896
+
(Cookeio.expires cookie)
830
897
| None -> Alcotest.fail "Expected expiry time for FMT3"
831
898
end
832
899
···
838
905
(* Test FMT4: "Wed, 21-Oct-2015 07:28:00 GMT" (variant) *)
839
906
let header = "session=abc; Expires=Wed, 21-Oct-2015 07:28:00 GMT" in
840
907
let cookie_opt =
841
841
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
908
908
+
of_set_cookie_header
909
909
+
~now:(fun () ->
910
910
+
Ptime.of_float_s (Eio.Time.now clock)
911
911
+
|> Option.value ~default:Ptime.epoch)
912
912
+
~domain:"example.com" ~path:"/" header
842
913
in
843
914
Alcotest.(check bool) "FMT4 cookie parsed" true (Option.is_some cookie_opt);
844
915
···
851
922
begin match expected with
852
923
| Some t ->
853
924
Alcotest.(check (option expiration_testable))
854
854
-
"FMT4 expiry correct" (Some (`DateTime t)) (Cookeio.expires cookie)
925
925
+
"FMT4 expiry correct"
926
926
+
(Some (`DateTime t))
927
927
+
(Cookeio.expires cookie)
855
928
| None -> Alcotest.fail "Expected expiry time for FMT4"
856
929
end
857
930
···
863
936
(* Year 95 should become 1995 *)
864
937
let header = "session=abc; Expires=Wed, 21-Oct-95 07:28:00 GMT" in
865
938
let cookie_opt =
866
866
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
939
939
+
of_set_cookie_header
940
940
+
~now:(fun () ->
941
941
+
Ptime.of_float_s (Eio.Time.now clock)
942
942
+
|> Option.value ~default:Ptime.epoch)
943
943
+
~domain:"example.com" ~path:"/" header
867
944
in
868
945
let cookie = Option.get cookie_opt in
869
946
let expected = Ptime.of_date_time ((1995, 10, 21), ((07, 28, 00), 0)) in
870
947
begin match expected with
871
948
| Some t ->
872
949
Alcotest.(check (option expiration_testable))
873
873
-
"year 95 becomes 1995" (Some (`DateTime t)) (Cookeio.expires cookie)
950
950
+
"year 95 becomes 1995"
951
951
+
(Some (`DateTime t))
952
952
+
(Cookeio.expires cookie)
874
953
| None -> Alcotest.fail "Expected expiry time for year 95"
875
954
end;
876
955
877
956
(* Year 69 should become 1969 *)
878
957
let header2 = "session=abc; Expires=Wed, 10-Sep-69 20:00:00 GMT" in
879
958
let cookie_opt2 =
880
880
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header2
959
959
+
of_set_cookie_header
960
960
+
~now:(fun () ->
961
961
+
Ptime.of_float_s (Eio.Time.now clock)
962
962
+
|> Option.value ~default:Ptime.epoch)
963
963
+
~domain:"example.com" ~path:"/" header2
881
964
in
882
965
let cookie2 = Option.get cookie_opt2 in
883
966
let expected2 = Ptime.of_date_time ((1969, 9, 10), ((20, 0, 0), 0)) in
884
967
begin match expected2 with
885
968
| Some t ->
886
969
Alcotest.(check (option expiration_testable))
887
887
-
"year 69 becomes 1969" (Some (`DateTime t)) (Cookeio.expires cookie2)
970
970
+
"year 69 becomes 1969"
971
971
+
(Some (`DateTime t))
972
972
+
(Cookeio.expires cookie2)
888
973
| None -> Alcotest.fail "Expected expiry time for year 69"
889
974
end;
890
975
891
976
(* Year 99 should become 1999 *)
892
977
let header3 = "session=abc; Expires=Thu, 10-Sep-99 20:00:00 GMT" in
893
978
let cookie_opt3 =
894
894
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header3
979
979
+
of_set_cookie_header
980
980
+
~now:(fun () ->
981
981
+
Ptime.of_float_s (Eio.Time.now clock)
982
982
+
|> Option.value ~default:Ptime.epoch)
983
983
+
~domain:"example.com" ~path:"/" header3
895
984
in
896
985
let cookie3 = Option.get cookie_opt3 in
897
986
let expected3 = Ptime.of_date_time ((1999, 9, 10), ((20, 0, 0), 0)) in
898
987
begin match expected3 with
899
988
| Some t ->
900
989
Alcotest.(check (option expiration_testable))
901
901
-
"year 99 becomes 1999" (Some (`DateTime t)) (Cookeio.expires cookie3)
990
990
+
"year 99 becomes 1999"
991
991
+
(Some (`DateTime t))
992
992
+
(Cookeio.expires cookie3)
902
993
| None -> Alcotest.fail "Expected expiry time for year 99"
903
994
end
904
995
···
910
1001
(* Year 25 should become 2025 *)
911
1002
let header = "session=abc; Expires=Wed, 21-Oct-25 07:28:00 GMT" in
912
1003
let cookie_opt =
913
913
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
1004
1004
+
of_set_cookie_header
1005
1005
+
~now:(fun () ->
1006
1006
+
Ptime.of_float_s (Eio.Time.now clock)
1007
1007
+
|> Option.value ~default:Ptime.epoch)
1008
1008
+
~domain:"example.com" ~path:"/" header
914
1009
in
915
1010
let cookie = Option.get cookie_opt in
916
1011
let expected = Ptime.of_date_time ((2025, 10, 21), ((07, 28, 00), 0)) in
917
1012
begin match expected with
918
1013
| Some t ->
919
1014
Alcotest.(check (option expiration_testable))
920
920
-
"year 25 becomes 2025" (Some (`DateTime t)) (Cookeio.expires cookie)
1015
1015
+
"year 25 becomes 2025"
1016
1016
+
(Some (`DateTime t))
1017
1017
+
(Cookeio.expires cookie)
921
1018
| None -> Alcotest.fail "Expected expiry time for year 25"
922
1019
end;
923
1020
924
1021
(* Year 0 should become 2000 *)
925
1022
let header2 = "session=abc; Expires=Fri, 01-Jan-00 00:00:00 GMT" in
926
1023
let cookie_opt2 =
927
927
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header2
1024
1024
+
of_set_cookie_header
1025
1025
+
~now:(fun () ->
1026
1026
+
Ptime.of_float_s (Eio.Time.now clock)
1027
1027
+
|> Option.value ~default:Ptime.epoch)
1028
1028
+
~domain:"example.com" ~path:"/" header2
928
1029
in
929
1030
let cookie2 = Option.get cookie_opt2 in
930
1031
let expected2 = Ptime.of_date_time ((2000, 1, 1), ((0, 0, 0), 0)) in
931
1032
begin match expected2 with
932
1033
| Some t ->
933
1034
Alcotest.(check (option expiration_testable))
934
934
-
"year 0 becomes 2000" (Some (`DateTime t)) (Cookeio.expires cookie2)
1035
1035
+
"year 0 becomes 2000"
1036
1036
+
(Some (`DateTime t))
1037
1037
+
(Cookeio.expires cookie2)
935
1038
| None -> Alcotest.fail "Expected expiry time for year 0"
936
1039
end;
937
1040
938
1041
(* Year 68 should become 2068 *)
939
1042
let header3 = "session=abc; Expires=Thu, 10-Sep-68 20:00:00 GMT" in
940
1043
let cookie_opt3 =
941
941
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header3
1044
1044
+
of_set_cookie_header
1045
1045
+
~now:(fun () ->
1046
1046
+
Ptime.of_float_s (Eio.Time.now clock)
1047
1047
+
|> Option.value ~default:Ptime.epoch)
1048
1048
+
~domain:"example.com" ~path:"/" header3
942
1049
in
943
1050
let cookie3 = Option.get cookie_opt3 in
944
1051
let expected3 = Ptime.of_date_time ((2068, 9, 10), ((20, 0, 0), 0)) in
945
1052
begin match expected3 with
946
1053
| Some t ->
947
1054
Alcotest.(check (option expiration_testable))
948
948
-
"year 68 becomes 2068" (Some (`DateTime t)) (Cookeio.expires cookie3)
1055
1055
+
"year 68 becomes 2068"
1056
1056
+
(Some (`DateTime t))
1057
1057
+
(Cookeio.expires cookie3)
949
1058
| None -> Alcotest.fail "Expected expiry time for year 68"
950
1059
end
951
1060
···
957
1066
(* Ensure RFC 3339 format still works for backward compatibility *)
958
1067
let header = "session=abc; Expires=2025-10-21T07:28:00Z" in
959
1068
let cookie_opt =
960
960
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
1069
1069
+
of_set_cookie_header
1070
1070
+
~now:(fun () ->
1071
1071
+
Ptime.of_float_s (Eio.Time.now clock)
1072
1072
+
|> Option.value ~default:Ptime.epoch)
1073
1073
+
~domain:"example.com" ~path:"/" header
961
1074
in
962
1075
Alcotest.(check bool)
963
1076
"RFC 3339 cookie parsed" true
···
973
1086
match expected with
974
1087
| Ok (time, _, _) ->
975
1088
Alcotest.(check (option expiration_testable))
976
976
-
"RFC 3339 expiry correct" (Some (`DateTime time)) (Cookeio.expires cookie)
1089
1089
+
"RFC 3339 expiry correct"
1090
1090
+
(Some (`DateTime time))
1091
1091
+
(Cookeio.expires cookie)
977
1092
| Error _ -> Alcotest.fail "Failed to parse expected RFC 3339 time"
978
1093
979
1094
let test_invalid_date_format_logs_warning () =
···
984
1099
(* Invalid date format should log a warning but still parse the cookie *)
985
1100
let header = "session=abc; Expires=InvalidDate" in
986
1101
let cookie_opt =
987
987
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
1102
1102
+
of_set_cookie_header
1103
1103
+
~now:(fun () ->
1104
1104
+
Ptime.of_float_s (Eio.Time.now clock)
1105
1105
+
|> Option.value ~default:Ptime.epoch)
1106
1106
+
~domain:"example.com" ~path:"/" header
988
1107
in
989
1108
990
1109
(* Cookie should still be parsed, just without expires *)
···
1016
1135
List.iter
1017
1136
(fun (header, description) ->
1018
1137
let cookie_opt =
1019
1019
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
1138
1138
+
of_set_cookie_header
1139
1139
+
~now:(fun () ->
1140
1140
+
Ptime.of_float_s (Eio.Time.now clock)
1141
1141
+
|> Option.value ~default:Ptime.epoch)
1142
1142
+
~domain:"example.com" ~path:"/" header
1020
1143
in
1021
1144
Alcotest.(check bool)
1022
1145
(description ^ " parsed") true
···
1058
1181
List.iter
1059
1182
(fun (header, description) ->
1060
1183
let cookie_opt =
1061
1061
-
of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/" header
1184
1184
+
of_set_cookie_header
1185
1185
+
~now:(fun () ->
1186
1186
+
Ptime.of_float_s (Eio.Time.now clock)
1187
1187
+
|> Option.value ~default:Ptime.epoch)
1188
1188
+
~domain:"example.com" ~path:"/" header
1062
1189
in
1063
1190
Alcotest.(check bool)
1064
1191
(description ^ " parsed") true
···
1384
1511
let test_partitioned_parsing env =
1385
1512
let clock = Eio.Stdenv.clock env in
1386
1513
1387
1387
-
match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"widget.com" ~path:"/"
1388
1388
-
"id=123; Partitioned; Secure" with
1514
1514
+
match
1515
1515
+
of_set_cookie_header
1516
1516
+
~now:(fun () ->
1517
1517
+
Ptime.of_float_s (Eio.Time.now clock)
1518
1518
+
|> Option.value ~default:Ptime.epoch)
1519
1519
+
~domain:"widget.com" ~path:"/" "id=123; Partitioned; Secure"
1520
1520
+
with
1389
1521
| Some c ->
1390
1522
Alcotest.(check bool) "partitioned flag" true (partitioned c);
1391
1523
Alcotest.(check bool) "secure flag" true (secure c)
···
1393
1525
1394
1526
let test_partitioned_serialization env =
1395
1527
let clock = Eio.Stdenv.clock env in
1396
1396
-
let now = Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch in
1528
1528
+
let now =
1529
1529
+
Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch
1530
1530
+
in
1397
1531
1398
1398
-
let cookie = make ~domain:"widget.com" ~path:"/" ~name:"id" ~value:"123"
1399
1399
-
~secure:true ~partitioned:true
1400
1400
-
~creation_time:now ~last_access:now () in
1532
1532
+
let cookie =
1533
1533
+
make ~domain:"widget.com" ~path:"/" ~name:"id" ~value:"123" ~secure:true
1534
1534
+
~partitioned:true ~creation_time:now ~last_access:now ()
1535
1535
+
in
1401
1536
1402
1537
let header = make_set_cookie_header cookie in
1403
1538
let contains_substring s sub =
···
1415
1550
let clock = Eio.Stdenv.clock env in
1416
1551
1417
1552
(* Partitioned without Secure should be rejected *)
1418
1418
-
match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"widget.com" ~path:"/"
1419
1419
-
"id=123; Partitioned" with
1553
1553
+
match
1554
1554
+
of_set_cookie_header
1555
1555
+
~now:(fun () ->
1556
1556
+
Ptime.of_float_s (Eio.Time.now clock)
1557
1557
+
|> Option.value ~default:Ptime.epoch)
1558
1558
+
~domain:"widget.com" ~path:"/" "id=123; Partitioned"
1559
1559
+
with
1420
1560
| None -> () (* Expected *)
1421
1561
| Some _ -> Alcotest.fail "Should reject Partitioned without Secure"
1422
1562
···
1424
1564
1425
1565
let test_expiration_variants env =
1426
1566
let clock = Eio.Stdenv.clock env in
1427
1427
-
let now = Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch in
1567
1567
+
let now =
1568
1568
+
Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch
1569
1569
+
in
1428
1570
let make_base ~name ?expires () =
1429
1429
-
make ~domain:"ex.com" ~path:"/" ~name ~value:"v"
1430
1430
-
?expires ~creation_time:now ~last_access:now ()
1571
1571
+
make ~domain:"ex.com" ~path:"/" ~name ~value:"v" ?expires ~creation_time:now
1572
1572
+
~last_access:now ()
1431
1573
in
1432
1574
1433
1575
(* No expiration *)
1434
1576
let c1 = make_base ~name:"no_expiry" () in
1435
1435
-
Alcotest.(check (option expiration_testable)) "no expiration"
1436
1436
-
None (expires c1);
1577
1577
+
Alcotest.(check (option expiration_testable))
1578
1578
+
"no expiration" None (expires c1);
1437
1579
1438
1580
(* Session cookie *)
1439
1581
let c2 = make_base ~name:"session" ~expires:`Session () in
1440
1440
-
Alcotest.(check (option expiration_testable)) "session cookie"
1441
1441
-
(Some `Session) (expires c2);
1582
1582
+
Alcotest.(check (option expiration_testable))
1583
1583
+
"session cookie" (Some `Session) (expires c2);
1442
1584
1443
1585
(* Explicit expiration *)
1444
1586
let future = Ptime.add_span now (Ptime.Span.of_int_s 3600) |> Option.get in
···
1451
1593
let clock = Eio.Stdenv.clock env in
1452
1594
1453
1595
(* Expires=0 should parse as Session *)
1454
1454
-
match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/"
1455
1455
-
"id=123; Expires=0" with
1596
1596
+
match
1597
1597
+
of_set_cookie_header
1598
1598
+
~now:(fun () ->
1599
1599
+
Ptime.of_float_s (Eio.Time.now clock)
1600
1600
+
|> Option.value ~default:Ptime.epoch)
1601
1601
+
~domain:"ex.com" ~path:"/" "id=123; Expires=0"
1602
1602
+
with
1456
1603
| Some c ->
1457
1457
-
Alcotest.(check (option expiration_testable)) "expires=0 is session"
1458
1458
-
(Some `Session) (expires c)
1604
1604
+
Alcotest.(check (option expiration_testable))
1605
1605
+
"expires=0 is session" (Some `Session) (expires c)
1459
1606
| None -> Alcotest.fail "Should parse Expires=0"
1460
1607
1461
1608
let test_serialize_expiration_variants env =
1462
1609
let clock = Eio.Stdenv.clock env in
1463
1463
-
let now = Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch in
1610
1610
+
let now =
1611
1611
+
Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch
1612
1612
+
in
1464
1613
let contains_substring s sub =
1465
1614
try
1466
1615
let _ = Str.search_forward (Str.regexp_string sub) s 0 in
···
1469
1618
in
1470
1619
1471
1620
(* Session cookie serialization *)
1472
1472
-
let c1 = make ~domain:"ex.com" ~path:"/" ~name:"s" ~value:"v"
1473
1473
-
~expires:`Session ~creation_time:now ~last_access:now () in
1621
1621
+
let c1 =
1622
1622
+
make ~domain:"ex.com" ~path:"/" ~name:"s" ~value:"v" ~expires:`Session
1623
1623
+
~creation_time:now ~last_access:now ()
1624
1624
+
in
1474
1625
let h1 = make_set_cookie_header c1 in
1475
1626
let has_expires = contains_substring h1 "Expires=" in
1476
1627
Alcotest.(check bool) "session has Expires" true has_expires;
1477
1628
1478
1629
(* DateTime serialization *)
1479
1630
let future = Ptime.add_span now (Ptime.Span.of_int_s 3600) |> Option.get in
1480
1480
-
let c2 = make ~domain:"ex.com" ~path:"/" ~name:"p" ~value:"v"
1481
1481
-
~expires:(`DateTime future) ~creation_time:now ~last_access:now () in
1631
1631
+
let c2 =
1632
1632
+
make ~domain:"ex.com" ~path:"/" ~name:"p" ~value:"v"
1633
1633
+
~expires:(`DateTime future) ~creation_time:now ~last_access:now ()
1634
1634
+
in
1482
1635
let h2 = make_set_cookie_header c2 in
1483
1636
let has_expires2 = contains_substring h2 "Expires=" in
1484
1637
Alcotest.(check bool) "datetime has Expires" true has_expires2
···
1487
1640
1488
1641
let test_quoted_cookie_values env =
1489
1642
let clock = Eio.Stdenv.clock env in
1490
1490
-
let test_cases = [
1491
1491
-
("name=value", "value", "value");
1492
1492
-
("name=\"value\"", "\"value\"", "value");
1493
1493
-
("name=\"partial", "\"partial", "\"partial");
1494
1494
-
("name=\"val\"\"", "\"val\"\"", "val\"");
1495
1495
-
("name=val\"", "val\"", "val\"");
1496
1496
-
("name=\"\"", "\"\"", "");
1497
1497
-
] in
1643
1643
+
let test_cases =
1644
1644
+
[
1645
1645
+
("name=value", "value", "value");
1646
1646
+
("name=\"value\"", "\"value\"", "value");
1647
1647
+
("name=\"partial", "\"partial", "\"partial");
1648
1648
+
("name=\"val\"\"", "\"val\"\"", "val\"");
1649
1649
+
("name=val\"", "val\"", "val\"");
1650
1650
+
("name=\"\"", "\"\"", "");
1651
1651
+
]
1652
1652
+
in
1498
1653
1499
1499
-
List.iter (fun (input, expected_raw, expected_trimmed) ->
1500
1500
-
match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/" input with
1501
1501
-
| Some c ->
1502
1502
-
Alcotest.(check string)
1503
1503
-
(Printf.sprintf "raw value for %s" input) expected_raw (value c);
1504
1504
-
Alcotest.(check string)
1505
1505
-
(Printf.sprintf "trimmed value for %s" input) expected_trimmed
1506
1506
-
(value_trimmed c)
1507
1507
-
| None -> Alcotest.fail ("Parse failed: " ^ input)
1508
1508
-
) test_cases
1654
1654
+
List.iter
1655
1655
+
(fun (input, expected_raw, expected_trimmed) ->
1656
1656
+
match
1657
1657
+
of_set_cookie_header
1658
1658
+
~now:(fun () ->
1659
1659
+
Ptime.of_float_s (Eio.Time.now clock)
1660
1660
+
|> Option.value ~default:Ptime.epoch)
1661
1661
+
~domain:"ex.com" ~path:"/" input
1662
1662
+
with
1663
1663
+
| Some c ->
1664
1664
+
Alcotest.(check string)
1665
1665
+
(Printf.sprintf "raw value for %s" input)
1666
1666
+
expected_raw (value c);
1667
1667
+
Alcotest.(check string)
1668
1668
+
(Printf.sprintf "trimmed value for %s" input)
1669
1669
+
expected_trimmed (value_trimmed c)
1670
1670
+
| None -> Alcotest.fail ("Parse failed: " ^ input))
1671
1671
+
test_cases
1509
1672
1510
1673
let test_trimmed_value_not_used_for_equality env =
1511
1674
let clock = Eio.Stdenv.clock env in
1512
1675
1513
1513
-
match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/"
1514
1514
-
"name=\"value\"" with
1515
1515
-
| Some c1 ->
1516
1516
-
begin match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/"
1517
1517
-
"name=value" with
1676
1676
+
match
1677
1677
+
of_set_cookie_header
1678
1678
+
~now:(fun () ->
1679
1679
+
Ptime.of_float_s (Eio.Time.now clock)
1680
1680
+
|> Option.value ~default:Ptime.epoch)
1681
1681
+
~domain:"ex.com" ~path:"/" "name=\"value\""
1682
1682
+
with
1683
1683
+
| Some c1 -> begin
1684
1684
+
match
1685
1685
+
of_set_cookie_header
1686
1686
+
~now:(fun () ->
1687
1687
+
Ptime.of_float_s (Eio.Time.now clock)
1688
1688
+
|> Option.value ~default:Ptime.epoch)
1689
1689
+
~domain:"ex.com" ~path:"/" "name=value"
1690
1690
+
with
1518
1691
| Some c2 ->
1519
1692
(* Different raw values *)
1520
1520
-
Alcotest.(check bool) "different raw values" false
1693
1693
+
Alcotest.(check bool)
1694
1694
+
"different raw values" false
1521
1695
(value c1 = value c2);
1522
1696
(* Same trimmed values *)
1523
1523
-
Alcotest.(check string) "same trimmed values"
1524
1524
-
(value_trimmed c1) (value_trimmed c2)
1697
1697
+
Alcotest.(check string)
1698
1698
+
"same trimmed values" (value_trimmed c1) (value_trimmed c2)
1525
1699
| None -> Alcotest.fail "Parse failed for unquoted"
1526
1526
-
end
1700
1700
+
end
1527
1701
| None -> Alcotest.fail "Parse failed for quoted"
1528
1702
1529
1703
(* Priority 2.4: Cookie Header Parsing *)
1530
1704
1531
1705
let test_cookie_header_parsing_basic env =
1532
1706
let clock = Eio.Stdenv.clock env in
1533
1533
-
let results = of_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/"
1534
1534
-
"session=abc123; theme=dark; lang=en" in
1707
1707
+
let results =
1708
1708
+
of_cookie_header
1709
1709
+
~now:(fun () ->
1710
1710
+
Ptime.of_float_s (Eio.Time.now clock)
1711
1711
+
|> Option.value ~default:Ptime.epoch)
1712
1712
+
~domain:"ex.com" ~path:"/" "session=abc123; theme=dark; lang=en"
1713
1713
+
in
1535
1714
1536
1715
let cookies = List.filter_map Result.to_option results in
1537
1716
Alcotest.(check int) "parsed 3 cookies" 3 (List.length cookies);
···
1544
1723
let test_cookie_header_defaults env =
1545
1724
let clock = Eio.Stdenv.clock env in
1546
1725
1547
1547
-
match of_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"example.com" ~path:"/app"
1548
1548
-
"session=xyz" with
1549
1549
-
| [Ok c] ->
1726
1726
+
match
1727
1727
+
of_cookie_header
1728
1728
+
~now:(fun () ->
1729
1729
+
Ptime.of_float_s (Eio.Time.now clock)
1730
1730
+
|> Option.value ~default:Ptime.epoch)
1731
1731
+
~domain:"example.com" ~path:"/app" "session=xyz"
1732
1732
+
with
1733
1733
+
| [ Ok c ] ->
1550
1734
(* Domain and path from request context *)
1551
1735
Alcotest.(check string) "domain from context" "example.com" (domain c);
1552
1736
Alcotest.(check string) "path from context" "/app" (path c);
···
1557
1741
Alcotest.(check bool) "partitioned default" false (partitioned c);
1558
1742
1559
1743
(* Optional attributes default to None *)
1560
1560
-
Alcotest.(check (option expiration_testable)) "no expiration"
1561
1561
-
None (expires c);
1562
1562
-
Alcotest.(check (option span_testable)) "no max_age"
1563
1563
-
None (max_age c);
1564
1564
-
Alcotest.(check (option same_site_testable)) "no same_site"
1565
1565
-
None (same_site c)
1744
1744
+
Alcotest.(check (option expiration_testable))
1745
1745
+
"no expiration" None (expires c);
1746
1746
+
Alcotest.(check (option span_testable)) "no max_age" None (max_age c);
1747
1747
+
Alcotest.(check (option same_site_testable))
1748
1748
+
"no same_site" None (same_site c)
1566
1749
| _ -> Alcotest.fail "Should parse single cookie"
1567
1750
1568
1751
let test_cookie_header_edge_cases env =
1569
1752
let clock = Eio.Stdenv.clock env in
1570
1753
1571
1754
let test input expected_count description =
1572
1572
-
let results = of_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/" input in
1755
1755
+
let results =
1756
1756
+
of_cookie_header
1757
1757
+
~now:(fun () ->
1758
1758
+
Ptime.of_float_s (Eio.Time.now clock)
1759
1759
+
|> Option.value ~default:Ptime.epoch)
1760
1760
+
~domain:"ex.com" ~path:"/" input
1761
1761
+
in
1573
1762
let cookies = List.filter_map Result.to_option results in
1574
1763
Alcotest.(check int) description expected_count (List.length cookies)
1575
1764
in
···
1584
1773
let clock = Eio.Stdenv.clock env in
1585
1774
1586
1775
(* Mix of valid and invalid cookies *)
1587
1587
-
let results = of_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/"
1588
1588
-
"valid=1;=noname;valid2=2" in
1776
1776
+
let results =
1777
1777
+
of_cookie_header
1778
1778
+
~now:(fun () ->
1779
1779
+
Ptime.of_float_s (Eio.Time.now clock)
1780
1780
+
|> Option.value ~default:Ptime.epoch)
1781
1781
+
~domain:"ex.com" ~path:"/" "valid=1;=noname;valid2=2"
1782
1782
+
in
1589
1783
1590
1784
Alcotest.(check int) "total results" 3 (List.length results);
1591
1785
···
1606
1800
| Error msg ->
1607
1801
let has_name = contains_substring msg "name" in
1608
1802
let has_empty = contains_substring msg "empty" in
1609
1609
-
Alcotest.(check bool) "error mentions name or empty" true
1610
1610
-
(has_name || has_empty)
1803
1803
+
Alcotest.(check bool)
1804
1804
+
"error mentions name or empty" true (has_name || has_empty)
1611
1805
| Ok _ -> Alcotest.fail "Expected error"
1612
1806
end
1613
1807
···
1615
1809
1616
1810
let test_max_age_and_expires_both_present env =
1617
1811
let clock = Eio.Stdenv.clock env in
1618
1618
-
let now = Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch in
1812
1812
+
let now =
1813
1813
+
Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch
1814
1814
+
in
1619
1815
let future = Ptime.add_span now (Ptime.Span.of_int_s 7200) |> Option.get in
1620
1816
1621
1817
(* Create cookie with both *)
1622
1622
-
let cookie = make ~domain:"ex.com" ~path:"/" ~name:"dual" ~value:"val"
1623
1623
-
~max_age:(Ptime.Span.of_int_s 3600)
1624
1624
-
~expires:(`DateTime future)
1625
1625
-
~creation_time:now ~last_access:now () in
1818
1818
+
let cookie =
1819
1819
+
make ~domain:"ex.com" ~path:"/" ~name:"dual" ~value:"val"
1820
1820
+
~max_age:(Ptime.Span.of_int_s 3600) ~expires:(`DateTime future)
1821
1821
+
~creation_time:now ~last_access:now ()
1822
1822
+
in
1626
1823
1627
1824
(* Both should be present *)
1628
1825
begin match max_age cookie with
1629
1629
-
| Some span ->
1630
1630
-
begin match Ptime.Span.to_int_s span with
1826
1826
+
| Some span -> begin
1827
1827
+
match Ptime.Span.to_int_s span with
1631
1828
| Some s ->
1632
1829
Alcotest.(check int64) "max_age present" 3600L (Int64.of_int s)
1633
1830
| None -> Alcotest.fail "max_age span could not be converted to int"
1634
1634
-
end
1831
1831
+
end
1635
1832
| None -> Alcotest.fail "max_age should be present"
1636
1833
end;
1637
1834
···
1657
1854
let clock = Eio.Stdenv.clock env in
1658
1855
1659
1856
(* Parse Set-Cookie with both attributes *)
1660
1660
-
match of_set_cookie_header ~now:(fun () -> Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch) ~domain:"ex.com" ~path:"/"
1661
1661
-
"id=123; Max-Age=3600; Expires=Wed, 21 Oct 2025 07:28:00 GMT" with
1857
1857
+
match
1858
1858
+
of_set_cookie_header
1859
1859
+
~now:(fun () ->
1860
1860
+
Ptime.of_float_s (Eio.Time.now clock)
1861
1861
+
|> Option.value ~default:Ptime.epoch)
1862
1862
+
~domain:"ex.com" ~path:"/"
1863
1863
+
"id=123; Max-Age=3600; Expires=Wed, 21 Oct 2025 07:28:00 GMT"
1864
1864
+
with
1662
1865
| Some c ->
1663
1866
(* Both should be stored *)
1664
1867
begin match max_age c with
1665
1665
-
| Some span ->
1666
1666
-
begin match Ptime.Span.to_int_s span with
1868
1868
+
| Some span -> begin
1869
1869
+
match Ptime.Span.to_int_s span with
1667
1870
| Some s ->
1668
1871
Alcotest.(check int64) "max_age parsed" 3600L (Int64.of_int s)
1669
1872
| None -> Alcotest.fail "max_age span could not be converted to int"
1670
1670
-
end
1873
1873
+
end
1671
1874
| None -> Alcotest.fail "max_age should be parsed"
1672
1875
end;
1673
1876