A batteries included HTTP/1.1 client in OCaml
at main 394 lines 13 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(** HTTP/2 Frame Layer. 7 8 This module implements HTTP/2 frame parsing and serialization as specified in 9 {{:https://datatracker.ietf.org/doc/html/rfc9113#section-4}RFC 9113 Section 4} 10 and {{:https://datatracker.ietf.org/doc/html/rfc9113#section-6}RFC 9113 Section 6}. 11 12 {2 Frame Structure} 13 14 All HTTP/2 frames share a common 9-octet header: 15 {v 16 +-----------------------------------------------+ 17 | Length (24) | 18 +---------------+---------------+---------------+ 19 | Type (8) | Flags (8) | 20 +-+-------------+---------------+-------------------------------+ 21 |R| Stream Identifier (31) | 22 +=+=============================================================+ 23 | Frame Payload (0...) ... 24 +---------------------------------------------------------------+ 25 v} 26 27 Per RFC 9113 Section 4.1: 28 - Length: 24-bit unsigned integer (payload length, not including header) 29 - Type: 8-bit frame type 30 - Flags: 8-bit flags specific to frame type 31 - R: Reserved 1-bit (MUST be 0 when sending, MUST be ignored when receiving) 32 - Stream Identifier: 31-bit unsigned integer (0 for connection-level frames) 33*) 34 35(** {1 Stream Identifier} *) 36 37type stream_id = int32 38(** Stream identifier. Per RFC 9113 Section 5.1.1: 39 - Client-initiated streams use odd numbers 40 - Server-initiated streams use even numbers 41 - Stream 0 is reserved for connection-level frames *) 42 43val stream_id_is_client_initiated : stream_id -> bool 44(** [stream_id_is_client_initiated id] returns true for odd stream IDs. *) 45 46val stream_id_is_server_initiated : stream_id -> bool 47(** [stream_id_is_server_initiated id] returns true for even non-zero stream IDs. *) 48 49(** {1 Frame Types} 50 51 Per {{:https://datatracker.ietf.org/doc/html/rfc9113#section-6}RFC 9113 Section 6}. *) 52 53type frame_type = 54 | Data (** 0x00 - RFC 9113 Section 6.1 *) 55 | Headers (** 0x01 - RFC 9113 Section 6.2 *) 56 | Priority (** 0x02 - RFC 9113 Section 6.3 (deprecated) *) 57 | Rst_stream (** 0x03 - RFC 9113 Section 6.4 *) 58 | Settings (** 0x04 - RFC 9113 Section 6.5 *) 59 | Push_promise (** 0x05 - RFC 9113 Section 6.6 *) 60 | Ping (** 0x06 - RFC 9113 Section 6.7 *) 61 | Goaway (** 0x07 - RFC 9113 Section 6.8 *) 62 | Window_update (** 0x08 - RFC 9113 Section 6.9 *) 63 | Continuation (** 0x09 - RFC 9113 Section 6.10 *) 64 | Unknown of int (** Unknown frame type - MUST be ignored per RFC 9113 Section 5.5 *) 65 66val frame_type_to_int : frame_type -> int 67(** Convert frame type to its numeric value. *) 68 69val frame_type_of_int : int -> frame_type 70(** Convert numeric value to frame type. *) 71 72val pp_frame_type : Format.formatter -> frame_type -> unit 73(** Pretty printer for frame types. *) 74 75(** {1 Frame Flags} *) 76 77module Flags : sig 78 type t = int 79 (** Frame flags as a bitmask. *) 80 81 (** {2 Common Flags} *) 82 83 val none : t 84 (** No flags set. *) 85 86 val end_stream : t 87 (** END_STREAM (0x01) - Indicates final frame in a stream. *) 88 89 val end_headers : t 90 (** END_HEADERS (0x04) - Indicates header block is complete. *) 91 92 val padded : t 93 (** PADDED (0x08) - Frame is padded. *) 94 95 val priority : t 96 (** PRIORITY (0x20) - Priority information present. *) 97 98 val ack : t 99 (** ACK (0x01) - Acknowledgment (for SETTINGS and PING). *) 100 101 (** {2 Flag Operations} *) 102 103 val test : t -> t -> bool 104 (** [test flags flag] returns true if [flag] is set in [flags]. *) 105 106 val set : t -> t -> t 107 (** [set flags flag] returns [flags] with [flag] set. *) 108 109 val clear : t -> t -> t 110 (** [clear flags flag] returns [flags] with [flag] cleared. *) 111 112 val pp : Format.formatter -> t -> unit 113 (** Pretty printer for flags. *) 114end 115 116(** {1 Frame Header} *) 117 118type frame_header = { 119 length : int; 120 (** Payload length (24-bit unsigned). MUST NOT exceed SETTINGS_MAX_FRAME_SIZE. *) 121 frame_type : frame_type; 122 (** Frame type (8-bit). *) 123 flags : Flags.t; 124 (** Frame-specific flags (8-bit). *) 125 stream_id : stream_id; 126 (** Stream identifier (31-bit). 0 for connection-level frames. *) 127} 128 129val frame_header_length : int 130(** Frame header is always 9 octets. *) 131 132val pp_frame_header : Format.formatter -> frame_header -> unit 133(** Pretty printer for frame headers. *) 134 135(** {1 Error Codes} 136 137 Per {{:https://datatracker.ietf.org/doc/html/rfc9113#section-7}RFC 9113 Section 7}. *) 138 139type error_code = 140 | No_error (** 0x00 - Graceful shutdown *) 141 | Protocol_error (** 0x01 - Protocol error detected *) 142 | Internal_error (** 0x02 - Implementation fault *) 143 | Flow_control_error (** 0x03 - Flow control limits exceeded *) 144 | Settings_timeout (** 0x04 - Settings not acknowledged in time *) 145 | Stream_closed (** 0x05 - Frame received for closed stream *) 146 | Frame_size_error (** 0x06 - Frame size incorrect *) 147 | Refused_stream (** 0x07 - Stream not processed *) 148 | Cancel (** 0x08 - Stream cancelled *) 149 | Compression_error (** 0x09 - Compression state not updated *) 150 | Connect_error (** 0x0a - TCP connection error for CONNECT *) 151 | Enhance_your_calm (** 0x0b - Processing capacity exceeded *) 152 | Inadequate_security (** 0x0c - Negotiated TLS parameters not acceptable *) 153 | Http_1_1_required (** 0x0d - Use HTTP/1.1 for this request *) 154 | Unknown_error of int32 (** Unknown error code *) 155 156val error_code_to_int32 : error_code -> int32 157(** Convert error code to its numeric value. *) 158 159val error_code_of_int32 : int32 -> error_code 160(** Convert numeric value to error code. *) 161 162val error_code_to_string : error_code -> string 163(** Convert error code to its string representation. *) 164 165val pp_error_code : Format.formatter -> error_code -> unit 166(** Pretty printer for error codes. *) 167 168(** {1 Settings} 169 170 Per {{:https://datatracker.ietf.org/doc/html/rfc9113#section-6.5.2}RFC 9113 Section 6.5.2}. *) 171 172type setting = 173 | Header_table_size of int (** 0x01 - HPACK dynamic table size *) 174 | Enable_push of bool (** 0x02 - Server push enabled *) 175 | Max_concurrent_streams of int32 (** 0x03 - Maximum concurrent streams *) 176 | Initial_window_size of int32 (** 0x04 - Initial flow control window *) 177 | Max_frame_size of int (** 0x05 - Maximum frame payload size *) 178 | Max_header_list_size of int (** 0x06 - Maximum header list size *) 179 | No_rfc7540_priorities of bool (** 0x09 - RFC 9113: Deprecate RFC 7540 priorities *) 180 | Unknown_setting of int * int32 (** Unknown setting (id, value) *) 181 182val setting_to_pair : setting -> int * int32 183(** Convert setting to (identifier, value) pair. *) 184 185val setting_of_pair : int -> int32 -> setting 186(** Convert (identifier, value) pair to setting. *) 187 188val pp_setting : Format.formatter -> setting -> unit 189(** Pretty printer for settings. *) 190 191(** {1 Priority} 192 193 Per {{:https://datatracker.ietf.org/doc/html/rfc9113#section-6.3}RFC 9113 Section 6.3}. 194 195 Note: Stream prioritization is deprecated in RFC 9113 but MUST still be parsed. *) 196 197type priority = { 198 exclusive : bool; 199 (** Exclusive dependency flag. *) 200 stream_dependency : stream_id; 201 (** Stream this one depends on. *) 202 weight : int; 203 (** Weight 1-256 (stored as 1-256, not 0-255). *) 204} 205 206val default_priority : priority 207(** Default priority: non-exclusive, depends on 0, weight 16. *) 208 209val pp_priority : Format.formatter -> priority -> unit 210(** Pretty printer for priority. *) 211 212(** {1 Frame Payloads} *) 213 214type frame_payload = 215 | Data_payload of { 216 data : Cstruct.t; 217 (** The actual data being transferred. *) 218 } 219 | Headers_payload of { 220 priority : priority option; 221 (** Priority if PRIORITY flag set. *) 222 header_block : Cstruct.t; 223 (** Encoded header block fragment (HPACK). *) 224 } 225 | Priority_payload of priority 226 (** Priority specification (deprecated). *) 227 | Rst_stream_payload of error_code 228 (** Error code for stream termination. *) 229 | Settings_payload of setting list 230 (** List of settings. Empty list for ACK. *) 231 | Push_promise_payload of { 232 promised_stream_id : stream_id; 233 (** Stream ID being reserved. *) 234 header_block : Cstruct.t; 235 (** Encoded header block fragment. *) 236 } 237 | Ping_payload of Cstruct.t 238 (** 8 bytes of opaque data. *) 239 | Goaway_payload of { 240 last_stream_id : stream_id; 241 (** Highest processed stream ID. *) 242 error_code : error_code; 243 (** Reason for closing connection. *) 244 debug_data : Cstruct.t; 245 (** Optional diagnostic data. *) 246 } 247 | Window_update_payload of int32 248 (** Window size increment (1 to 2^31-1). *) 249 | Continuation_payload of { 250 header_block : Cstruct.t; 251 (** Continuation of header block. *) 252 } 253 | Unknown_payload of Cstruct.t 254 (** Payload for unknown frame types. *) 255 256(** {1 Complete Frame} *) 257 258type frame = { 259 header : frame_header; 260 (** Frame header. *) 261 payload : frame_payload; 262 (** Frame payload. *) 263} 264 265val pp_frame : Format.formatter -> frame -> unit 266(** Pretty printer for frames. *) 267 268(** {1 Frame Parsing} 269 270 Parse frames from Eio buffered input. *) 271 272type parse_error = 273 | Incomplete 274 (** Not enough data available. *) 275 | Frame_size_error of string 276 (** Frame size exceeds limits. *) 277 | Protocol_error of string 278 (** Protocol violation. *) 279 280val pp_parse_error : Format.formatter -> parse_error -> unit 281(** Pretty printer for parse errors. *) 282 283val parse_frame_header : Cstruct.t -> (frame_header, parse_error) result 284(** [parse_frame_header buf] parses a 9-byte frame header. 285 Returns [Error Incomplete] if buffer is too small. *) 286 287val parse_frame_payload : 288 frame_header -> 289 Cstruct.t -> 290 (frame_payload, parse_error) result 291(** [parse_frame_payload header buf] parses frame payload based on type. 292 The buffer should contain exactly [header.length] bytes. *) 293 294val parse_frame : 295 Cstruct.t -> 296 max_frame_size:int -> 297 (frame * int, parse_error) result 298(** [parse_frame buf ~max_frame_size] parses a complete frame. 299 Returns the frame and number of bytes consumed. 300 [max_frame_size] is the current SETTINGS_MAX_FRAME_SIZE value. 301 Returns [Error Frame_size_error] if payload exceeds limit. *) 302 303(** {1 Frame Serialization} 304 305 Serialize frames to Eio buffered output. *) 306 307val serialize_frame_header : frame_header -> Cstruct.t 308(** [serialize_frame_header header] serializes a frame header to 9 bytes. *) 309 310val serialize_frame : frame -> Cstruct.t 311(** [serialize_frame frame] serializes a complete frame. *) 312 313val write_frame : Eio.Buf_write.t -> frame -> unit 314(** [write_frame writer frame] writes a frame to the buffer. *) 315 316(** {1 Frame Construction Helpers} *) 317 318val make_data : 319 stream_id:stream_id -> 320 ?end_stream:bool -> 321 Cstruct.t -> 322 frame 323(** [make_data ~stream_id ?end_stream data] creates a DATA frame. *) 324 325val make_headers : 326 stream_id:stream_id -> 327 ?end_stream:bool -> 328 ?end_headers:bool -> 329 ?priority:priority -> 330 Cstruct.t -> 331 frame 332(** [make_headers ~stream_id ?end_stream ?end_headers ?priority block] 333 creates a HEADERS frame. *) 334 335val make_rst_stream : 336 stream_id:stream_id -> 337 error_code -> 338 frame 339(** [make_rst_stream ~stream_id code] creates a RST_STREAM frame. *) 340 341val make_settings : 342 ?ack:bool -> 343 setting list -> 344 frame 345(** [make_settings ?ack settings] creates a SETTINGS frame. *) 346 347val make_ping : 348 ?ack:bool -> 349 Cstruct.t -> 350 frame 351(** [make_ping ?ack data] creates a PING frame. 352 [data] must be exactly 8 bytes. *) 353 354val make_goaway : 355 last_stream_id:stream_id -> 356 error_code -> 357 ?debug:string -> 358 unit -> 359 frame 360(** [make_goaway ~last_stream_id code ?debug ()] creates a GOAWAY frame. *) 361 362val make_window_update : 363 stream_id:stream_id -> 364 int32 -> 365 frame 366(** [make_window_update ~stream_id increment] creates a WINDOW_UPDATE frame. *) 367 368val make_continuation : 369 stream_id:stream_id -> 370 ?end_headers:bool -> 371 Cstruct.t -> 372 frame 373(** [make_continuation ~stream_id ?end_headers block] creates a CONTINUATION frame. *) 374 375(** {1 Constants} *) 376 377val default_max_frame_size : int 378(** Default SETTINGS_MAX_FRAME_SIZE: 16384 (2^14). *) 379 380val max_max_frame_size : int 381(** Maximum SETTINGS_MAX_FRAME_SIZE: 16777215 (2^24 - 1). *) 382 383val default_initial_window_size : int32 384(** Default initial flow control window: 65535 (2^16 - 1). *) 385 386val max_window_size : int32 387(** Maximum flow control window: 2147483647 (2^31 - 1). *) 388 389val connection_preface : string 390(** HTTP/2 connection preface (magic string): 391 "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" *) 392 393val connection_preface_length : int 394(** Length of connection preface: 24 bytes. *)