···4040 secure : bool;
4141 http_only : bool;
4242 partitioned : bool;
4343+ host_only : bool;
4344 expires : Expiration.t option;
4445 max_age : Ptime.Span.t option;
4546 same_site : SameSite.t option;
···6768let secure cookie = cookie.secure
6869let http_only cookie = cookie.http_only
6970let partitioned cookie = cookie.partitioned
7171+let host_only cookie = cookie.host_only
7072let expires cookie = cookie.expires
7173let max_age cookie = cookie.max_age
7274let same_site cookie = cookie.same_site
···7476let last_access cookie = cookie.last_access
75777678let make ~domain ~path ~name ~value ?(secure = false) ?(http_only = false)
7777- ?expires ?max_age ?same_site ?(partitioned = false) ~creation_time
7878- ~last_access () =
7979+ ?expires ?max_age ?same_site ?(partitioned = false) ?(host_only = false)
8080+ ~creation_time ~last_access () =
7981 {
8082 domain;
8183 path;
···8486 secure;
8587 http_only;
8688 partitioned;
8989+ host_only;
8790 expires;
8891 max_age;
8992 same_site;
···304307 in
305308 samesite_valid && partitioned_valid
306309307307-(** Build final cookie from name/value and accumulated attributes *)
310310+(** Build final cookie from name/value and accumulated attributes.
311311+ Per RFC 6265 Section 5.3:
312312+ - If Domain attribute is present, host_only = false, domain = attribute value
313313+ - If Domain attribute is absent, host_only = true, domain = request host *)
308314let build_cookie ~request_domain ~request_path ~name ~value attrs ~now =
309309- let domain =
310310- normalize_domain (Option.value attrs.domain ~default:request_domain)
315315+ let host_only, domain =
316316+ match attrs.domain with
317317+ | Some d -> (false, normalize_domain d)
318318+ | None -> (true, request_domain)
311319 in
312320 let path = Option.value attrs.path ~default:request_path in
313321 make ~domain ~path ~name ~value ~secure:attrs.secure
314322 ~http_only:attrs.http_only ?expires:attrs.expires ?max_age:attrs.max_age
315315- ?same_site:attrs.same_site ~partitioned:attrs.partitioned ~creation_time:now
316316- ~last_access:now ()
323323+ ?same_site:attrs.same_site ~partitioned:attrs.partitioned ~host_only
324324+ ~creation_time:now ~last_access:now ()
317325318326(** {1 Pretty Printing} *)
319327320328let pp ppf cookie =
321329 Format.fprintf ppf
322330 "@[<hov 2>{ name=%S;@ value=%S;@ domain=%S;@ path=%S;@ secure=%b;@ \
323323- http_only=%b;@ partitioned=%b;@ expires=%a;@ max_age=%a;@ same_site=%a \
324324- }@]"
331331+ http_only=%b;@ partitioned=%b;@ host_only=%b;@ expires=%a;@ max_age=%a;@ \
332332+ same_site=%a }@]"
325333 (name cookie) (value cookie) (domain cookie) (path cookie) (secure cookie)
326326- (http_only cookie) (partitioned cookie)
334334+ (http_only cookie) (partitioned cookie) (host_only cookie)
327335 (Format.pp_print_option Expiration.pp)
328336 (expires cookie)
329337 (Format.pp_print_option Ptime.Span.pp)
···410418 |> String.trim
411419 in
412420 let current_time = now () in
413413- (* Create cookie with defaults from Cookie header context *)
421421+ (* Create cookie with defaults from Cookie header context.
422422+ Cookies from Cookie headers have host_only=true since we don't
423423+ know if they originally had a Domain attribute. *)
414424 let cookie =
415425 make ~domain ~path ~name:cookie_name ~value:cookie_value
416416- ~secure:false ~http_only:false ~partitioned:false
426426+ ~secure:false ~http_only:false ~partitioned:false ~host_only:true
417427 ~creation_time:current_time ~last_access:current_time ()
418428 in
419429 Ok cookie)
+20
lib/core/cookeio.mli
···115115 privacy-preserving third-party cookie functionality. Partitioned cookies
116116 must always be Secure. *)
117117118118+val host_only : t -> bool
119119+(** Check if cookie has the host-only flag set.
120120+121121+ Per RFC 6265 Section 5.3:
122122+ - If the Set-Cookie header included a Domain attribute, host_only is false
123123+ and the cookie matches the domain and all subdomains.
124124+ - If no Domain attribute was present, host_only is true and the cookie
125125+ only matches the exact request host.
126126+127127+ Example:
128128+ - Cookie set on "example.com" with Domain=example.com: host_only=false,
129129+ matches example.com and sub.example.com
130130+ - Cookie set on "example.com" without Domain attribute: host_only=true,
131131+ matches only example.com, not sub.example.com *)
132132+118133val expires : t -> Expiration.t option
119134(** Get the expiration attribute if set.
120135···152167 ?max_age:Ptime.Span.t ->
153168 ?same_site:SameSite.t ->
154169 ?partitioned:bool ->
170170+ ?host_only:bool ->
155171 creation_time:Ptime.t ->
156172 last_access:Ptime.t ->
157173 unit ->
158174 t
159175(** Create a new cookie with the given attributes.
176176+177177+ @param host_only If true, the cookie only matches the exact domain (no
178178+ subdomains). Defaults to false. Per RFC 6265, this should be true when no
179179+ Domain attribute was present in the Set-Cookie header.
160180161181 Note: If [partitioned] is [true], the cookie must also be [secure]. Invalid
162182 combinations will result in validation errors. *)
+26-14
lib/jar/cookeio_jar.ml
···3434 String.sub domain 1 (String.length domain - 1)
3535 | _ -> domain
36363737-let domain_matches cookie_domain request_domain =
3838- (* Cookie domains are stored without leading dots per RFC 6265.
3939- A cookie with domain "example.com" should match both "example.com" (exact)
4040- and "sub.example.com" (subdomain). *)
3737+let domain_matches ~host_only cookie_domain request_domain =
3838+ (* RFC 6265 Section 5.4: Domain matching for Cookie header.
3939+ Cookie domains are stored without leading dots per RFC 6265. *)
4140 request_domain = cookie_domain
4242- || String.ends_with ~suffix:("." ^ cookie_domain) request_domain
4141+ || (not host_only
4242+ && String.ends_with ~suffix:("." ^ cookie_domain) request_domain)
43434444let path_matches cookie_path request_path =
4545- (* Cookie path /foo matches /foo, /foo/, /foo/bar *)
4646- String.starts_with ~prefix:cookie_path request_path
4545+ (* RFC 6265 Section 5.1.4: A request-path path-matches a cookie-path if:
4646+ 1. The cookie-path and the request-path are identical, OR
4747+ 2. The cookie-path is a prefix of request-path AND cookie-path ends with "/", OR
4848+ 3. The cookie-path is a prefix of request-path AND the first char of
4949+ request-path not in cookie-path is "/" *)
5050+ if cookie_path = request_path then true
5151+ else if String.starts_with ~prefix:cookie_path request_path then
5252+ let cookie_len = String.length cookie_path in
5353+ String.ends_with ~suffix:"/" cookie_path
5454+ || (String.length request_path > cookie_len && request_path.[cookie_len] = '/')
5555+ else false
47564857(** {1 HTTP Date Parsing} *)
4958let is_expired cookie clock =
···123132 ~http_only:(Cookeio.http_only cookie) ~expires:(`DateTime past_expiry)
124133 ~max_age:(Ptime.Span.of_int_s 0) ?same_site:(Cookeio.same_site cookie)
125134 ~partitioned:(Cookeio.partitioned cookie)
135135+ ~host_only:(Cookeio.host_only cookie)
126136 ~creation_time:now ~last_access:now ()
127137128138let remove jar ~clock cookie =
···184194 (fun cookie ->
185195 Cookeio.value cookie <> ""
186196 (* Exclude removal cookies *)
187187- && domain_matches (Cookeio.domain cookie) request_domain
197197+ && domain_matches ~host_only:(Cookeio.host_only cookie)
198198+ (Cookeio.domain cookie) request_domain
188199 && path_matches (Cookeio.path cookie) request_path
189200 && ((not (Cookeio.secure cookie)) || is_secure))
190201 unique_cookies
···204215 ?expires:(Cookeio.expires c) ?max_age:(Cookeio.max_age c)
205216 ?same_site:(Cookeio.same_site c)
206217 ~partitioned:(Cookeio.partitioned c)
218218+ ~host_only:(Cookeio.host_only c)
207219 ~creation_time:(Cookeio.creation_time c) ~last_access:now ()
208220 else c)
209221 cookies
···319331320332 List.iter
321333 (fun cookie ->
322322- let include_subdomains =
323323- if String.starts_with ~prefix:"." (Cookeio.domain cookie) then "TRUE"
324324- else "FALSE"
325325- in
334334+ (* Mozilla format: include_subdomains=TRUE means host_only=false *)
335335+ let include_subdomains = if Cookeio.host_only cookie then "FALSE" else "TRUE" in
326336 let secure_flag = if Cookeio.secure cookie then "TRUE" else "FALSE" in
327337 let expires_str =
328338 match Cookeio.expires cookie with
···357367 let line = String.trim line in
358368 if line <> "" && not (String.starts_with ~prefix:"#" line) then
359369 match String.split_on_char '\t' line with
360360- | [ domain; _include_subdomains; path; secure; expires; name; value ] ->
370370+ | [ domain; include_subdomains; path; secure; expires; name; value ] ->
361371 let now =
362372 Ptime.of_float_s (Eio.Time.now clock)
363373 |> Option.value ~default:Ptime.epoch
···370380 | Some t -> Some (`DateTime t)
371381 | None -> None
372382 in
383383+ (* Mozilla format: include_subdomains=TRUE means host_only=false *)
384384+ let host_only = include_subdomains <> "TRUE" in
373385374386 let cookie =
375387 Cookeio.make ~domain:(normalize_domain domain) ~path ~name ~value
376388 ~secure:(secure = "TRUE") ~http_only:false ?expires
377377- ?max_age:None ?same_site:None ~partitioned:false
389389+ ?max_age:None ?same_site:None ~partitioned:false ~host_only
378390 ~creation_time:now ~last_access:now ()
379391 in
380392 add_original jar cookie;
+348
test/test_cookeio.ml
···18851885 end
18861886 | None -> Alcotest.fail "Should parse cookie with both attributes"
1887188718881888+(* ============================================================================ *)
18891889+(* Host-Only Flag Tests (RFC 6265 Section 5.3) *)
18901890+(* ============================================================================ *)
18911891+18921892+let test_host_only_without_domain_attribute () =
18931893+ Eio_mock.Backend.run @@ fun () ->
18941894+ let clock = Eio_mock.Clock.make () in
18951895+ Eio_mock.Clock.set_time clock 1000.0;
18961896+18971897+ (* Cookie without Domain attribute should have host_only=true *)
18981898+ let header = "session=abc123; Secure; HttpOnly" in
18991899+ let cookie_opt =
19001900+ of_set_cookie_header
19011901+ ~now:(fun () ->
19021902+ Ptime.of_float_s (Eio.Time.now clock)
19031903+ |> Option.value ~default:Ptime.epoch)
19041904+ ~domain:"example.com" ~path:"/" header
19051905+ in
19061906+ Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
19071907+ let cookie = Option.get cookie_opt in
19081908+ Alcotest.(check bool) "host_only is true" true (Cookeio.host_only cookie);
19091909+ Alcotest.(check string) "domain is request host" "example.com" (Cookeio.domain cookie)
19101910+19111911+let test_host_only_with_domain_attribute () =
19121912+ Eio_mock.Backend.run @@ fun () ->
19131913+ let clock = Eio_mock.Clock.make () in
19141914+ Eio_mock.Clock.set_time clock 1000.0;
19151915+19161916+ (* Cookie with Domain attribute should have host_only=false *)
19171917+ let header = "session=abc123; Domain=example.com; Secure" in
19181918+ let cookie_opt =
19191919+ of_set_cookie_header
19201920+ ~now:(fun () ->
19211921+ Ptime.of_float_s (Eio.Time.now clock)
19221922+ |> Option.value ~default:Ptime.epoch)
19231923+ ~domain:"example.com" ~path:"/" header
19241924+ in
19251925+ Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
19261926+ let cookie = Option.get cookie_opt in
19271927+ Alcotest.(check bool) "host_only is false" false (Cookeio.host_only cookie);
19281928+ Alcotest.(check string) "domain is attribute value" "example.com" (Cookeio.domain cookie)
19291929+19301930+let test_host_only_with_dotted_domain_attribute () =
19311931+ Eio_mock.Backend.run @@ fun () ->
19321932+ let clock = Eio_mock.Clock.make () in
19331933+ Eio_mock.Clock.set_time clock 1000.0;
19341934+19351935+ (* Cookie with .domain should have host_only=false and normalized domain *)
19361936+ let header = "session=abc123; Domain=.example.com" in
19371937+ let cookie_opt =
19381938+ of_set_cookie_header
19391939+ ~now:(fun () ->
19401940+ Ptime.of_float_s (Eio.Time.now clock)
19411941+ |> Option.value ~default:Ptime.epoch)
19421942+ ~domain:"example.com" ~path:"/" header
19431943+ in
19441944+ Alcotest.(check bool) "cookie parsed" true (Option.is_some cookie_opt);
19451945+ let cookie = Option.get cookie_opt in
19461946+ Alcotest.(check bool) "host_only is false" false (Cookeio.host_only cookie);
19471947+ Alcotest.(check string) "domain normalized" "example.com" (Cookeio.domain cookie)
19481948+19491949+let test_host_only_domain_matching () =
19501950+ Eio_mock.Backend.run @@ fun () ->
19511951+ let clock = Eio_mock.Clock.make () in
19521952+ Eio_mock.Clock.set_time clock 1000.0;
19531953+19541954+ let jar = create () in
19551955+19561956+ (* Add a host-only cookie (no Domain attribute) *)
19571957+ let host_only_cookie =
19581958+ Cookeio.make ~domain:"example.com" ~path:"/" ~name:"host_only" ~value:"val1"
19591959+ ~secure:false ~http_only:false ~host_only:true
19601960+ ~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
19611961+ ~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
19621962+ in
19631963+ add_cookie jar host_only_cookie;
19641964+19651965+ (* Add a domain cookie (with Domain attribute) *)
19661966+ let domain_cookie =
19671967+ Cookeio.make ~domain:"example.com" ~path:"/" ~name:"domain" ~value:"val2"
19681968+ ~secure:false ~http_only:false ~host_only:false
19691969+ ~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
19701970+ ~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
19711971+ in
19721972+ add_cookie jar domain_cookie;
19731973+19741974+ (* Both cookies should match exact domain *)
19751975+ let cookies_exact =
19761976+ get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
19771977+ in
19781978+ Alcotest.(check int) "both match exact domain" 2 (List.length cookies_exact);
19791979+19801980+ (* Only domain cookie should match subdomain *)
19811981+ let cookies_sub =
19821982+ get_cookies jar ~clock ~domain:"sub.example.com" ~path:"/" ~is_secure:false
19831983+ in
19841984+ Alcotest.(check int) "only domain cookie matches subdomain" 1 (List.length cookies_sub);
19851985+ let sub_cookie = List.hd cookies_sub in
19861986+ Alcotest.(check string) "subdomain match is domain cookie" "domain" (Cookeio.name sub_cookie)
19871987+19881988+let test_host_only_cookie_header_parsing () =
19891989+ Eio_mock.Backend.run @@ fun () ->
19901990+ let clock = Eio_mock.Clock.make () in
19911991+ Eio_mock.Clock.set_time clock 1000.0;
19921992+19931993+ (* Cookies from Cookie header should have host_only=true *)
19941994+ let results =
19951995+ of_cookie_header
19961996+ ~now:(fun () ->
19971997+ Ptime.of_float_s (Eio.Time.now clock)
19981998+ |> Option.value ~default:Ptime.epoch)
19991999+ ~domain:"example.com" ~path:"/" "session=abc; theme=dark"
20002000+ in
20012001+ let cookies = List.filter_map Result.to_option results in
20022002+ Alcotest.(check int) "parsed 2 cookies" 2 (List.length cookies);
20032003+ List.iter (fun c ->
20042004+ Alcotest.(check bool)
20052005+ ("host_only is true for " ^ Cookeio.name c)
20062006+ true (Cookeio.host_only c)
20072007+ ) cookies
20082008+20092009+let test_host_only_mozilla_format_round_trip () =
20102010+ Eio_mock.Backend.run @@ fun () ->
20112011+ let clock = Eio_mock.Clock.make () in
20122012+ Eio_mock.Clock.set_time clock 1000.0;
20132013+20142014+ let jar = create () in
20152015+20162016+ (* Add host-only cookie *)
20172017+ let host_only =
20182018+ Cookeio.make ~domain:"example.com" ~path:"/" ~name:"hostonly" ~value:"v1"
20192019+ ~secure:false ~http_only:false ~host_only:true
20202020+ ~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
20212021+ ~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
20222022+ in
20232023+ add_cookie jar host_only;
20242024+20252025+ (* Add domain cookie *)
20262026+ let domain_cookie =
20272027+ Cookeio.make ~domain:"example.com" ~path:"/" ~name:"domain" ~value:"v2"
20282028+ ~secure:false ~http_only:false ~host_only:false
20292029+ ~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
20302030+ ~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
20312031+ in
20322032+ add_cookie jar domain_cookie;
20332033+20342034+ (* Round trip through Mozilla format *)
20352035+ let mozilla = to_mozilla_format jar in
20362036+ let jar2 = from_mozilla_format ~clock mozilla in
20372037+ let cookies = get_all_cookies jar2 in
20382038+20392039+ Alcotest.(check int) "2 cookies after round trip" 2 (List.length cookies);
20402040+20412041+ let find name_val = List.find (fun c -> Cookeio.name c = name_val) cookies in
20422042+ Alcotest.(check bool) "hostonly preserved" true (Cookeio.host_only (find "hostonly"));
20432043+ Alcotest.(check bool) "domain preserved" false (Cookeio.host_only (find "domain"))
20442044+20452045+(* ============================================================================ *)
20462046+(* Path Matching Tests (RFC 6265 Section 5.1.4) *)
20472047+(* ============================================================================ *)
20482048+20492049+let test_path_matching_identical () =
20502050+ Eio_mock.Backend.run @@ fun () ->
20512051+ let clock = Eio_mock.Clock.make () in
20522052+ Eio_mock.Clock.set_time clock 1000.0;
20532053+20542054+ let jar = create () in
20552055+ let cookie =
20562056+ Cookeio.make ~domain:"example.com" ~path:"/foo" ~name:"test" ~value:"val"
20572057+ ~secure:false ~http_only:false
20582058+ ~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
20592059+ ~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
20602060+ in
20612061+ add_cookie jar cookie;
20622062+20632063+ (* Identical path should match *)
20642064+ let cookies =
20652065+ get_cookies jar ~clock ~domain:"example.com" ~path:"/foo" ~is_secure:false
20662066+ in
20672067+ Alcotest.(check int) "identical path matches" 1 (List.length cookies)
20682068+20692069+let test_path_matching_with_trailing_slash () =
20702070+ Eio_mock.Backend.run @@ fun () ->
20712071+ let clock = Eio_mock.Clock.make () in
20722072+ Eio_mock.Clock.set_time clock 1000.0;
20732073+20742074+ let jar = create () in
20752075+ let cookie =
20762076+ Cookeio.make ~domain:"example.com" ~path:"/foo/" ~name:"test" ~value:"val"
20772077+ ~secure:false ~http_only:false
20782078+ ~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
20792079+ ~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
20802080+ in
20812081+ add_cookie jar cookie;
20822082+20832083+ (* Cookie path /foo/ should match /foo/bar *)
20842084+ let cookies =
20852085+ get_cookies jar ~clock ~domain:"example.com" ~path:"/foo/bar" ~is_secure:false
20862086+ in
20872087+ Alcotest.(check int) "/foo/ matches /foo/bar" 1 (List.length cookies);
20882088+20892089+ (* Cookie path /foo/ should match /foo/ *)
20902090+ let cookies2 =
20912091+ get_cookies jar ~clock ~domain:"example.com" ~path:"/foo/" ~is_secure:false
20922092+ in
20932093+ Alcotest.(check int) "/foo/ matches /foo/" 1 (List.length cookies2)
20942094+20952095+let test_path_matching_prefix_with_slash () =
20962096+ Eio_mock.Backend.run @@ fun () ->
20972097+ let clock = Eio_mock.Clock.make () in
20982098+ Eio_mock.Clock.set_time clock 1000.0;
20992099+21002100+ let jar = create () in
21012101+ let cookie =
21022102+ Cookeio.make ~domain:"example.com" ~path:"/foo" ~name:"test" ~value:"val"
21032103+ ~secure:false ~http_only:false
21042104+ ~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
21052105+ ~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
21062106+ in
21072107+ add_cookie jar cookie;
21082108+21092109+ (* Cookie path /foo should match /foo/bar (next char is /) *)
21102110+ let cookies =
21112111+ get_cookies jar ~clock ~domain:"example.com" ~path:"/foo/bar" ~is_secure:false
21122112+ in
21132113+ Alcotest.(check int) "/foo matches /foo/bar" 1 (List.length cookies);
21142114+21152115+ (* Cookie path /foo should match /foo/ *)
21162116+ let cookies2 =
21172117+ get_cookies jar ~clock ~domain:"example.com" ~path:"/foo/" ~is_secure:false
21182118+ in
21192119+ Alcotest.(check int) "/foo matches /foo/" 1 (List.length cookies2)
21202120+21212121+let test_path_matching_no_false_prefix () =
21222122+ Eio_mock.Backend.run @@ fun () ->
21232123+ let clock = Eio_mock.Clock.make () in
21242124+ Eio_mock.Clock.set_time clock 1000.0;
21252125+21262126+ let jar = create () in
21272127+ let cookie =
21282128+ Cookeio.make ~domain:"example.com" ~path:"/foo" ~name:"test" ~value:"val"
21292129+ ~secure:false ~http_only:false
21302130+ ~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
21312131+ ~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
21322132+ in
21332133+ add_cookie jar cookie;
21342134+21352135+ (* Cookie path /foo should NOT match /foobar (no / separator) *)
21362136+ let cookies =
21372137+ get_cookies jar ~clock ~domain:"example.com" ~path:"/foobar" ~is_secure:false
21382138+ in
21392139+ Alcotest.(check int) "/foo does NOT match /foobar" 0 (List.length cookies);
21402140+21412141+ (* Cookie path /foo should NOT match /foob *)
21422142+ let cookies2 =
21432143+ get_cookies jar ~clock ~domain:"example.com" ~path:"/foob" ~is_secure:false
21442144+ in
21452145+ Alcotest.(check int) "/foo does NOT match /foob" 0 (List.length cookies2)
21462146+21472147+let test_path_matching_root () =
21482148+ Eio_mock.Backend.run @@ fun () ->
21492149+ let clock = Eio_mock.Clock.make () in
21502150+ Eio_mock.Clock.set_time clock 1000.0;
21512151+21522152+ let jar = create () in
21532153+ let cookie =
21542154+ Cookeio.make ~domain:"example.com" ~path:"/" ~name:"test" ~value:"val"
21552155+ ~secure:false ~http_only:false
21562156+ ~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
21572157+ ~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
21582158+ in
21592159+ add_cookie jar cookie;
21602160+21612161+ (* Root path should match everything *)
21622162+ let cookies1 =
21632163+ get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
21642164+ in
21652165+ Alcotest.(check int) "/ matches /" 1 (List.length cookies1);
21662166+21672167+ let cookies2 =
21682168+ get_cookies jar ~clock ~domain:"example.com" ~path:"/foo" ~is_secure:false
21692169+ in
21702170+ Alcotest.(check int) "/ matches /foo" 1 (List.length cookies2);
21712171+21722172+ let cookies3 =
21732173+ get_cookies jar ~clock ~domain:"example.com" ~path:"/foo/bar/baz" ~is_secure:false
21742174+ in
21752175+ Alcotest.(check int) "/ matches /foo/bar/baz" 1 (List.length cookies3)
21762176+21772177+let test_path_matching_no_match () =
21782178+ Eio_mock.Backend.run @@ fun () ->
21792179+ let clock = Eio_mock.Clock.make () in
21802180+ Eio_mock.Clock.set_time clock 1000.0;
21812181+21822182+ let jar = create () in
21832183+ let cookie =
21842184+ Cookeio.make ~domain:"example.com" ~path:"/foo/bar" ~name:"test" ~value:"val"
21852185+ ~secure:false ~http_only:false
21862186+ ~creation_time:(Ptime.of_float_s 1000.0 |> Option.get)
21872187+ ~last_access:(Ptime.of_float_s 1000.0 |> Option.get) ()
21882188+ in
21892189+ add_cookie jar cookie;
21902190+21912191+ (* Cookie path /foo/bar should NOT match /foo *)
21922192+ let cookies =
21932193+ get_cookies jar ~clock ~domain:"example.com" ~path:"/foo" ~is_secure:false
21942194+ in
21952195+ Alcotest.(check int) "/foo/bar does NOT match /foo" 0 (List.length cookies);
21962196+21972197+ (* Cookie path /foo/bar should NOT match / *)
21982198+ let cookies2 =
21992199+ get_cookies jar ~clock ~domain:"example.com" ~path:"/" ~is_secure:false
22002200+ in
22012201+ Alcotest.(check int) "/foo/bar does NOT match /" 0 (List.length cookies2);
22022202+22032203+ (* Cookie path /foo/bar should NOT match /baz *)
22042204+ let cookies3 =
22052205+ get_cookies jar ~clock ~domain:"example.com" ~path:"/baz" ~is_secure:false
22062206+ in
22072207+ Alcotest.(check int) "/foo/bar does NOT match /baz" 0 (List.length cookies3)
22082208+18882209let () =
18892210 Eio_main.run @@ fun env ->
18902211 let open Alcotest in
···20182339 test_max_age_and_expires_both_present env);
20192340 test_case "parse both" `Quick (fun () ->
20202341 test_parse_max_age_and_expires env);
23422342+ ] );
23432343+ ( "host_only_flag",
23442344+ [
23452345+ test_case "host_only without Domain attribute" `Quick
23462346+ test_host_only_without_domain_attribute;
23472347+ test_case "host_only with Domain attribute" `Quick
23482348+ test_host_only_with_domain_attribute;
23492349+ test_case "host_only with dotted Domain attribute" `Quick
23502350+ test_host_only_with_dotted_domain_attribute;
23512351+ test_case "host_only domain matching" `Quick
23522352+ test_host_only_domain_matching;
23532353+ test_case "host_only Cookie header parsing" `Quick
23542354+ test_host_only_cookie_header_parsing;
23552355+ test_case "host_only Mozilla format round trip" `Quick
23562356+ test_host_only_mozilla_format_round_trip;
23572357+ ] );
23582358+ ( "path_matching",
23592359+ [
23602360+ test_case "identical path" `Quick test_path_matching_identical;
23612361+ test_case "path with trailing slash" `Quick
23622362+ test_path_matching_with_trailing_slash;
23632363+ test_case "prefix with slash separator" `Quick
23642364+ test_path_matching_prefix_with_slash;
23652365+ test_case "no false prefix match" `Quick
23662366+ test_path_matching_no_false_prefix;
23672367+ test_case "root path matches all" `Quick test_path_matching_root;
23682368+ test_case "path no match" `Quick test_path_matching_no_match;
20212369 ] );
20222370 ]