My agentic slop goes here. Not intended for anyone else!

more

-382
stack/requests/lib/cookie_jar.ml
···
-
let src = Logs.Src.create "requests.cookie_jar" ~doc:"HTTP Cookie Jar"
-
module Log = (val Logs.src_log src : Logs.LOG)
-
-
(** Cookie same-site policy *)
-
type same_site = [`Strict | `Lax | `None]
-
-
(** HTTP Cookie *)
-
type cookie = {
-
domain : string;
-
path : string;
-
name : string;
-
value : string;
-
secure : bool;
-
http_only : bool;
-
expires : Ptime.t option;
-
same_site : same_site option;
-
creation_time : Ptime.t;
-
last_access : Ptime.t;
-
}
-
-
(** Cookie jar for storing and managing cookies *)
-
type t = {
-
mutable cookies : cookie list;
-
mutex : Eio.Mutex.t;
-
}
-
-
(** {1 Creation} *)
-
-
let create () =
-
Log.debug (fun m -> m "Creating new empty cookie jar");
-
{ cookies = []; mutex = Eio.Mutex.create () }
-
-
(** {1 Cookie Matching Helpers} *)
-
-
let domain_matches cookie_domain request_domain =
-
(* Cookie domain .example.com matches example.com and sub.example.com *)
-
if String.starts_with ~prefix:"." cookie_domain then
-
let domain = String.sub cookie_domain 1 (String.length cookie_domain - 1) in
-
request_domain = domain ||
-
String.ends_with ~suffix:("." ^ domain) request_domain
-
else
-
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
-
-
let is_expired cookie clock =
-
match cookie.expires with
-
| None -> false (* Session cookie *)
-
| Some exp_time ->
-
let now = Ptime.of_float_s (Eio.Time.now clock) |> Option.value ~default:(Ptime.epoch) in
-
Ptime.compare now exp_time > 0
-
-
(** {1 Cookie Parsing} *)
-
-
let parse_cookie_attribute ~url:_ attr value cookie =
-
let attr_lower = String.lowercase_ascii attr in
-
match attr_lower with
-
| "domain" -> { cookie with domain = value }
-
| "path" -> { cookie with path = value }
-
| "expires" ->
-
(* Parse various date formats *)
-
(try
-
let time, _tz_offset, _tz_string = Ptime.of_rfc3339 value |> Result.get_ok in
-
{ cookie with expires = Some time }
-
with _ ->
-
Log.debug (fun m -> m "Failed to parse expires: %s" value);
-
cookie)
-
| "max-age" ->
-
(try
-
let seconds = int_of_string value in
-
let now = Unix.time () in
-
let expires = Ptime.of_float_s (now +. float_of_int seconds) in
-
{ cookie with expires }
-
with _ -> cookie)
-
| "secure" -> { cookie with secure = true }
-
| "httponly" -> { cookie with http_only = true }
-
| "samesite" ->
-
let same_site = match String.lowercase_ascii value with
-
| "strict" -> Some `Strict
-
| "lax" -> Some `Lax
-
| "none" -> Some `None
-
| _ -> None
-
in
-
{ cookie with same_site }
-
| _ -> cookie
-
-
let rec parse_set_cookie ~url value =
-
Log.debug (fun m -> m "Parsing Set-Cookie: %s" value);
-
-
let uri = Uri.of_string url in
-
let default_domain = Uri.host_with_default ~default:"localhost" uri in
-
let default_path =
-
let p = Uri.path uri in
-
if p = "" then "/"
-
else
-
let last_slash = String.rindex_opt p '/' in
-
match last_slash with
-
| None -> "/"
-
| Some i -> String.sub p 0 (i + 1)
-
in
-
-
(* Split into attributes *)
-
let parts = String.split_on_char ';' value |> List.map String.trim in
-
-
match parts with
-
| [] -> None
-
| name_value :: attrs ->
-
(* Parse name=value *)
-
(match String.index_opt name_value '=' with
-
| None -> None
-
| Some eq_pos ->
-
let name = String.sub name_value 0 eq_pos |> String.trim in
-
let value = String.sub name_value (eq_pos + 1)
-
(String.length name_value - eq_pos - 1) |> String.trim in
-
-
let now = Ptime.of_float_s (Unix.time ()) |> Option.value ~default:Ptime.epoch in
-
let base_cookie = {
-
name;
-
value;
-
domain = default_domain;
-
path = default_path;
-
secure = false;
-
http_only = false;
-
expires = None;
-
same_site = None;
-
creation_time = now;
-
last_access = now;
-
} in
-
-
(* Parse attributes *)
-
let cookie = List.fold_left (fun cookie attr ->
-
match String.index_opt attr '=' with
-
| None -> parse_cookie_attribute ~url attr "" cookie
-
| Some eq ->
-
let attr_name = String.sub attr 0 eq |> String.trim in
-
let attr_value = String.sub attr (eq + 1)
-
(String.length attr - eq - 1) |> String.trim in
-
parse_cookie_attribute ~url attr_name attr_value cookie
-
) base_cookie attrs in
-
-
Log.debug (fun m -> m "Parsed cookie: %a" pp_cookie cookie);
-
Some cookie)
-
-
and make_cookie_header cookies =
-
cookies
-
|> List.map (fun c -> Printf.sprintf "%s=%s" c.name c.value)
-
|> String.concat "; "
-
-
(** {1 Pretty Printing} *)
-
-
and pp_same_site ppf = function
-
| `Strict -> Format.pp_print_string ppf "Strict"
-
| `Lax -> Format.pp_print_string ppf "Lax"
-
| `None -> Format.pp_print_string ppf "None"
-
-
and pp_cookie ppf cookie =
-
Format.fprintf ppf "@[<hov 2>{ name=%S;@ value=%S;@ domain=%S;@ path=%S;@ \
-
secure=%b;@ http_only=%b;@ expires=%a;@ same_site=%a }@]"
-
cookie.name
-
cookie.value
-
cookie.domain
-
cookie.path
-
cookie.secure
-
cookie.http_only
-
(Format.pp_print_option Ptime.pp) cookie.expires
-
(Format.pp_print_option pp_same_site) cookie.same_site
-
-
let pp ppf t =
-
Eio.Mutex.lock t.mutex;
-
let cookies = t.cookies in
-
Eio.Mutex.unlock t.mutex;
-
-
Format.fprintf ppf "@[<v>CookieJar with %d cookies:@," (List.length cookies);
-
List.iter (fun cookie ->
-
Format.fprintf ppf " %a@," pp_cookie cookie
-
) cookies;
-
Format.fprintf ppf "@]"
-
-
(** {1 Cookie Management} *)
-
-
let add_cookie t cookie =
-
Log.debug (fun m -> m "Adding cookie: %s=%s for domain %s"
-
cookie.name cookie.value cookie.domain);
-
-
Eio.Mutex.lock t.mutex;
-
(* Remove existing cookie with same name, domain, and path *)
-
t.cookies <- List.filter (fun c ->
-
not (c.name = cookie.name && c.domain = cookie.domain && c.path = cookie.path)
-
) t.cookies;
-
t.cookies <- cookie :: t.cookies;
-
Eio.Mutex.unlock t.mutex
-
-
let extract_from_headers t ~url headers =
-
Log.debug (fun m -> m "Extracting cookies from headers for URL: %s" url);
-
-
let set_cookie_values = Headers.get_multi "set-cookie" headers in
-
List.iter (fun value ->
-
match parse_set_cookie ~url value with
-
| Some cookie -> add_cookie t cookie
-
| None -> Log.warn (fun m -> m "Failed to parse Set-Cookie header: %s" value)
-
) set_cookie_values
-
-
let get_cookies t ~url =
-
let uri = Uri.of_string url in
-
let domain = Uri.host_with_default ~default:"localhost" uri in
-
let path = Uri.path uri in
-
let is_secure = Uri.scheme uri = Some "https" in
-
-
Log.debug (fun m -> m "Getting cookies for domain=%s path=%s secure=%b"
-
domain path is_secure);
-
-
Eio.Mutex.lock t.mutex;
-
let applicable = List.filter (fun cookie ->
-
domain_matches cookie.domain domain &&
-
path_matches cookie.path path &&
-
(not cookie.secure || is_secure)
-
) t.cookies in
-
-
(* Update last access time *)
-
let now = Ptime.of_float_s (Unix.time ()) |> Option.value ~default:Ptime.epoch in
-
let updated = List.map (fun c ->
-
if List.memq c applicable then
-
{ c with last_access = now }
-
else c
-
) t.cookies in
-
t.cookies <- updated;
-
Eio.Mutex.unlock t.mutex;
-
-
Log.debug (fun m -> m "Found %d applicable cookies" (List.length applicable));
-
applicable
-
-
let add_to_headers t ~url headers =
-
let cookies = get_cookies t ~url in
-
if cookies = [] then headers
-
else
-
let cookie_header = make_cookie_header cookies in
-
Log.debug (fun m -> m "Adding Cookie header: %s" cookie_header);
-
Headers.add "cookie" cookie_header headers
-
-
let clear t =
-
Log.info (fun m -> m "Clearing all cookies");
-
Eio.Mutex.lock t.mutex;
-
t.cookies <- [];
-
Eio.Mutex.unlock t.mutex
-
-
let clear_expired t ~clock =
-
Eio.Mutex.lock t.mutex;
-
let before_count = List.length t.cookies in
-
t.cookies <- List.filter (fun c -> not (is_expired c clock)) t.cookies;
-
let removed = before_count - List.length t.cookies in
-
Eio.Mutex.unlock t.mutex;
-
Log.info (fun m -> m "Cleared %d expired cookies" removed)
-
-
let clear_session_cookies t =
-
Eio.Mutex.lock t.mutex;
-
let before_count = List.length t.cookies in
-
t.cookies <- List.filter (fun c -> c.expires <> None) t.cookies;
-
let removed = before_count - List.length t.cookies in
-
Eio.Mutex.unlock t.mutex;
-
Log.info (fun m -> m "Cleared %d session cookies" removed)
-
-
let count t =
-
Eio.Mutex.lock t.mutex;
-
let n = List.length t.cookies in
-
Eio.Mutex.unlock t.mutex;
-
n
-
-
(** {1 Mozilla Format} *)
-
-
let to_mozilla_format_internal t =
-
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";
-
-
List.iter (fun cookie ->
-
let include_subdomains =
-
if String.starts_with ~prefix:"." cookie.domain then "TRUE" else "FALSE" in
-
let secure = if cookie.secure then "TRUE" else "FALSE" in
-
let expires = match cookie.expires with
-
| None -> "0" (* Session cookie *)
-
| Some t ->
-
let epoch = Ptime.to_float_s t |> int_of_float |> string_of_int in
-
epoch
-
in
-
-
Buffer.add_string buffer (Printf.sprintf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
-
cookie.domain
-
include_subdomains
-
cookie.path
-
secure
-
expires
-
cookie.name
-
cookie.value)
-
) t.cookies;
-
-
Buffer.contents buffer
-
-
let to_mozilla_format t =
-
Eio.Mutex.lock t.mutex;
-
let result = to_mozilla_format_internal t in
-
Eio.Mutex.unlock t.mutex;
-
result
-
-
let from_mozilla_format content =
-
Log.debug (fun m -> m "Parsing Mozilla format cookies");
-
let jar = create () in
-
-
let lines = String.split_on_char '\n' content in
-
List.iter (fun line ->
-
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] ->
-
let now = Ptime.of_float_s (Unix.time ()) |> 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 Ptime.of_float_s (float_of_int exp_int)
-
in
-
-
let cookie = {
-
domain;
-
path;
-
name;
-
value;
-
secure = (secure = "TRUE");
-
http_only = false; (* Not stored in Mozilla format *)
-
expires;
-
same_site = None; (* Not stored in Mozilla format *)
-
creation_time = now;
-
last_access = now;
-
} in
-
add_cookie jar cookie;
-
Log.debug (fun m -> m "Loaded cookie: %s=%s" name value)
-
| _ ->
-
Log.warn (fun m -> m "Invalid cookie line: %s" line)
-
) lines;
-
-
Log.info (fun m -> m "Loaded %d cookies" (List.length jar.cookies));
-
jar
-
-
(** {1 File Operations} *)
-
-
(** Get cookie file path - uses XDG data directory or provided path *)
-
let get_cookie_file ?xdg ?path () =
-
match xdg, path with
-
| Some xdg_ctx, _ ->
-
(* Use XDG data directory for cookies *)
-
let data_dir = Xdge.data_dir xdg_ctx in
-
Eio.Path.(data_dir / "cookies.txt")
-
| None, Some p -> p
-
| None, None ->
-
failwith "Cookie_jar: either xdg or path must be provided"
-
-
let load ?xdg ?path () =
-
let cookie_file = get_cookie_file ?xdg ?path () in
-
Log.info (fun m -> m "Loading cookies from %a" Eio.Path.pp cookie_file);
-
-
try
-
let content = Eio.Path.load cookie_file in
-
from_mozilla_format content
-
with
-
| Eio.Io _ ->
-
Log.info (fun m -> m "Cookie file not found, creating empty jar");
-
create ()
-
| exn ->
-
Log.err (fun m -> m "Failed to load cookies: %s" (Printexc.to_string exn));
-
create ()
-
-
let save ?xdg ?path t =
-
let cookie_file = get_cookie_file ?xdg ?path () in
-
Log.info (fun m -> m "Saving %d cookies to %a" (List.length t.cookies) Eio.Path.pp cookie_file);
-
-
let content = to_mozilla_format t in
-
-
try
-
Eio.Path.save ~create:(`Or_truncate 0o600) cookie_file content;
-
Log.debug (fun m -> m "Cookies saved successfully")
-
with exn ->
-
Log.err (fun m -> m "Failed to save cookies: %s" (Printexc.to_string exn))
-104
stack/requests/lib/cookie_jar.mli
···
-
(** HTTP Cookie Jar with Mozilla format persistence support *)
-
-
open Eio
-
-
(** Cookie same-site policy *)
-
type same_site = [`Strict | `Lax | `None]
-
-
(** HTTP Cookie *)
-
type cookie = {
-
domain : string; (** Domain that set the cookie *)
-
path : string; (** Path scope for the cookie *)
-
name : string; (** Cookie name *)
-
value : string; (** Cookie value *)
-
secure : bool; (** Only send over HTTPS *)
-
http_only : bool; (** Not accessible to JavaScript *)
-
expires : Ptime.t option; (** Expiry time, None for session cookies *)
-
same_site : same_site option; (** Same-site policy *)
-
creation_time : Ptime.t; (** When cookie was created *)
-
last_access : Ptime.t; (** Last time cookie was accessed *)
-
}
-
-
(** Cookie jar for storing and managing cookies *)
-
type t
-
-
(** {1 Creation and Loading} *)
-
-
(** Create an empty cookie jar *)
-
val create : unit -> t
-
-
(** Load cookies from Mozilla format file.
-
If xdg is provided, uses XDG data directory, otherwise uses provided path. *)
-
val load : ?xdg:Xdge.t -> ?path:Eio.Fs.dir_ty Path.t -> unit -> t
-
-
(** Save cookies to Mozilla format file.
-
If xdg is provided, uses XDG data directory, otherwise uses provided path. *)
-
val save : ?xdg:Xdge.t -> ?path:Eio.Fs.dir_ty Path.t -> t -> unit
-
-
(** {1 Cookie Management} *)
-
-
(** Add a cookie to the jar *)
-
val add_cookie : t -> cookie -> unit
-
-
(** Extract cookies from Set-Cookie headers *)
-
val extract_from_headers : t -> url:string -> Headers.t -> unit
-
-
(** Get cookies applicable for a URL *)
-
val get_cookies : t -> url:string -> cookie list
-
-
(** Add Cookie header for a request *)
-
val add_to_headers : t -> url:string -> Headers.t -> Headers.t
-
-
(** Clear all cookies *)
-
val clear : t -> unit
-
-
(** Clear expired cookies *)
-
val clear_expired : t -> clock:_ Time.clock -> unit
-
-
(** Clear session cookies (those without expiry) *)
-
val clear_session_cookies : t -> unit
-
-
(** Get the number of cookies in the jar *)
-
val count : t -> int
-
-
(** {1 Cookie Creation} *)
-
-
(** Parse Set-Cookie header value into a cookie *)
-
val parse_set_cookie : url:string -> string -> cookie option
-
-
(** Create cookie header value from cookies *)
-
val make_cookie_header : cookie list -> string
-
-
(** {1 Pretty Printing} *)
-
-
(** Pretty print a cookie *)
-
val pp_cookie : Format.formatter -> cookie -> unit
-
-
(** Pretty print a cookie jar *)
-
val pp : Format.formatter -> t -> unit
-
-
(** {1 Mozilla Format} *)
-
-
(** Mozilla cookies.txt format:
-
# Netscape HTTP Cookie File
-
# This is a generated file! Do not edit.
-
-
domain include_subdomains path secure expires name value
-
-
Where:
-
- domain: The domain that created the cookie
-
- include_subdomains: TRUE if cookie applies to subdomains, FALSE otherwise
-
- path: The path the cookie is valid for
-
- secure: TRUE if cookie requires secure connection
-
- expires: Unix timestamp when cookie expires (0 for session cookies)
-
- name: Cookie name
-
- value: Cookie value
-
-
Example:
-
.github.com TRUE / TRUE 1735689600 _gh_sess abc123... *)
-
-
(** Write cookies in Mozilla format *)
-
val to_mozilla_format : t -> string
-
-
(** Parse Mozilla format cookies *)
-
val from_mozilla_format : string -> t
+1
stack/requests/lib/dune
···
yojson
base64
cacheio
+
cookeio
xdge
logs
ptime
-1
stack/requests/lib/requests.ml
···
module Status = Status
module Error = Error
module Session = Session
-
module Cookie_jar = Cookie_jar
module Retry = Retry
+1 -4
stack/requests/lib/requests.mli
···
(** Stateful HTTP sessions with cookies and configuration persistence *)
module Session = Session
-
(** Cookie storage and management *)
-
module Cookie_jar = Cookie_jar
-
(** Retry policies and backoff strategies *)
module Retry = Retry
···
module Mime = Mime
(** Timeout configuration for requests *)
-
module Timeout = Timeout
+
module Timeout = Timeout
+47 -14
stack/requests/lib/session.ml
···
sw : Eio.Switch.t;
client : ('clock, 'net) Client.t;
clock : 'clock;
-
cookie_jar : Cookie_jar.t;
+
cookie_jar : Cookeio.jar;
mutable default_headers : Headers.t;
mutable auth : Auth.t option;
mutable timeout : Timeout.t;
···
| Some jar, _, _ -> jar
| None, true, Some xdg_ctx ->
Log.debug (fun m -> m "Loading persistent cookie jar from XDG data dir");
-
Cookie_jar.load ~xdg:xdg_ctx ()
+
let data_dir = Xdge.data_dir xdg_ctx in
+
let cookie_file = Eio.Path.(data_dir / "cookies.txt") in
+
Cookeio.load cookie_file
| None, _, _ ->
-
Cookie_jar.create ()
+
Cookeio.create ()
in
let session = {
···
Log.info (fun m -> m "Closing session after %d requests" session.requests_made);
if persist_cookies && Option.is_some xdg then begin
Log.info (fun m -> m "Saving cookies on session close");
-
Cookie_jar.save ?xdg session.cookie_jar
+
let data_dir = Xdge.data_dir (Option.get xdg) in
+
let cookie_file = Eio.Path.(data_dir / "cookies.txt") in
+
Cookeio.save cookie_file session.cookie_jar
end
);
···
let save_cookies : ('a, 'b) t -> unit = fun t ->
if t.persist_cookies && Option.is_some t.xdg then
-
Cookie_jar.save ?xdg:t.xdg t.cookie_jar
+
let data_dir = Xdge.data_dir (Option.get t.xdg) in
+
let cookie_file = Eio.Path.(data_dir / "cookies.txt") in
+
Cookeio.save cookie_file t.cookie_jar
let load_cookies : ('a, 'b) t -> unit = fun t ->
if t.persist_cookies && Option.is_some t.xdg then
-
let loaded = Cookie_jar.load ?xdg:t.xdg () in
+
let data_dir = Xdge.data_dir (Option.get t.xdg) in
+
let cookie_file = Eio.Path.(data_dir / "cookies.txt") in
+
let loaded = Cookeio.load cookie_file in
(* Copy loaded cookies into our jar *)
-
Cookie_jar.clear t.cookie_jar;
-
let cookies_from_loaded = Cookie_jar.to_mozilla_format loaded in
-
let _reloaded = Cookie_jar.from_mozilla_format cookies_from_loaded in
+
Cookeio.clear t.cookie_jar;
+
let cookies_from_loaded = Cookeio.to_mozilla_format loaded in
+
let _reloaded = Cookeio.from_mozilla_format cookies_from_loaded in
(* This is a bit convoluted but maintains the same jar reference *)
()
···
let cookies t = t.cookie_jar
let clear_cookies t =
-
Cookie_jar.clear t.cookie_jar
+
Cookeio.clear t.cookie_jar
(** {1 Internal Request Function} *)
···
let headers =
t.default_headers
|> Headers.merge (Option.value headers ~default:Headers.empty)
-
|> Cookie_jar.add_to_headers t.cookie_jar ~url
+
|> (fun headers ->
+
let uri = Uri.of_string url in
+
let domain = Uri.host_with_default ~default:"localhost" uri in
+
let path = Uri.path uri in
+
let is_secure = Uri.scheme uri = Some "https" in
+
let cookies = Cookeio.get_cookies t.cookie_jar ~domain ~path ~is_secure in
+
if cookies = [] then headers
+
else
+
let cookie_header = Cookeio.make_cookie_header cookies in
+
Headers.add "cookie" cookie_header headers)
in
(* Use provided auth or session default *)
···
in
(* Extract cookies from response *)
-
Cookie_jar.extract_from_headers t.cookie_jar ~url (Response.headers response);
+
let uri = Uri.of_string url in
+
let domain = Uri.host_with_default ~default:"localhost" uri in
+
let path =
+
let p = Uri.path uri in
+
if p = "" then "/"
+
else
+
let last_slash = String.rindex_opt p '/' in
+
match last_slash with
+
| None -> "/"
+
| Some i -> String.sub p 0 (i + 1)
+
in
+
let set_cookie_values = Headers.get_multi "set-cookie" (Response.headers response) in
+
List.iter (fun value ->
+
match Cookeio.parse_set_cookie ~domain ~path value with
+
| Some cookie -> Cookeio.add_cookie t.cookie_jar cookie
+
| None -> Log.warn (fun m -> m "Failed to parse Set-Cookie header: %s" value)
+
) set_cookie_values;
(* Update statistics *)
Mutex.lock t.mutex;
···
let pp ppf t =
Mutex.lock t.mutex;
let stats = t.requests_made, t.total_time,
-
Cookie_jar.count t.cookie_jar in
+
Cookeio.count t.cookie_jar in
Mutex.unlock t.mutex;
let requests, time, cookies = stats in
Format.fprintf ppf "@[<v>Session:@,\
···
let result = Stats.{
requests_made = t.requests_made;
total_time = t.total_time;
-
cookies_count = Cookie_jar.count t.cookie_jar;
+
cookies_count = Cookeio.count t.cookie_jar;
retries_count = t.retries_count;
} in
Mutex.unlock t.mutex;
+2 -2
stack/requests/lib/session.mli
···
val create :
sw:Eio.Switch.t ->
?client:('clock Eio.Time.clock,'net Eio.Net.t) Client.t ->
-
?cookie_jar:Cookie_jar.t ->
+
?cookie_jar:Cookeio.jar ->
?default_headers:Headers.t ->
?auth:Auth.t ->
?timeout:Timeout.t ->
···
(** {1 Cookie Management} *)
-
val cookies : ('clock, 'net) t -> Cookie_jar.t
+
val cookies : ('clock, 'net) t -> Cookeio.jar
(** Get the session's cookie jar for direct manipulation *)
val clear_cookies : ('clock, 'net) t -> unit