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)