···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3+ SPDX-License-Identifier: ISC
4+ ---------------------------------------------------------------------------*)
5+6+(** HTTP 100-Continue configuration
7+8+ Configuration for the HTTP 100-Continue protocol, which allows clients
9+ to check if the server will accept a request before sending a large body.
10+ Per RFC 9110 Section 10.1.1 (Expect) and Section 15.2.1 (100 Continue). *)
11+12+type t
13+(** Abstract type representing HTTP 100-Continue configuration. *)
14+15+val default : t
16+(** Default configuration:
17+ - enabled: true
18+ - threshold: 1MB
19+ - timeout: 1.0s *)
20+21+val make :
22+ ?enabled:bool ->
23+ ?threshold:int64 ->
24+ ?timeout:float ->
25+ unit -> t
26+(** Create custom 100-Continue configuration. All parameters are optional
27+ and default to the values in {!default}. *)
28+29+val disabled : t
30+(** Configuration with 100-Continue disabled. *)
31+32+val enabled : t -> bool
33+(** Whether 100-continue is enabled. *)
34+35+val threshold : t -> int64
36+(** Body size threshold in bytes to trigger 100-continue. *)
37+38+val timeout : t -> float
39+(** Timeout in seconds to wait for 100 response. *)
40+41+val should_use : t -> int64 -> bool
42+(** [should_use t body_size] returns [true] if 100-continue should be used
43+ for a request with the given [body_size]. *)
44+45+val pp : Format.formatter -> t -> unit
46+(** Pretty-printer for 100-Continue configuration. *)
47+48+val to_string : t -> string
49+(** Convert configuration to a human-readable string. *)
+17-18
lib/http_client.ml
···9 {!Http_read} for response parsing, leveraging Eio's Buf_write and Buf_read
10 for efficient I/O.
1112- Types are imported from {!Http_types} and re-exported for API convenience. *)
01314let src = Logs.Src.create "requests.http_client" ~doc:"Low-level HTTP client"
15module Log = (val Logs.src_log src : Logs.LOG)
1617(** {1 Types}
1819- Re-exported from {!Http_types} for API convenience.
20- We open Http_types to bring record field names into scope. *)
21-22-open Http_types
2324-type limits = Http_types.limits
25-let default_limits = Http_types.default_limits
2627-type expect_100_config = Http_types.expect_100_config
28-let default_expect_100_config = Http_types.default_expect_100_config
2930(** {1 Decompression Support} *)
31···119let check_decompression_limits ~limits ~compressed_size decompressed =
120 let decompressed_size = Int64.of_int (String.length decompressed) in
121 let compressed_size_i64 = Int64.of_int compressed_size in
0122123 (* Check absolute size *)
124- if decompressed_size > limits.max_decompressed_size then begin
125 let ratio = Int64.to_float decompressed_size /. Int64.to_float compressed_size_i64 in
126 raise (Error.err (Error.Decompression_bomb {
127- limit = limits.max_decompressed_size;
128 ratio
129 }))
130 end;
···132 (* Check ratio - only if compressed size is > 0 to avoid division by zero *)
133 if compressed_size > 0 then begin
134 let ratio = Int64.to_float decompressed_size /. Int64.to_float compressed_size_i64 in
135- if ratio > limits.max_compression_ratio then
136 raise (Error.err (Error.Decompression_bomb {
137- limit = limits.max_decompressed_size;
138 ratio
139 }))
140 end;
···281282 (* Determine if we should use 100-continue *)
283 let use_100_continue =
284- expect_100.enabled &&
285- body_len >= expect_100.threshold &&
286 body_len > 0L &&
287 not (Headers.mem "expect" headers) (* Don't override explicit Expect header *)
288 in
···290 if not use_100_continue then begin
291 (* Standard request without 100-continue *)
292 Log.debug (fun m -> m "100-continue not used (body_len=%Ld, threshold=%Ld, enabled=%b)"
293- body_len expect_100.threshold expect_100.enabled);
294 make_request ~limits ~sw ~method_ ~uri ~headers ~body flow
295 end else begin
296 Log.info (fun m -> m "Using 100-continue for large body (%Ld bytes)" body_len);
···311 (* Wait for 100 Continue or error response with timeout *)
312 let result =
313 try
314- Eio.Time.with_timeout_exn clock expect_100.timeout (fun () ->
315- wait_for_100_continue ~limits ~timeout:expect_100.timeout flow
316 )
317 with Eio.Time.Timeout ->
318 Log.debug (fun m -> m "100-continue timeout expired, sending body anyway");
···9 {!Http_read} for response parsing, leveraging Eio's Buf_write and Buf_read
10 for efficient I/O.
1112+ Types are imported from domain-specific modules ({!Response_limits},
13+ {!Expect_continue}) and re-exported for API convenience. *)
1415let src = Logs.Src.create "requests.http_client" ~doc:"Low-level HTTP client"
16module Log = (val Logs.src_log src : Logs.LOG)
1718(** {1 Types}
1920+ Re-exported from domain-specific modules for API convenience. *)
0002122+type limits = Response_limits.t
23+let default_limits = Response_limits.default
2425+type expect_100_config = Expect_continue.t
26+let default_expect_100_config = Expect_continue.default
2728(** {1 Decompression Support} *)
29···117let check_decompression_limits ~limits ~compressed_size decompressed =
118 let decompressed_size = Int64.of_int (String.length decompressed) in
119 let compressed_size_i64 = Int64.of_int compressed_size in
120+ let max_decompressed = Response_limits.max_decompressed_size limits in
121122 (* Check absolute size *)
123+ if decompressed_size > max_decompressed then begin
124 let ratio = Int64.to_float decompressed_size /. Int64.to_float compressed_size_i64 in
125 raise (Error.err (Error.Decompression_bomb {
126+ limit = max_decompressed;
127 ratio
128 }))
129 end;
···131 (* Check ratio - only if compressed size is > 0 to avoid division by zero *)
132 if compressed_size > 0 then begin
133 let ratio = Int64.to_float decompressed_size /. Int64.to_float compressed_size_i64 in
134+ if ratio > Response_limits.max_compression_ratio limits then
135 raise (Error.err (Error.Decompression_bomb {
136+ limit = max_decompressed;
137 ratio
138 }))
139 end;
···280281 (* Determine if we should use 100-continue *)
282 let use_100_continue =
283+ Expect_continue.enabled expect_100 &&
284+ body_len >= Expect_continue.threshold expect_100 &&
285 body_len > 0L &&
286 not (Headers.mem "expect" headers) (* Don't override explicit Expect header *)
287 in
···289 if not use_100_continue then begin
290 (* Standard request without 100-continue *)
291 Log.debug (fun m -> m "100-continue not used (body_len=%Ld, threshold=%Ld, enabled=%b)"
292+ body_len (Expect_continue.threshold expect_100) (Expect_continue.enabled expect_100));
293 make_request ~limits ~sw ~method_ ~uri ~headers ~body flow
294 end else begin
295 Log.info (fun m -> m "Using 100-continue for large body (%Ld bytes)" body_len);
···310 (* Wait for 100 Continue or error response with timeout *)
311 let result =
312 try
313+ Eio.Time.with_timeout_exn clock (Expect_continue.timeout expect_100) (fun () ->
314+ wait_for_100_continue ~limits ~timeout:(Expect_continue.timeout expect_100) flow
315 )
316 with Eio.Time.Timeout ->
317 Log.debug (fun m -> m "100-continue timeout expired, sending body anyway");
+21-18
lib/http_read.ml
···1314module Read = Eio.Buf_read
1516-(** Import limits from Http_types - the single source of truth.
17- We open Http_types to bring record field names into scope. *)
18-open Http_types
19-type limits = Http_types.limits
2021(** {1 Character Predicates} *)
22···109110(** Parse all headers with size and count limits *)
111let headers ~limits r =
00112 let rec loop acc count =
113 (* Check header count limit *)
114- if count >= limits.max_header_count then
115 raise (Error.err (Error.Headers_too_large {
116- limit = limits.max_header_count;
117 actual = count + 1
118 }));
119···126 end else begin
127 (* Check header line size limit *)
128 let line_len = String.length name + String.length value + 2 in
129- if line_len > limits.max_header_size then
130 raise (Error.err (Error.Headers_too_large {
131- limit = limits.max_header_size;
132 actual = line_len
133 }));
134···141142(** Read a fixed-length body with size limit checking *)
143let fixed_body ~limits ~length r =
0144 (* Check size limit before allocating *)
145- if length > limits.max_response_body_size then
146 raise (Error.err (Error.Body_too_large {
147- limit = limits.max_response_body_size;
148 actual = Some length
149 }));
150···203(** Read a chunked transfer-encoded body with size limit checking *)
204let chunked_body ~limits r =
205 Log.debug (fun m -> m "Reading chunked body");
0206 let buf = Buffer.create 4096 in
207 let total_size = ref 0L in
208···217 end else begin
218 (* Check size limit before reading chunk *)
219 let new_total = Int64.add !total_size (Int64.of_int size) in
220- if new_total > limits.max_response_body_size then
221 raise (Error.err (Error.Body_too_large {
222- limit = limits.max_response_body_size;
223 actual = Some new_total
224 }));
225···260end
261262let fixed_body_stream ~limits ~length buf_read =
0263 (* Check size limit *)
264- if length > limits.max_response_body_size then
265 raise (Error.err (Error.Body_too_large {
266- limit = limits.max_response_body_size;
267 actual = Some length
268 }));
269···283 buf_read : Read.t;
284 mutable state : state;
285 mutable total_read : int64;
286- limits : limits;
287 }
288289 let read_chunk_size t =
···315 end else begin
316 (* Check size limit *)
317 let new_total = Int64.add t.total_read (Int64.of_int size) in
318- if new_total > t.limits.max_response_body_size then
319 raise (Error.err (Error.Body_too_large {
320- limit = t.limits.max_response_body_size;
321 actual = Some new_total
322 }));
323 t.state <- Reading_chunk size;
···352 Chunked_body_source.buf_read;
353 state = Reading_size;
354 total_read = 0L;
355- limits
356 } in
357 let ops = Eio.Flow.Pi.source (module Chunked_body_source) in
358 Eio.Resource.T (t, ops)
···1314module Read = Eio.Buf_read
1516+(** Import limits from Response_limits module. *)
17+type limits = Response_limits.t
001819(** {1 Character Predicates} *)
20···107108(** Parse all headers with size and count limits *)
109let headers ~limits r =
110+ let max_count = Response_limits.max_header_count limits in
111+ let max_size = Response_limits.max_header_size limits in
112 let rec loop acc count =
113 (* Check header count limit *)
114+ if count >= max_count then
115 raise (Error.err (Error.Headers_too_large {
116+ limit = max_count;
117 actual = count + 1
118 }));
119···126 end else begin
127 (* Check header line size limit *)
128 let line_len = String.length name + String.length value + 2 in
129+ if line_len > max_size then
130 raise (Error.err (Error.Headers_too_large {
131+ limit = max_size;
132 actual = line_len
133 }));
134···141142(** Read a fixed-length body with size limit checking *)
143let fixed_body ~limits ~length r =
144+ let max_body = Response_limits.max_response_body_size limits in
145 (* Check size limit before allocating *)
146+ if length > max_body then
147 raise (Error.err (Error.Body_too_large {
148+ limit = max_body;
149 actual = Some length
150 }));
151···204(** Read a chunked transfer-encoded body with size limit checking *)
205let chunked_body ~limits r =
206 Log.debug (fun m -> m "Reading chunked body");
207+ let max_body = Response_limits.max_response_body_size limits in
208 let buf = Buffer.create 4096 in
209 let total_size = ref 0L in
210···219 end else begin
220 (* Check size limit before reading chunk *)
221 let new_total = Int64.add !total_size (Int64.of_int size) in
222+ if new_total > max_body then
223 raise (Error.err (Error.Body_too_large {
224+ limit = max_body;
225 actual = Some new_total
226 }));
227···262end
263264let fixed_body_stream ~limits ~length buf_read =
265+ let max_body = Response_limits.max_response_body_size limits in
266 (* Check size limit *)
267+ if length > max_body then
268 raise (Error.err (Error.Body_too_large {
269+ limit = max_body;
270 actual = Some length
271 }));
272···286 buf_read : Read.t;
287 mutable state : state;
288 mutable total_read : int64;
289+ max_body_size : int64;
290 }
291292 let read_chunk_size t =
···318 end else begin
319 (* Check size limit *)
320 let new_total = Int64.add t.total_read (Int64.of_int size) in
321+ if new_total > t.max_body_size then
322 raise (Error.err (Error.Body_too_large {
323+ limit = t.max_body_size;
324 actual = Some new_total
325 }));
326 t.state <- Reading_chunk size;
···355 Chunked_body_source.buf_read;
356 state = Reading_size;
357 total_read = 0L;
358+ max_body_size = Response_limits.max_response_body_size limits;
359 } in
360 let ops = Eio.Flow.Pi.source (module Chunked_body_source) in
361 Eio.Resource.T (t, ops)
+3-3
lib/http_read.mli
···1617(** {1 Response Limits}
1819- This module uses {!Http_types.limits} from the shared types module. *)
2021-type limits = Http_types.limits
22-(** Alias for {!Http_types.limits}. See {!Http_types} for field documentation. *)
2324(** {1 Low-level Parsers} *)
25
···1617(** {1 Response Limits}
1819+ This module uses {!Response_limits.t} for size limit configuration. *)
2021+type limits = Response_limits.t
22+(** Alias for {!Response_limits.t}. See {!Response_limits} for documentation. *)
2324(** {1 Low-level Parsers} *)
25
-48
lib/http_types.ml
···1-(*---------------------------------------------------------------------------
2- Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3- SPDX-License-Identifier: ISC
4- ---------------------------------------------------------------------------*)
5-6-(** Shared types for HTTP protocol handling
7-8- This module contains type definitions used across the HTTP client modules.
9- It serves as the single source of truth for types shared between
10- {!Http_read}, {!Http_write}, and {!Http_client}. *)
11-12-(** {1 Response Limits}
13-14- Per Recommendation #2: Configurable limits for response body size,
15- header count, and header length to prevent DoS attacks. *)
16-17-type limits = {
18- max_response_body_size: int64; (** Maximum response body size in bytes (default: 100MB) *)
19- max_header_size: int; (** Maximum size of a single header line (default: 16KB) *)
20- max_header_count: int; (** Maximum number of headers (default: 100) *)
21- max_decompressed_size: int64; (** Maximum decompressed size (default: 100MB) *)
22- max_compression_ratio: float; (** Maximum compression ratio allowed (default: 100:1) *)
23-}
24-25-let default_limits = {
26- max_response_body_size = 104_857_600L; (* 100MB *)
27- max_header_size = 16_384; (* 16KB *)
28- max_header_count = 100;
29- max_decompressed_size = 104_857_600L; (* 100MB *)
30- max_compression_ratio = 100.0; (* 100:1 *)
31-}
32-33-(** {1 HTTP 100-Continue Configuration}
34-35- Per Recommendation #7: HTTP 100-Continue Support for Large Uploads.
36- RFC 9110 Section 10.1.1 (Expect) and Section 15.2.1 (100 Continue) *)
37-38-type expect_100_config = {
39- enabled : bool; (** Whether to use 100-continue at all *)
40- threshold : int64; (** Body size threshold to trigger 100-continue (default: 1MB) *)
41- timeout : float; (** Timeout to wait for 100 response (default: 1.0s) *)
42-}
43-44-let default_expect_100_config = {
45- enabled = true;
46- threshold = 1_048_576L; (* 1MB *)
47- timeout = 1.0; (* 1 second *)
48-}
···1-(*---------------------------------------------------------------------------
2- Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3- SPDX-License-Identifier: ISC
4- ---------------------------------------------------------------------------*)
5-6-(** Shared types for HTTP protocol handling
7-8- This module contains type definitions used across the HTTP client modules.
9- It serves as the single source of truth for types shared between
10- {!Http_read}, {!Http_write}, and {!Http_client}. *)
11-12-(** {1 Response Limits}
13-14- Configurable limits for response body size, header count, and header length
15- to prevent DoS attacks. *)
16-17-type limits = {
18- max_response_body_size: int64; (** Maximum response body size in bytes *)
19- max_header_size: int; (** Maximum size of a single header line *)
20- max_header_count: int; (** Maximum number of headers *)
21- max_decompressed_size: int64; (** Maximum decompressed size *)
22- max_compression_ratio: float; (** Maximum compression ratio allowed *)
23-}
24-(** Response size limits to prevent resource exhaustion. *)
25-26-val default_limits : limits
27-(** Default limits:
28- - max_response_body_size: 100MB
29- - max_header_size: 16KB
30- - max_header_count: 100
31- - max_decompressed_size: 100MB
32- - max_compression_ratio: 100:1 *)
33-34-(** {1 HTTP 100-Continue Configuration}
35-36- Configuration for the HTTP 100-Continue protocol, which allows clients
37- to check if the server will accept a request before sending a large body. *)
38-39-type expect_100_config = {
40- enabled : bool; (** Whether to use 100-continue at all *)
41- threshold : int64; (** Body size threshold to trigger 100-continue *)
42- timeout : float; (** Timeout to wait for 100 response in seconds *)
43-}
44-(** Configuration for HTTP 100-Continue support. *)
45-46-val default_expect_100_config : expect_100_config
47-(** Default configuration:
48- - enabled: true
49- - threshold: 1MB
50- - timeout: 1.0s *)
···1+(*---------------------------------------------------------------------------
2+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3+ SPDX-License-Identifier: ISC
4+ ---------------------------------------------------------------------------*)
5+6+(** Response limits for HTTP protocol handling
7+8+ Configurable limits for response body size, header count, and header length
9+ to prevent DoS attacks. *)
10+11+type t
12+(** Abstract type representing HTTP response limits. *)
13+14+val default : t
15+(** Default limits:
16+ - max_response_body_size: 100MB
17+ - max_header_size: 16KB
18+ - max_header_count: 100
19+ - max_decompressed_size: 100MB
20+ - max_compression_ratio: 100:1 *)
21+22+val make :
23+ ?max_response_body_size:int64 ->
24+ ?max_header_size:int ->
25+ ?max_header_count:int ->
26+ ?max_decompressed_size:int64 ->
27+ ?max_compression_ratio:float ->
28+ unit -> t
29+(** Create custom response limits. All parameters are optional and default
30+ to the values in {!default}. *)
31+32+val max_response_body_size : t -> int64
33+(** Maximum response body size in bytes. *)
34+35+val max_header_size : t -> int
36+(** Maximum size of a single header line in bytes. *)
37+38+val max_header_count : t -> int
39+(** Maximum number of headers allowed. *)
40+41+val max_decompressed_size : t -> int64
42+(** Maximum decompressed size in bytes. *)
43+44+val max_compression_ratio : t -> float
45+(** Maximum compression ratio allowed (e.g., 100.0 means 100:1). *)
46+47+val pp : Format.formatter -> t -> unit
48+(** Pretty-printer for response limits. *)
49+50+val to_string : t -> string
51+(** Convert response limits to a human-readable string. *)