FastCGI implementation in OCaml
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")