···
-
let src = Logs.Src.create "cookeio" ~doc:"Cookie management"
-
module Log = (val Logs.src_log src : Logs.LOG)
-
module SameSite = struct
-
type t = [ `Strict | `Lax | `None ]
-
| `Strict -> Format.pp_print_string ppf "Strict"
-
| `Lax -> Format.pp_print_string ppf "Lax"
-
| `None -> Format.pp_print_string ppf "None"
-
module Expiration = struct
-
type t = [ `Session | `DateTime of Ptime.t ]
-
| `Session, `Session -> true
-
| `DateTime t1, `DateTime t2 -> Ptime.equal t1 t2
-
| `Session -> Format.pp_print_string ppf "Session"
-
| `DateTime t -> Format.fprintf ppf "DateTime(%a)" Ptime.pp t
-
expires : Expiration.t option;
-
max_age : Ptime.Span.t option;
-
same_site : SameSite.t option;
-
creation_time : Ptime.t;
-
mutable original_cookies : t list; (* from client *)
-
mutable delta_cookies : t list; (* to send back *)
-
(** Cookie jar for storing and managing cookies *)
-
(** {1 Cookie Accessors} *)
-
let domain cookie = cookie.domain
-
let path cookie = cookie.path
-
let name cookie = cookie.name
-
let value cookie = cookie.value
-
let value_trimmed cookie =
-
let v = cookie.value in
-
let len = String.length v in
-
match (v.[0], v.[len - 1]) with
-
| '"', '"' -> String.sub v 1 (len - 2)
-
let secure cookie = cookie.secure
-
let http_only cookie = cookie.http_only
-
let partitioned cookie = cookie.partitioned
-
let expires cookie = cookie.expires
-
let max_age cookie = cookie.max_age
-
let same_site cookie = cookie.same_site
-
let creation_time cookie = cookie.creation_time
-
let last_access cookie = cookie.last_access
-
let make ~domain ~path ~name ~value ?(secure = false) ?(http_only = false)
-
?expires ?max_age ?same_site ?(partitioned = false) ~creation_time
-
(** {1 Cookie Jar Creation} *)
-
Log.debug (fun m -> m "Creating new empty cookie jar");
-
{ original_cookies = []; delta_cookies = []; mutex = Eio.Mutex.create () }
-
(** {1 Cookie Matching Helpers} *)
-
let cookie_identity_matches c1 c2 =
-
name c1 = name c2 && domain c1 = domain c2 && path c1 = path c2
-
let normalize_domain domain =
-
(* Strip leading dot per RFC 6265 *)
-
match String.starts_with ~prefix:"." domain with
-
| true when String.length domain > 1 ->
-
String.sub domain 1 (String.length domain - 1)
-
let domain_matches cookie_domain request_domain =
-
(* Cookie domains are stored without leading dots per RFC 6265.
-
A cookie with domain "example.com" should match both "example.com" (exact)
-
and "sub.example.com" (subdomain). *)
-
request_domain = cookie_domain
-
|| String.ends_with ~suffix:("." ^ cookie_domain) request_domain
-
let path_matches cookie_path request_path =
-
(* Cookie path /foo matches /foo, /foo/, /foo/bar *)
-
String.starts_with ~prefix:cookie_path request_path
-
(** {1 HTTP Date Parsing} *)
-
let is_expired cookie clock =
-
match cookie.expires with
-
| None -> false (* No expiration *)
-
| Some `Session -> false (* Session cookie - not expired until browser closes *)
-
| Some (`DateTime exp_time) ->
-
Ptime.of_float_s (Eio.Time.now clock)
-
|> Option.value ~default:Ptime.epoch
-
Ptime.compare now exp_time > 0
-
module DateParser = struct
-
(** Month name to number mapping (case-insensitive) *)
-
let month_of_string s =
-
match String.lowercase_ascii s with
-
(** Normalize abbreviated years:
-
- Years 69-99 get 1900 added (e.g., 95 → 1995)
-
- Years 0-68 get 2000 added (e.g., 25 → 2025)
-
- Years >= 100 are returned as-is *)
-
let normalize_year year =
-
if year >= 0 && year <= 68 then year + 2000
-
else if year >= 69 && year <= 99 then year + 1900
-
(** Parse FMT1: "Wed, 21 Oct 2015 07:28:00 GMT" (RFC 1123) *)
-
Scanf.sscanf s "%s %d %s %d %d:%d:%d %s"
-
(fun _wday day mon year hour min sec tz ->
-
(* Check timezone is GMT (case-insensitive) *)
-
if String.lowercase_ascii tz <> "gmt" then None
-
match month_of_string mon with
-
let year = normalize_year year in
-
Ptime.of_date_time ((year, month, day), ((hour, min, sec), 0)))
-
(** Parse FMT2: "Wednesday, 21-Oct-15 07:28:00 GMT" (RFC 850) *)
-
Scanf.sscanf s "%[^,], %d-%3s-%d %d:%d:%d %s"
-
(fun _wday day mon year hour min sec tz ->
-
(* Check timezone is GMT (case-insensitive) *)
-
if String.lowercase_ascii tz <> "gmt" then None
-
match month_of_string mon with
-
let year = normalize_year year in
-
Ptime.of_date_time ((year, month, day), ((hour, min, sec), 0)))
-
(** Parse FMT3: "Wed Oct 21 07:28:00 2015" (asctime) *)
-
Scanf.sscanf s "%s %s %d %d:%d:%d %d"
-
(fun _wday mon day hour min sec year ->
-
match month_of_string mon with
-
let year = normalize_year year in
-
Ptime.of_date_time ((year, month, day), ((hour, min, sec), 0)))
-
(** Parse FMT4: "Wed, 21-Oct-2015 07:28:00 GMT" (variant) *)
-
Scanf.sscanf s "%s %d-%3s-%d %d:%d:%d %s"
-
(fun _wday day mon year hour min sec tz ->
-
(* Check timezone is GMT (case-insensitive) *)
-
if String.lowercase_ascii tz <> "gmt" then None
-
match month_of_string mon with
-
let year = normalize_year year in
-
Ptime.of_date_time ((year, month, day), ((hour, min, sec), 0)))
-
(** Parse HTTP date by trying all supported formats in sequence *)
-
let parse_http_date s =
-
match parse_fmt1 s with
-
match parse_fmt2 s with
-
match parse_fmt3 s with Some t -> Some t | None -> parse_fmt4 s))
-
(** {1 Cookie Parsing} *)
-
type cookie_attributes = {
-
mutable domain : string option;
-
mutable path : string option;
-
mutable http_only : bool;
-
mutable partitioned : bool;
-
mutable expires : Expiration.t option;
-
mutable max_age : Ptime.Span.t option;
-
mutable same_site : SameSite.t option;
-
(** Accumulated attributes from parsing Set-Cookie header *)
-
(** Create empty attribute accumulator *)
-
let empty_attributes () =
-
(** Parse a single attribute and update the accumulator in-place *)
-
let parse_attribute clock attrs attr_name attr_value =
-
let attr_lower = String.lowercase_ascii attr_name in
-
| "domain" -> attrs.domain <- Some (normalize_domain attr_value)
-
| "path" -> attrs.path <- Some attr_value
-
(* Special case: Expires=0 means session cookie *)
-
if attr_value = "0" then attrs.expires <- Some `Session
-
match Ptime.of_rfc3339 attr_value with
-
| Ok (time, _, _) -> attrs.expires <- Some (`DateTime time)
-
| Error (`RFC3339 (_, err)) -> (
-
(* Try HTTP date format as fallback *)
-
match DateParser.parse_http_date attr_value with
-
| Some time -> attrs.expires <- Some (`DateTime time)
-
m "Failed to parse expires attribute '%s': %a" attr_value
-
Ptime.pp_rfc3339_error err)))
-
match int_of_string_opt attr_value with
-
(* Handle negative values as 0 per RFC 6265 *)
-
let seconds = max 0 seconds in
-
let now = Eio.Time.now clock in
-
(* Store the max-age as a Ptime.Span *)
-
attrs.max_age <- Some (Ptime.Span.of_int_s seconds);
-
(* Also compute and store expires as DateTime *)
-
let expires = Ptime.of_float_s (now +. float_of_int seconds) in
-
| Some time -> attrs.expires <- Some (`DateTime time)
-
Log.debug (fun m -> m "Parsed Max-Age: %d seconds" seconds)
-
m "Failed to parse max-age attribute '%s'" attr_value))
-
| "secure" -> attrs.secure <- true
-
| "httponly" -> attrs.http_only <- true
-
| "partitioned" -> attrs.partitioned <- true
-
match String.lowercase_ascii attr_value with
-
| "strict" -> attrs.same_site <- Some `Strict
-
| "lax" -> attrs.same_site <- Some `Lax
-
| "none" -> attrs.same_site <- Some `None
-
m "Invalid samesite value '%s', ignoring" attr_value))
-
Log.debug (fun m -> m "Unknown cookie attribute '%s', ignoring" attr_name)
-
(** Validate cookie attributes and log warnings for invalid combinations *)
-
let validate_attributes attrs =
-
(* SameSite=None requires Secure flag *)
-
match attrs.same_site with
-
| Some `None when not attrs.secure ->
-
"Cookie has SameSite=None but Secure flag is not set; this \
-
violates RFC requirements");
-
(* Partitioned requires Secure flag *)
-
let partitioned_valid =
-
if attrs.partitioned && not attrs.secure then (
-
"Cookie has Partitioned attribute but Secure flag is not set; \
-
this violates CHIPS requirements");
-
samesite_valid && partitioned_valid
-
(** Build final cookie from name/value and accumulated attributes *)
-
let build_cookie ~request_domain ~request_path ~name ~value attrs ~now =
-
normalize_domain (Option.value attrs.domain ~default:request_domain)
-
let path = Option.value attrs.path ~default:request_path in
-
make ~domain ~path ~name ~value ~secure:attrs.secure
-
~http_only:attrs.http_only ?expires:attrs.expires ?max_age:attrs.max_age
-
?same_site:attrs.same_site ~partitioned:attrs.partitioned
-
~creation_time:now ~last_access:now ()
-
let rec parse_set_cookie ~clock ~domain:request_domain ~path:request_path
-
Log.debug (fun m -> m "Parsing Set-Cookie: %s" header_value);
-
(* Split into attributes *)
-
let parts = String.split_on_char ';' header_value |> List.map String.trim in
-
| name_value :: attrs -> (
-
match String.index_opt name_value '=' with
-
let name = String.sub name_value 0 eq_pos |> String.trim in
-
String.sub name_value (eq_pos + 1)
-
(String.length name_value - eq_pos - 1)
-
Ptime.of_float_s (Eio.Time.now clock)
-
|> Option.value ~default:Ptime.epoch
-
(* Parse all attributes into mutable accumulator *)
-
let accumulated_attrs = empty_attributes () in
-
match String.index_opt attr '=' with
-
(* Attribute without value (e.g., Secure, HttpOnly) *)
-
parse_attribute clock accumulated_attrs attr ""
-
let attr_name = String.sub attr 0 eq |> String.trim in
-
String.sub attr (eq + 1) (String.length attr - eq - 1)
-
parse_attribute clock accumulated_attrs attr_name attr_value)
-
(* Validate attributes *)
-
if not (validate_attributes accumulated_attrs) then (
-
Log.warn (fun m -> m "Cookie validation failed, rejecting cookie");
-
build_cookie ~request_domain ~request_path ~name
-
~value:cookie_value accumulated_attrs ~now
-
Log.debug (fun m -> m "Parsed cookie: %a" pp cookie);
-
and of_cookie_header ~clock ~domain ~path header_value =
-
Log.debug (fun m -> m "Parsing Cookie header: %s" header_value);
-
(* Split on semicolons *)
-
let parts = String.split_on_char ';' header_value |> List.map String.trim in
-
(* Filter out empty parts *)
-
let parts = List.filter (fun s -> String.length s > 0) parts in
-
(* Parse each name=value pair *)
-
match String.index_opt name_value '=' with
-
Error (Printf.sprintf "Cookie missing '=' separator: %s" name_value)
-
let cookie_name = String.sub name_value 0 eq_pos |> String.trim in
-
if String.length cookie_name = 0 then
-
Error "Cookie has empty name"
-
String.sub name_value (eq_pos + 1)
-
(String.length name_value - eq_pos - 1)
-
Ptime.of_float_s (Eio.Time.now clock)
-
|> Option.value ~default:Ptime.epoch
-
(* Create cookie with defaults from Cookie header context *)
-
make ~domain ~path ~name:cookie_name ~value:cookie_value
-
~secure:false ~http_only:false ~partitioned:false ~creation_time:now
-
and make_cookie_header cookies =
-
|> List.map (fun c -> Printf.sprintf "%s=%s" (name c) (value c))
-
and make_set_cookie_header cookie =
-
let buffer = Buffer.create 128 in
-
Buffer.add_string buffer (Printf.sprintf "%s=%s" (name cookie) (value cookie));
-
(* Add Max-Age if present *)
-
(match max_age cookie with
-
match Ptime.Span.to_int_s span with
-
Buffer.add_string buffer (Printf.sprintf "; Max-Age=%d" seconds)
-
(* Add Expires if present *)
-
(match expires cookie with
-
(* 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)
-
Buffer.add_string buffer (Printf.sprintf "; Domain=%s" (domain cookie));
-
Buffer.add_string buffer (Printf.sprintf "; Path=%s" (path cookie));
-
if secure cookie then Buffer.add_string buffer "; Secure";
-
(* Add HttpOnly flag *)
-
if http_only cookie then Buffer.add_string buffer "; HttpOnly";
-
(* Add Partitioned flag *)
-
if partitioned cookie then Buffer.add_string buffer "; Partitioned";
-
(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"
-
(** {1 Pretty Printing} *)
-
"@[<hov 2>{ name=%S;@ value=%S;@ domain=%S;@ path=%S;@ secure=%b;@ \
-
http_only=%b;@ partitioned=%b;@ expires=%a;@ max_age=%a;@ same_site=%a }@]"
-
(name cookie) (value cookie) (domain cookie) (path cookie) (secure cookie)
-
(http_only cookie) (partitioned cookie)
-
(Format.pp_print_option Expiration.pp)
-
(Format.pp_print_option Ptime.Span.pp)
-
(Format.pp_print_option SameSite.pp)
-
Eio.Mutex.lock jar.mutex;
-
let original = jar.original_cookies in
-
let delta = jar.delta_cookies in
-
Eio.Mutex.unlock jar.mutex;
-
let all_cookies = original @ delta in
-
Format.fprintf ppf "@[<v>CookieJar with %d cookies (%d original, %d delta):@,"
-
(List.length all_cookies) (List.length original) (List.length delta);
-
List.iter (fun cookie -> Format.fprintf ppf " %a@," pp cookie) all_cookies;
-
Format.fprintf ppf "@]"
-
(** {1 Cookie Management} *)
-
let add_cookie jar cookie =
-
m "Adding cookie to delta: %s=%s for domain %s" (name cookie)
-
(value cookie) (domain cookie));
-
Eio.Mutex.lock jar.mutex;
-
(* Remove existing cookie with same identity from delta *)
-
(fun c -> not (cookie_identity_matches c cookie))
-
jar.delta_cookies <- cookie :: jar.delta_cookies;
-
Eio.Mutex.unlock jar.mutex
-
let add_original jar cookie =
-
m "Adding original cookie: %s=%s for domain %s" (name cookie)
-
(value cookie) (domain cookie));
-
Eio.Mutex.lock jar.mutex;
-
(* Remove existing cookie with same identity from original *)
-
jar.original_cookies <-
-
(fun c -> not (cookie_identity_matches c cookie))
-
jar.original_cookies <- cookie :: jar.original_cookies;
-
Eio.Mutex.unlock jar.mutex
-
Eio.Mutex.lock jar.mutex;
-
let result = jar.delta_cookies in
-
Eio.Mutex.unlock jar.mutex;
-
Log.debug (fun m -> m "Returning %d delta cookies" (List.length result));
-
let make_removal_cookie cookie ~clock =
-
Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch
-
(* Create a cookie with Max-Age=0 and past expiration (1 year ago) *)
-
Ptime.sub_span now (Ptime.Span.of_int_s (365 * 24 * 60 * 60))
-
|> Option.value ~default:Ptime.epoch
-
make ~domain:(domain cookie) ~path:(path cookie) ~name:(name cookie) ~value:""
-
~secure:(secure cookie) ~http_only:(http_only cookie)
-
~expires:(`DateTime past_expiry) ~max_age:(Ptime.Span.of_int_s 0)
-
?same_site:(same_site cookie) ~partitioned:(partitioned cookie)
-
~creation_time:now ~last_access:now ()
-
let remove jar ~clock cookie =
-
m "Removing cookie: %s=%s for domain %s" (name cookie) (value cookie)
-
Eio.Mutex.lock jar.mutex;
-
(* Check if this cookie exists in original_cookies *)
-
List.exists (fun c -> cookie_identity_matches c cookie) jar.original_cookies
-
(* Create a removal cookie and add it to delta *)
-
let removal = make_removal_cookie cookie ~clock in
-
(fun c -> not (cookie_identity_matches c removal))
-
jar.delta_cookies <- removal :: jar.delta_cookies;
-
Log.debug (fun m -> m "Created removal cookie in delta for original cookie"))
-
(* Just remove from delta if it exists there *)
-
(fun c -> not (cookie_identity_matches c cookie))
-
Log.debug (fun m -> m "Removed cookie from delta"));
-
Eio.Mutex.unlock jar.mutex
-
let get_cookies jar ~clock ~domain:request_domain ~path:request_path ~is_secure
-
m "Getting cookies for domain=%s path=%s secure=%b" request_domain
-
request_path is_secure);
-
Eio.Mutex.lock jar.mutex;
-
(* 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
-
(* Keep this cookie only if no later cookie has the same identity *)
-
List.exists (fun c2 -> cookie_identity_matches c c2) rest
-
if has_duplicate then dedup acc rest else dedup (c :: acc) rest
-
let unique_cookies = dedup [] all_cookies in
-
(* Filter for applicable cookies, excluding removal cookies (empty value) *)
-
(* Exclude removal cookies *)
-
&& domain_matches (domain cookie) request_domain
-
&& path_matches (path cookie) request_path
-
&& ((not (secure cookie)) || is_secure))
-
(* Update last access time in both lists *)
-
Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch
-
let update_last_access cookies =
-
if List.exists (fun a -> cookie_identity_matches a c) applicable then
-
make ~domain:(domain c) ~path:(path c) ~name:(name c) ~value:(value c)
-
~secure:(secure c) ~http_only:(http_only c) ?expires:(expires c)
-
?max_age:(max_age c) ?same_site:(same_site c)
-
~creation_time:(creation_time c) ~last_access:now ()
-
jar.original_cookies <- update_last_access jar.original_cookies;
-
jar.delta_cookies <- update_last_access jar.delta_cookies;
-
Eio.Mutex.unlock jar.mutex;
-
Log.debug (fun m -> m "Found %d applicable cookies" (List.length applicable));
-
Log.info (fun m -> m "Clearing all cookies");
-
Eio.Mutex.lock jar.mutex;
-
jar.original_cookies <- [];
-
jar.delta_cookies <- [];
-
Eio.Mutex.unlock jar.mutex
-
let clear_expired jar ~clock =
-
Eio.Mutex.lock jar.mutex;
-
List.length jar.original_cookies + List.length jar.delta_cookies
-
jar.original_cookies <-
-
List.filter (fun c -> not (is_expired c clock)) jar.original_cookies;
-
List.filter (fun c -> not (is_expired c clock)) jar.delta_cookies;
-
- (List.length jar.original_cookies + List.length jar.delta_cookies)
-
Eio.Mutex.unlock jar.mutex;
-
Log.info (fun m -> m "Cleared %d expired cookies" removed)
-
let clear_session_cookies jar =
-
Eio.Mutex.lock jar.mutex;
-
List.length jar.original_cookies + List.length jar.delta_cookies
-
(* Keep only cookies that are NOT session cookies *)
-
| Some `Session -> false (* This is a session cookie, remove it *)
-
| None | Some (`DateTime _) -> true (* Keep these *)
-
jar.original_cookies <- List.filter is_not_session jar.original_cookies;
-
jar.delta_cookies <- List.filter is_not_session jar.delta_cookies;
-
- (List.length jar.original_cookies + List.length jar.delta_cookies)
-
Eio.Mutex.unlock jar.mutex;
-
Log.info (fun m -> m "Cleared %d session cookies" removed)
-
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.exists (fun c2 -> cookie_identity_matches c c2) rest
-
if has_duplicate then dedup acc rest else dedup (c :: acc) rest
-
let unique = dedup [] all_cookies in
-
let n = List.length unique in
-
Eio.Mutex.unlock jar.mutex;
-
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.exists (fun c2 -> cookie_identity_matches c c2) rest
-
if has_duplicate then dedup acc rest else dedup (c :: acc) rest
-
let unique = dedup [] all_cookies in
-
Eio.Mutex.unlock jar.mutex;
-
Eio.Mutex.lock jar.mutex;
-
let empty = jar.original_cookies = [] && jar.delta_cookies = [] in
-
Eio.Mutex.unlock jar.mutex;
-
(** {1 Mozilla Format} *)
-
let to_mozilla_format_internal jar =
-
let buffer = Buffer.create 1024 in
-
Buffer.add_string buffer "# Netscape HTTP Cookie File\n";
-
Buffer.add_string buffer "# This is a generated file! Do not edit.\n\n";
-
(* Combine and deduplicate cookies *)
-
let all_cookies = jar.original_cookies @ jar.delta_cookies in
-
let rec dedup acc = function
-
List.exists (fun c2 -> cookie_identity_matches c c2) rest
-
if has_duplicate then dedup acc rest else dedup (c :: acc) rest
-
let unique = dedup [] all_cookies in
-
let include_subdomains =
-
if String.starts_with ~prefix:"." (domain cookie) then "TRUE"
-
let secure_flag = if secure cookie then "TRUE" else "FALSE" in
-
match expires cookie with
-
| None -> "0" (* No expiration *)
-
| Some `Session -> "0" (* Session cookie *)
-
| Some (`DateTime t) ->
-
let epoch = Ptime.to_float_s t |> int_of_float |> string_of_int in
-
Buffer.add_string buffer
-
(Printf.sprintf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" (domain cookie)
-
include_subdomains (path cookie) secure_flag expires_str
-
(name cookie) (value cookie)))
-
let to_mozilla_format jar =
-
Eio.Mutex.lock jar.mutex;
-
let result = to_mozilla_format_internal jar in
-
Eio.Mutex.unlock jar.mutex;
-
let from_mozilla_format ~clock content =
-
Log.debug (fun m -> m "Parsing Mozilla format cookies");
-
let lines = String.split_on_char '\n' content in
-
let line = String.trim line in
-
if line <> "" && not (String.starts_with ~prefix:"#" line) then
-
match String.split_on_char '\t' line with
-
| [ domain; _include_subdomains; path; secure; expires; name; value ] ->
-
Ptime.of_float_s (Eio.Time.now clock)
-
|> Option.value ~default:Ptime.epoch
-
let exp_int = try int_of_string expires with _ -> 0 in
-
if exp_int = 0 then None
-
match Ptime.of_float_s (float_of_int exp_int) with
-
| Some t -> Some (`DateTime t)
-
make ~domain:(normalize_domain domain) ~path ~name ~value
-
~secure:(secure = "TRUE") ~http_only:false ?expires ?max_age:None
-
?same_site:None ~partitioned:false ~creation_time:now
-
add_original jar cookie;
-
Log.debug (fun m -> m "Loaded cookie: %s=%s" name value)
-
| _ -> Log.warn (fun m -> m "Invalid cookie line: %s" line))
-
Log.info (fun m -> m "Loaded %d cookies" (List.length jar.original_cookies));
-
(** {1 File Operations} *)
-
Log.info (fun m -> m "Loading cookies from %a" Eio.Path.pp path);
-
let content = Eio.Path.load path in
-
from_mozilla_format ~clock content
-
Log.info (fun m -> m "Cookie file not found, creating empty jar");
-
Log.err (fun m -> m "Failed to load cookies: %s" (Printexc.to_string exn));
-
Eio.Mutex.lock jar.mutex;
-
List.length jar.original_cookies + List.length jar.delta_cookies
-
Eio.Mutex.unlock jar.mutex;
-
Log.info (fun m -> m "Saving %d cookies to %a" total_cookies Eio.Path.pp path);
-
let content = to_mozilla_format jar in
-
Eio.Path.save ~create:(`Or_truncate 0o600) path content;
-
Log.debug (fun m -> m "Cookies saved successfully")
-
Log.err (fun m -> m "Failed to save cookies: %s" (Printexc.to_string exn))