FastCGI implementation in OCaml
at main 13 kB view raw
1#!/usr/bin/env python3 2""" 3Generate FastCGI test case files based on the specification. 4""" 5import struct 6import os 7 8# FastCGI constants from the specification 9FCGI_VERSION_1 = 1 10 11# Record types 12FCGI_BEGIN_REQUEST = 1 13FCGI_ABORT_REQUEST = 2 14FCGI_END_REQUEST = 3 15FCGI_PARAMS = 4 16FCGI_STDIN = 5 17FCGI_STDOUT = 6 18FCGI_STDERR = 7 19FCGI_DATA = 8 20FCGI_GET_VALUES = 9 21FCGI_GET_VALUES_RESULT = 10 22FCGI_UNKNOWN_TYPE = 11 23 24# Roles 25FCGI_RESPONDER = 1 26FCGI_AUTHORIZER = 2 27FCGI_FILTER = 3 28 29# Flags 30FCGI_KEEP_CONN = 1 31 32# Protocol status 33FCGI_REQUEST_COMPLETE = 0 34FCGI_CANT_MPX_CONN = 1 35FCGI_OVERLOADED = 2 36FCGI_UNKNOWN_ROLE = 3 37 38def create_record_header(version, record_type, request_id, content_length, padding_length=0): 39 """Create a FastCGI record header.""" 40 return struct.pack('>BBHHBB', 41 version, 42 record_type, 43 request_id, 44 content_length, 45 padding_length, 46 0) # reserved 47 48def encode_name_value_pair(name, value): 49 """Encode a name-value pair according to FastCGI spec.""" 50 name_bytes = name.encode('utf-8') 51 value_bytes = value.encode('utf-8') 52 53 name_len = len(name_bytes) 54 value_len = len(value_bytes) 55 56 # Encode lengths 57 if name_len < 128: 58 name_len_encoded = struct.pack('B', name_len) 59 else: 60 name_len_encoded = struct.pack('>I', name_len | 0x80000000) 61 62 if value_len < 128: 63 value_len_encoded = struct.pack('B', value_len) 64 else: 65 value_len_encoded = struct.pack('>I', value_len | 0x80000000) 66 67 return name_len_encoded + value_len_encoded + name_bytes + value_bytes 68 69def create_begin_request(): 70 """Create FCGI_BEGIN_REQUEST record.""" 71 # FCGI_BeginRequestBody 72 body = struct.pack('>HB5x', FCGI_RESPONDER, FCGI_KEEP_CONN) 73 header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 1, len(body)) 74 return header + body 75 76def create_begin_request_no_keep_conn(): 77 """Create FCGI_BEGIN_REQUEST record without keep connection.""" 78 body = struct.pack('>HB5x', FCGI_RESPONDER, 0) 79 header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 1, len(body)) 80 return header + body 81 82def create_begin_request_authorizer(): 83 """Create FCGI_BEGIN_REQUEST record for authorizer role.""" 84 body = struct.pack('>HB5x', FCGI_AUTHORIZER, 0) 85 header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 2, len(body)) 86 return header + body 87 88def create_begin_request_filter(): 89 """Create FCGI_BEGIN_REQUEST record for filter role.""" 90 body = struct.pack('>HB5x', FCGI_FILTER, FCGI_KEEP_CONN) 91 header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 3, len(body)) 92 return header + body 93 94def create_end_request(): 95 """Create FCGI_END_REQUEST record.""" 96 # FCGI_EndRequestBody 97 body = struct.pack('>IB3x', 0, FCGI_REQUEST_COMPLETE) # app_status=0, protocol_status=COMPLETE 98 header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 1, len(body)) 99 return header + body 100 101def create_end_request_error(): 102 """Create FCGI_END_REQUEST record with error status.""" 103 body = struct.pack('>IB3x', 1, FCGI_REQUEST_COMPLETE) # app_status=1 (error) 104 header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 1, len(body)) 105 return header + body 106 107def create_params_record(): 108 """Create FCGI_PARAMS record with CGI environment variables.""" 109 # Typical CGI parameters 110 params = [ 111 ("REQUEST_METHOD", "GET"), 112 ("SCRIPT_NAME", "/test.cgi"), 113 ("REQUEST_URI", "/test.cgi?foo=bar"), 114 ("QUERY_STRING", "foo=bar"), 115 ("SERVER_NAME", "localhost"), 116 ("SERVER_PORT", "80"), 117 ("HTTP_HOST", "localhost"), 118 ("HTTP_USER_AGENT", "Mozilla/5.0"), 119 ("CONTENT_TYPE", ""), 120 ("CONTENT_LENGTH", "0") 121 ] 122 123 body = b'' 124 for name, value in params: 125 body += encode_name_value_pair(name, value) 126 127 header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 1, len(body)) 128 return header + body 129 130def create_params_record_post(): 131 """Create FCGI_PARAMS record for POST request.""" 132 params = [ 133 ("REQUEST_METHOD", "POST"), 134 ("SCRIPT_NAME", "/form.cgi"), 135 ("REQUEST_URI", "/form.cgi"), 136 ("QUERY_STRING", ""), 137 ("SERVER_NAME", "localhost"), 138 ("SERVER_PORT", "443"), 139 ("HTTPS", "on"), 140 ("HTTP_HOST", "localhost"), 141 ("HTTP_USER_AGENT", "curl/7.68.0"), 142 ("CONTENT_TYPE", "application/x-www-form-urlencoded"), 143 ("CONTENT_LENGTH", "23") 144 ] 145 146 body = b'' 147 for name, value in params: 148 body += encode_name_value_pair(name, value) 149 150 header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 1, len(body)) 151 return header + body 152 153def create_empty_params(): 154 """Create empty FCGI_PARAMS record (end of params stream).""" 155 header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 1, 0) 156 return header 157 158def create_stdin_record(): 159 """Create FCGI_STDIN record with form data.""" 160 data = b"name=John&email=john@example.com" 161 header = create_record_header(FCGI_VERSION_1, FCGI_STDIN, 1, len(data)) 162 return header + data 163 164def create_empty_stdin(): 165 """Create empty FCGI_STDIN record (end of stdin stream).""" 166 header = create_record_header(FCGI_VERSION_1, FCGI_STDIN, 1, 0) 167 return header 168 169def create_stdout_record(): 170 """Create FCGI_STDOUT record with HTTP response.""" 171 response = b"Content-Type: text/html\r\n\r\n<html><body>Hello World</body></html>" 172 header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(response)) 173 return header + response 174 175def create_empty_stdout(): 176 """Create empty FCGI_STDOUT record (end of stdout stream).""" 177 header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, 0) 178 return header 179 180def create_stderr_record(): 181 """Create FCGI_STDERR record with error message.""" 182 error_msg = b"Warning: Configuration file not found\n" 183 header = create_record_header(FCGI_VERSION_1, FCGI_STDERR, 1, len(error_msg)) 184 return header + error_msg 185 186def create_empty_stderr(): 187 """Create empty FCGI_STDERR record (end of stderr stream).""" 188 header = create_record_header(FCGI_VERSION_1, FCGI_STDERR, 1, 0) 189 return header 190 191def create_get_values(): 192 """Create FCGI_GET_VALUES record.""" 193 # Request standard capability variables 194 body = (encode_name_value_pair("FCGI_MAX_CONNS", "") + 195 encode_name_value_pair("FCGI_MAX_REQS", "") + 196 encode_name_value_pair("FCGI_MPXS_CONNS", "")) 197 header = create_record_header(FCGI_VERSION_1, FCGI_GET_VALUES, 0, len(body)) 198 return header + body 199 200def create_get_values_result(): 201 """Create FCGI_GET_VALUES_RESULT record.""" 202 body = (encode_name_value_pair("FCGI_MAX_CONNS", "1") + 203 encode_name_value_pair("FCGI_MAX_REQS", "1") + 204 encode_name_value_pair("FCGI_MPXS_CONNS", "0")) 205 header = create_record_header(FCGI_VERSION_1, FCGI_GET_VALUES_RESULT, 0, len(body)) 206 return header + body 207 208def create_unknown_type(): 209 """Create FCGI_UNKNOWN_TYPE record.""" 210 # FCGI_UnknownTypeBody 211 body = struct.pack('B7x', 99) # unknown type 99 212 header = create_record_header(FCGI_VERSION_1, FCGI_UNKNOWN_TYPE, 0, len(body)) 213 return header + body 214 215def create_abort_request(): 216 """Create FCGI_ABORT_REQUEST record.""" 217 header = create_record_header(FCGI_VERSION_1, FCGI_ABORT_REQUEST, 1, 0) 218 return header 219 220def create_data_record(): 221 """Create FCGI_DATA record (for Filter role).""" 222 file_data = b"This is file content that needs to be filtered\nLine 2\nLine 3\n" 223 header = create_record_header(FCGI_VERSION_1, FCGI_DATA, 3, len(file_data)) 224 return header + file_data 225 226def create_empty_data(): 227 """Create empty FCGI_DATA record (end of data stream).""" 228 header = create_record_header(FCGI_VERSION_1, FCGI_DATA, 3, 0) 229 return header 230 231def create_multiplexed_records(): 232 """Create a sequence showing multiplexed requests.""" 233 records = [] 234 235 # Request 1 begins 236 records.append(create_begin_request()) 237 records.append(create_params_record()) 238 records.append(create_empty_params()) 239 240 # Request 2 begins (different request ID) 241 body = struct.pack('>HB5x', FCGI_RESPONDER, FCGI_KEEP_CONN) 242 header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 2, len(body)) 243 records.append(header + body) 244 245 # Request 1 continues 246 records.append(create_empty_stdin()) 247 248 # Request 2 params 249 params = [("REQUEST_METHOD", "POST"), ("SCRIPT_NAME", "/other.cgi")] 250 body = b'' 251 for name, value in params: 252 body += encode_name_value_pair(name, value) 253 header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 2, len(body)) 254 records.append(header + body) 255 256 # Request 2 empty params 257 header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 2, 0) 258 records.append(header) 259 260 # Request 2 completes first 261 response = b"Content-Type: text/plain\r\n\r\nRequest 2 done" 262 header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 2, len(response)) 263 records.append(header + response) 264 265 header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 2, 0) 266 records.append(header) 267 268 body = struct.pack('>IB3x', 0, FCGI_REQUEST_COMPLETE) 269 header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 2, len(body)) 270 records.append(header + body) 271 272 # Request 1 completes 273 response = b"Content-Type: text/html\r\n\r\n<html><body>Request 1 done</body></html>" 274 header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(response)) 275 records.append(header + response) 276 277 header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, 0) 278 records.append(header) 279 280 body = struct.pack('>IB3x', 0, FCGI_REQUEST_COMPLETE) 281 header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 1, len(body)) 282 records.append(header + body) 283 284 return b''.join(records) 285 286def create_large_record(): 287 """Create a record with maximum content size.""" 288 # Create a large response (just under 64KB) 289 large_content = b"x" * 65000 290 header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(large_content)) 291 return header + large_content 292 293def create_padded_record(): 294 """Create a record with padding for alignment.""" 295 data = b"Hello" # 5 bytes 296 padding_length = 3 # to align to 8-byte boundary (5 + 3 = 8) 297 header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(data), padding_length) 298 padding = b'\x00' * padding_length 299 return header + data + padding 300 301# Test case definitions 302test_cases = { 303 "begin_request_responder.bin": create_begin_request, 304 "begin_request_no_keep.bin": create_begin_request_no_keep_conn, 305 "begin_request_authorizer.bin": create_begin_request_authorizer, 306 "begin_request_filter.bin": create_begin_request_filter, 307 "end_request_success.bin": create_end_request, 308 "end_request_error.bin": create_end_request_error, 309 "params_get.bin": create_params_record, 310 "params_post.bin": create_params_record_post, 311 "params_empty.bin": create_empty_params, 312 "stdin_form_data.bin": create_stdin_record, 313 "stdin_empty.bin": create_empty_stdin, 314 "stdout_response.bin": create_stdout_record, 315 "stdout_empty.bin": create_empty_stdout, 316 "stderr_message.bin": create_stderr_record, 317 "stderr_empty.bin": create_empty_stderr, 318 "get_values.bin": create_get_values, 319 "get_values_result.bin": create_get_values_result, 320 "unknown_type.bin": create_unknown_type, 321 "abort_request.bin": create_abort_request, 322 "data_filter.bin": create_data_record, 323 "data_empty.bin": create_empty_data, 324 "multiplexed_requests.bin": create_multiplexed_records, 325 "large_record.bin": create_large_record, 326 "padded_record.bin": create_padded_record, 327} 328 329if __name__ == "__main__": 330 for filename, creator in test_cases.items(): 331 with open(filename, 'wb') as f: 332 f.write(creator()) 333 print(f"Created {filename}") 334 335 print(f"\nGenerated {len(test_cases)} test case files") 336 print("\nTest case descriptions:") 337 print("- begin_request_*.bin: Various BEGIN_REQUEST records for different roles") 338 print("- end_request_*.bin: END_REQUEST records with different status codes") 339 print("- params_*.bin: PARAMS records with CGI environment variables") 340 print("- stdin_*.bin: STDIN records with request body data") 341 print("- stdout_*.bin: STDOUT records with response data") 342 print("- stderr_*.bin: STDERR records with error messages") 343 print("- get_values*.bin: Management records for capability negotiation") 344 print("- unknown_type.bin: Unknown record type handling") 345 print("- abort_request.bin: Request abortion") 346 print("- data_*.bin: DATA records for Filter role") 347 print("- multiplexed_requests.bin: Multiple concurrent requests") 348 print("- large_record.bin: Maximum size record") 349 print("- padded_record.bin: Record with padding for alignment")