OCaml HTTP cookie handling library with support for Eio-based storage jars

optimise

Changed files
+68 -113
lib
+40 -54
lib/core/cookeio.ml
···
(* IP addresses bypass PSL check per RFC 6265 Section 5.1.3 *)
match Ipaddr.of_string cookie_domain with
| Ok _ -> Ok () (* IP addresses are not subject to PSL rules *)
-
| Error _ ->
+
| Error _ -> (
let psl = Lazy.force psl in
-
(match Publicsuffix.is_public_suffix psl cookie_domain with
-
| Error _ ->
-
(* If PSL lookup fails (e.g., invalid domain), allow the cookie.
-
Domain name validation is handled separately. *)
-
Ok ()
-
| Ok false ->
-
(* Not a public suffix, allow the cookie *)
+
match Publicsuffix.is_public_suffix psl cookie_domain with
+
| Error _ | Ok false ->
+
(* If PSL lookup fails (e.g., invalid domain) or not a public suffix,
+
allow the cookie. Domain name validation is handled separately. *)
Ok ()
| Ok true ->
(* It's a public suffix - only allow if request host matches exactly.
···
(** Parse HTTP date by trying all supported formats in sequence *)
let parse_http_date s =
-
match parse_fmt1 s with
-
| Some t -> Some t
-
| None -> (
-
match parse_fmt2 s with
-
| Some t -> Some t
-
| None -> (
-
match parse_fmt3 s with Some t -> Some t | None -> parse_fmt4 s))
+
let ( <|> ) a b = match a with Some _ -> a | None -> b () in
+
parse_fmt1 s <|> fun () ->
+
parse_fmt2 s <|> fun () ->
+
parse_fmt3 s <|> fun () ->
+
parse_fmt4 s
end
(** {1 Cookie Parsing} *)
···
@see <https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7> RFC 6265bis Section 5.4.7 - SameSite
@see <https://developer.chrome.com/docs/privacy-sandbox/chips/> CHIPS - Cookies Having Independent Partitioned State *)
let validate_attributes attrs =
-
(* SameSite=None requires Secure flag *)
-
let samesite_valid =
-
match attrs.same_site with
-
| Some `None when not attrs.secure ->
-
Log.warn (fun m ->
-
m
-
"Cookie has SameSite=None but Secure flag is not set; this \
-
violates RFC requirements");
-
false
-
| _ -> true
-
in
-
(* Partitioned requires Secure flag *)
-
let partitioned_valid =
-
if attrs.partitioned && not attrs.secure then (
+
match (attrs.same_site, attrs.secure, attrs.partitioned) with
+
| Some `None, false, _ ->
+
Log.warn (fun m ->
+
m
+
"Cookie has SameSite=None but Secure flag is not set; this \
+
violates RFC requirements");
+
false
+
| _, false, true ->
Log.warn (fun m ->
m
"Cookie has Partitioned attribute but Secure flag is not set; this \
violates CHIPS requirements");
-
false)
-
else true
-
in
-
samesite_valid && partitioned_valid
+
false
+
| _ -> true
(** Build final cookie from name/value and accumulated attributes.
···
Buffer.add_string buffer (Printf.sprintf "%s=%s" (name cookie) (value cookie));
(* Add Max-Age if present *)
-
(match max_age cookie with
-
| Some span -> (
-
match Ptime.Span.to_int_s span with
-
| Some seconds ->
-
Buffer.add_string buffer (Printf.sprintf "; Max-Age=%d" seconds)
-
| None -> ())
-
| None -> ());
+
Option.iter
+
(fun span ->
+
Option.iter
+
(fun seconds ->
+
Buffer.add_string buffer (Printf.sprintf "; Max-Age=%d" seconds))
+
(Ptime.Span.to_int_s span))
+
(max_age cookie);
(* Add Expires if present *)
-
(match expires cookie with
-
| Some `Session ->
-
(* Session cookies can be indicated with Expires=0 or a past date *)
-
Buffer.add_string buffer "; Expires=0"
-
| Some (`DateTime exp_time) ->
-
(* Format as HTTP date *)
-
let exp_str = Ptime.to_rfc3339 ~tz_offset_s:0 exp_time in
-
Buffer.add_string buffer (Printf.sprintf "; Expires=%s" exp_str)
-
| None -> ());
+
Option.iter
+
(function
+
| `Session -> Buffer.add_string buffer "; Expires=0"
+
| `DateTime exp_time ->
+
let exp_str = Ptime.to_rfc3339 ~tz_offset_s:0 exp_time in
+
Buffer.add_string buffer (Printf.sprintf "; Expires=%s" exp_str))
+
(expires cookie);
(* Add Domain *)
Buffer.add_string buffer (Printf.sprintf "; Domain=%s" (domain cookie));
···
if partitioned cookie then Buffer.add_string buffer "; Partitioned";
(* Add SameSite *)
-
(match same_site cookie with
-
| Some `Strict -> Buffer.add_string buffer "; SameSite=Strict"
-
| Some `Lax -> Buffer.add_string buffer "; SameSite=Lax"
-
| Some `None -> Buffer.add_string buffer "; SameSite=None"
-
| None -> ());
+
Option.iter
+
(function
+
| `Strict -> Buffer.add_string buffer "; SameSite=Strict"
+
| `Lax -> Buffer.add_string buffer "; SameSite=Lax"
+
| `None -> Buffer.add_string buffer "; SameSite=None")
+
(same_site cookie);
Buffer.contents buffer
+28 -59
lib/jar/cookeio_jar.ml
···
String.sub domain 1 (String.length domain - 1)
| _ -> domain
+
(** Remove duplicate cookies, keeping the last occurrence.
+
+
Used to deduplicate combined cookie lists where delta cookies should
+
take precedence over original cookies. *)
+
let dedup_by_identity cookies =
+
let rec aux acc = function
+
| [] -> List.rev acc
+
| c :: rest ->
+
let has_duplicate =
+
List.exists (fun c2 -> cookie_identity_matches c c2) rest
+
in
+
if has_duplicate then aux acc rest else aux (c :: acc) rest
+
in
+
aux [] cookies
+
(** Check if a string is an IP address (IPv4 or IPv6).
Per RFC 6265 Section 5.1.3, domain matching should only apply to hostnames,
not IP addresses. IP addresses require exact match only.
@see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3> RFC 6265 Section 5.1.3 - Domain Matching *)
-
let is_ip_address domain =
-
match Ipaddr.of_string domain with
-
| Ok _ -> true
-
| Error _ -> false
+
let is_ip_address domain = Result.is_ok (Ipaddr.of_string domain)
(** Check if a cookie domain matches a request domain.
···
@see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3> RFC 6265 Section 5.1.3 - Domain Matching
@see <https://datatracker.ietf.org/doc/html/rfc6265#section-5.3> RFC 6265 Section 5.3 - Storage Model (host-only-flag) *)
let domain_matches ~host_only cookie_domain request_domain =
-
if is_ip_address request_domain then
-
(* IP addresses: exact match only per Section 5.1.3 *)
-
request_domain = cookie_domain
-
else
-
(* Hostnames: exact match or subdomain match (if not host_only) *)
-
request_domain = cookie_domain
-
|| (not host_only
-
&& String.ends_with ~suffix:("." ^ cookie_domain) request_domain)
+
request_domain = cookie_domain
+
|| (not (is_ip_address request_domain || host_only)
+
&& String.ends_with ~suffix:("." ^ cookie_domain) request_domain)
(** Check if a cookie path matches a request path.
···
(* Combine original and delta cookies, with delta taking precedence *)
let all_cookies = jar.original_cookies @ jar.delta_cookies in
-
-
(* Filter out duplicates, keeping the last occurrence (from delta) *)
-
let rec dedup acc = function
-
| [] -> List.rev acc
-
| c :: rest ->
-
(* Keep this cookie only if no later cookie has the same identity *)
-
let has_duplicate =
-
List.exists (fun c2 -> cookie_identity_matches c c2) rest
-
in
-
if has_duplicate then dedup acc rest else dedup (c :: acc) rest
-
in
-
let unique_cookies = dedup [] all_cookies in
+
let unique_cookies = dedup_by_identity all_cookies in
(* Filter for applicable cookies, excluding removal cookies and expired cookies *)
let applicable =
···
let count jar =
Eio.Mutex.lock jar.mutex;
-
(* Combine and deduplicate cookies for count *)
let all_cookies = jar.original_cookies @ jar.delta_cookies in
-
let rec dedup acc = function
-
| [] -> List.rev acc
-
| c :: rest ->
-
let has_duplicate =
-
List.exists (fun c2 -> cookie_identity_matches c c2) rest
-
in
-
if has_duplicate then dedup acc rest else dedup (c :: acc) rest
-
in
-
let unique = dedup [] all_cookies in
+
let unique = dedup_by_identity all_cookies in
let n = List.length unique in
Eio.Mutex.unlock jar.mutex;
n
let get_all_cookies jar =
Eio.Mutex.lock jar.mutex;
-
(* Combine and deduplicate, with delta taking precedence *)
let all_cookies = jar.original_cookies @ jar.delta_cookies in
-
let rec dedup acc = function
-
| [] -> List.rev acc
-
| c :: rest ->
-
let has_duplicate =
-
List.exists (fun c2 -> cookie_identity_matches c c2) rest
-
in
-
if has_duplicate then dedup acc rest else dedup (c :: acc) rest
-
in
-
let unique = dedup [] all_cookies in
+
let unique = dedup_by_identity all_cookies in
Eio.Mutex.unlock jar.mutex;
unique
···
(* Combine and deduplicate cookies *)
let all_cookies = jar.original_cookies @ jar.delta_cookies in
-
let rec dedup acc = function
-
| [] -> List.rev acc
-
| c :: rest ->
-
let has_duplicate =
-
List.exists (fun c2 -> cookie_identity_matches c c2) rest
-
in
-
if has_duplicate then dedup acc rest else dedup (c :: acc) rest
-
in
-
let unique = dedup [] all_cookies in
+
let unique = dedup_by_identity all_cookies in
List.iter
(fun cookie ->
···
|> Option.value ~default:Ptime.epoch
in
let expires =
-
let exp_int = try int_of_string expires with _ -> 0 in
-
if exp_int = 0 then None
-
else
-
match Ptime.of_float_s (float_of_int exp_int) with
-
| Some t -> Some (`DateTime t)
-
| None -> None
+
match int_of_string_opt expires with
+
| Some exp_int when exp_int <> 0 ->
+
Option.map (fun t -> `DateTime t)
+
(Ptime.of_float_s (float_of_int exp_int))
+
| _ -> None
in
(* Mozilla format: include_subdomains=TRUE means host_only=false *)
let host_only = include_subdomains <> "TRUE" in