FastCGI implementation in OCaml
1(** FastCGI wire protocol parsing and serialization.
2
3 This module handles the low-level FastCGI protocol details including
4 record parsing, name-value pair encoding, and binary data serialization
5 using Eio's Buf_read and Buf_write modules.
6
7 The FastCGI protocol uses a binary record format where all data is
8 transmitted as 8-byte fixed headers followed by variable-length content
9 and optional padding. Multi-byte integers use network byte order for
10 platform independence.
11
12 The protocol supports several key features:
13 - Request multiplexing over single connections
14 - Variable-length encoding for efficient name-value pairs
15 - Stream-based data transmission with proper termination
16 - Alignment padding for optimal performance
17
18 This module provides the low-level primitives for encoding and decoding
19 these protocol elements, allowing higher-level modules to focus on
20 application logic rather than binary protocol details. *)
21
22(** Protocol parsing error.
23
24 Raised when invalid or malformed protocol data is encountered during
25 parsing operations. This includes version mismatches, invalid record
26 structures, malformed length encodings, and other protocol violations. *)
27exception Protocol_error of string
28
29(** {1 Protocol Parsing}
30
31 These functions parse incoming FastCGI records and protocol elements
32 from binary data streams. They validate protocol compliance and convert
33 binary data into OCaml types for processing. *)
34
35(** Parse a FastCGI record header from a buffer.
36
37 Reads exactly 8 bytes from the buffer to construct a record header.
38 The header contains version, record type, request ID, content length,
39 and padding length fields using network byte order.
40
41 The function validates that the version is supported (currently only
42 version 1) and that the record type is recognized.
43
44 @param buf Input buffer positioned at the start of a record header
45 @return Parsed record header with validated fields
46 @raise Protocol_error if version is unsupported or data is malformed *)
47val parse_record_header : Eio.Buf_read.t -> Fastcgi_types.record_header
48
49(** Parse a complete FastCGI record from a buffer.
50
51 Reads a record header followed by the specified amount of content data
52 and padding. This is the primary function for reading FastCGI records
53 from network streams.
54
55 The function ensures that exactly the number of content and padding
56 bytes specified in the header are read, maintaining proper stream
57 alignment for subsequent records.
58
59 @param buf Input buffer positioned at the start of a complete record
60 @return Complete parsed record with header, content, and optional padding
61 @raise Protocol_error if the record structure is invalid *)
62val parse_record : Eio.Buf_read.t -> Fastcgi_types.record
63
64(** Parse begin request body from record content.
65
66 Extracts role and connection flags from the 8-byte Begin_request record
67 body. The role indicates whether the application should act as a
68 Responder, Authorizer, or Filter. The flags control connection lifetime
69 management.
70
71 @param content Record content bytes (must be exactly 8 bytes)
72 @return Begin request body with role and flags
73 @raise Protocol_error if content length is wrong or role is unknown *)
74val parse_begin_request : bytes -> Fastcgi_types.begin_request_body
75
76(** Parse end request body from record content.
77
78 Extracts application status and protocol status from the 8-byte
79 End_request record body. The application status is similar to a
80 program exit code, while protocol status indicates completion
81 or rejection reasons.
82
83 @param content Record content bytes (must be exactly 8 bytes)
84 @return End request body with status codes
85 @raise Protocol_error if content length is wrong or status is invalid *)
86val parse_end_request : bytes -> Fastcgi_types.end_request_body
87
88(** Parse name-value pairs from record content.
89
90 Decodes the compact binary encoding used for parameter transmission.
91 Names and values can be up to 127 bytes (1-byte length) or longer
92 (4-byte length with high bit set). This encoding allows efficient
93 transmission of CGI environment variables and other parameters.
94
95 @param content Record content bytes containing encoded name-value pairs
96 @return List of decoded name-value pairs
97 @raise Protocol_error if the encoding is malformed *)
98val parse_name_value_pairs : bytes -> Fastcgi_types.name_value_pair list
99
100(** {1 Protocol Serialization}
101
102 These functions serialize OCaml types into the binary FastCGI protocol
103 format for transmission to web servers. All multi-byte integers are
104 written in network byte order for platform independence. *)
105
106(** Write a FastCGI record header to a buffer.
107
108 Serializes a record header into exactly 8 bytes using network byte order.
109 The header format is: version (1 byte), type (1 byte), request ID (2 bytes),
110 content length (2 bytes), padding length (1 byte), reserved (1 byte).
111
112 @param buf Output buffer to write the header to
113 @param header Record header to serialize *)
114val write_record_header : Eio.Buf_write.t -> Fastcgi_types.record_header -> unit
115
116(** Write a complete FastCGI record to a buffer.
117
118 Serializes a complete record including header, content data, and padding.
119 This is the primary function for sending FastCGI records over network
120 connections. The function ensures proper alignment by writing any
121 specified padding bytes.
122
123 @param buf Output buffer to write the complete record to
124 @param record Complete record with header, content, and optional padding *)
125val write_record : Eio.Buf_write.t -> Fastcgi_types.record -> unit
126
127(** Write begin request body to bytes.
128
129 Serializes role and connection flags into the 8-byte Begin_request record
130 body format. The first 2 bytes contain the role, the next byte contains
131 flags, and the remaining 5 bytes are reserved (set to zero).
132
133 @param body Begin request body with role and flags
134 @return 8-byte serialized record body *)
135val write_begin_request : Fastcgi_types.begin_request_body -> bytes
136
137(** Write end request body to bytes.
138
139 Serializes application and protocol status into the 8-byte End_request
140 record body format. The first 4 bytes contain the application status,
141 the next byte contains the protocol status, and the remaining 3 bytes
142 are reserved.
143
144 @param body End request body with status codes
145 @return 8-byte serialized record body *)
146val write_end_request : Fastcgi_types.end_request_body -> bytes
147
148(** Write name-value pairs to bytes.
149
150 Serializes name-value pairs using the compact binary encoding where
151 each pair is encoded as: name length, value length, name data, value data.
152 Lengths up to 127 bytes use 1-byte encoding; longer lengths use 4-byte
153 encoding with the high bit set.
154
155 @param pairs List of name-value pairs to encode
156 @return Serialized bytes containing all encoded pairs *)
157val write_name_value_pairs : Fastcgi_types.name_value_pair list -> bytes
158
159(** {1 Type Conversions} *)
160
161(** Convert record type to integer. *)
162val record_type_to_int : Fastcgi_types.record_type -> int
163
164(** Convert integer to record type.
165
166 @param i Integer value
167 @return Record type
168 @raise Protocol_error if unknown *)
169val record_type_of_int : int -> Fastcgi_types.record_type
170
171(** Convert role to integer. *)
172val role_to_int : Fastcgi_types.role -> int
173
174(** Convert integer to role.
175
176 @param i Integer value
177 @return Role
178 @raise Protocol_error if unknown *)
179val role_of_int : int -> Fastcgi_types.role
180
181(** Convert protocol status to integer. *)
182val protocol_status_to_int : Fastcgi_types.protocol_status -> int
183
184(** Convert integer to protocol status.
185
186 @param i Integer value
187 @return Protocol status
188 @raise Protocol_error if unknown *)
189val protocol_status_of_int : int -> Fastcgi_types.protocol_status
190
191(** {1 Length Encoding}
192
193 FastCGI uses a variable-length encoding for efficient transmission of
194 length values in name-value pairs. This encoding minimizes overhead
195 for short strings while supporting arbitrarily long values. *)
196
197(** Encode a length value using FastCGI variable-length encoding.
198
199 Short lengths (0-127) are encoded in a single byte with the high bit
200 clear. Longer lengths (128 and above) are encoded in 4 bytes with the
201 high bit of the first byte set to 1, and the remaining 31 bits containing
202 the actual length value.
203
204 This encoding provides optimal space efficiency for typical use cases
205 where most parameter names and values are relatively short.
206
207 @param length Length value to encode (must be non-negative)
208 @return 1-byte or 4-byte encoded length
209 @raise Invalid_argument if length is negative *)
210val encode_length : int -> bytes
211
212(** Decode a length value from FastCGI variable-length encoding.
213
214 Reads either 1 or 4 bytes from the buffer depending on the high bit
215 of the first byte. Returns both the decoded length and the number of
216 bytes consumed to allow proper buffer advancement.
217
218 @param buf Input buffer positioned at the start of an encoded length
219 @return Tuple of (decoded length, bytes consumed)
220 @raise Protocol_error if the encoding is invalid or buffer is too short *)
221val decode_length : Eio.Buf_read.t -> int * int
222
223(** {1 Record Construction}
224
225 These convenience functions create properly formatted FastCGI records
226 with correct headers and content. They handle the binary encoding details
227 and ensure protocol compliance. *)
228
229(** Create a begin request record.
230
231 Constructs a Begin_request record to initiate a new FastCGI request.
232 This record type is sent by web servers to start request processing
233 and specifies the role the application should play.
234
235 The record contains the application role (Responder, Authorizer, or Filter)
236 and connection flags that control whether the connection should be kept
237 alive after the request completes.
238
239 @param request_id Unique request identifier for multiplexing
240 @param role Role the application should play for this request
241 @param flags Connection management flags
242 @return Complete Begin_request record ready for transmission *)
243val make_begin_request :
244 request_id:Fastcgi_types.request_id ->
245 role:Fastcgi_types.role ->
246 flags:Fastcgi_types.connection_flags ->
247 Fastcgi_types.record
248
249(** Create an end request record.
250
251 Constructs an End_request record to complete a FastCGI request.
252 This record is sent by applications to indicate request completion
253 and provide both application-level and protocol-level status information.
254
255 The application status is similar to a program exit code, where 0
256 indicates success and non-zero values indicate errors. The protocol
257 status indicates whether the request was completed normally or rejected
258 for protocol-related reasons.
259
260 @param request_id Request ID that is being completed
261 @param app_status Application exit status (0 for success)
262 @param protocol_status Protocol completion status
263 @return Complete End_request record ready for transmission *)
264val make_end_request :
265 request_id:Fastcgi_types.request_id ->
266 app_status:Fastcgi_types.app_status ->
267 protocol_status:Fastcgi_types.protocol_status ->
268 Fastcgi_types.record
269
270(** Create a params record.
271
272 @param request_id Request ID
273 @param params Parameter name-value pairs
274 @return Params record *)
275val make_params_record :
276 request_id:Fastcgi_types.request_id ->
277 params:(string * string) list ->
278 Fastcgi_types.record
279
280(** Create a stream record (stdin, stdout, stderr, data).
281
282 @param record_type Stream record type
283 @param request_id Request ID
284 @param data Stream data
285 @return Stream record *)
286val make_stream_record :
287 record_type:Fastcgi_types.record_type ->
288 request_id:Fastcgi_types.request_id ->
289 data:bytes ->
290 Fastcgi_types.record
291
292(** Create an empty stream record (marks end of stream).
293
294 @param record_type Stream record type
295 @param request_id Request ID
296 @return Empty stream record *)
297val make_empty_stream_record :
298 record_type:Fastcgi_types.record_type ->
299 request_id:Fastcgi_types.request_id ->
300 Fastcgi_types.record
301
302(** Create a get values record.
303
304 @param variables Variable names to query
305 @return Get values record *)
306val make_get_values_record :
307 variables:string list ->
308 Fastcgi_types.record
309
310(** Create a get values result record.
311
312 @param values Variable name-value pairs
313 @return Get values result record *)
314val make_get_values_result_record :
315 values:(string * string) list ->
316 Fastcgi_types.record
317
318(** Create an unknown type record.
319
320 @param unknown_type Unknown record type value
321 @return Unknown type record *)
322val make_unknown_type_record :
323 unknown_type:int ->
324 Fastcgi_types.record
325
326(** Create an abort request record.
327
328 @param request_id Request ID to abort
329 @return Abort request record *)
330val make_abort_request_record :
331 request_id:Fastcgi_types.request_id ->
332 Fastcgi_types.record
333
334(** {1 Validation}
335
336 These functions validate protocol elements for compliance with FastCGI
337 requirements. They help detect malformed data and ensure protocol
338 correctness during parsing and construction. *)
339
340(** Validate a record header.
341
342 Checks that a record header contains valid values for all fields.
343 This includes verifying the protocol version, record type bounds,
344 content length limits, and padding length constraints.
345
346 @param header Record header to validate
347 @return True if the header is valid and protocol-compliant
348 @raise Protocol_error if any field contains invalid values *)
349val validate_record_header : Fastcgi_types.record_header -> bool
350
351(** Validate a complete record.
352
353 Performs comprehensive validation of a complete record including
354 header validation and content/padding length consistency checks.
355 Ensures the record can be safely transmitted or processed.
356
357 @param record Complete record to validate
358 @return True if the entire record is valid and well-formed
359 @raise Protocol_error if any part of the record is invalid *)
360val validate_record : Fastcgi_types.record -> bool
361
362(** Check if a record type is a management record.
363
364 Management records use request ID 0 and contain protocol-level information
365 such as capability queries (Get_values) and unknown type responses.
366 They are not associated with any specific request.
367
368 @param record_type Record type to classify
369 @return True if this is a management record type *)
370val is_management_record : Fastcgi_types.record_type -> bool
371
372(** Check if a record type is a stream record.
373
374 Stream records (Params, Stdin, Stdout, Stderr, Data) can be sent in
375 multiple parts and are terminated with an empty record of the same type.
376 This allows streaming of arbitrarily large data without buffering.
377
378 @param record_type Record type to classify
379 @return True if this is a stream record type *)
380val is_stream_record : Fastcgi_types.record_type -> bool
381
382(** {1 Protocol Constants}
383
384 Essential constants and utility functions for FastCGI protocol
385 implementation. These values are defined by the protocol specification. *)
386
387(** Null request ID for management records.
388
389 Management records always use request ID 0 to indicate they are not
390 associated with any specific request. Applications must use non-zero
391 request IDs for all application records. *)
392val null_request_id : Fastcgi_types.request_id
393
394(** Maximum content length for a single record.
395
396 FastCGI records can contain at most 65535 bytes of content data.
397 Larger data must be split across multiple records of the same type. *)
398val max_content_length : int
399
400(** Maximum padding length for a record.
401
402 Records can have at most 255 bytes of padding for alignment purposes.
403 Padding bytes are ignored by the receiver. *)
404val max_padding_length : int
405
406(** Calculate optimal padding for 8-byte alignment.
407
408 Computes the number of padding bytes needed to align the total record
409 size to an 8-byte boundary. This provides optimal performance on most
410 architectures by ensuring proper memory alignment.
411
412 @param content_length Length of the record content data
413 @return Number of padding bytes needed (0-7) *)
414val calculate_padding : int -> int