···203203 let ops = Eio.Flow.Pi.source (module Strings_source) in
204204 (t, Eio.Resource.T (t, ops))
205205206206-let to_cohttp_body ~sw = function
206206+let to_flow_source ~sw = function
207207 | Empty -> None
208208- | String { content; _ } -> Some (Cohttp_eio.Body.of_string content)
208208+ | String { content; _ } -> Some (Eio.Flow.string_source content)
209209 | Stream { source; _ } -> Some source
210210 | File { file; _ } ->
211211 (* Open file and stream it directly without loading into memory *)
···263263264264(* Private module *)
265265module Private = struct
266266- let to_cohttp_body = to_cohttp_body
266266+ let to_flow_source = to_flow_source
267267268268 let to_string = function
269269 | Empty -> ""
+2-2
lib/body.mli
···137137(** Internal functions exposed for use by other modules in the library.
138138 These are not part of the public API and may change between versions. *)
139139module Private : sig
140140- val to_cohttp_body : sw:Eio.Switch.t -> t -> Cohttp_eio.Body.t option
141141- (** [to_cohttp_body ~sw body] converts the body to cohttp-eio format.
140140+ val to_flow_source : sw:Eio.Switch.t -> t -> Eio.Flow.source_ty Eio.Resource.t option
141141+ (** [to_flow_source ~sw body] converts the body to an Eio flow source.
142142 Uses the switch to manage resources like file handles.
143143 This function is used internally by the Client module. *)
144144
-291
lib/digest_auth.ml
···11-(** RFC 2617 HTTP Digest Authentication implementation *)
22-33-module Log = (val Logs.src_log (Logs.Src.create "requests.digest_auth" ~doc:"HTTP Digest Authentication") : Logs.LOG)
44-55-(** Digest auth challenge parameters from WWW-Authenticate header *)
66-type challenge = {
77- realm : string;
88- domain : string option;
99- nonce : string;
1010- opaque : string option;
1111- stale : bool;
1212- algorithm : [`MD5 | `MD5_sess | `SHA256 | `SHA256_sess];
1313- qop : [`Auth | `Auth_int] list option; (* quality of protection *)
1414- charset : string option;
1515- userhash : bool;
1616-}
1717-1818-(** Client's chosen parameters for response *)
1919-type client_data = {
2020- username : string;
2121- password : string;
2222- nc : int; (* nonce count *)
2323- cnonce : string; (* client nonce *)
2424- qop_chosen : [`Auth | `Auth_int] option;
2525-}
2626-2727-(** Parse WWW-Authenticate header for Digest challenge *)
2828-let parse_challenge header_value =
2929- (* Remove "Digest " prefix if present *)
3030- let value =
3131- if String.starts_with ~prefix:"Digest " header_value then
3232- String.sub header_value 7 (String.length header_value - 7)
3333- else header_value
3434- in
3535-3636- (* Parse comma-separated key=value pairs *)
3737- let parse_params str =
3838- let rec parse_one pos acc =
3939- if pos >= String.length str then acc
4040- else
4141- (* Skip whitespace *)
4242- let pos = ref pos in
4343- while !pos < String.length str && str.[!pos] = ' ' do incr pos done;
4444- if !pos >= String.length str then acc
4545- else
4646- (* Find key *)
4747- let key_start = !pos in
4848- while !pos < String.length str && str.[!pos] <> '=' do incr pos done;
4949- if !pos >= String.length str then acc
5050- else
5151- let key = String.trim (String.sub str key_start (!pos - key_start)) in
5252- incr pos; (* Skip '=' *)
5353-5454- (* Parse value - may be quoted *)
5555- let value, next_pos =
5656- if !pos < String.length str && str.[!pos] = '"' then begin
5757- (* Quoted value *)
5858- incr pos;
5959- let value_start = !pos in
6060- while !pos < String.length str && str.[!pos] <> '"' do
6161- if str.[!pos] = '\\' && !pos + 1 < String.length str then
6262- pos := !pos + 2 (* Skip escaped character *)
6363- else
6464- incr pos
6565- done;
6666- let value = String.sub str value_start (!pos - value_start) in
6767- if !pos < String.length str then incr pos; (* Skip closing quote *)
6868- (* Skip to next comma *)
6969- while !pos < String.length str && str.[!pos] <> ',' do incr pos done;
7070- if !pos < String.length str then incr pos; (* Skip comma *)
7171- (value, !pos)
7272- end else begin
7373- (* Unquoted value *)
7474- let value_start = !pos in
7575- while !pos < String.length str && str.[!pos] <> ',' do incr pos done;
7676- let value = String.trim (String.sub str value_start (!pos - value_start)) in
7777- if !pos < String.length str then incr pos; (* Skip comma *)
7878- (value, !pos)
7979- end
8080- in
8181- parse_one next_pos ((key, value) :: acc)
8282- in
8383- List.rev (parse_one 0 [])
8484- in
8585-8686- let params = parse_params value in
8787-8888- (* Extract required and optional parameters *)
8989- let get_param name = List.assoc_opt name params in
9090- let get_param_req name =
9191- match get_param name with
9292- | Some v -> v
9393- | None -> failwith (Printf.sprintf "Missing required Digest parameter: %s" name)
9494- in
9595-9696- try
9797- let realm = get_param_req "realm" in
9898- let nonce = get_param_req "nonce" in
9999-100100- let algorithm = match get_param "algorithm" with
101101- | Some "MD5" | None -> `MD5
102102- | Some "MD5-sess" -> `MD5_sess
103103- | Some "SHA-256" -> `SHA256
104104- | Some "SHA-256-sess" -> `SHA256_sess
105105- | Some a ->
106106- Log.warn (fun m -> m "Unknown digest algorithm: %s, using MD5" a);
107107- `MD5
108108- in
109109-110110- let qop = match get_param "qop" with
111111- | None -> None
112112- | Some qop_str ->
113113- let qops = String.split_on_char ',' qop_str |> List.map String.trim in
114114- Some (List.filter_map (function
115115- | "auth" -> Some `Auth
116116- | "auth-int" -> Some `Auth_int
117117- | _ -> None
118118- ) qops)
119119- in
120120-121121- Some {
122122- realm;
123123- domain = get_param "domain";
124124- nonce;
125125- opaque = get_param "opaque";
126126- stale = (match get_param "stale" with
127127- | Some "true" | Some "TRUE" -> true
128128- | _ -> false);
129129- algorithm;
130130- qop;
131131- charset = get_param "charset";
132132- userhash = (match get_param "userhash" with
133133- | Some "true" | Some "TRUE" -> true
134134- | _ -> false);
135135- }
136136- with
137137- | Failure msg ->
138138- Log.warn (fun m -> m "Failed to parse Digest challenge: %s" msg);
139139- None
140140- | Not_found -> None
141141-142142-(** Generate client nonce *)
143143-let generate_cnonce () =
144144- let rand_bytes = Mirage_crypto_rng.generate 16 in
145145- Base64.encode_string rand_bytes
146146-147147-(** Hash function based on algorithm *)
148148-let hash_function = function
149149- | `MD5 | `MD5_sess ->
150150- fun s -> Digestif.MD5.(to_hex (digest_string s))
151151- | `SHA256 | `SHA256_sess ->
152152- fun s -> Digestif.SHA256.(to_hex (digest_string s))
153153-154154-(** Calculate H(A1) according to RFC 2617 *)
155155-let calculate_ha1 ~algorithm ~username ~realm ~password ~nonce ~cnonce =
156156- let hash = hash_function algorithm in
157157- match algorithm with
158158- | `MD5 | `SHA256 ->
159159- hash (Printf.sprintf "%s:%s:%s" username realm password)
160160- | `MD5_sess | `SHA256_sess ->
161161- let ha1_base = hash (Printf.sprintf "%s:%s:%s" username realm password) in
162162- hash (Printf.sprintf "%s:%s:%s" ha1_base nonce cnonce)
163163-164164-(** Calculate H(A2) according to RFC 2617 *)
165165-let calculate_ha2 ~algorithm ~meth ~uri ~qop ~body =
166166- let hash = hash_function algorithm in
167167- let method_str = match meth with
168168- | `GET -> "GET" | `POST -> "POST" | `PUT -> "PUT"
169169- | `DELETE -> "DELETE" | `HEAD -> "HEAD" | `OPTIONS -> "OPTIONS"
170170- | `PATCH -> "PATCH" | `TRACE -> "TRACE" | `CONNECT -> "CONNECT"
171171- | `Other s -> s
172172- in
173173- match qop with
174174- | None | Some `Auth ->
175175- hash (Printf.sprintf "%s:%s" method_str (Uri.path_and_query uri))
176176- | Some `Auth_int ->
177177- (* For auth-int, include hash of entity body *)
178178- let body_hash = match body with
179179- | None -> hash ""
180180- | Some b -> hash b
181181- in
182182- hash (Printf.sprintf "%s:%s:%s" method_str (Uri.path_and_query uri) body_hash)
183183-184184-(** Calculate the response hash *)
185185-let calculate_response ~ha1 ~ha2 ~nonce ~nc ~cnonce ~qop =
186186- let hash = hash_function `MD5 in (* Response always uses the same hash as HA1 *)
187187- match qop with
188188- | None ->
189189- hash (Printf.sprintf "%s:%s:%s" ha1 nonce ha2)
190190- | Some qop_value ->
191191- let qop_str = match qop_value with
192192- | `Auth -> "auth"
193193- | `Auth_int -> "auth-int"
194194- in
195195- let nc_str = Printf.sprintf "%08x" nc in
196196- hash (Printf.sprintf "%s:%s:%s:%s:%s:%s" ha1 nonce nc_str cnonce qop_str ha2)
197197-198198-(** Generate Authorization header value for Digest auth *)
199199-let generate_auth_header ~challenge ~client_data ~meth ~uri ~body =
200200- let { username; password; nc; cnonce; qop_chosen } = client_data in
201201- let { realm; nonce; opaque; algorithm; _ } = challenge in
202202-203203- (* Calculate hashes *)
204204- let ha1 = calculate_ha1 ~algorithm ~username ~realm ~password ~nonce ~cnonce in
205205- let ha2 = calculate_ha2 ~algorithm ~meth ~uri ~qop:qop_chosen ~body in
206206- let response = calculate_response ~ha1 ~ha2 ~nonce ~nc ~cnonce ~qop:qop_chosen in
207207-208208- (* Build Authorization header *)
209209- let params = [
210210- ("username", Printf.sprintf "\"%s\"" username);
211211- ("realm", Printf.sprintf "\"%s\"" realm);
212212- ("nonce", Printf.sprintf "\"%s\"" nonce);
213213- ("uri", Printf.sprintf "\"%s\"" (Uri.path_and_query uri));
214214- ("response", Printf.sprintf "\"%s\"" response);
215215- ] in
216216-217217- let params = match algorithm with
218218- | `MD5 -> params (* MD5 is default, don't need to specify *)
219219- | `MD5_sess -> ("algorithm", "MD5-sess") :: params
220220- | `SHA256 -> ("algorithm", "SHA-256") :: params
221221- | `SHA256_sess -> ("algorithm", "SHA-256-sess") :: params
222222- in
223223-224224- let params = match opaque with
225225- | Some o -> ("opaque", Printf.sprintf "\"%s\"" o) :: params
226226- | None -> params
227227- in
228228-229229- let params = match qop_chosen with
230230- | None -> params
231231- | Some qop ->
232232- let qop_str = match qop with `Auth -> "auth" | `Auth_int -> "auth-int" in
233233- let nc_str = Printf.sprintf "%08x" nc in
234234- ("qop", qop_str) ::
235235- ("nc", nc_str) ::
236236- ("cnonce", Printf.sprintf "\"%s\"" cnonce) ::
237237- params
238238- in
239239-240240- "Digest " ^ String.concat ", " (List.map (fun (k, v) -> k ^ "=" ^ v) params)
241241-242242-(** Nonce counter storage - in production should be persistent *)
243243-module NonceCounter = struct
244244- let table = Hashtbl.create 16
245245-246246- let get_and_increment ~nonce =
247247- let current = try Hashtbl.find table nonce with Not_found -> 0 in
248248- Hashtbl.replace table nonce (current + 1);
249249- current + 1
250250-251251- let reset ~nonce =
252252- Hashtbl.remove table nonce
253253-end
254254-255255-(** Apply Digest authentication to a request *)
256256-let apply_digest_auth ~username ~password ~meth ~uri ~headers ~body ~challenge_header =
257257- match parse_challenge challenge_header with
258258- | None ->
259259- Log.warn (fun m -> m "Failed to parse Digest challenge");
260260- headers
261261- | Some challenge ->
262262- (* Choose QOP if server offers options *)
263263- let qop_chosen = match challenge.qop with
264264- | None -> None
265265- | Some qops ->
266266- (* Prefer auth over auth-int for simplicity *)
267267- if List.mem `Auth qops then Some `Auth
268268- else if List.mem `Auth_int qops then Some `Auth_int
269269- else None
270270- in
271271-272272- (* Get or generate client nonce *)
273273- let cnonce = generate_cnonce () in
274274-275275- (* Get and increment nonce counter *)
276276- let nc = NonceCounter.get_and_increment ~nonce:challenge.nonce in
277277-278278- let client_data = { username; password; nc; cnonce; qop_chosen } in
279279- let auth_value = generate_auth_header ~challenge ~client_data ~meth ~uri ~body in
280280-281281- Cohttp.Header.add headers "Authorization" auth_value
282282-283283-(** Check if a response requires digest auth *)
284284-let is_digest_challenge response =
285285- let status = Cohttp.Response.status response in
286286- match Cohttp.Code.code_of_status status with
287287- | 401 ->
288288- (match Cohttp.Header.get (Cohttp.Response.headers response) "www-authenticate" with
289289- | Some header when String.starts_with ~prefix:"Digest" header -> Some header
290290- | _ -> None)
291291- | _ -> None