FastCGI implementation in OCaml
1open Fastcgi
2open Record
3
4let hex_dump ?(max_bytes=256) content =
5 let len = String.length content in
6 let display_len = min len max_bytes in
7 let bytes_per_line = 16 in
8 let lines_to_show = (display_len - 1) / bytes_per_line + 1 in
9
10 for i = 0 to lines_to_show - 1 do
11 let start = i * bytes_per_line in
12 let end_ = min (start + bytes_per_line) display_len in
13 Printf.printf " %04x: " start;
14
15 (* Print hex bytes *)
16 for j = start to end_ - 1 do
17 Printf.printf "%02x " (Char.code content.[j]);
18 if j = start + 7 then Printf.printf " "
19 done;
20
21 (* Pad remaining space *)
22 for _ = end_ to start + bytes_per_line - 1 do
23 Printf.printf " ";
24 if end_ <= start + 7 then Printf.printf " "
25 done;
26
27 Printf.printf " |";
28
29 (* Print ASCII representation *)
30 for j = start to end_ - 1 do
31 let c = content.[j] in
32 if c >= ' ' && c <= '~' then
33 Printf.printf "%c" c
34 else
35 Printf.printf "."
36 done;
37
38 Printf.printf "|\n%!"
39 done;
40
41 if len > max_bytes then
42 Printf.printf " ... (%d more bytes truncated)\n%!" (len - max_bytes)
43
44
45let test_cases = [
46 ("abort_request.bin", Abort_request, 1);
47 ("begin_request_authorizer.bin", Begin_request, 2);
48 ("begin_request_filter.bin", Begin_request, 3);
49 ("begin_request_no_keep.bin", Begin_request, 1);
50 ("begin_request_responder.bin", Begin_request, 1);
51 ("data_empty.bin", Data, 3);
52 ("data_filter.bin", Data, 3);
53 ("end_request_error.bin", End_request, 1);
54 ("end_request_success.bin", End_request, 1);
55 ("get_values.bin", Get_values, 0);
56 ("get_values_result.bin", Get_values_result, 0);
57 ("params_empty.bin", Params, 1);
58 ("params_get.bin", Params, 1);
59 ("params_post.bin", Params, 1);
60 ("stderr_empty.bin", Stderr, 1);
61 ("stderr_message.bin", Stderr, 1);
62 ("stdin_empty.bin", Stdin, 1);
63 ("stdin_form_data.bin", Stdin, 1);
64 ("stdout_empty.bin", Stdout, 1);
65 ("stdout_response.bin", Stdout, 1);
66 ("unknown_type.bin", Unknown_type, 0);
67]
68
69let test_binary_file ~fs filename expected_type expected_request_id =
70 Printf.printf "Testing %s...\n" filename;
71
72 let (raw_content, parsed) =
73 Eio.Path.with_open_in (fs, "test_cases/" ^ filename) @@ fun flow ->
74 let buf = Buffer.create 1024 in
75 Eio.Flow.copy flow (Eio.Flow.buffer_sink buf);
76 let raw_content = Buffer.contents buf in
77 let buf_read = Eio.Buf_read.of_string raw_content in
78 let parsed = Record.read buf_read in
79 (raw_content, parsed)
80 in
81
82 Printf.printf "\nRaw file contents (%d bytes):\n" (String.length raw_content);
83 hex_dump raw_content;
84
85 Printf.printf "\nParsed record:\n";
86 Format.printf "%a\n" (fun ppf -> Record.pp ppf) parsed;
87
88 (* If this is a Params record, also show the decoded key-value pairs *)
89 if parsed.record_type = Params && String.length parsed.content > 0 then (
90 let params = Record.KV.decode parsed.content in
91 Printf.printf "\nDecoded parameters:\n";
92 Format.printf "%a\n" Record.KV.pp params
93 );
94
95 assert (parsed.version = 1);
96 assert (parsed.record_type = expected_type);
97 assert (parsed.request_id = expected_request_id);
98
99 Printf.printf "✓ %s passed\n\n%!" filename
100
101let test_params_decoding ~fs =
102 Printf.printf "Testing params record content decoding...\n";
103
104 let parsed =
105 Eio.Path.with_open_in (fs, "test_cases/params_get.bin") @@ fun flow ->
106 let buf_read = Eio.Buf_read.of_flow flow ~max_size:1000000 in
107 Record.read buf_read
108 in
109
110 Printf.printf "\nParsed params record:\n";
111 Format.printf "%a\n" (fun ppf -> Record.pp ppf) parsed;
112
113 (* Decode the params content *)
114 let params = Record.KV.decode parsed.content in
115
116 Printf.printf "\nDecoded parameters:\n";
117 Format.printf "%a\n" Record.KV.pp params;
118
119 (* Check some expected environment variables *)
120 assert (Record.KV.find "REQUEST_METHOD" params = "GET");
121 assert (Record.KV.find "SERVER_NAME" params = "localhost");
122 assert (Record.KV.find "SERVER_PORT" params = "80");
123
124 Printf.printf "✓ params decoding passed\n\n%!"
125
126let test_large_record ~fs =
127 Printf.printf "Testing large record...\n";
128
129 let parsed =
130 Eio.Path.with_open_in (fs, "test_cases/large_record.bin") @@ fun flow ->
131 let buf_read = Eio.Buf_read.of_flow flow ~max_size:1000000 in
132 Record.read buf_read
133 in
134
135 Printf.printf "\nParsed large record:\n";
136 Format.printf "%a\n" (fun ppf -> Record.pp ppf) parsed;
137
138 assert (parsed.version = 1);
139 assert (parsed.record_type = Stdout);
140 assert (parsed.request_id = 1);
141 assert (String.length parsed.content = 65000);
142
143 Printf.printf "✓ large record test passed\n\n%!"
144
145let test_padded_record ~fs =
146 Printf.printf "Testing padded record...\n";
147
148 let parsed =
149 Eio.Path.with_open_in (fs, "test_cases/padded_record.bin") @@ fun flow ->
150 let buf_read = Eio.Buf_read.of_flow flow ~max_size:1000000 in
151 Record.read buf_read
152 in
153
154 Printf.printf "\nParsed padded record:\n";
155 Format.printf "%a\n" (fun ppf -> Record.pp ppf) parsed;
156
157 assert (parsed.version = 1);
158 assert (parsed.record_type = Stdout);
159 assert (parsed.request_id = 1);
160 assert (parsed.content = "Hello");
161
162 Printf.printf "✓ padded record test passed\n\n%!"
163
164let test_multiplexed_records ~fs =
165 Printf.printf "Testing multiplexed records...\n";
166
167 let records =
168 Eio.Path.with_open_in (fs, "test_cases/multiplexed_requests.bin") @@ fun flow ->
169 let buf_read = Eio.Buf_read.of_flow flow ~max_size:1000000 in
170 let records = ref [] in
171
172 (* Read all records from the multiplexed stream *)
173 (try
174 while true do
175 let record = Record.read buf_read in
176 records := record :: !records
177 done
178 with End_of_file -> ());
179 !records
180 in
181
182 let records = List.rev records in
183
184 Printf.printf "\nParsed %d multiplexed records:\n" (List.length records);
185 List.iteri (fun i record ->
186 Printf.printf "\nRecord %d:\n" (i + 1);
187 Format.printf "%a\n" (fun ppf -> Record.pp ppf) record
188 ) records;
189
190 (* Should have multiple records with different request IDs *)
191 assert (List.length records > 5);
192
193 (* Check that we have records for both request ID 1 and 2 *)
194 let request_ids = List.map (fun r -> r.Record.request_id) records in
195 assert (List.mem 1 request_ids);
196 assert (List.mem 2 request_ids);
197
198 Printf.printf "✓ multiplexed records test passed\n\n%!"
199
200let run_all_tests ~fs =
201 Printf.printf "Validating all FastCGI test case files...\n\n%!";
202
203 (* Test individual files *)
204 List.iter (fun (filename, expected_type, expected_request_id) ->
205 test_binary_file ~fs filename expected_type expected_request_id
206 ) test_cases;
207
208 Printf.printf "\nTesting specific content decoding...\n%!";
209 test_params_decoding ~fs;
210 test_large_record ~fs;
211 test_padded_record ~fs;
212 test_multiplexed_records ~fs;
213
214 Printf.printf "\n✅ All %d test case files validated successfully!\n%!" (List.length test_cases);
215 Printf.printf "✅ FastCGI Record implementation is working correctly!\n%!"
216
217let () = Eio_main.run @@ fun env ->
218 let fs = Eio.Stdenv.cwd env in
219 run_all_tests ~fs:(fst fs)