FastCGI implementation in OCaml
1
2(** {1 Error Handling Helpers} *)
3
4(** Helper functions for consistent error handling *)
5let invalid_version version =
6 invalid_arg (Printf.sprintf "Unsupported FastCGI version: %d" version)
7
8let unknown_record_type n =
9 invalid_arg (Printf.sprintf "Unknown FastCGI record type: %d" n)
10
11let buffer_underflow msg =
12 failwith (Printf.sprintf "Unexpected end of buffer while %s" msg)
13
14let check_bounds buf pos len context =
15 if pos + len > String.length buf then
16 buffer_underflow context
17
18type version = int
19
20type record =
21 | Begin_request
22 | Abort_request
23 | End_request
24 | Params
25 | Stdin
26 | Stdout
27 | Stderr
28 | Data
29 | Get_values
30 | Get_values_result
31 | Unknown_type
32
33let record_to_int = function
34 | Begin_request -> 1
35 | Abort_request -> 2
36 | End_request -> 3
37 | Params -> 4
38 | Stdin -> 5
39 | Stdout -> 6
40 | Stderr -> 7
41 | Data -> 8
42 | Get_values -> 9
43 | Get_values_result -> 10
44 | Unknown_type -> 11
45
46let record_of_int = function
47 | 1 -> Begin_request
48 | 2 -> Abort_request
49 | 3 -> End_request
50 | 4 -> Params
51 | 5 -> Stdin
52 | 6 -> Stdout
53 | 7 -> Stderr
54 | 8 -> Data
55 | 9 -> Get_values
56 | 10 -> Get_values_result
57 | 11 -> Unknown_type
58 | n -> unknown_record_type n
59
60let pp_record ppf = function
61 | Begin_request -> Format.pp_print_string ppf "Begin_request"
62 | Abort_request -> Format.pp_print_string ppf "Abort_request"
63 | End_request -> Format.pp_print_string ppf "End_request"
64 | Params -> Format.pp_print_string ppf "Params"
65 | Stdin -> Format.pp_print_string ppf "Stdin"
66 | Stdout -> Format.pp_print_string ppf "Stdout"
67 | Stderr -> Format.pp_print_string ppf "Stderr"
68 | Data -> Format.pp_print_string ppf "Data"
69 | Get_values -> Format.pp_print_string ppf "Get_values"
70 | Get_values_result -> Format.pp_print_string ppf "Get_values_result"
71 | Unknown_type -> Format.pp_print_string ppf "Unknown_type"
72
73type request_id = int
74
75type t = {
76 version : version;
77 record_type : record;
78 request_id : request_id;
79 content : string;
80 offset : int;
81 length : int;
82}
83
84let pp ?(max_content_len=100) ppf record =
85 let actual_content = String.sub record.content record.offset record.length in
86 let truncated_content =
87 let len = String.length actual_content in
88 if len <= max_content_len then actual_content
89 else String.sub actual_content 0 max_content_len ^ "..." ^ Printf.sprintf " (%d more bytes)" (len - max_content_len)
90 in
91 Format.fprintf ppf
92 "@[<2>{ version = %d;@ record_type = %a;@ request_id = %d;@ content = %S;@ offset = %d;@ length = %d }@]"
93 record.version
94 pp_record record.record_type
95 record.request_id
96 truncated_content
97 record.offset
98 record.length
99
100(* FastCGI constants *)
101let fcgi_version_1 = 1
102let fcgi_header_len = 8
103
104let read buf_read =
105 Printf.eprintf "[DEBUG] Fastcgi_record.read: Starting to read record\n%!";
106 (* Read the 8-byte header *)
107 Printf.eprintf "[DEBUG] Fastcgi_record.read: Reading %d-byte header\n%!" fcgi_header_len;
108 let header = Eio.Buf_read.take fcgi_header_len buf_read in
109
110 (* Parse header fields *)
111 let version = Char.code header.[0] in
112 let record_type_int = Char.code header.[1] in
113 let request_id = (Char.code header.[2] lsl 8) lor (Char.code header.[3]) in
114 let content_length = (Char.code header.[4] lsl 8) lor (Char.code header.[5]) in
115 let padding_length = Char.code header.[6] in
116 let _reserved = Char.code header.[7] in
117
118 Printf.eprintf "[DEBUG] Fastcgi_record.read: Header parsed - version=%d, type=%d, id=%d, content_len=%d, padding=%d\n%!"
119 version record_type_int request_id content_length padding_length;
120
121 (* Validate version *)
122 if version <> fcgi_version_1 then invalid_version version;
123
124 (* Convert record type *)
125 let record_type = record_of_int record_type_int in
126 Printf.eprintf "[DEBUG] Fastcgi_record.read: Record type = %s\n%!"
127 (Format.asprintf "%a" pp_record record_type);
128
129 (* Read content *)
130 let content =
131 if content_length = 0 then (
132 Printf.eprintf "[DEBUG] Fastcgi_record.read: No content to read (length=0)\n%!";
133 ""
134 ) else (
135 Printf.eprintf "[DEBUG] Fastcgi_record.read: Reading %d bytes of content\n%!" content_length;
136 let c = Eio.Buf_read.take content_length buf_read in
137 Printf.eprintf "[DEBUG] Fastcgi_record.read: Successfully read %d bytes\n%!" (String.length c);
138 c
139 )
140 in
141
142 (* Skip padding *)
143 if padding_length > 0 then (
144 Printf.eprintf "[DEBUG] Fastcgi_record.read: Skipping %d bytes of padding\n%!" padding_length;
145 ignore (Eio.Buf_read.take padding_length buf_read)
146 );
147
148 let record = { version; record_type; request_id; content; offset = 0; length = String.length content } in
149 Printf.eprintf "[DEBUG] Fastcgi_record.read: Complete record = %s\n%!"
150 (Format.asprintf "%a" (pp ~max_content_len:50) record);
151 record
152
153let write buf_write record =
154 let total_content_length = String.length record.content in
155 let content_offset = record.offset in
156 let content_length = record.length in
157
158 (* Validate bounds *)
159 if content_offset < 0 || content_offset > total_content_length then
160 invalid_arg "Fastcgi_record.write: offset out of bounds";
161 if content_length < 0 || content_offset + content_length > total_content_length then
162 invalid_arg "Fastcgi_record.write: length out of bounds";
163
164 (* Calculate padding for 8-byte alignment *)
165 let padding_length = (8 - (content_length land 7)) land 7 in
166
167 (* Create and write header *)
168 let header = Bytes.create fcgi_header_len in
169 Bytes.set_uint8 header 0 record.version;
170 Bytes.set_uint8 header 1 (record_to_int record.record_type);
171 Bytes.set_uint16_be header 2 record.request_id;
172 Bytes.set_uint16_be header 4 content_length;
173 Bytes.set_uint8 header 6 padding_length;
174 Bytes.set_uint8 header 7 0; (* reserved *)
175
176 Eio.Buf_write.string buf_write (Bytes.to_string header);
177
178 (* Write content with offset and length *)
179 if content_length > 0 then
180 Eio.Buf_write.string buf_write record.content ~off:content_offset ~len:content_length;
181
182 (* Write padding *)
183 if padding_length > 0 then
184 Eio.Buf_write.string buf_write (String.make padding_length '\000')
185
186let create ?(version=1) ~record ~request_id ~content ?(offset=0) ?length () =
187 let content_length = match length with
188 | None -> String.length content - offset
189 | Some l -> l
190 in
191 { version; record_type = record; request_id; content; offset; length = content_length }
192
193module KV = struct
194 type t = (string * string) list
195
196 let empty = []
197
198 let add key value kvs = (key, value) :: kvs
199
200 let remove key kvs = List.filter (fun (k, _) -> k <> key) kvs
201
202 let cardinal kvs = List.length kvs
203
204 let find key kvs =
205 try List.assoc key kvs
206 with Not_found -> raise Not_found
207
208 let find_opt key kvs =
209 try Some (List.assoc key kvs)
210 with Not_found -> None
211
212 let to_seq kvs = List.to_seq kvs
213
214 let of_seq seq = List.of_seq seq
215
216 (** Helper functions for length encoding/decoding *)
217 let is_long_length first_byte = first_byte land 0x80 <> 0
218
219 let decode_short_length first_byte = first_byte
220
221 let decode_long_length buf pos =
222 check_bounds buf pos 4 "reading 4-byte length";
223 let first_byte = Char.code buf.[pos] in
224 ((first_byte land 0x7f) lsl 24) lor
225 ((Char.code buf.[pos + 1]) lsl 16) lor
226 ((Char.code buf.[pos + 2]) lsl 8) lor
227 (Char.code buf.[pos + 3])
228
229 let encode_length len =
230 if len <= 127 then
231 String.make 1 (Char.chr len)
232 else
233 let b = Bytes.create 4 in
234 Bytes.set_int32_be b 0 (Int32.logor (Int32.of_int len) 0x80000000l);
235 Bytes.to_string b
236
237 let decode_length buf pos =
238 check_bounds buf pos 1 "reading length";
239 let first_byte = Char.code buf.[pos] in
240 if is_long_length first_byte then
241 (* Four byte length *)
242 let len = decode_long_length buf pos in
243 (len, pos + 4)
244 else
245 (* Single byte length *)
246 (decode_short_length first_byte, pos + 1)
247
248 let encode kvs =
249 let buf = Buffer.create 256 in
250 List.iter (fun (key, value) ->
251 let key_len = String.length key in
252 let value_len = String.length value in
253 Buffer.add_string buf (encode_length key_len);
254 Buffer.add_string buf (encode_length value_len);
255 Buffer.add_string buf key;
256 Buffer.add_string buf value
257 ) kvs;
258 Buffer.contents buf
259
260 (** Extract key-value pair from buffer at given position *)
261 let extract_kv_pair content pos name_len value_len =
262 check_bounds content pos (name_len + value_len) "reading key-value data";
263 let name = String.sub content pos name_len in
264 let value = String.sub content (pos + name_len) value_len in
265 (name, value)
266
267 let decode content =
268 let len = String.length content in
269 let rec loop pos acc =
270 if pos >= len then
271 List.rev acc
272 else
273 (* Read name length *)
274 let name_len, pos = decode_length content pos in
275 (* Read value length *)
276 let value_len, pos = decode_length content pos in
277
278 (* Extract name and value *)
279 let name, value = extract_kv_pair content pos name_len value_len in
280
281 loop (pos + name_len + value_len) ((name, value) :: acc)
282 in
283 loop 0 []
284
285 let read buf_read =
286 (* For reading from a stream, we need to determine how much data to read.
287 Since we're in a record context, we should read all available data
288 until we hit the end of the record content. *)
289 let content = Eio.Buf_read.take_all buf_read in
290 decode content
291
292 let write buf_write kvs =
293 let encoded = encode kvs in
294 Eio.Buf_write.string buf_write encoded
295
296 let pp ppf kvs =
297 Format.fprintf ppf "@[<2>[@[";
298 let first = ref true in
299 List.iter (fun (key, value) ->
300 if not !first then Format.fprintf ppf ";@ ";
301 first := false;
302 Format.fprintf ppf "(%S, %S)" key value
303 ) kvs;
304 Format.fprintf ppf "@]]@]"
305end