A batteries included HTTP/1.1 client in OCaml
at main 449 lines 17 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2019 Antonio Nuno Monteiro. 3 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. 4 5 All rights reserved. 6 7 Redistribution and use in source and binary forms, with or without 8 modification, are permitted provided that the following conditions are met: 9 10 1. Redistributions of source code must retain the above copyright notice, 11 this list of conditions and the following disclaimer. 12 13 2. Redistributions in binary form must reproduce the above copyright notice, 14 this list of conditions and the following disclaimer in the documentation 15 and/or other materials provided with the distribution. 16 17 3. Neither the name of the copyright holder nor the names of its contributors 18 may be used to endorse or promote products derived from this software 19 without specific prior written permission. 20 21 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 24 ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 25 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 26 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 27 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 28 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 29 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 30 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 POSSIBILITY OF SUCH DAMAGE. 32 SPDX-License-Identifier: BSD-3-Clause 33 ---------------------------------------------------------------------------*) 34 35(** HTTP/2 Stream State Machine per RFC 9113 Section 5.1. 36 37 Implements the stream lifecycle: 38 - idle -> open -> half-closed -> closed 39 - With reserved states for server push (PUSH_PROMISE) 40 41 See {{:https://datatracker.ietf.org/doc/html/rfc9113#section-5.1}RFC 9113 Section 5.1}. *) 42 43(* ============================================================ 44 Stream States 45 ============================================================ *) 46 47(** Why a stream was closed. *) 48type closed_reason = 49 | Finished 50 (** Both endpoints sent END_STREAM. *) 51 | ResetByUs of H2_frame.error_code 52 (** We sent RST_STREAM. *) 53 | ResetByThem of H2_frame.error_code 54 (** Peer sent RST_STREAM. *) 55 56(** Stream state per RFC 9113 Section 5.1. *) 57type state = 58 | Idle 59 (** Initial state. Transitions to open on send/recv HEADERS, 60 or to reserved on PUSH_PROMISE. *) 61 | Reserved_local 62 (** We sent PUSH_PROMISE. Can only send HEADERS (-> half_closed_remote) 63 or RST_STREAM (-> closed). *) 64 | Reserved_remote 65 (** Peer sent PUSH_PROMISE. Can only receive HEADERS (-> half_closed_local) 66 or send/recv RST_STREAM (-> closed). *) 67 | Open 68 (** Both sides can send frames. Transitions to half_closed on END_STREAM, 69 or closed on RST_STREAM. *) 70 | Half_closed_local 71 (** We sent END_STREAM. We can only receive frames. 72 Transitions to closed on recv END_STREAM or RST_STREAM. *) 73 | Half_closed_remote 74 (** Peer sent END_STREAM. We can only send frames. 75 Transitions to closed on send END_STREAM or RST_STREAM. *) 76 | Closed of closed_reason 77 (** Terminal state. *) 78 79(** Events that cause state transitions. *) 80type event = 81 | Send_headers of { end_stream : bool } 82 | Recv_headers of { end_stream : bool } 83 | Send_data of { end_stream : bool } 84 | Recv_data of { end_stream : bool } 85 | Send_push_promise 86 | Recv_push_promise 87 | Send_rst_stream of H2_frame.error_code 88 | Recv_rst_stream of H2_frame.error_code 89 | Send_end_stream 90 | Recv_end_stream 91 92(** Result of applying an event to a state. *) 93type transition_result = 94 | Transition_ok of state 95 | Transition_error of H2_frame.error_code * string 96 97(* ============================================================ 98 State Transition Logic 99 ============================================================ *) 100 101(** [transition state event] applies [event] to [state] and returns 102 the new state or an error. 103 104 Implements RFC 9113 Section 5.1 state machine. *) 105let transition state event = 106 match state, event with 107 (* === Idle state transitions === *) 108 | Idle, Send_headers { end_stream = false } -> 109 Transition_ok Open 110 | Idle, Send_headers { end_stream = true } -> 111 Transition_ok Half_closed_local 112 | Idle, Recv_headers { end_stream = false } -> 113 Transition_ok Open 114 | Idle, Recv_headers { end_stream = true } -> 115 Transition_ok Half_closed_remote 116 | Idle, Send_push_promise -> 117 Transition_ok Reserved_local 118 | Idle, Recv_push_promise -> 119 Transition_ok Reserved_remote 120 | Idle, _ -> 121 Transition_error (H2_frame.Protocol_error, 122 "Invalid frame on idle stream") 123 124 (* === Reserved (local) state transitions === *) 125 | Reserved_local, Send_headers { end_stream = false } -> 126 Transition_ok Half_closed_remote 127 | Reserved_local, Send_headers { end_stream = true } -> 128 (* Send HEADERS with END_STREAM on reserved -> closed *) 129 Transition_ok (Closed Finished) 130 | Reserved_local, Send_rst_stream code -> 131 Transition_ok (Closed (ResetByUs code)) 132 | Reserved_local, Recv_rst_stream code -> 133 Transition_ok (Closed (ResetByThem code)) 134 | Reserved_local, _ -> 135 Transition_error (H2_frame.Protocol_error, 136 "Invalid frame on reserved (local) stream") 137 138 (* === Reserved (remote) state transitions === *) 139 | Reserved_remote, Recv_headers { end_stream = false } -> 140 Transition_ok Half_closed_local 141 | Reserved_remote, Recv_headers { end_stream = true } -> 142 (* Recv HEADERS with END_STREAM on reserved -> closed *) 143 Transition_ok (Closed Finished) 144 | Reserved_remote, Send_rst_stream code -> 145 Transition_ok (Closed (ResetByUs code)) 146 | Reserved_remote, Recv_rst_stream code -> 147 Transition_ok (Closed (ResetByThem code)) 148 | Reserved_remote, _ -> 149 Transition_error (H2_frame.Protocol_error, 150 "Invalid frame on reserved (remote) stream") 151 152 (* === Open state transitions === *) 153 | Open, Send_headers { end_stream = true } -> 154 Transition_ok Half_closed_local 155 | Open, Send_headers { end_stream = false } -> 156 Transition_ok Open (* Trailers without END_STREAM, unusual but valid *) 157 | Open, Recv_headers { end_stream = true } -> 158 Transition_ok Half_closed_remote 159 | Open, Recv_headers { end_stream = false } -> 160 Transition_ok Open (* Trailers without END_STREAM *) 161 | Open, Send_data { end_stream = true } -> 162 Transition_ok Half_closed_local 163 | Open, Send_data { end_stream = false } -> 164 Transition_ok Open 165 | Open, Recv_data { end_stream = true } -> 166 Transition_ok Half_closed_remote 167 | Open, Recv_data { end_stream = false } -> 168 Transition_ok Open 169 | Open, Send_end_stream -> 170 Transition_ok Half_closed_local 171 | Open, Recv_end_stream -> 172 Transition_ok Half_closed_remote 173 | Open, Send_rst_stream code -> 174 Transition_ok (Closed (ResetByUs code)) 175 | Open, Recv_rst_stream code -> 176 Transition_ok (Closed (ResetByThem code)) 177 | Open, Send_push_promise | Open, Recv_push_promise -> 178 (* PUSH_PROMISE is sent on an existing stream but creates a new reserved stream *) 179 Transition_ok Open 180 181 (* === Half-closed (local) state transitions === *) 182 | Half_closed_local, Recv_headers { end_stream = true } -> 183 Transition_ok (Closed Finished) 184 | Half_closed_local, Recv_headers { end_stream = false } -> 185 Transition_ok Half_closed_local 186 | Half_closed_local, Recv_data { end_stream = true } -> 187 Transition_ok (Closed Finished) 188 | Half_closed_local, Recv_data { end_stream = false } -> 189 Transition_ok Half_closed_local 190 | Half_closed_local, Recv_end_stream -> 191 Transition_ok (Closed Finished) 192 | Half_closed_local, Send_rst_stream code -> 193 Transition_ok (Closed (ResetByUs code)) 194 | Half_closed_local, Recv_rst_stream code -> 195 Transition_ok (Closed (ResetByThem code)) 196 | Half_closed_local, (Send_headers _ | Send_data _ | Send_end_stream) -> 197 Transition_error (H2_frame.Stream_closed, 198 "Cannot send on half-closed (local) stream") 199 | Half_closed_local, _ -> 200 Transition_ok Half_closed_local (* WINDOW_UPDATE, PRIORITY allowed *) 201 202 (* === Half-closed (remote) state transitions === *) 203 | Half_closed_remote, Send_headers { end_stream = true } -> 204 Transition_ok (Closed Finished) 205 | Half_closed_remote, Send_headers { end_stream = false } -> 206 Transition_ok Half_closed_remote 207 | Half_closed_remote, Send_data { end_stream = true } -> 208 Transition_ok (Closed Finished) 209 | Half_closed_remote, Send_data { end_stream = false } -> 210 Transition_ok Half_closed_remote 211 | Half_closed_remote, Send_end_stream -> 212 Transition_ok (Closed Finished) 213 | Half_closed_remote, Send_rst_stream code -> 214 Transition_ok (Closed (ResetByUs code)) 215 | Half_closed_remote, Recv_rst_stream code -> 216 Transition_ok (Closed (ResetByThem code)) 217 | Half_closed_remote, (Recv_headers _ | Recv_data _ | Recv_end_stream) -> 218 Transition_error (H2_frame.Stream_closed, 219 "Received data on half-closed (remote) stream") 220 | Half_closed_remote, _ -> 221 Transition_ok Half_closed_remote (* WINDOW_UPDATE, PRIORITY allowed *) 222 223 (* === Closed state - terminal === *) 224 | Closed reason, Recv_rst_stream _ -> 225 (* Can receive RST_STREAM on closed stream (race condition) *) 226 Transition_ok (Closed reason) 227 | Closed _, _ -> 228 Transition_error (H2_frame.Stream_closed, 229 "Stream is closed") 230 231(* ============================================================ 232 Stream Type 233 ============================================================ *) 234 235(** Flow control state for a stream. *) 236type flow_control = { 237 mutable send_window : int; 238 (** Bytes we're allowed to send. *) 239 mutable recv_window : int; 240 (** Bytes we've advertised we can receive. *) 241 initial_send_window : int; 242 (** Initial send window from SETTINGS. *) 243 initial_recv_window : int; 244 (** Initial receive window from SETTINGS. *) 245} 246 247(** A single HTTP/2 stream. *) 248type t = { 249 id : int32; 250 (** Stream identifier (odd = client-initiated, even = server-initiated). *) 251 mutable state : state; 252 (** Current stream state. *) 253 flow : flow_control; 254 (** Flow control windows. *) 255 mutable request_headers : H2_hpack.header list option; 256 (** Request headers once received/sent. *) 257 mutable response_headers : H2_hpack.header list option; 258 (** Response headers once received/sent. *) 259 mutable trailers : H2_hpack.header list option; 260 (** Trailing headers. *) 261} 262 263(** Default initial flow control window size per RFC 9113. *) 264let default_initial_window_size = 65535 265 266(** Create a new stream with the given ID. *) 267let create ?(initial_send_window = default_initial_window_size) 268 ?(initial_recv_window = default_initial_window_size) 269 id = 270 { id; 271 state = Idle; 272 flow = { 273 send_window = initial_send_window; 274 recv_window = initial_recv_window; 275 initial_send_window; 276 initial_recv_window; 277 }; 278 request_headers = None; 279 response_headers = None; 280 trailers = None; 281 } 282 283(** Get the stream ID. *) 284let id t = t.id 285 286(** Get the current state. *) 287let state t = t.state 288 289(** Helper for state predicate checks. *) 290let state_matches pred t = pred t.state 291 292(** Check if stream is in idle state. *) 293let is_idle = state_matches (function Idle -> true | _ -> false) 294 295(** Check if stream is open (including half-closed). *) 296let is_active = state_matches (function 297 | Open | Half_closed_local | Half_closed_remote -> true 298 | _ -> false) 299 300(** Check if stream is fully open (both directions). *) 301let is_open = state_matches (function Open -> true | _ -> false) 302 303(** Check if stream is closed. *) 304let is_closed = state_matches (function Closed _ -> true | _ -> false) 305 306(** Check if we can send on this stream. *) 307let can_send = state_matches (function 308 | Open | Half_closed_remote | Reserved_local -> true 309 | _ -> false) 310 311(** Check if we can receive on this stream. *) 312let can_recv = state_matches (function 313 | Open | Half_closed_local | Reserved_remote -> true 314 | _ -> false) 315 316(** Apply an event to the stream, updating its state. 317 Returns Ok () on success, or Error with an error code and message. *) 318let apply_event t event = 319 match transition t.state event with 320 | Transition_ok new_state -> 321 t.state <- new_state; 322 Ok () 323 | Transition_error (code, msg) -> 324 Error (code, msg) 325 326(* ============================================================ 327 Flow Control 328 ============================================================ *) 329 330(** Consume bytes from the send window. 331 Returns the number of bytes actually consumed (may be less if window exhausted). *) 332let consume_send_window t bytes = 333 let available = min bytes t.flow.send_window in 334 t.flow.send_window <- t.flow.send_window - available; 335 available 336 337(** Add bytes to the send window (from WINDOW_UPDATE). *) 338let credit_send_window t increment = 339 let new_window = t.flow.send_window + increment in 340 if new_window > 0x7FFFFFFF then 341 Error (H2_frame.Flow_control_error, 342 "Flow control window overflow") 343 else begin 344 t.flow.send_window <- new_window; 345 Ok () 346 end 347 348(** Consume bytes from the receive window. 349 Call this when receiving DATA frames. *) 350let consume_recv_window t bytes = 351 t.flow.recv_window <- t.flow.recv_window - bytes 352 353(** Credit the receive window (we're sending WINDOW_UPDATE). *) 354let credit_recv_window t increment = 355 t.flow.recv_window <- t.flow.recv_window + increment 356 357(** Get available send window. *) 358let send_window t = t.flow.send_window 359 360(** Get available receive window. *) 361let recv_window t = t.flow.recv_window 362 363(** Get initial receive window size. *) 364let initial_recv_window t = t.flow.initial_recv_window 365 366(** Update initial window size (from SETTINGS). 367 Adjusts the current window by the delta. *) 368let update_initial_window_size t new_initial_size = 369 let delta = new_initial_size - t.flow.initial_send_window in 370 let new_window = t.flow.send_window + delta in 371 if new_window > 0x7FFFFFFF || new_window < 0 then 372 Error (H2_frame.Flow_control_error, 373 "Flow control window overflow after SETTINGS update") 374 else begin 375 t.flow.send_window <- new_window; 376 Ok () 377 end 378 379(* ============================================================ 380 Stream Identifier Management 381 ============================================================ *) 382 383(** Check if stream ID is client-initiated (odd). *) 384let is_client_initiated id = Int32.(logand id 1l = 1l) 385 386(** Check if stream ID is server-initiated (even, non-zero). *) 387let is_server_initiated id = Int32.(id > 0l && logand id 1l = 0l) 388 389(** Check if stream ID is valid (non-zero). *) 390let is_valid_id id = Int32.compare id 0l > 0 391 392(** Connection-level stream ID is 0. *) 393let connection_stream_id = 0l 394 395(** Check if this is the connection-level stream. *) 396let is_connection_stream id = Int32.equal id 0l 397 398(* ============================================================ 399 Pretty Printing 400 ============================================================ *) 401 402let pp_closed_reason fmt = function 403 | Finished -> Format.fprintf fmt "Finished" 404 | ResetByUs code -> 405 Format.fprintf fmt "ResetByUs(%a)" H2_frame.pp_error_code code 406 | ResetByThem code -> 407 Format.fprintf fmt "ResetByThem(%a)" H2_frame.pp_error_code code 408 409let pp_state fmt = function 410 | Idle -> Format.fprintf fmt "Idle" 411 | Reserved_local -> Format.fprintf fmt "Reserved(local)" 412 | Reserved_remote -> Format.fprintf fmt "Reserved(remote)" 413 | Open -> Format.fprintf fmt "Open" 414 | Half_closed_local -> Format.fprintf fmt "HalfClosed(local)" 415 | Half_closed_remote -> Format.fprintf fmt "HalfClosed(remote)" 416 | Closed reason -> Format.fprintf fmt "Closed(%a)" pp_closed_reason reason 417 418let state_to_string state = 419 Format.asprintf "%a" pp_state state 420 421let pp fmt t = 422 Format.fprintf fmt "Stream{id=%ld; state=%a; send_window=%d; recv_window=%d}" 423 t.id pp_state t.state t.flow.send_window t.flow.recv_window 424 425(* ============================================================ 426 Headers Management 427 ============================================================ *) 428 429(** Set request headers. *) 430let set_request_headers t headers = t.request_headers <- Some headers 431 432(** Get request headers. *) 433let request_headers t = t.request_headers 434 435(** Set response headers. *) 436let set_response_headers t headers = t.response_headers <- Some headers 437 438(** Get response headers. *) 439let response_headers t = t.response_headers 440 441(** Set trailers. *) 442let set_trailers t headers = t.trailers <- Some headers 443 444(** Get trailers. *) 445let trailers t = t.trailers 446 447(** Reset the stream with an error code. *) 448let reset t code = 449 ignore (apply_event t (Send_rst_stream code))