···
(** {1 Cookie Parsing} *)
+
(** Accumulated attributes from parsing Set-Cookie header *)
+
type cookie_attributes = {
+
mutable domain : string option;
+
mutable path : string option;
+
mutable http_only : bool;
+
mutable expires : Ptime.t option;
+
mutable same_site : same_site option;
+
(** 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 attr_value
+
| "path" -> attrs.path <- Some attr_value
+
match Ptime.of_rfc3339 attr_value with
+
| Ok (time, _, _) -> attrs.expires <- Some time
+
| Error (`RFC3339 (_, err)) ->
+
m "Failed to parse expires attribute '%s': %a" attr_value
+
Ptime.pp_rfc3339_error err))
+
match int_of_string_opt attr_value with
+
let now = Eio.Time.now clock in
+
let expires = Ptime.of_float_s (now +. float_of_int seconds) in
+
attrs.expires <- expires
+
Log.warn (fun m -> m "Failed to parse max-age attribute '%s'" attr_value))
+
| "secure" -> attrs.secure <- true
+
| "httponly" -> attrs.http_only <- 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
+
Log.warn (fun m -> 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 \
+
(** Build final cookie from name/value and accumulated attributes *)
+
let build_cookie ~request_domain ~request_path ~name ~value attrs ~now =
+
let domain = Option.value attrs.domain ~default:request_domain in
+
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 ?same_site:attrs.same_site ~creation_time: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 *)
···
+
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
+
Log.debug (fun m -> m "Parsed cookie: %a" pp cookie);
and make_cookie_header cookies =
···
jar.cookies <- cookie :: jar.cookies;
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;
···
(* Update last access time *)
+
Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:Ptime.epoch
···
if List.memq 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)
+
?same_site:(same_site c) ~creation_time:(creation_time c)
···
Eio.Mutex.unlock jar.mutex;
+
let from_mozilla_format ~clock content =
Log.debug (fun m -> m "Parsing Mozilla format cookies");
···
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 else Ptime.of_float_s (float_of_int exp_int)
+
make ~domain ~path ~name ~value ~secure:(secure = "TRUE")
+
~http_only:false ?expires ?same_site:None ~creation_time:now
Log.debug (fun m -> m "Loaded cookie: %s=%s" name value)
···
(** {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");