···356356 (* IP addresses bypass PSL check per RFC 6265 Section 5.1.3 *)
357357 match Ipaddr.of_string cookie_domain with
358358 | Ok _ -> Ok () (* IP addresses are not subject to PSL rules *)
359359- | Error _ ->
359359+ | Error _ -> (
360360 let psl = Lazy.force psl in
361361- (match Publicsuffix.is_public_suffix psl cookie_domain with
362362- | Error _ ->
363363- (* If PSL lookup fails (e.g., invalid domain), allow the cookie.
364364- Domain name validation is handled separately. *)
365365- Ok ()
366366- | Ok false ->
367367- (* Not a public suffix, allow the cookie *)
361361+ match Publicsuffix.is_public_suffix psl cookie_domain with
362362+ | Error _ | Ok false ->
363363+ (* If PSL lookup fails (e.g., invalid domain) or not a public suffix,
364364+ allow the cookie. Domain name validation is handled separately. *)
368365 Ok ()
369366 | Ok true ->
370367 (* It's a public suffix - only allow if request host matches exactly.
···500497501498 (** Parse HTTP date by trying all supported formats in sequence *)
502499 let parse_http_date s =
503503- match parse_fmt1 s with
504504- | Some t -> Some t
505505- | None -> (
506506- match parse_fmt2 s with
507507- | Some t -> Some t
508508- | None -> (
509509- match parse_fmt3 s with Some t -> Some t | None -> parse_fmt4 s))
500500+ let ( <|> ) a b = match a with Some _ -> a | None -> b () in
501501+ parse_fmt1 s <|> fun () ->
502502+ parse_fmt2 s <|> fun () ->
503503+ parse_fmt3 s <|> fun () ->
504504+ parse_fmt4 s
510505end
511506512507(** {1 Cookie Parsing} *)
···605600 @see <https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7> RFC 6265bis Section 5.4.7 - SameSite
606601 @see <https://developer.chrome.com/docs/privacy-sandbox/chips/> CHIPS - Cookies Having Independent Partitioned State *)
607602let validate_attributes attrs =
608608- (* SameSite=None requires Secure flag *)
609609- let samesite_valid =
610610- match attrs.same_site with
611611- | Some `None when not attrs.secure ->
612612- Log.warn (fun m ->
613613- m
614614- "Cookie has SameSite=None but Secure flag is not set; this \
615615- violates RFC requirements");
616616- false
617617- | _ -> true
618618- in
619619- (* Partitioned requires Secure flag *)
620620- let partitioned_valid =
621621- if attrs.partitioned && not attrs.secure then (
603603+ match (attrs.same_site, attrs.secure, attrs.partitioned) with
604604+ | Some `None, false, _ ->
605605+ Log.warn (fun m ->
606606+ m
607607+ "Cookie has SameSite=None but Secure flag is not set; this \
608608+ violates RFC requirements");
609609+ false
610610+ | _, false, true ->
622611 Log.warn (fun m ->
623612 m
624613 "Cookie has Partitioned attribute but Secure flag is not set; this \
625614 violates CHIPS requirements");
626626- false)
627627- else true
628628- in
629629- samesite_valid && partitioned_valid
615615+ false
616616+ | _ -> true
630617631618(** Build final cookie from name/value and accumulated attributes.
632619···896883 Buffer.add_string buffer (Printf.sprintf "%s=%s" (name cookie) (value cookie));
897884898885 (* Add Max-Age if present *)
899899- (match max_age cookie with
900900- | Some span -> (
901901- match Ptime.Span.to_int_s span with
902902- | Some seconds ->
903903- Buffer.add_string buffer (Printf.sprintf "; Max-Age=%d" seconds)
904904- | None -> ())
905905- | None -> ());
886886+ Option.iter
887887+ (fun span ->
888888+ Option.iter
889889+ (fun seconds ->
890890+ Buffer.add_string buffer (Printf.sprintf "; Max-Age=%d" seconds))
891891+ (Ptime.Span.to_int_s span))
892892+ (max_age cookie);
906893907894 (* Add Expires if present *)
908908- (match expires cookie with
909909- | Some `Session ->
910910- (* Session cookies can be indicated with Expires=0 or a past date *)
911911- Buffer.add_string buffer "; Expires=0"
912912- | Some (`DateTime exp_time) ->
913913- (* Format as HTTP date *)
914914- let exp_str = Ptime.to_rfc3339 ~tz_offset_s:0 exp_time in
915915- Buffer.add_string buffer (Printf.sprintf "; Expires=%s" exp_str)
916916- | None -> ());
895895+ Option.iter
896896+ (function
897897+ | `Session -> Buffer.add_string buffer "; Expires=0"
898898+ | `DateTime exp_time ->
899899+ let exp_str = Ptime.to_rfc3339 ~tz_offset_s:0 exp_time in
900900+ Buffer.add_string buffer (Printf.sprintf "; Expires=%s" exp_str))
901901+ (expires cookie);
917902918903 (* Add Domain *)
919904 Buffer.add_string buffer (Printf.sprintf "; Domain=%s" (domain cookie));
···931916 if partitioned cookie then Buffer.add_string buffer "; Partitioned";
932917933918 (* Add SameSite *)
934934- (match same_site cookie with
935935- | Some `Strict -> Buffer.add_string buffer "; SameSite=Strict"
936936- | Some `Lax -> Buffer.add_string buffer "; SameSite=Lax"
937937- | Some `None -> Buffer.add_string buffer "; SameSite=None"
938938- | None -> ());
919919+ Option.iter
920920+ (function
921921+ | `Strict -> Buffer.add_string buffer "; SameSite=Strict"
922922+ | `Lax -> Buffer.add_string buffer "; SameSite=Lax"
923923+ | `None -> Buffer.add_string buffer "; SameSite=None")
924924+ (same_site cookie);
939925940926 Buffer.contents buffer
+28-59
lib/jar/cookeio_jar.ml
···4343 String.sub domain 1 (String.length domain - 1)
4444 | _ -> domain
45454646+(** Remove duplicate cookies, keeping the last occurrence.
4747+4848+ Used to deduplicate combined cookie lists where delta cookies should
4949+ take precedence over original cookies. *)
5050+let dedup_by_identity cookies =
5151+ let rec aux acc = function
5252+ | [] -> List.rev acc
5353+ | c :: rest ->
5454+ let has_duplicate =
5555+ List.exists (fun c2 -> cookie_identity_matches c c2) rest
5656+ in
5757+ if has_duplicate then aux acc rest else aux (c :: acc) rest
5858+ in
5959+ aux [] cookies
6060+4661(** Check if a string is an IP address (IPv4 or IPv6).
47624863 Per RFC 6265 Section 5.1.3, domain matching should only apply to hostnames,
4964 not IP addresses. IP addresses require exact match only.
50655166 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3> RFC 6265 Section 5.1.3 - Domain Matching *)
5252-let is_ip_address domain =
5353- match Ipaddr.of_string domain with
5454- | Ok _ -> true
5555- | Error _ -> false
6767+let is_ip_address domain = Result.is_ok (Ipaddr.of_string domain)
56685769(** Check if a cookie domain matches a request domain.
5870···7486 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3> RFC 6265 Section 5.1.3 - Domain Matching
7587 @see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model (host-only-flag) *)
7688let domain_matches ~host_only cookie_domain request_domain =
7777- if is_ip_address request_domain then
7878- (* IP addresses: exact match only per Section 5.1.3 *)
7979- request_domain = cookie_domain
8080- else
8181- (* Hostnames: exact match or subdomain match (if not host_only) *)
8282- request_domain = cookie_domain
8383- || (not host_only
8484- && String.ends_with ~suffix:("." ^ cookie_domain) request_domain)
8989+ request_domain = cookie_domain
9090+ || (not (is_ip_address request_domain || host_only)
9191+ && String.ends_with ~suffix:("." ^ cookie_domain) request_domain)
85928693(** Check if a cookie path matches a request path.
8794···347354348355 (* Combine original and delta cookies, with delta taking precedence *)
349356 let all_cookies = jar.original_cookies @ jar.delta_cookies in
350350-351351- (* Filter out duplicates, keeping the last occurrence (from delta) *)
352352- let rec dedup acc = function
353353- | [] -> List.rev acc
354354- | c :: rest ->
355355- (* Keep this cookie only if no later cookie has the same identity *)
356356- let has_duplicate =
357357- List.exists (fun c2 -> cookie_identity_matches c c2) rest
358358- in
359359- if has_duplicate then dedup acc rest else dedup (c :: acc) rest
360360- in
361361- let unique_cookies = dedup [] all_cookies in
357357+ let unique_cookies = dedup_by_identity all_cookies in
362358363359 (* Filter for applicable cookies, excluding removal cookies and expired cookies *)
364360 let applicable =
···452448453449let count jar =
454450 Eio.Mutex.lock jar.mutex;
455455- (* Combine and deduplicate cookies for count *)
456451 let all_cookies = jar.original_cookies @ jar.delta_cookies in
457457- let rec dedup acc = function
458458- | [] -> List.rev acc
459459- | c :: rest ->
460460- let has_duplicate =
461461- List.exists (fun c2 -> cookie_identity_matches c c2) rest
462462- in
463463- if has_duplicate then dedup acc rest else dedup (c :: acc) rest
464464- in
465465- let unique = dedup [] all_cookies in
452452+ let unique = dedup_by_identity all_cookies in
466453 let n = List.length unique in
467454 Eio.Mutex.unlock jar.mutex;
468455 n
469456470457let get_all_cookies jar =
471458 Eio.Mutex.lock jar.mutex;
472472- (* Combine and deduplicate, with delta taking precedence *)
473459 let all_cookies = jar.original_cookies @ jar.delta_cookies in
474474- let rec dedup acc = function
475475- | [] -> List.rev acc
476476- | c :: rest ->
477477- let has_duplicate =
478478- List.exists (fun c2 -> cookie_identity_matches c c2) rest
479479- in
480480- if has_duplicate then dedup acc rest else dedup (c :: acc) rest
481481- in
482482- let unique = dedup [] all_cookies in
460460+ let unique = dedup_by_identity all_cookies in
483461 Eio.Mutex.unlock jar.mutex;
484462 unique
485463···498476499477 (* Combine and deduplicate cookies *)
500478 let all_cookies = jar.original_cookies @ jar.delta_cookies in
501501- let rec dedup acc = function
502502- | [] -> List.rev acc
503503- | c :: rest ->
504504- let has_duplicate =
505505- List.exists (fun c2 -> cookie_identity_matches c c2) rest
506506- in
507507- if has_duplicate then dedup acc rest else dedup (c :: acc) rest
508508- in
509509- let unique = dedup [] all_cookies in
479479+ let unique = dedup_by_identity all_cookies in
510480511481 List.iter
512482 (fun cookie ->
···552522 |> Option.value ~default:Ptime.epoch
553523 in
554524 let expires =
555555- let exp_int = try int_of_string expires with _ -> 0 in
556556- if exp_int = 0 then None
557557- else
558558- match Ptime.of_float_s (float_of_int exp_int) with
559559- | Some t -> Some (`DateTime t)
560560- | None -> None
525525+ match int_of_string_opt expires with
526526+ | Some exp_int when exp_int <> 0 ->
527527+ Option.map (fun t -> `DateTime t)
528528+ (Ptime.of_float_s (float_of_int exp_int))
529529+ | _ -> None
561530 in
562531 (* Mozilla format: include_subdomains=TRUE means host_only=false *)
563532 let host_only = include_subdomains <> "TRUE" in