FastCGI implementation in OCaml

Compare changes

Choose any two refs to compare.

+4
bin/dune
···
+
(executable
+
(public_name fcgi-server)
+
(name fcgi_server)
+
(libraries cmdliner eio eio_main fastcgi))
+79
bin/fcgi_server.ml
···
+
open Cmdliner
+
+
(* Handler function that processes FastCGI requests *)
+
let handler ~stdout ~stderr request =
+
Eio.traceln "Processing request: %a" Fastcgi.Request.pp request;
+
+
(* Get request parameters *)
+
let params = request.Fastcgi.Request.params in
+
let method_ = Fastcgi.Record.KV.find_opt "REQUEST_METHOD" params |> Option.value ~default:"GET" in
+
let uri = Fastcgi.Record.KV.find_opt "REQUEST_URI" params |> Option.value ~default:"/" in
+
let script_name = Fastcgi.Record.KV.find_opt "SCRIPT_NAME" params |> Option.value ~default:"" in
+
+
(* Log request info *)
+
Eio.traceln " Method: %s" method_;
+
Eio.traceln " URI: %s" uri;
+
Eio.traceln " Script: %s" script_name;
+
+
(* Generate simple HTTP response *)
+
let response_body =
+
Printf.sprintf
+
"<!DOCTYPE html>\n\
+
<html>\n\
+
<head><title>FastCGI OCaml Server</title></head>\n\
+
<body>\n\
+
<h1>FastCGI OCaml Server</h1>\n\
+
<p>Request processed successfully!</p>\n\
+
<ul>\n\
+
<li>Method: %s</li>\n\
+
<li>URI: %s</li>\n\
+
<li>Script: %s</li>\n\
+
</ul>\n\
+
<h2>All Parameters:</h2>\n\
+
<pre>%s</pre>\n\
+
</body>\n\
+
</html>\n"
+
method_ uri script_name
+
(let params_seq = Fastcgi.Record.KV.to_seq params in
+
let params_list = List.of_seq params_seq in
+
String.concat "\n" (List.map (fun (k, v) -> Printf.sprintf "%s = %s" k v) params_list))
+
in
+
+
(* Write HTTP response using FastCGI STDOUT records *)
+
let response_headers =
+
Printf.sprintf
+
"Status: 200 OK\r\n\
+
Content-Type: text/html; charset=utf-8\r\n\
+
Content-Length: %d\r\n\
+
\r\n"
+
(String.length response_body)
+
in
+
stdout response_headers;
+
stderr "stderr stuff";
+
stdout response_body
+
+
let run port =
+
Eio_main.run @@ fun env ->
+
Eio.Switch.run @@ fun sw ->
+
let net = Eio.Stdenv.net env in
+
let addr = `Tcp (Eio.Net.Ipaddr.V4.loopback, port) in
+
let server_socket = Eio.Net.listen net ~backlog:10 ~reuse_addr:true ~sw addr in
+
Eio.traceln "FastCGI server listening on port %d" port;
+
+
(* Run the FastCGI server *)
+
Fastcgi.run server_socket
+
~on_error:(fun ex ->
+
Eio.traceln "Error: %s" (Printexc.to_string ex);
+
Eio.traceln "bt: %s" (Printexc.get_backtrace ()))
+
handler
+
+
let port =
+
let doc = "Port to listen on" in
+
Arg.(value & opt int 9000 & info ["p"; "port"] ~docv:"PORT" ~doc)
+
+
let cmd =
+
let doc = "FastCGI server" in
+
let info = Cmd.info "fcgi-server" ~doc in
+
Cmd.v info Term.(const run $ port)
+
+
let () = exit (Cmd.eval cmd)
+7
config/Caddyfile
···
+
{
+
debug
+
}
+
+
localhost:80 {
+
php_fastcgi 127.0.0.1:9000
+
}
+3 -1
dune-project
···
(depends
ocaml
dune
-
eio)
+
eio
+
cmdliner
+
eio_main)
(synopsis "FastCGI protocol implementation for OCaml using Eio")
(description "A type-safe implementation of the FastCGI protocol for OCaml using the Eio effects-based IO library. Supports all three FastCGI roles: Responder, Authorizer, and Filter."))
+2
fastcgi.opam
···
"ocaml"
"dune" {>= "3.0"}
"eio"
+
"cmdliner"
+
"eio_main"
"odoc" {with-doc}
]
build: [
+1 -4
lib/dune
···
(modules
fastcgi
fastcgi_record
-
)
-
(modules_without_implementation
-
fastcgi
-
fastcgi_record
+
fastcgi_request
)
)
+50
lib/fastcgi.ml
···
+
module Record = Fastcgi_record
+
+
(** Request-level state machine and application interface *)
+
module Request = Fastcgi_request
+
+
(* The lifetime of the handler is that the fiber should return when the
+
stdout and stderr flows are closed, or an abort request has been received *)
+
let handle req bw cancel fn =
+
let cancel () =
+
Eio.Promise.await cancel;
+
Eio.traceln "cancelled TODO"
+
in
+
let stdout buf = Request.write_stdout_records bw req.Request.request_id buf in
+
let stderr buf = Request.write_stderr_records bw req.Request.request_id buf in
+
let run () =
+
fn ~stdout ~stderr req;
+
Request.write_end_request bw req.Request.request_id 0 Request.Request_complete
+
in
+
Eio.Fiber.first run cancel
+
+
let run ?max_connections ?additional_domains ?stop ~on_error socket handler =
+
Eio.Net.run_server socket ?max_connections ?additional_domains ?stop ~on_error
+
(fun socket peer_address ->
+
let ids = Hashtbl.create 7 in
+
Eio.Switch.run @@ fun sw ->
+
Eio.traceln "%a: accept connection" Eio.Net.Sockaddr.pp peer_address;
+
let input = Eio.Buf_read.of_flow ~max_size:max_int socket in
+
Eio.Buf_write.with_flow socket @@ fun output ->
+
let cont = ref true in
+
try while !cont do
+
match Request.read_request input with
+
| Error msg ->
+
Eio.traceln "%a: failed to read request: %s" Eio.Net.Sockaddr.pp peer_address msg;
+
failwith "done";
+
| Ok req ->
+
cont := req.Request.keep_conn;
+
Eio.traceln "%a: %b read request %a" Eio.Net.Sockaddr.pp peer_address !cont Request.pp req;
+
Eio.Fiber.fork ~sw (fun () ->
+
Eio.Switch.run ~name:"req_handler" @@ fun sw ->
+
let cancel, canceler = Eio.Promise.create () in
+
Hashtbl.add ids req.Request.request_id canceler;
+
Eio.Switch.on_release sw (fun () ->
+
Hashtbl.remove ids req.Request.request_id
+
);
+
handle req output cancel handler;
+
);
+
done
+
with Eio.Io (Eio.Net.E (Connection_reset _), _) ->
+
Eio.traceln "%a: connection reset" Eio.Net.Sockaddr.pp peer_address
+
)
+20 -1
lib/fastcgi.mli
···
This library provides a complete implementation of the FastCGI protocol
for building high-performance web applications in OCaml. *)
+
(** {1 Core Protocol Components} *)
(** Record-level protocol handling *)
-
module Record = Fastcgi_record
+
module Record = Fastcgi_record
+
+
(** Request-level state machine and application interface *)
+
module Request = Fastcgi_request
+
+
(** {1 High-level Request Processing} *)
+
+
(** [handle_connection ~sw flow handler] handles complete FastCGI connection.
+
Reads requests from flow, processes them with handler, multiplexes responses.
+
Continues until connection is closed. *)
+
val run :
+
?max_connections:int ->
+
?additional_domains:[> Eio.Domain_manager.ty ] Eio.Resource.t *
+
int ->
+
?stop:'a Eio__core.Promise.t ->
+
on_error:(exn -> unit) ->
+
[> [> `Generic ] Eio.Net.listening_socket_ty ] Eio.Resource.t ->
+
(stdout:(string -> unit) ->
+
stderr:(string -> unit) -> Request.t -> unit) -> 'a
+305
lib/fastcgi_record.ml
···
+
+
(** {1 Error Handling Helpers} *)
+
+
(** Helper functions for consistent error handling *)
+
let invalid_version version =
+
invalid_arg (Printf.sprintf "Unsupported FastCGI version: %d" version)
+
+
let unknown_record_type n =
+
invalid_arg (Printf.sprintf "Unknown FastCGI record type: %d" n)
+
+
let buffer_underflow msg =
+
failwith (Printf.sprintf "Unexpected end of buffer while %s" msg)
+
+
let check_bounds buf pos len context =
+
if pos + len > String.length buf then
+
buffer_underflow context
+
+
type version = int
+
+
type record =
+
| Begin_request
+
| Abort_request
+
| End_request
+
| Params
+
| Stdin
+
| Stdout
+
| Stderr
+
| Data
+
| Get_values
+
| Get_values_result
+
| Unknown_type
+
+
let record_to_int = function
+
| Begin_request -> 1
+
| Abort_request -> 2
+
| End_request -> 3
+
| Params -> 4
+
| Stdin -> 5
+
| Stdout -> 6
+
| Stderr -> 7
+
| Data -> 8
+
| Get_values -> 9
+
| Get_values_result -> 10
+
| Unknown_type -> 11
+
+
let record_of_int = function
+
| 1 -> Begin_request
+
| 2 -> Abort_request
+
| 3 -> End_request
+
| 4 -> Params
+
| 5 -> Stdin
+
| 6 -> Stdout
+
| 7 -> Stderr
+
| 8 -> Data
+
| 9 -> Get_values
+
| 10 -> Get_values_result
+
| 11 -> Unknown_type
+
| n -> unknown_record_type n
+
+
let pp_record ppf = function
+
| Begin_request -> Format.pp_print_string ppf "Begin_request"
+
| Abort_request -> Format.pp_print_string ppf "Abort_request"
+
| End_request -> Format.pp_print_string ppf "End_request"
+
| Params -> Format.pp_print_string ppf "Params"
+
| Stdin -> Format.pp_print_string ppf "Stdin"
+
| Stdout -> Format.pp_print_string ppf "Stdout"
+
| Stderr -> Format.pp_print_string ppf "Stderr"
+
| Data -> Format.pp_print_string ppf "Data"
+
| Get_values -> Format.pp_print_string ppf "Get_values"
+
| Get_values_result -> Format.pp_print_string ppf "Get_values_result"
+
| Unknown_type -> Format.pp_print_string ppf "Unknown_type"
+
+
type request_id = int
+
+
type t = {
+
version : version;
+
record_type : record;
+
request_id : request_id;
+
content : string;
+
offset : int;
+
length : int;
+
}
+
+
let pp ?(max_content_len=100) ppf record =
+
let actual_content = String.sub record.content record.offset record.length in
+
let truncated_content =
+
let len = String.length actual_content in
+
if len <= max_content_len then actual_content
+
else String.sub actual_content 0 max_content_len ^ "..." ^ Printf.sprintf " (%d more bytes)" (len - max_content_len)
+
in
+
Format.fprintf ppf
+
"@[<2>{ version = %d;@ record_type = %a;@ request_id = %d;@ content = %S;@ offset = %d;@ length = %d }@]"
+
record.version
+
pp_record record.record_type
+
record.request_id
+
truncated_content
+
record.offset
+
record.length
+
+
(* FastCGI constants *)
+
let fcgi_version_1 = 1
+
let fcgi_header_len = 8
+
+
let read buf_read =
+
Printf.eprintf "[DEBUG] Fastcgi_record.read: Starting to read record\n%!";
+
(* Read the 8-byte header *)
+
Printf.eprintf "[DEBUG] Fastcgi_record.read: Reading %d-byte header\n%!" fcgi_header_len;
+
let header = Eio.Buf_read.take fcgi_header_len buf_read in
+
+
(* Parse header fields *)
+
let version = Char.code header.[0] in
+
let record_type_int = Char.code header.[1] in
+
let request_id = (Char.code header.[2] lsl 8) lor (Char.code header.[3]) in
+
let content_length = (Char.code header.[4] lsl 8) lor (Char.code header.[5]) in
+
let padding_length = Char.code header.[6] in
+
let _reserved = Char.code header.[7] in
+
+
Printf.eprintf "[DEBUG] Fastcgi_record.read: Header parsed - version=%d, type=%d, id=%d, content_len=%d, padding=%d\n%!"
+
version record_type_int request_id content_length padding_length;
+
+
(* Validate version *)
+
if version <> fcgi_version_1 then invalid_version version;
+
+
(* Convert record type *)
+
let record_type = record_of_int record_type_int in
+
Printf.eprintf "[DEBUG] Fastcgi_record.read: Record type = %s\n%!"
+
(Format.asprintf "%a" pp_record record_type);
+
+
(* Read content *)
+
let content =
+
if content_length = 0 then (
+
Printf.eprintf "[DEBUG] Fastcgi_record.read: No content to read (length=0)\n%!";
+
""
+
) else (
+
Printf.eprintf "[DEBUG] Fastcgi_record.read: Reading %d bytes of content\n%!" content_length;
+
let c = Eio.Buf_read.take content_length buf_read in
+
Printf.eprintf "[DEBUG] Fastcgi_record.read: Successfully read %d bytes\n%!" (String.length c);
+
c
+
)
+
in
+
+
(* Skip padding *)
+
if padding_length > 0 then (
+
Printf.eprintf "[DEBUG] Fastcgi_record.read: Skipping %d bytes of padding\n%!" padding_length;
+
ignore (Eio.Buf_read.take padding_length buf_read)
+
);
+
+
let record = { version; record_type; request_id; content; offset = 0; length = String.length content } in
+
Printf.eprintf "[DEBUG] Fastcgi_record.read: Complete record = %s\n%!"
+
(Format.asprintf "%a" (pp ~max_content_len:50) record);
+
record
+
+
let write buf_write record =
+
let total_content_length = String.length record.content in
+
let content_offset = record.offset in
+
let content_length = record.length in
+
+
(* Validate bounds *)
+
if content_offset < 0 || content_offset > total_content_length then
+
invalid_arg "Fastcgi_record.write: offset out of bounds";
+
if content_length < 0 || content_offset + content_length > total_content_length then
+
invalid_arg "Fastcgi_record.write: length out of bounds";
+
+
(* Calculate padding for 8-byte alignment *)
+
let padding_length = (8 - (content_length land 7)) land 7 in
+
+
(* Create and write header *)
+
let header = Bytes.create fcgi_header_len in
+
Bytes.set_uint8 header 0 record.version;
+
Bytes.set_uint8 header 1 (record_to_int record.record_type);
+
Bytes.set_uint16_be header 2 record.request_id;
+
Bytes.set_uint16_be header 4 content_length;
+
Bytes.set_uint8 header 6 padding_length;
+
Bytes.set_uint8 header 7 0; (* reserved *)
+
+
Eio.Buf_write.string buf_write (Bytes.to_string header);
+
+
(* Write content with offset and length *)
+
if content_length > 0 then
+
Eio.Buf_write.string buf_write record.content ~off:content_offset ~len:content_length;
+
+
(* Write padding *)
+
if padding_length > 0 then
+
Eio.Buf_write.string buf_write (String.make padding_length '\000')
+
+
let create ?(version=1) ~record ~request_id ~content ?(offset=0) ?length () =
+
let content_length = match length with
+
| None -> String.length content - offset
+
| Some l -> l
+
in
+
{ version; record_type = record; request_id; content; offset; length = content_length }
+
+
module KV = struct
+
type t = (string * string) list
+
+
let empty = []
+
+
let add key value kvs = (key, value) :: kvs
+
+
let remove key kvs = List.filter (fun (k, _) -> k <> key) kvs
+
+
let cardinal kvs = List.length kvs
+
+
let find key kvs =
+
try List.assoc key kvs
+
with Not_found -> raise Not_found
+
+
let find_opt key kvs =
+
try Some (List.assoc key kvs)
+
with Not_found -> None
+
+
let to_seq kvs = List.to_seq kvs
+
+
let of_seq seq = List.of_seq seq
+
+
(** Helper functions for length encoding/decoding *)
+
let is_long_length first_byte = first_byte land 0x80 <> 0
+
+
let decode_short_length first_byte = first_byte
+
+
let decode_long_length buf pos =
+
check_bounds buf pos 4 "reading 4-byte length";
+
let first_byte = Char.code buf.[pos] in
+
((first_byte land 0x7f) lsl 24) lor
+
((Char.code buf.[pos + 1]) lsl 16) lor
+
((Char.code buf.[pos + 2]) lsl 8) lor
+
(Char.code buf.[pos + 3])
+
+
let encode_length len =
+
if len <= 127 then
+
String.make 1 (Char.chr len)
+
else
+
let b = Bytes.create 4 in
+
Bytes.set_int32_be b 0 (Int32.logor (Int32.of_int len) 0x80000000l);
+
Bytes.to_string b
+
+
let decode_length buf pos =
+
check_bounds buf pos 1 "reading length";
+
let first_byte = Char.code buf.[pos] in
+
if is_long_length first_byte then
+
(* Four byte length *)
+
let len = decode_long_length buf pos in
+
(len, pos + 4)
+
else
+
(* Single byte length *)
+
(decode_short_length first_byte, pos + 1)
+
+
let encode kvs =
+
let buf = Buffer.create 256 in
+
List.iter (fun (key, value) ->
+
let key_len = String.length key in
+
let value_len = String.length value in
+
Buffer.add_string buf (encode_length key_len);
+
Buffer.add_string buf (encode_length value_len);
+
Buffer.add_string buf key;
+
Buffer.add_string buf value
+
) kvs;
+
Buffer.contents buf
+
+
(** Extract key-value pair from buffer at given position *)
+
let extract_kv_pair content pos name_len value_len =
+
check_bounds content pos (name_len + value_len) "reading key-value data";
+
let name = String.sub content pos name_len in
+
let value = String.sub content (pos + name_len) value_len in
+
(name, value)
+
+
let decode content =
+
let len = String.length content in
+
let rec loop pos acc =
+
if pos >= len then
+
List.rev acc
+
else
+
(* Read name length *)
+
let name_len, pos = decode_length content pos in
+
(* Read value length *)
+
let value_len, pos = decode_length content pos in
+
+
(* Extract name and value *)
+
let name, value = extract_kv_pair content pos name_len value_len in
+
+
loop (pos + name_len + value_len) ((name, value) :: acc)
+
in
+
loop 0 []
+
+
let read buf_read =
+
(* For reading from a stream, we need to determine how much data to read.
+
Since we're in a record context, we should read all available data
+
until we hit the end of the record content. *)
+
let content = Eio.Buf_read.take_all buf_read in
+
decode content
+
+
let write buf_write kvs =
+
let encoded = encode kvs in
+
Eio.Buf_write.string buf_write encoded
+
+
let pp ppf kvs =
+
Format.fprintf ppf "@[<2>[@[";
+
let first = ref true in
+
List.iter (fun (key, value) ->
+
if not !first then Format.fprintf ppf ";@ ";
+
first := false;
+
Format.fprintf ppf "(%S, %S)" key value
+
) kvs;
+
Format.fprintf ppf "@]]@]"
+
end
+17 -9
lib/fastcgi_record.mli
···
content and optional padding for alignment. *)
type t = {
version : version; (** Protocol version (always 1) *)
-
record_type : record; (** Type of this record *)
+
record_type : record; (** Type of this record *)
request_id : request_id; (** Request identifier *)
content : string; (** Record content data *)
+
offset : int; (** Offset within content string (default: 0) *)
+
length : int; (** Length to use from content (default: String.length content) *)
}
-
(** [pp ppf record] pretty-prints a FastCGI record *)
-
val pp : Format.formatter -> t -> unit
+
(** [pp ?max_content_len ppf record] pretty-prints a FastCGI record.
+
[max_content_len] limits the displayed content length (default: 100 bytes) *)
+
val pp : ?max_content_len:int -> Format.formatter -> t -> unit
(** {1 Record Operations} *)
···
(** [write buf_write record] writes a FastCGI record to the output buffer.
The record header is automatically constructed from the record fields,
and appropriate padding is added to align the record on 8-byte boundaries
-
for optimal performance. *)
+
for optimal performance. Uses the record's offset and length fields to
+
determine which portion of the content to write. *)
val write : Eio.Buf_write.t -> t -> unit
-
(** [create ~version ~record ~request_id ~content] creates a new record
-
with the specified parameters. The content length is automatically
-
calculated from the content string. *)
-
val create : version:version -> record:record ->
-
request_id:request_id -> content:string -> t
+
(** [create ?version ~record ~request_id ~content ?offset ?length] creates a new record
+
with the specified parameters. Version defaults to 1 (the only supported version).
+
If offset and length are not provided, the entire content string is used. *)
+
val create : ?version:version -> record:record ->
+
request_id:request_id -> content:string ->
+
?offset:int -> ?length:int -> unit -> t
(** {1 Key-Value Pairs} *)
···
(** [of_seq pairs] creates from a sequence of (key, value) tuples *)
val of_seq : (string * string) Seq.t -> t
+
+
(** [cardinal pairs] returns the number of key-value pairs *)
+
val cardinal : t -> int
(** [read buf_read] reads key-value pairs from a buffer.
Handles the FastCGI variable-length encoding where lengths ≤ 127 bytes
+279
lib/fastcgi_request.ml
···
+
open Fastcgi_record
+
+
(** {1 Request Roles} *)
+
+
type role =
+
| Responder
+
| Authorizer
+
| Filter
+
+
let pp_role ppf = function
+
| Responder -> Format.pp_print_string ppf "Responder"
+
| Authorizer -> Format.pp_print_string ppf "Authorizer"
+
| Filter -> Format.pp_print_string ppf "Filter"
+
+
let role_of_begin_request record =
+
if record.record_type <> Begin_request then
+
Error "Expected BEGIN_REQUEST record"
+
else if String.length record.content <> 8 then
+
Error "Invalid BEGIN_REQUEST content length"
+
else
+
let content = record.content in
+
let role_int = (Char.code content.[0] lsl 8) lor (Char.code content.[1]) in
+
match role_int with
+
| 1 -> Ok Responder
+
| 2 -> Ok Authorizer
+
| 3 -> Ok Filter
+
| n -> Error (Printf.sprintf "Unknown FastCGI role: %d" n)
+
+
(** {1 Request Context} *)
+
+
type t = {
+
request_id : request_id;
+
role : role;
+
keep_conn : bool;
+
params : KV.t;
+
stdin_data : string;
+
data_stream : string option;
+
}
+
+
let pp ppf request =
+
let data_str = match request.data_stream with
+
| None -> "None"
+
| Some d -> Printf.sprintf "Some(%d bytes)" (String.length d)
+
in
+
Format.fprintf ppf
+
"@[<2>{ request_id = %d;@ role = %a;@ keep_conn = %b;@ params = %a;@ stdin = %d bytes;@ data = %s }@]"
+
request.request_id
+
pp_role request.role
+
request.keep_conn
+
(KV.pp) request.params
+
(String.length request.stdin_data)
+
data_str
+
+
let create record =
+
match role_of_begin_request record with
+
| Error _ as e -> e
+
| Ok role ->
+
if String.length record.content <> 8 then
+
Error "Invalid BEGIN_REQUEST content length"
+
else
+
let flags_int = Char.code record.content.[2] in
+
let keep_conn = (flags_int land 1) <> 0 in
+
Ok {
+
request_id = record.request_id;
+
role;
+
keep_conn;
+
params = KV.empty;
+
stdin_data = "";
+
data_stream = if role = Filter then Some "" else None;
+
}
+
+
+
(** {1 Stream Processing} *)
+
+
(** Helper functions for result binding to simplify nested pattern matching *)
+
let ( let* ) = Result.bind
+
+
let is_stream_terminator record =
+
String.length record.content = 0
+
+
+
let read_params buf_read =
+
Printf.eprintf "[DEBUG] read_params_from_flow: Starting\n%!";
+
let params = ref KV.empty in
+
let rec loop () =
+
try
+
Printf.eprintf "[DEBUG] read_params_from_flow: Reading next PARAMS record\n%!";
+
let record = Fastcgi_record.read buf_read in
+
Printf.eprintf "[DEBUG] read_params_from_flow: Got record type=%s, content_length=%d\n%!"
+
(Format.asprintf "%a" pp_record record.record_type)
+
(String.length record.content);
+
if record.record_type <> Params then
+
Error (Printf.sprintf "Expected PARAMS record, got %s"
+
(Format.asprintf "%a" pp_record record.record_type))
+
else if is_stream_terminator record then (
+
Printf.eprintf "[DEBUG] read_params_from_flow: Got stream terminator, returning %d params\n%!"
+
(Fastcgi_record.KV.cardinal !params);
+
Ok !params
+
) else (
+
let record_params = KV.decode record.content in
+
Printf.eprintf "[DEBUG] read_params_from_flow: Decoded %d params from record\n%!"
+
(Fastcgi_record.KV.cardinal record_params);
+
params := KV.to_seq record_params
+
|> Seq.fold_left (fun acc (k, v) -> KV.add k v acc) !params;
+
loop ()
+
)
+
with
+
| End_of_file ->
+
Printf.eprintf "[DEBUG] read_params_from_flow: Hit End_of_file\n%!";
+
Error "Unexpected end of stream while reading PARAMS"
+
| exn ->
+
Printf.eprintf "[DEBUG] read_params_from_flow: Exception: %s\n%!" (Printexc.to_string exn);
+
Error (Printf.sprintf "Error reading PARAMS: %s" (Printexc.to_string exn))
+
in
+
loop ()
+
+
let read_stdin buf_read =
+
Printf.eprintf "[DEBUG] read_stdin_from_flow: Starting\n%!";
+
let data = Buffer.create 1024 in
+
let rec loop () =
+
try
+
Printf.eprintf "[DEBUG] read_stdin_from_flow: Reading next STDIN record\n%!";
+
let record = Fastcgi_record.read buf_read in
+
Printf.eprintf "[DEBUG] read_stdin_from_flow: Got record type=%s, content_length=%d\n%!"
+
(Format.asprintf "%a" pp_record record.record_type)
+
(String.length record.content);
+
if record.record_type <> Stdin then
+
Error (Printf.sprintf "Expected STDIN record, got %s"
+
(Format.asprintf "%a" pp_record record.record_type))
+
else if is_stream_terminator record then (
+
Printf.eprintf "[DEBUG] read_stdin_from_flow: Got stream terminator, total stdin=%d bytes\n%!"
+
(Buffer.length data);
+
Ok (Buffer.contents data)
+
) else (
+
Buffer.add_string data record.content;
+
Printf.eprintf "[DEBUG] read_stdin_from_flow: Added %d bytes, total now %d\n%!"
+
(String.length record.content) (Buffer.length data);
+
loop ()
+
)
+
with
+
| End_of_file ->
+
Printf.eprintf "[DEBUG] read_stdin_from_flow: Hit End_of_file\n%!";
+
Error "Unexpected end of stream while reading STDIN"
+
| exn ->
+
Printf.eprintf "[DEBUG] read_stdin_from_flow: Exception: %s\n%!" (Printexc.to_string exn);
+
Error (Printf.sprintf "Error reading STDIN: %s" (Printexc.to_string exn))
+
in
+
loop ()
+
+
(** Read DATA stream for Filter role *)
+
let read_data buf_read =
+
let data_buf = Buffer.create 1024 in
+
let rec read_data () =
+
try
+
let record = Fastcgi_record.read buf_read in
+
if record.record_type <> Data then
+
Error "Expected DATA record"
+
else if is_stream_terminator record then
+
Ok (Buffer.contents data_buf)
+
else (
+
Buffer.add_string data_buf record.content;
+
read_data ()
+
)
+
with
+
| End_of_file -> Error "Unexpected end of DATA stream"
+
| exn -> Error (Printf.sprintf "Error reading DATA: %s" (Printexc.to_string exn))
+
in
+
read_data ()
+
+
(** Read request streams based on role *)
+
let read_request_streams request buf_read =
+
Printf.eprintf "[DEBUG] read_request_streams: Processing role=%s\n%!"
+
(Format.asprintf "%a" pp_role request.role);
+
match request.role with
+
| Authorizer ->
+
Printf.eprintf "[DEBUG] read_request_streams: Authorizer role, no streams to read\n%!";
+
Ok request
+
| Responder ->
+
Printf.eprintf "[DEBUG] read_request_streams: Responder role, reading STDIN\n%!";
+
let* stdin_data = read_stdin buf_read in
+
Printf.eprintf "[DEBUG] read_request_streams: Got STDIN data, %d bytes\n%!" (String.length stdin_data);
+
Ok { request with stdin_data }
+
| Filter ->
+
Printf.eprintf "[DEBUG] read_request_streams: Filter role, reading STDIN and DATA\n%!";
+
let* stdin_data = read_stdin buf_read in
+
Printf.eprintf "[DEBUG] read_request_streams: Got STDIN data, %d bytes\n%!" (String.length stdin_data);
+
let request = { request with stdin_data } in
+
let* data = read_data buf_read in
+
Printf.eprintf "[DEBUG] read_request_streams: Got DATA stream, %d bytes\n%!" (String.length data);
+
Ok { request with data_stream = Some data }
+
+
let read_request buf_read =
+
Printf.eprintf "[DEBUG] read_request: Starting\n%!";
+
try
+
(* Read BEGIN_REQUEST *)
+
Printf.eprintf "[DEBUG] read_request: Reading BEGIN_REQUEST record\n%!";
+
let begin_record = Fastcgi_record.read buf_read in
+
Printf.eprintf "[DEBUG] read_request: Got BEGIN_REQUEST record: %s\n%!"
+
(Format.asprintf "%a" (Fastcgi_record.pp ~max_content_len:50) begin_record);
+
let* request = create begin_record in
+
Printf.eprintf "[DEBUG] read_request: Created request with role=%s, id=%d\n%!"
+
(Format.asprintf "%a" pp_role request.role) request.request_id;
+
(* Read PARAMS stream *)
+
Printf.eprintf "[DEBUG] read_request: Reading PARAMS stream\n%!";
+
let* params = read_params buf_read in
+
Printf.eprintf "[DEBUG] read_request: Got %d params\n%!" (Fastcgi_record.KV.cardinal params);
+
let request = { request with params } in
+
(* Read remaining streams based on role *)
+
Printf.eprintf "[DEBUG] read_request: Reading streams for role=%s\n%!"
+
(Format.asprintf "%a" pp_role request.role);
+
let result = read_request_streams request buf_read in
+
Printf.eprintf "[DEBUG] read_request: Finished reading request\n%!";
+
result
+
with
+
| End_of_file ->
+
Printf.eprintf "[DEBUG] read_request: Hit End_of_file\n%!";
+
Error "Unexpected end of stream"
+
| exn ->
+
Printf.eprintf "[DEBUG] read_request: Exception: %s\n%!" (Printexc.to_string exn);
+
Error (Printf.sprintf "Error reading request: %s" (Printexc.to_string exn))
+
+
(** {1 Response Generation} *)
+
+
type app_status = int
+
type protocol_status =
+
| Request_complete
+
| Cant_mpx_conn
+
| Overloaded
+
| Unknown_role
+
+
let pp_protocol_status ppf = function
+
| Request_complete -> Format.pp_print_string ppf "Request_complete"
+
| Cant_mpx_conn -> Format.pp_print_string ppf "Cant_mpx_conn"
+
| Overloaded -> Format.pp_print_string ppf "Overloaded"
+
| Unknown_role -> Format.pp_print_string ppf "Unknown_role"
+
+
let protocol_status_to_int = function
+
| Request_complete -> 0
+
| Cant_mpx_conn -> 1
+
| Overloaded -> 2
+
| Unknown_role -> 3
+
+
let write_stream_records buf_write request_id record_type content =
+
let max_chunk = 65535 in (* FastCGI max record content length *)
+
let len = String.length content in
+
let rec chunk_string pos =
+
if pos < len then begin
+
let chunk_len = min max_chunk (len - pos) in
+
let record = Fastcgi_record.create ~record:record_type ~request_id ~content ~offset:pos ~length:chunk_len () in
+
Fastcgi_record.write buf_write record;
+
chunk_string (pos + chunk_len)
+
end
+
in
+
chunk_string 0;
+
let terminator = Fastcgi_record.create ~record:record_type ~request_id ~content:"" () in
+
Fastcgi_record.write buf_write terminator
+
+
let write_stdout_records buf_write request_id content =
+
Printf.eprintf "[DEBUG] write_stdout_records: Writing %d bytes for request_id=%d\n%!"
+
(String.length content) request_id;
+
write_stream_records buf_write request_id Stdout content
+
+
let write_stderr_records buf_write request_id content =
+
Printf.eprintf "[DEBUG] write_stderr_records: Writing %d bytes for request_id=%d\n%!"
+
(String.length content) request_id;
+
write_stream_records buf_write request_id Stderr content
+
+
let write_end_request buf_write request_id app_status protocol_status =
+
let content =
+
let buf = Bytes.create 8 in
+
Bytes.set_int32_be buf 0 (Int32.of_int app_status);
+
Bytes.set_uint8 buf 4 (protocol_status_to_int protocol_status);
+
Bytes.set_uint8 buf 5 0; (* reserved *)
+
Bytes.set_uint8 buf 6 0; (* reserved *)
+
Bytes.set_uint8 buf 7 0; (* reserved *)
+
Bytes.to_string buf
+
in
+
let record = Fastcgi_record.create ~record:End_request ~request_id ~content () in
+
Fastcgi_record.write buf_write record
+80
lib/fastcgi_request.mli
···
+
(** FastCGI request handling with functional state management and Eio flows.
+
+
This module provides a functional approach to FastCGI request processing,
+
using immutable data structures and higher-order functions. Input and output
+
are handled through Eio flows for efficient streaming I/O. *)
+
+
(** {1 Request Roles} *)
+
+
(** FastCGI application roles defining request processing behavior *)
+
type role =
+
| Responder (** Standard CGI-like request/response processing *)
+
| Authorizer (** Authorization decision with optional variables *)
+
| Filter (** Content filtering with additional data stream *)
+
+
(** [pp_role ppf role] pretty-prints a FastCGI role *)
+
val pp_role : Format.formatter -> role -> unit
+
+
(** [role_of_begin_request record] extracts role from BEGIN_REQUEST record *)
+
val role_of_begin_request : Fastcgi_record.t -> (role, string) result
+
+
(** {1 Request Context} *)
+
+
(** Immutable request context containing all request data *)
+
type t = private {
+
request_id : Fastcgi_record.request_id; (** Request identifier *)
+
role : role; (** Application role *)
+
keep_conn : bool; (** Connection keep-alive flag *)
+
params : Fastcgi_record.KV.t; (** Environment parameters *)
+
stdin_data : string; (** Complete STDIN content *)
+
data_stream : string option; (** DATA stream for Filter role *)
+
}
+
+
(** [pp ppf request] pretty-prints a request context *)
+
val pp : Format.formatter -> t -> unit
+
+
(** [create record] creates request context from BEGIN_REQUEST record *)
+
val create : Fastcgi_record.t -> (t, string) result
+
+
(** {1 Stream Processing} *)
+
+
(** [read_params buf_read] reads PARAMS stream from buf_read until empty record.
+
Returns the accumulated parameters. *)
+
val read_params : Eio.Buf_read.t -> (Fastcgi_record.KV.t, string) result
+
+
(** [read_stdin buf_read] reads STDIN stream from buf_read until empty record.
+
Returns the accumulated data. *)
+
val read_stdin : Eio.Buf_read.t -> (string, string) result
+
+
(** [read_data buf_read] reads DATA stream from buf_read until empty record.
+
Returns the accumulated data for Filter role. *)
+
val read_data : Eio.Buf_read.t -> (string, string) result
+
+
(** [read_request buf_read] reads a complete FastCGI request from buf_read.
+
Processes BEGIN_REQUEST, PARAMS, STDIN, and DATA records until complete.
+
Returns the populated request context. *)
+
val read_request : Eio.Buf_read.t -> (t, string) result
+
+
(** {1 Response Generation} *)
+
+
(** Response status codes *)
+
type app_status = int
+
type protocol_status =
+
| Request_complete
+
| Cant_mpx_conn
+
| Overloaded
+
| Unknown_role
+
+
(** [pp_protocol_status ppf status] pretty-prints protocol status *)
+
val pp_protocol_status : Format.formatter -> protocol_status -> unit
+
+
(** [write_stdout_records buf_write request_id content] writes STDOUT stream records.
+
Splits content into chunks and writes with terminator. *)
+
val write_stdout_records : Eio.Buf_write.t -> Fastcgi_record.request_id -> string -> unit
+
+
(** [write_stderr_records buf_write request_id content] writes STDERR stream records.
+
Splits content into chunks and writes with terminator. *)
+
val write_stderr_records : Eio.Buf_write.t -> Fastcgi_record.request_id -> string -> unit
+
+
(** [write_end_request buf_write request_id app_status protocol_status] writes END_REQUEST record. *)
+
val write_end_request : Eio.Buf_write.t -> Fastcgi_record.request_id -> app_status -> protocol_status -> unit
+13
test/dune
···
+
(test
+
(name simple_test)
+
(modules simple_test)
+
(libraries fastcgi eio eio_main)
+
(deps (source_tree test_cases))
+
)
+
+
(test
+
(name validate_all_test_cases)
+
(modules validate_all_test_cases)
+
(libraries fastcgi eio eio_main)
+
(deps (source_tree test_cases))
+
)
+161
test/simple_test.ml
···
+
open Fastcgi
+
+
let hex_dump ?(max_bytes=256) content =
+
let len = String.length content in
+
let display_len = min len max_bytes in
+
let bytes_per_line = 16 in
+
let lines_to_show = (display_len - 1) / bytes_per_line + 1 in
+
+
for i = 0 to lines_to_show - 1 do
+
let start = i * bytes_per_line in
+
let end_ = min (start + bytes_per_line) display_len in
+
Printf.printf " %04x: " start;
+
+
(* Print hex bytes *)
+
for j = start to end_ - 1 do
+
Printf.printf "%02x " (Char.code content.[j]);
+
if j = start + 7 then Printf.printf " "
+
done;
+
+
(* Pad remaining space *)
+
for _ = end_ to start + bytes_per_line - 1 do
+
Printf.printf " ";
+
if end_ <= start + 7 then Printf.printf " "
+
done;
+
+
Printf.printf " |";
+
+
(* Print ASCII representation *)
+
for j = start to end_ - 1 do
+
let c = content.[j] in
+
if c >= ' ' && c <= '~' then
+
Printf.printf "%c" c
+
else
+
Printf.printf "."
+
done;
+
+
Printf.printf "|\n%!"
+
done;
+
+
if len > max_bytes then
+
Printf.printf " ... (%d more bytes truncated)\n%!" (len - max_bytes)
+
+
+
let test_record_types () =
+
Printf.printf "Testing record type conversions...\n%!";
+
+
let test_type rt expected_int =
+
assert (Record.record_to_int rt = expected_int);
+
assert (Record.record_of_int expected_int = rt)
+
in
+
+
test_type Begin_request 1;
+
test_type Abort_request 2;
+
test_type End_request 3;
+
test_type Params 4;
+
test_type Stdin 5;
+
test_type Stdout 6;
+
test_type Stderr 7;
+
test_type Data 8;
+
test_type Get_values 9;
+
test_type Get_values_result 10;
+
test_type Unknown_type 11;
+
+
(* Test invalid record type *)
+
(try
+
let _ = Record.record_of_int 99 in
+
assert false (* Should not reach here *)
+
with Invalid_argument _ -> ());
+
+
Printf.printf "✓ Record type conversion test passed\n%!"
+
+
let test_kv_encoding () =
+
Printf.printf "Testing key-value encoding...\n%!";
+
+
let kv = Record.KV.empty
+
|> Record.KV.add "REQUEST_METHOD" "GET"
+
|> Record.KV.add "SERVER_NAME" "localhost"
+
|> Record.KV.add "SERVER_PORT" "80"
+
in
+
+
let encoded = Record.KV.encode kv in
+
let decoded = Record.KV.decode encoded in
+
+
(* Check that all original pairs are present *)
+
assert (Record.KV.find "REQUEST_METHOD" decoded = "GET");
+
assert (Record.KV.find "SERVER_NAME" decoded = "localhost");
+
assert (Record.KV.find "SERVER_PORT" decoded = "80");
+
+
Printf.printf "✓ Key-value encoding test passed\n%!"
+
+
let test_long_key_value () =
+
Printf.printf "Testing long key-value encoding...\n%!";
+
+
(* Create a long key and value to test 4-byte length encoding *)
+
let long_key = String.make 200 'k' in
+
let long_value = String.make 300 'v' in
+
+
let kv = Record.KV.empty
+
|> Record.KV.add long_key long_value
+
|> Record.KV.add "short" "val"
+
in
+
+
let encoded = Record.KV.encode kv in
+
let decoded = Record.KV.decode encoded in
+
+
assert (Record.KV.find long_key decoded = long_value);
+
assert (Record.KV.find "short" decoded = "val");
+
+
Printf.printf "✓ Long key-value encoding test passed\n%!"
+
+
let test_with_binary_test_case ~fs filename expected_type expected_request_id =
+
Printf.printf "Testing with binary test case: %s...\n%!" filename;
+
+
let (raw_content, parsed) =
+
Eio.Path.with_open_in (fs, "test_cases/" ^ filename) @@ fun flow ->
+
let buf = Buffer.create 1024 in
+
Eio.Flow.copy flow (Eio.Flow.buffer_sink buf);
+
let raw_content = Buffer.contents buf in
+
let buf_read = Eio.Buf_read.of_string raw_content in
+
let parsed = Record.read buf_read in
+
(raw_content, parsed)
+
in
+
+
Printf.printf "\nRaw file contents (%d bytes):\n" (String.length raw_content);
+
hex_dump raw_content;
+
+
Printf.printf "\nParsed record:\n";
+
Format.printf "%a\n" (fun ppf -> Record.pp ppf) parsed;
+
+
(* If this is a Params record, also show the decoded key-value pairs *)
+
if parsed.record_type = Params && String.length parsed.content > 0 then (
+
let params = Record.KV.decode parsed.content in
+
Printf.printf "\nDecoded parameters:\n";
+
Format.printf "%a\n" Record.KV.pp params
+
);
+
+
assert (parsed.version = 1);
+
assert (parsed.record_type = expected_type);
+
assert (parsed.request_id = expected_request_id);
+
+
Printf.printf "✓ Binary test case %s passed\n%!" filename
+
+
let run_tests ~fs =
+
Printf.printf "Running FastCGI Record tests...\n\n%!";
+
+
test_record_types ();
+
test_kv_encoding ();
+
test_long_key_value ();
+
+
(* Test with some of our binary test cases *)
+
test_with_binary_test_case ~fs "begin_request_responder.bin" Begin_request 1;
+
test_with_binary_test_case ~fs "params_empty.bin" Params 1;
+
test_with_binary_test_case ~fs "end_request_success.bin" End_request 1;
+
test_with_binary_test_case ~fs "get_values.bin" Get_values 0;
+
test_with_binary_test_case ~fs "abort_request.bin" Abort_request 1;
+
+
Printf.printf "\n✅ All FastCGI Record tests passed!\n%!"
+
+
let () = Eio_main.run @@ fun env ->
+
let fs = Eio.Stdenv.cwd env in
+
run_tests ~fs:(fst fs)
+123
test/test_cases/README.md
···
+
# FastCGI Test Cases
+
+
This directory contains binary test case files representing various FastCGI records as defined in the FastCGI specification. These files can be used to test a FastCGI parser implementation.
+
+
## Test Case Files
+
+
### Management Records (requestId = 0)
+
+
- **`get_values.bin`** - FCGI_GET_VALUES record requesting capability information
+
- **`get_values_result.bin`** - FCGI_GET_VALUES_RESULT record with capability responses
+
- **`unknown_type.bin`** - FCGI_UNKNOWN_TYPE record for unrecognized record types
+
+
### Application Records (requestId > 0)
+
+
#### BEGIN_REQUEST Records
+
- **`begin_request_responder.bin`** - Begin request for Responder role with KEEP_CONN flag
+
- **`begin_request_no_keep.bin`** - Begin request for Responder role without KEEP_CONN flag
+
- **`begin_request_authorizer.bin`** - Begin request for Authorizer role
+
- **`begin_request_filter.bin`** - Begin request for Filter role
+
+
#### Stream Records
+
- **`params_get.bin`** - FCGI_PARAMS record with GET request environment variables
+
- **`params_post.bin`** - FCGI_PARAMS record with POST request environment variables
+
- **`params_empty.bin`** - Empty FCGI_PARAMS record (end of params stream)
+
- **`stdin_form_data.bin`** - FCGI_STDIN record with form data
+
- **`stdin_empty.bin`** - Empty FCGI_STDIN record (end of stdin stream)
+
- **`stdout_response.bin`** - FCGI_STDOUT record with HTTP response
+
- **`stdout_empty.bin`** - Empty FCGI_STDOUT record (end of stdout stream)
+
- **`stderr_message.bin`** - FCGI_STDERR record with error message
+
- **`stderr_empty.bin`** - Empty FCGI_STDERR record (end of stderr stream)
+
- **`data_filter.bin`** - FCGI_DATA record with file content (for Filter role)
+
- **`data_empty.bin`** - Empty FCGI_DATA record (end of data stream)
+
+
#### Control Records
+
- **`end_request_success.bin`** - FCGI_END_REQUEST record with successful completion
+
- **`end_request_error.bin`** - FCGI_END_REQUEST record with error status
+
- **`abort_request.bin`** - FCGI_ABORT_REQUEST record
+
+
### Complex Scenarios
+
- **`multiplexed_requests.bin`** - Multiple concurrent requests on same connection
+
- **`large_record.bin`** - Record with maximum content size (65KB)
+
- **`padded_record.bin`** - Record with padding for 8-byte alignment
+
+
## Record Format
+
+
All records follow the FastCGI record format:
+
+
```
+
typedef struct {
+
unsigned char version; // FCGI_VERSION_1 (1)
+
unsigned char type; // Record type (1-11)
+
unsigned char requestIdB1; // Request ID high byte
+
unsigned char requestIdB0; // Request ID low byte
+
unsigned char contentLengthB1; // Content length high byte
+
unsigned char contentLengthB0; // Content length low byte
+
unsigned char paddingLength; // Padding length
+
unsigned char reserved; // Reserved (always 0)
+
// contentData[contentLength]
+
// paddingData[paddingLength]
+
} FCGI_Record;
+
```
+
+
## Usage for Parser Testing
+
+
These test cases can be used to verify:
+
+
1. **Header parsing** - Correct extraction of version, type, requestId, lengths
+
2. **Content parsing** - Proper handling of record body data
+
3. **Stream handling** - Recognition of stream start/end patterns
+
4. **Name-value pair parsing** - Decoding of FCGI_PARAMS format
+
5. **Role-specific parsing** - Different record sequences for each role
+
6. **Error handling** - Response to unknown types, malformed data
+
7. **Multiplexing** - Handling multiple concurrent request IDs
+
8. **Padding** - Correct skipping of padding bytes
+
+
## Typical Protocol Flows
+
+
### Simple Responder Request
+
1. `begin_request_responder.bin`
+
2. `params_get.bin`
+
3. `params_empty.bin`
+
4. `stdin_empty.bin`
+
5. `stdout_response.bin`
+
6. `stdout_empty.bin`
+
7. `end_request_success.bin`
+
+
### POST Request with Form Data
+
1. `begin_request_responder.bin`
+
2. `params_post.bin`
+
3. `params_empty.bin`
+
4. `stdin_form_data.bin`
+
5. `stdin_empty.bin`
+
6. `stdout_response.bin`
+
7. `stdout_empty.bin`
+
8. `end_request_success.bin`
+
+
### Authorizer Request
+
1. `begin_request_authorizer.bin`
+
2. `params_get.bin`
+
3. `params_empty.bin`
+
4. `stdout_response.bin`
+
5. `stdout_empty.bin`
+
6. `end_request_success.bin`
+
+
### Filter Request
+
1. `begin_request_filter.bin`
+
2. `params_get.bin`
+
3. `params_empty.bin`
+
4. `stdin_empty.bin`
+
5. `data_filter.bin`
+
6. `data_empty.bin`
+
7. `stdout_response.bin`
+
8. `stdout_empty.bin`
+
9. `end_request_success.bin`
+
+
## Binary Format Verification
+
+
You can inspect the binary content using hexdump:
+
```bash
+
hexdump -C begin_request_responder.bin
+
```
+
+
The first 8 bytes should always be the FastCGI header, followed by the record content and any padding.
test/test_cases/abort_request.bin

This is a binary file and will not be displayed.

test/test_cases/begin_request_authorizer.bin

This is a binary file and will not be displayed.

test/test_cases/begin_request_filter.bin

This is a binary file and will not be displayed.

test/test_cases/begin_request_no_keep.bin

This is a binary file and will not be displayed.

test/test_cases/begin_request_responder.bin

This is a binary file and will not be displayed.

test/test_cases/data_empty.bin

This is a binary file and will not be displayed.

test/test_cases/data_filter.bin

This is a binary file and will not be displayed.

test/test_cases/end_request_error.bin

This is a binary file and will not be displayed.

test/test_cases/end_request_success.bin

This is a binary file and will not be displayed.

+349
test/test_cases/generate_test_cases.py
···
+
#!/usr/bin/env python3
+
"""
+
Generate FastCGI test case files based on the specification.
+
"""
+
import struct
+
import os
+
+
# FastCGI constants from the specification
+
FCGI_VERSION_1 = 1
+
+
# Record types
+
FCGI_BEGIN_REQUEST = 1
+
FCGI_ABORT_REQUEST = 2
+
FCGI_END_REQUEST = 3
+
FCGI_PARAMS = 4
+
FCGI_STDIN = 5
+
FCGI_STDOUT = 6
+
FCGI_STDERR = 7
+
FCGI_DATA = 8
+
FCGI_GET_VALUES = 9
+
FCGI_GET_VALUES_RESULT = 10
+
FCGI_UNKNOWN_TYPE = 11
+
+
# Roles
+
FCGI_RESPONDER = 1
+
FCGI_AUTHORIZER = 2
+
FCGI_FILTER = 3
+
+
# Flags
+
FCGI_KEEP_CONN = 1
+
+
# Protocol status
+
FCGI_REQUEST_COMPLETE = 0
+
FCGI_CANT_MPX_CONN = 1
+
FCGI_OVERLOADED = 2
+
FCGI_UNKNOWN_ROLE = 3
+
+
def create_record_header(version, record_type, request_id, content_length, padding_length=0):
+
"""Create a FastCGI record header."""
+
return struct.pack('>BBHHBB',
+
version,
+
record_type,
+
request_id,
+
content_length,
+
padding_length,
+
0) # reserved
+
+
def encode_name_value_pair(name, value):
+
"""Encode a name-value pair according to FastCGI spec."""
+
name_bytes = name.encode('utf-8')
+
value_bytes = value.encode('utf-8')
+
+
name_len = len(name_bytes)
+
value_len = len(value_bytes)
+
+
# Encode lengths
+
if name_len < 128:
+
name_len_encoded = struct.pack('B', name_len)
+
else:
+
name_len_encoded = struct.pack('>I', name_len | 0x80000000)
+
+
if value_len < 128:
+
value_len_encoded = struct.pack('B', value_len)
+
else:
+
value_len_encoded = struct.pack('>I', value_len | 0x80000000)
+
+
return name_len_encoded + value_len_encoded + name_bytes + value_bytes
+
+
def create_begin_request():
+
"""Create FCGI_BEGIN_REQUEST record."""
+
# FCGI_BeginRequestBody
+
body = struct.pack('>HB5x', FCGI_RESPONDER, FCGI_KEEP_CONN)
+
header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 1, len(body))
+
return header + body
+
+
def create_begin_request_no_keep_conn():
+
"""Create FCGI_BEGIN_REQUEST record without keep connection."""
+
body = struct.pack('>HB5x', FCGI_RESPONDER, 0)
+
header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 1, len(body))
+
return header + body
+
+
def create_begin_request_authorizer():
+
"""Create FCGI_BEGIN_REQUEST record for authorizer role."""
+
body = struct.pack('>HB5x', FCGI_AUTHORIZER, 0)
+
header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 2, len(body))
+
return header + body
+
+
def create_begin_request_filter():
+
"""Create FCGI_BEGIN_REQUEST record for filter role."""
+
body = struct.pack('>HB5x', FCGI_FILTER, FCGI_KEEP_CONN)
+
header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 3, len(body))
+
return header + body
+
+
def create_end_request():
+
"""Create FCGI_END_REQUEST record."""
+
# FCGI_EndRequestBody
+
body = struct.pack('>IB3x', 0, FCGI_REQUEST_COMPLETE) # app_status=0, protocol_status=COMPLETE
+
header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 1, len(body))
+
return header + body
+
+
def create_end_request_error():
+
"""Create FCGI_END_REQUEST record with error status."""
+
body = struct.pack('>IB3x', 1, FCGI_REQUEST_COMPLETE) # app_status=1 (error)
+
header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 1, len(body))
+
return header + body
+
+
def create_params_record():
+
"""Create FCGI_PARAMS record with CGI environment variables."""
+
# Typical CGI parameters
+
params = [
+
("REQUEST_METHOD", "GET"),
+
("SCRIPT_NAME", "/test.cgi"),
+
("REQUEST_URI", "/test.cgi?foo=bar"),
+
("QUERY_STRING", "foo=bar"),
+
("SERVER_NAME", "localhost"),
+
("SERVER_PORT", "80"),
+
("HTTP_HOST", "localhost"),
+
("HTTP_USER_AGENT", "Mozilla/5.0"),
+
("CONTENT_TYPE", ""),
+
("CONTENT_LENGTH", "0")
+
]
+
+
body = b''
+
for name, value in params:
+
body += encode_name_value_pair(name, value)
+
+
header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 1, len(body))
+
return header + body
+
+
def create_params_record_post():
+
"""Create FCGI_PARAMS record for POST request."""
+
params = [
+
("REQUEST_METHOD", "POST"),
+
("SCRIPT_NAME", "/form.cgi"),
+
("REQUEST_URI", "/form.cgi"),
+
("QUERY_STRING", ""),
+
("SERVER_NAME", "localhost"),
+
("SERVER_PORT", "443"),
+
("HTTPS", "on"),
+
("HTTP_HOST", "localhost"),
+
("HTTP_USER_AGENT", "curl/7.68.0"),
+
("CONTENT_TYPE", "application/x-www-form-urlencoded"),
+
("CONTENT_LENGTH", "23")
+
]
+
+
body = b''
+
for name, value in params:
+
body += encode_name_value_pair(name, value)
+
+
header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 1, len(body))
+
return header + body
+
+
def create_empty_params():
+
"""Create empty FCGI_PARAMS record (end of params stream)."""
+
header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 1, 0)
+
return header
+
+
def create_stdin_record():
+
"""Create FCGI_STDIN record with form data."""
+
data = b"name=John&email=john@example.com"
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDIN, 1, len(data))
+
return header + data
+
+
def create_empty_stdin():
+
"""Create empty FCGI_STDIN record (end of stdin stream)."""
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDIN, 1, 0)
+
return header
+
+
def create_stdout_record():
+
"""Create FCGI_STDOUT record with HTTP response."""
+
response = b"Content-Type: text/html\r\n\r\n<html><body>Hello World</body></html>"
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(response))
+
return header + response
+
+
def create_empty_stdout():
+
"""Create empty FCGI_STDOUT record (end of stdout stream)."""
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, 0)
+
return header
+
+
def create_stderr_record():
+
"""Create FCGI_STDERR record with error message."""
+
error_msg = b"Warning: Configuration file not found\n"
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDERR, 1, len(error_msg))
+
return header + error_msg
+
+
def create_empty_stderr():
+
"""Create empty FCGI_STDERR record (end of stderr stream)."""
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDERR, 1, 0)
+
return header
+
+
def create_get_values():
+
"""Create FCGI_GET_VALUES record."""
+
# Request standard capability variables
+
body = (encode_name_value_pair("FCGI_MAX_CONNS", "") +
+
encode_name_value_pair("FCGI_MAX_REQS", "") +
+
encode_name_value_pair("FCGI_MPXS_CONNS", ""))
+
header = create_record_header(FCGI_VERSION_1, FCGI_GET_VALUES, 0, len(body))
+
return header + body
+
+
def create_get_values_result():
+
"""Create FCGI_GET_VALUES_RESULT record."""
+
body = (encode_name_value_pair("FCGI_MAX_CONNS", "1") +
+
encode_name_value_pair("FCGI_MAX_REQS", "1") +
+
encode_name_value_pair("FCGI_MPXS_CONNS", "0"))
+
header = create_record_header(FCGI_VERSION_1, FCGI_GET_VALUES_RESULT, 0, len(body))
+
return header + body
+
+
def create_unknown_type():
+
"""Create FCGI_UNKNOWN_TYPE record."""
+
# FCGI_UnknownTypeBody
+
body = struct.pack('B7x', 99) # unknown type 99
+
header = create_record_header(FCGI_VERSION_1, FCGI_UNKNOWN_TYPE, 0, len(body))
+
return header + body
+
+
def create_abort_request():
+
"""Create FCGI_ABORT_REQUEST record."""
+
header = create_record_header(FCGI_VERSION_1, FCGI_ABORT_REQUEST, 1, 0)
+
return header
+
+
def create_data_record():
+
"""Create FCGI_DATA record (for Filter role)."""
+
file_data = b"This is file content that needs to be filtered\nLine 2\nLine 3\n"
+
header = create_record_header(FCGI_VERSION_1, FCGI_DATA, 3, len(file_data))
+
return header + file_data
+
+
def create_empty_data():
+
"""Create empty FCGI_DATA record (end of data stream)."""
+
header = create_record_header(FCGI_VERSION_1, FCGI_DATA, 3, 0)
+
return header
+
+
def create_multiplexed_records():
+
"""Create a sequence showing multiplexed requests."""
+
records = []
+
+
# Request 1 begins
+
records.append(create_begin_request())
+
records.append(create_params_record())
+
records.append(create_empty_params())
+
+
# Request 2 begins (different request ID)
+
body = struct.pack('>HB5x', FCGI_RESPONDER, FCGI_KEEP_CONN)
+
header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 2, len(body))
+
records.append(header + body)
+
+
# Request 1 continues
+
records.append(create_empty_stdin())
+
+
# Request 2 params
+
params = [("REQUEST_METHOD", "POST"), ("SCRIPT_NAME", "/other.cgi")]
+
body = b''
+
for name, value in params:
+
body += encode_name_value_pair(name, value)
+
header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 2, len(body))
+
records.append(header + body)
+
+
# Request 2 empty params
+
header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 2, 0)
+
records.append(header)
+
+
# Request 2 completes first
+
response = b"Content-Type: text/plain\r\n\r\nRequest 2 done"
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 2, len(response))
+
records.append(header + response)
+
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 2, 0)
+
records.append(header)
+
+
body = struct.pack('>IB3x', 0, FCGI_REQUEST_COMPLETE)
+
header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 2, len(body))
+
records.append(header + body)
+
+
# Request 1 completes
+
response = b"Content-Type: text/html\r\n\r\n<html><body>Request 1 done</body></html>"
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(response))
+
records.append(header + response)
+
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, 0)
+
records.append(header)
+
+
body = struct.pack('>IB3x', 0, FCGI_REQUEST_COMPLETE)
+
header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 1, len(body))
+
records.append(header + body)
+
+
return b''.join(records)
+
+
def create_large_record():
+
"""Create a record with maximum content size."""
+
# Create a large response (just under 64KB)
+
large_content = b"x" * 65000
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(large_content))
+
return header + large_content
+
+
def create_padded_record():
+
"""Create a record with padding for alignment."""
+
data = b"Hello" # 5 bytes
+
padding_length = 3 # to align to 8-byte boundary (5 + 3 = 8)
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(data), padding_length)
+
padding = b'\x00' * padding_length
+
return header + data + padding
+
+
# Test case definitions
+
test_cases = {
+
"begin_request_responder.bin": create_begin_request,
+
"begin_request_no_keep.bin": create_begin_request_no_keep_conn,
+
"begin_request_authorizer.bin": create_begin_request_authorizer,
+
"begin_request_filter.bin": create_begin_request_filter,
+
"end_request_success.bin": create_end_request,
+
"end_request_error.bin": create_end_request_error,
+
"params_get.bin": create_params_record,
+
"params_post.bin": create_params_record_post,
+
"params_empty.bin": create_empty_params,
+
"stdin_form_data.bin": create_stdin_record,
+
"stdin_empty.bin": create_empty_stdin,
+
"stdout_response.bin": create_stdout_record,
+
"stdout_empty.bin": create_empty_stdout,
+
"stderr_message.bin": create_stderr_record,
+
"stderr_empty.bin": create_empty_stderr,
+
"get_values.bin": create_get_values,
+
"get_values_result.bin": create_get_values_result,
+
"unknown_type.bin": create_unknown_type,
+
"abort_request.bin": create_abort_request,
+
"data_filter.bin": create_data_record,
+
"data_empty.bin": create_empty_data,
+
"multiplexed_requests.bin": create_multiplexed_records,
+
"large_record.bin": create_large_record,
+
"padded_record.bin": create_padded_record,
+
}
+
+
if __name__ == "__main__":
+
for filename, creator in test_cases.items():
+
with open(filename, 'wb') as f:
+
f.write(creator())
+
print(f"Created {filename}")
+
+
print(f"\nGenerated {len(test_cases)} test case files")
+
print("\nTest case descriptions:")
+
print("- begin_request_*.bin: Various BEGIN_REQUEST records for different roles")
+
print("- end_request_*.bin: END_REQUEST records with different status codes")
+
print("- params_*.bin: PARAMS records with CGI environment variables")
+
print("- stdin_*.bin: STDIN records with request body data")
+
print("- stdout_*.bin: STDOUT records with response data")
+
print("- stderr_*.bin: STDERR records with error messages")
+
print("- get_values*.bin: Management records for capability negotiation")
+
print("- unknown_type.bin: Unknown record type handling")
+
print("- abort_request.bin: Request abortion")
+
print("- data_*.bin: DATA records for Filter role")
+
print("- multiplexed_requests.bin: Multiple concurrent requests")
+
print("- large_record.bin: Maximum size record")
+
print("- padded_record.bin: Record with padding for alignment")
test/test_cases/get_values.bin

This is a binary file and will not be displayed.

test/test_cases/get_values_result.bin

This is a binary file and will not be displayed.

test/test_cases/large_record.bin

This is a binary file and will not be displayed.

test/test_cases/multiplexed_requests.bin

This is a binary file and will not be displayed.

test/test_cases/padded_record.bin

This is a binary file and will not be displayed.

test/test_cases/params_empty.bin

This is a binary file and will not be displayed.

test/test_cases/params_get.bin

This is a binary file and will not be displayed.

test/test_cases/params_post.bin

This is a binary file and will not be displayed.

test/test_cases/stderr_empty.bin

This is a binary file and will not be displayed.

test/test_cases/stderr_message.bin

This is a binary file and will not be displayed.

test/test_cases/stdin_empty.bin

This is a binary file and will not be displayed.

test/test_cases/stdin_form_data.bin

This is a binary file and will not be displayed.

test/test_cases/stdout_empty.bin

This is a binary file and will not be displayed.

test/test_cases/stdout_response.bin

This is a binary file and will not be displayed.

+24
test/test_cases/test_case_sizes.txt
···
+
abort_request.bin 8 bytes
+
begin_request_authorizer.bin 16 bytes
+
begin_request_filter.bin 16 bytes
+
begin_request_no_keep.bin 16 bytes
+
begin_request_responder.bin 16 bytes
+
data_empty.bin 8 bytes
+
data_filter.bin 69 bytes
+
end_request_error.bin 16 bytes
+
end_request_success.bin 16 bytes
+
get_values_result.bin 59 bytes
+
get_values.bin 56 bytes
+
large_record.bin 65008 bytes
+
multiplexed_requests.bin 496 bytes
+
padded_record.bin 16 bytes
+
params_empty.bin 8 bytes
+
params_get.bin 216 bytes
+
params_post.bin 246 bytes
+
stderr_empty.bin 8 bytes
+
stderr_message.bin 46 bytes
+
stdin_empty.bin 8 bytes
+
stdin_form_data.bin 40 bytes
+
stdout_empty.bin 8 bytes
+
stdout_response.bin 72 bytes
+
unknown_type.bin 16 bytes
test/test_cases/unknown_type.bin

This is a binary file and will not be displayed.

+130
test/test_cases/validate_test_cases.py
···
+
#!/usr/bin/env python3
+
"""
+
Validate that the generated FastCGI test cases are properly formatted.
+
"""
+
import struct
+
import os
+
import glob
+
+
def parse_record_header(data):
+
"""Parse a FastCGI record header."""
+
if len(data) < 8:
+
return None
+
+
version, record_type, request_id, content_length, padding_length, reserved = struct.unpack('>BBHHBB', data[:8])
+
return {
+
'version': version,
+
'type': record_type,
+
'request_id': request_id,
+
'content_length': content_length,
+
'padding_length': padding_length,
+
'reserved': reserved,
+
'total_length': 8 + content_length + padding_length
+
}
+
+
def validate_file(filename):
+
"""Validate a single test case file."""
+
print(f"\nValidating {filename}:")
+
+
with open(filename, 'rb') as f:
+
data = f.read()
+
+
if len(data) < 8:
+
print(f" ❌ File too short: {len(data)} bytes")
+
return False
+
+
# Parse all records in the file
+
offset = 0
+
record_count = 0
+
+
while offset < len(data):
+
if offset + 8 > len(data):
+
print(f" ❌ Incomplete header at offset {offset}")
+
return False
+
+
header = parse_record_header(data[offset:])
+
if not header:
+
print(f" ❌ Failed to parse header at offset {offset}")
+
return False
+
+
record_count += 1
+
print(f" Record {record_count}:")
+
print(f" Version: {header['version']}")
+
print(f" Type: {header['type']}")
+
print(f" Request ID: {header['request_id']}")
+
print(f" Content Length: {header['content_length']}")
+
print(f" Padding Length: {header['padding_length']}")
+
print(f" Reserved: {header['reserved']}")
+
+
# Validate header fields
+
if header['version'] != 1:
+
print(f" ❌ Invalid version: {header['version']}")
+
return False
+
+
if header['type'] < 1 or header['type'] > 11:
+
print(f" ❌ Invalid record type: {header['type']}")
+
return False
+
+
if header['reserved'] != 0:
+
print(f" ❌ Reserved field not zero: {header['reserved']}")
+
return False
+
+
# Check if we have enough data for content and padding
+
expected_end = offset + header['total_length']
+
if expected_end > len(data):
+
print(f" ❌ Not enough data: need {header['total_length']}, have {len(data) - offset}")
+
return False
+
+
# Extract content
+
content_start = offset + 8
+
content_end = content_start + header['content_length']
+
content = data[content_start:content_end]
+
+
# Extract padding
+
padding_start = content_end
+
padding_end = padding_start + header['padding_length']
+
padding = data[padding_start:padding_end]
+
+
print(f" Content: {len(content)} bytes")
+
if header['padding_length'] > 0:
+
print(f" Padding: {len(padding)} bytes")
+
+
# Show content preview for small records
+
if len(content) <= 32:
+
print(f" Content hex: {content.hex()}")
+
else:
+
print(f" Content preview: {content[:16].hex()}...")
+
+
print(f" ✅ Record valid")
+
+
offset = expected_end
+
+
print(f" ✅ File valid: {record_count} record(s), {len(data)} total bytes")
+
return True
+
+
def main():
+
"""Validate all test case files."""
+
test_files = glob.glob("*.bin")
+
test_files.sort()
+
+
print(f"Found {len(test_files)} test case files")
+
+
valid_count = 0
+
for filename in test_files:
+
if validate_file(filename):
+
valid_count += 1
+
+
print(f"\n{'='*50}")
+
print(f"Validation complete: {valid_count}/{len(test_files)} files valid")
+
+
if valid_count == len(test_files):
+
print("✅ All test cases are valid!")
+
else:
+
print("❌ Some test cases failed validation")
+
return 1
+
+
return 0
+
+
if __name__ == "__main__":
+
import sys
+
sys.exit(main())
+438
test/test_cases/validation_results.txt
···
+
Found 24 test case files
+
+
Validating abort_request.bin:
+
Record 1:
+
Version: 1
+
Type: 2
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
✅ Record valid
+
✅ File valid: 1 record(s), 8 total bytes
+
+
Validating begin_request_authorizer.bin:
+
Record 1:
+
Version: 1
+
Type: 1
+
Request ID: 2
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0002000000000000
+
✅ Record valid
+
✅ File valid: 1 record(s), 16 total bytes
+
+
Validating begin_request_filter.bin:
+
Record 1:
+
Version: 1
+
Type: 1
+
Request ID: 3
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0003010000000000
+
✅ Record valid
+
✅ File valid: 1 record(s), 16 total bytes
+
+
Validating begin_request_no_keep.bin:
+
Record 1:
+
Version: 1
+
Type: 1
+
Request ID: 1
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0001000000000000
+
✅ Record valid
+
✅ File valid: 1 record(s), 16 total bytes
+
+
Validating begin_request_responder.bin:
+
Record 1:
+
Version: 1
+
Type: 1
+
Request ID: 1
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0001010000000000
+
✅ Record valid
+
✅ File valid: 1 record(s), 16 total bytes
+
+
Validating data_empty.bin:
+
Record 1:
+
Version: 1
+
Type: 8
+
Request ID: 3
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
✅ Record valid
+
✅ File valid: 1 record(s), 8 total bytes
+
+
Validating data_filter.bin:
+
Record 1:
+
Version: 1
+
Type: 8
+
Request ID: 3
+
Content Length: 61
+
Padding Length: 0
+
Reserved: 0
+
Content: 61 bytes
+
Content preview: 546869732069732066696c6520636f6e...
+
✅ Record valid
+
✅ File valid: 1 record(s), 69 total bytes
+
+
Validating end_request_error.bin:
+
Record 1:
+
Version: 1
+
Type: 3
+
Request ID: 1
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0000000100000000
+
✅ Record valid
+
✅ File valid: 1 record(s), 16 total bytes
+
+
Validating end_request_success.bin:
+
Record 1:
+
Version: 1
+
Type: 3
+
Request ID: 1
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0000000000000000
+
✅ Record valid
+
✅ File valid: 1 record(s), 16 total bytes
+
+
Validating get_values.bin:
+
Record 1:
+
Version: 1
+
Type: 9
+
Request ID: 0
+
Content Length: 48
+
Padding Length: 0
+
Reserved: 0
+
Content: 48 bytes
+
Content preview: 0e00464347495f4d41585f434f4e4e53...
+
✅ Record valid
+
✅ File valid: 1 record(s), 56 total bytes
+
+
Validating get_values_result.bin:
+
Record 1:
+
Version: 1
+
Type: 10
+
Request ID: 0
+
Content Length: 51
+
Padding Length: 0
+
Reserved: 0
+
Content: 51 bytes
+
Content preview: 0e01464347495f4d41585f434f4e4e53...
+
✅ Record valid
+
✅ File valid: 1 record(s), 59 total bytes
+
+
Validating large_record.bin:
+
Record 1:
+
Version: 1
+
Type: 6
+
Request ID: 1
+
Content Length: 65000
+
Padding Length: 0
+
Reserved: 0
+
Content: 65000 bytes
+
Content preview: 78787878787878787878787878787878...
+
✅ Record valid
+
✅ File valid: 1 record(s), 65008 total bytes
+
+
Validating multiplexed_requests.bin:
+
Record 1:
+
Version: 1
+
Type: 1
+
Request ID: 1
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0001010000000000
+
✅ Record valid
+
Record 2:
+
Version: 1
+
Type: 4
+
Request ID: 1
+
Content Length: 208
+
Padding Length: 0
+
Reserved: 0
+
Content: 208 bytes
+
Content preview: 0e03524551554553545f4d4554484f44...
+
✅ Record valid
+
Record 3:
+
Version: 1
+
Type: 4
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
✅ Record valid
+
Record 4:
+
Version: 1
+
Type: 1
+
Request ID: 2
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0001010000000000
+
✅ Record valid
+
Record 5:
+
Version: 1
+
Type: 5
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
✅ Record valid
+
Record 6:
+
Version: 1
+
Type: 4
+
Request ID: 2
+
Content Length: 43
+
Padding Length: 0
+
Reserved: 0
+
Content: 43 bytes
+
Content preview: 0e04524551554553545f4d4554484f44...
+
✅ Record valid
+
Record 7:
+
Version: 1
+
Type: 4
+
Request ID: 2
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
✅ Record valid
+
Record 8:
+
Version: 1
+
Type: 6
+
Request ID: 2
+
Content Length: 42
+
Padding Length: 0
+
Reserved: 0
+
Content: 42 bytes
+
Content preview: 436f6e74656e742d547970653a207465...
+
✅ Record valid
+
Record 9:
+
Version: 1
+
Type: 6
+
Request ID: 2
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
✅ Record valid
+
Record 10:
+
Version: 1
+
Type: 3
+
Request ID: 2
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0000000000000000
+
✅ Record valid
+
Record 11:
+
Version: 1
+
Type: 6
+
Request ID: 1
+
Content Length: 67
+
Padding Length: 0
+
Reserved: 0
+
Content: 67 bytes
+
Content preview: 436f6e74656e742d547970653a207465...
+
✅ Record valid
+
Record 12:
+
Version: 1
+
Type: 6
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
✅ Record valid
+
Record 13:
+
Version: 1
+
Type: 3
+
Request ID: 1
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0000000000000000
+
✅ Record valid
+
✅ File valid: 13 record(s), 496 total bytes
+
+
Validating padded_record.bin:
+
Record 1:
+
Version: 1
+
Type: 6
+
Request ID: 1
+
Content Length: 5
+
Padding Length: 3
+
Reserved: 0
+
Content: 5 bytes
+
Padding: 3 bytes
+
Content hex: 48656c6c6f
+
✅ Record valid
+
✅ File valid: 1 record(s), 16 total bytes
+
+
Validating params_empty.bin:
+
Record 1:
+
Version: 1
+
Type: 4
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
✅ Record valid
+
✅ File valid: 1 record(s), 8 total bytes
+
+
Validating params_get.bin:
+
Record 1:
+
Version: 1
+
Type: 4
+
Request ID: 1
+
Content Length: 208
+
Padding Length: 0
+
Reserved: 0
+
Content: 208 bytes
+
Content preview: 0e03524551554553545f4d4554484f44...
+
✅ Record valid
+
✅ File valid: 1 record(s), 216 total bytes
+
+
Validating params_post.bin:
+
Record 1:
+
Version: 1
+
Type: 4
+
Request ID: 1
+
Content Length: 238
+
Padding Length: 0
+
Reserved: 0
+
Content: 238 bytes
+
Content preview: 0e04524551554553545f4d4554484f44...
+
✅ Record valid
+
✅ File valid: 1 record(s), 246 total bytes
+
+
Validating stderr_empty.bin:
+
Record 1:
+
Version: 1
+
Type: 7
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
✅ Record valid
+
✅ File valid: 1 record(s), 8 total bytes
+
+
Validating stderr_message.bin:
+
Record 1:
+
Version: 1
+
Type: 7
+
Request ID: 1
+
Content Length: 38
+
Padding Length: 0
+
Reserved: 0
+
Content: 38 bytes
+
Content preview: 5761726e696e673a20436f6e66696775...
+
✅ Record valid
+
✅ File valid: 1 record(s), 46 total bytes
+
+
Validating stdin_empty.bin:
+
Record 1:
+
Version: 1
+
Type: 5
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
✅ Record valid
+
✅ File valid: 1 record(s), 8 total bytes
+
+
Validating stdin_form_data.bin:
+
Record 1:
+
Version: 1
+
Type: 5
+
Request ID: 1
+
Content Length: 32
+
Padding Length: 0
+
Reserved: 0
+
Content: 32 bytes
+
Content hex: 6e616d653d4a6f686e26656d61696c3d6a6f686e406578616d706c652e636f6d
+
✅ Record valid
+
✅ File valid: 1 record(s), 40 total bytes
+
+
Validating stdout_empty.bin:
+
Record 1:
+
Version: 1
+
Type: 6
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
✅ Record valid
+
✅ File valid: 1 record(s), 8 total bytes
+
+
Validating stdout_response.bin:
+
Record 1:
+
Version: 1
+
Type: 6
+
Request ID: 1
+
Content Length: 64
+
Padding Length: 0
+
Reserved: 0
+
Content: 64 bytes
+
Content preview: 436f6e74656e742d547970653a207465...
+
✅ Record valid
+
✅ File valid: 1 record(s), 72 total bytes
+
+
Validating unknown_type.bin:
+
Record 1:
+
Version: 1
+
Type: 11
+
Request ID: 0
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 6300000000000000
+
✅ Record valid
+
✅ File valid: 1 record(s), 16 total bytes
+
+
==================================================
+
Validation complete: 24/24 files valid
+
✅ All test cases are valid!
+219
test/validate_all_test_cases.ml
···
+
open Fastcgi
+
open Record
+
+
let hex_dump ?(max_bytes=256) content =
+
let len = String.length content in
+
let display_len = min len max_bytes in
+
let bytes_per_line = 16 in
+
let lines_to_show = (display_len - 1) / bytes_per_line + 1 in
+
+
for i = 0 to lines_to_show - 1 do
+
let start = i * bytes_per_line in
+
let end_ = min (start + bytes_per_line) display_len in
+
Printf.printf " %04x: " start;
+
+
(* Print hex bytes *)
+
for j = start to end_ - 1 do
+
Printf.printf "%02x " (Char.code content.[j]);
+
if j = start + 7 then Printf.printf " "
+
done;
+
+
(* Pad remaining space *)
+
for _ = end_ to start + bytes_per_line - 1 do
+
Printf.printf " ";
+
if end_ <= start + 7 then Printf.printf " "
+
done;
+
+
Printf.printf " |";
+
+
(* Print ASCII representation *)
+
for j = start to end_ - 1 do
+
let c = content.[j] in
+
if c >= ' ' && c <= '~' then
+
Printf.printf "%c" c
+
else
+
Printf.printf "."
+
done;
+
+
Printf.printf "|\n%!"
+
done;
+
+
if len > max_bytes then
+
Printf.printf " ... (%d more bytes truncated)\n%!" (len - max_bytes)
+
+
+
let test_cases = [
+
("abort_request.bin", Abort_request, 1);
+
("begin_request_authorizer.bin", Begin_request, 2);
+
("begin_request_filter.bin", Begin_request, 3);
+
("begin_request_no_keep.bin", Begin_request, 1);
+
("begin_request_responder.bin", Begin_request, 1);
+
("data_empty.bin", Data, 3);
+
("data_filter.bin", Data, 3);
+
("end_request_error.bin", End_request, 1);
+
("end_request_success.bin", End_request, 1);
+
("get_values.bin", Get_values, 0);
+
("get_values_result.bin", Get_values_result, 0);
+
("params_empty.bin", Params, 1);
+
("params_get.bin", Params, 1);
+
("params_post.bin", Params, 1);
+
("stderr_empty.bin", Stderr, 1);
+
("stderr_message.bin", Stderr, 1);
+
("stdin_empty.bin", Stdin, 1);
+
("stdin_form_data.bin", Stdin, 1);
+
("stdout_empty.bin", Stdout, 1);
+
("stdout_response.bin", Stdout, 1);
+
("unknown_type.bin", Unknown_type, 0);
+
]
+
+
let test_binary_file ~fs filename expected_type expected_request_id =
+
Printf.printf "Testing %s...\n" filename;
+
+
let (raw_content, parsed) =
+
Eio.Path.with_open_in (fs, "test_cases/" ^ filename) @@ fun flow ->
+
let buf = Buffer.create 1024 in
+
Eio.Flow.copy flow (Eio.Flow.buffer_sink buf);
+
let raw_content = Buffer.contents buf in
+
let buf_read = Eio.Buf_read.of_string raw_content in
+
let parsed = Record.read buf_read in
+
(raw_content, parsed)
+
in
+
+
Printf.printf "\nRaw file contents (%d bytes):\n" (String.length raw_content);
+
hex_dump raw_content;
+
+
Printf.printf "\nParsed record:\n";
+
Format.printf "%a\n" (fun ppf -> Record.pp ppf) parsed;
+
+
(* If this is a Params record, also show the decoded key-value pairs *)
+
if parsed.record_type = Params && String.length parsed.content > 0 then (
+
let params = Record.KV.decode parsed.content in
+
Printf.printf "\nDecoded parameters:\n";
+
Format.printf "%a\n" Record.KV.pp params
+
);
+
+
assert (parsed.version = 1);
+
assert (parsed.record_type = expected_type);
+
assert (parsed.request_id = expected_request_id);
+
+
Printf.printf "✓ %s passed\n\n%!" filename
+
+
let test_params_decoding ~fs =
+
Printf.printf "Testing params record content decoding...\n";
+
+
let parsed =
+
Eio.Path.with_open_in (fs, "test_cases/params_get.bin") @@ fun flow ->
+
let buf_read = Eio.Buf_read.of_flow flow ~max_size:1000000 in
+
Record.read buf_read
+
in
+
+
Printf.printf "\nParsed params record:\n";
+
Format.printf "%a\n" (fun ppf -> Record.pp ppf) parsed;
+
+
(* Decode the params content *)
+
let params = Record.KV.decode parsed.content in
+
+
Printf.printf "\nDecoded parameters:\n";
+
Format.printf "%a\n" Record.KV.pp params;
+
+
(* Check some expected environment variables *)
+
assert (Record.KV.find "REQUEST_METHOD" params = "GET");
+
assert (Record.KV.find "SERVER_NAME" params = "localhost");
+
assert (Record.KV.find "SERVER_PORT" params = "80");
+
+
Printf.printf "✓ params decoding passed\n\n%!"
+
+
let test_large_record ~fs =
+
Printf.printf "Testing large record...\n";
+
+
let parsed =
+
Eio.Path.with_open_in (fs, "test_cases/large_record.bin") @@ fun flow ->
+
let buf_read = Eio.Buf_read.of_flow flow ~max_size:1000000 in
+
Record.read buf_read
+
in
+
+
Printf.printf "\nParsed large record:\n";
+
Format.printf "%a\n" (fun ppf -> Record.pp ppf) parsed;
+
+
assert (parsed.version = 1);
+
assert (parsed.record_type = Stdout);
+
assert (parsed.request_id = 1);
+
assert (String.length parsed.content = 65000);
+
+
Printf.printf "✓ large record test passed\n\n%!"
+
+
let test_padded_record ~fs =
+
Printf.printf "Testing padded record...\n";
+
+
let parsed =
+
Eio.Path.with_open_in (fs, "test_cases/padded_record.bin") @@ fun flow ->
+
let buf_read = Eio.Buf_read.of_flow flow ~max_size:1000000 in
+
Record.read buf_read
+
in
+
+
Printf.printf "\nParsed padded record:\n";
+
Format.printf "%a\n" (fun ppf -> Record.pp ppf) parsed;
+
+
assert (parsed.version = 1);
+
assert (parsed.record_type = Stdout);
+
assert (parsed.request_id = 1);
+
assert (parsed.content = "Hello");
+
+
Printf.printf "✓ padded record test passed\n\n%!"
+
+
let test_multiplexed_records ~fs =
+
Printf.printf "Testing multiplexed records...\n";
+
+
let records =
+
Eio.Path.with_open_in (fs, "test_cases/multiplexed_requests.bin") @@ fun flow ->
+
let buf_read = Eio.Buf_read.of_flow flow ~max_size:1000000 in
+
let records = ref [] in
+
+
(* Read all records from the multiplexed stream *)
+
(try
+
while true do
+
let record = Record.read buf_read in
+
records := record :: !records
+
done
+
with End_of_file -> ());
+
!records
+
in
+
+
let records = List.rev records in
+
+
Printf.printf "\nParsed %d multiplexed records:\n" (List.length records);
+
List.iteri (fun i record ->
+
Printf.printf "\nRecord %d:\n" (i + 1);
+
Format.printf "%a\n" (fun ppf -> Record.pp ppf) record
+
) records;
+
+
(* Should have multiple records with different request IDs *)
+
assert (List.length records > 5);
+
+
(* Check that we have records for both request ID 1 and 2 *)
+
let request_ids = List.map (fun r -> r.Record.request_id) records in
+
assert (List.mem 1 request_ids);
+
assert (List.mem 2 request_ids);
+
+
Printf.printf "✓ multiplexed records test passed\n\n%!"
+
+
let run_all_tests ~fs =
+
Printf.printf "Validating all FastCGI test case files...\n\n%!";
+
+
(* Test individual files *)
+
List.iter (fun (filename, expected_type, expected_request_id) ->
+
test_binary_file ~fs filename expected_type expected_request_id
+
) test_cases;
+
+
Printf.printf "\nTesting specific content decoding...\n%!";
+
test_params_decoding ~fs;
+
test_large_record ~fs;
+
test_padded_record ~fs;
+
test_multiplexed_records ~fs;
+
+
Printf.printf "\n✅ All %d test case files validated successfully!\n%!" (List.length test_cases);
+
Printf.printf "✅ FastCGI Record implementation is working correctly!\n%!"
+
+
let () = Eio_main.run @@ fun env ->
+
let fs = Eio.Stdenv.cwd env in
+
run_all_tests ~fs:(fst fs)
-123
test_cases/README.md
···
-
# FastCGI Test Cases
-
-
This directory contains binary test case files representing various FastCGI records as defined in the FastCGI specification. These files can be used to test a FastCGI parser implementation.
-
-
## Test Case Files
-
-
### Management Records (requestId = 0)
-
-
- **`get_values.bin`** - FCGI_GET_VALUES record requesting capability information
-
- **`get_values_result.bin`** - FCGI_GET_VALUES_RESULT record with capability responses
-
- **`unknown_type.bin`** - FCGI_UNKNOWN_TYPE record for unrecognized record types
-
-
### Application Records (requestId > 0)
-
-
#### BEGIN_REQUEST Records
-
- **`begin_request_responder.bin`** - Begin request for Responder role with KEEP_CONN flag
-
- **`begin_request_no_keep.bin`** - Begin request for Responder role without KEEP_CONN flag
-
- **`begin_request_authorizer.bin`** - Begin request for Authorizer role
-
- **`begin_request_filter.bin`** - Begin request for Filter role
-
-
#### Stream Records
-
- **`params_get.bin`** - FCGI_PARAMS record with GET request environment variables
-
- **`params_post.bin`** - FCGI_PARAMS record with POST request environment variables
-
- **`params_empty.bin`** - Empty FCGI_PARAMS record (end of params stream)
-
- **`stdin_form_data.bin`** - FCGI_STDIN record with form data
-
- **`stdin_empty.bin`** - Empty FCGI_STDIN record (end of stdin stream)
-
- **`stdout_response.bin`** - FCGI_STDOUT record with HTTP response
-
- **`stdout_empty.bin`** - Empty FCGI_STDOUT record (end of stdout stream)
-
- **`stderr_message.bin`** - FCGI_STDERR record with error message
-
- **`stderr_empty.bin`** - Empty FCGI_STDERR record (end of stderr stream)
-
- **`data_filter.bin`** - FCGI_DATA record with file content (for Filter role)
-
- **`data_empty.bin`** - Empty FCGI_DATA record (end of data stream)
-
-
#### Control Records
-
- **`end_request_success.bin`** - FCGI_END_REQUEST record with successful completion
-
- **`end_request_error.bin`** - FCGI_END_REQUEST record with error status
-
- **`abort_request.bin`** - FCGI_ABORT_REQUEST record
-
-
### Complex Scenarios
-
- **`multiplexed_requests.bin`** - Multiple concurrent requests on same connection
-
- **`large_record.bin`** - Record with maximum content size (65KB)
-
- **`padded_record.bin`** - Record with padding for 8-byte alignment
-
-
## Record Format
-
-
All records follow the FastCGI record format:
-
-
```
-
typedef struct {
-
unsigned char version; // FCGI_VERSION_1 (1)
-
unsigned char type; // Record type (1-11)
-
unsigned char requestIdB1; // Request ID high byte
-
unsigned char requestIdB0; // Request ID low byte
-
unsigned char contentLengthB1; // Content length high byte
-
unsigned char contentLengthB0; // Content length low byte
-
unsigned char paddingLength; // Padding length
-
unsigned char reserved; // Reserved (always 0)
-
// contentData[contentLength]
-
// paddingData[paddingLength]
-
} FCGI_Record;
-
```
-
-
## Usage for Parser Testing
-
-
These test cases can be used to verify:
-
-
1. **Header parsing** - Correct extraction of version, type, requestId, lengths
-
2. **Content parsing** - Proper handling of record body data
-
3. **Stream handling** - Recognition of stream start/end patterns
-
4. **Name-value pair parsing** - Decoding of FCGI_PARAMS format
-
5. **Role-specific parsing** - Different record sequences for each role
-
6. **Error handling** - Response to unknown types, malformed data
-
7. **Multiplexing** - Handling multiple concurrent request IDs
-
8. **Padding** - Correct skipping of padding bytes
-
-
## Typical Protocol Flows
-
-
### Simple Responder Request
-
1. `begin_request_responder.bin`
-
2. `params_get.bin`
-
3. `params_empty.bin`
-
4. `stdin_empty.bin`
-
5. `stdout_response.bin`
-
6. `stdout_empty.bin`
-
7. `end_request_success.bin`
-
-
### POST Request with Form Data
-
1. `begin_request_responder.bin`
-
2. `params_post.bin`
-
3. `params_empty.bin`
-
4. `stdin_form_data.bin`
-
5. `stdin_empty.bin`
-
6. `stdout_response.bin`
-
7. `stdout_empty.bin`
-
8. `end_request_success.bin`
-
-
### Authorizer Request
-
1. `begin_request_authorizer.bin`
-
2. `params_get.bin`
-
3. `params_empty.bin`
-
4. `stdout_response.bin`
-
5. `stdout_empty.bin`
-
6. `end_request_success.bin`
-
-
### Filter Request
-
1. `begin_request_filter.bin`
-
2. `params_get.bin`
-
3. `params_empty.bin`
-
4. `stdin_empty.bin`
-
5. `data_filter.bin`
-
6. `data_empty.bin`
-
7. `stdout_response.bin`
-
8. `stdout_empty.bin`
-
9. `end_request_success.bin`
-
-
## Binary Format Verification
-
-
You can inspect the binary content using hexdump:
-
```bash
-
hexdump -C begin_request_responder.bin
-
```
-
-
The first 8 bytes should always be the FastCGI header, followed by the record content and any padding.
test_cases/abort_request.bin

This is a binary file and will not be displayed.

test_cases/begin_request_authorizer.bin

This is a binary file and will not be displayed.

test_cases/begin_request_filter.bin

This is a binary file and will not be displayed.

test_cases/begin_request_no_keep.bin

This is a binary file and will not be displayed.

test_cases/begin_request_responder.bin

This is a binary file and will not be displayed.

test_cases/data_empty.bin

This is a binary file and will not be displayed.

test_cases/data_filter.bin

This is a binary file and will not be displayed.

test_cases/end_request_error.bin

This is a binary file and will not be displayed.

test_cases/end_request_success.bin

This is a binary file and will not be displayed.

-349
test_cases/generate_test_cases.py
···
-
#!/usr/bin/env python3
-
"""
-
Generate FastCGI test case files based on the specification.
-
"""
-
import struct
-
import os
-
-
# FastCGI constants from the specification
-
FCGI_VERSION_1 = 1
-
-
# Record types
-
FCGI_BEGIN_REQUEST = 1
-
FCGI_ABORT_REQUEST = 2
-
FCGI_END_REQUEST = 3
-
FCGI_PARAMS = 4
-
FCGI_STDIN = 5
-
FCGI_STDOUT = 6
-
FCGI_STDERR = 7
-
FCGI_DATA = 8
-
FCGI_GET_VALUES = 9
-
FCGI_GET_VALUES_RESULT = 10
-
FCGI_UNKNOWN_TYPE = 11
-
-
# Roles
-
FCGI_RESPONDER = 1
-
FCGI_AUTHORIZER = 2
-
FCGI_FILTER = 3
-
-
# Flags
-
FCGI_KEEP_CONN = 1
-
-
# Protocol status
-
FCGI_REQUEST_COMPLETE = 0
-
FCGI_CANT_MPX_CONN = 1
-
FCGI_OVERLOADED = 2
-
FCGI_UNKNOWN_ROLE = 3
-
-
def create_record_header(version, record_type, request_id, content_length, padding_length=0):
-
"""Create a FastCGI record header."""
-
return struct.pack('>BBHHBB',
-
version,
-
record_type,
-
request_id,
-
content_length,
-
padding_length,
-
0) # reserved
-
-
def encode_name_value_pair(name, value):
-
"""Encode a name-value pair according to FastCGI spec."""
-
name_bytes = name.encode('utf-8')
-
value_bytes = value.encode('utf-8')
-
-
name_len = len(name_bytes)
-
value_len = len(value_bytes)
-
-
# Encode lengths
-
if name_len < 128:
-
name_len_encoded = struct.pack('B', name_len)
-
else:
-
name_len_encoded = struct.pack('>I', name_len | 0x80000000)
-
-
if value_len < 128:
-
value_len_encoded = struct.pack('B', value_len)
-
else:
-
value_len_encoded = struct.pack('>I', value_len | 0x80000000)
-
-
return name_len_encoded + value_len_encoded + name_bytes + value_bytes
-
-
def create_begin_request():
-
"""Create FCGI_BEGIN_REQUEST record."""
-
# FCGI_BeginRequestBody
-
body = struct.pack('>HB5x', FCGI_RESPONDER, FCGI_KEEP_CONN)
-
header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 1, len(body))
-
return header + body
-
-
def create_begin_request_no_keep_conn():
-
"""Create FCGI_BEGIN_REQUEST record without keep connection."""
-
body = struct.pack('>HB5x', FCGI_RESPONDER, 0)
-
header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 1, len(body))
-
return header + body
-
-
def create_begin_request_authorizer():
-
"""Create FCGI_BEGIN_REQUEST record for authorizer role."""
-
body = struct.pack('>HB5x', FCGI_AUTHORIZER, 0)
-
header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 2, len(body))
-
return header + body
-
-
def create_begin_request_filter():
-
"""Create FCGI_BEGIN_REQUEST record for filter role."""
-
body = struct.pack('>HB5x', FCGI_FILTER, FCGI_KEEP_CONN)
-
header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 3, len(body))
-
return header + body
-
-
def create_end_request():
-
"""Create FCGI_END_REQUEST record."""
-
# FCGI_EndRequestBody
-
body = struct.pack('>IB3x', 0, FCGI_REQUEST_COMPLETE) # app_status=0, protocol_status=COMPLETE
-
header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 1, len(body))
-
return header + body
-
-
def create_end_request_error():
-
"""Create FCGI_END_REQUEST record with error status."""
-
body = struct.pack('>IB3x', 1, FCGI_REQUEST_COMPLETE) # app_status=1 (error)
-
header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 1, len(body))
-
return header + body
-
-
def create_params_record():
-
"""Create FCGI_PARAMS record with CGI environment variables."""
-
# Typical CGI parameters
-
params = [
-
("REQUEST_METHOD", "GET"),
-
("SCRIPT_NAME", "/test.cgi"),
-
("REQUEST_URI", "/test.cgi?foo=bar"),
-
("QUERY_STRING", "foo=bar"),
-
("SERVER_NAME", "localhost"),
-
("SERVER_PORT", "80"),
-
("HTTP_HOST", "localhost"),
-
("HTTP_USER_AGENT", "Mozilla/5.0"),
-
("CONTENT_TYPE", ""),
-
("CONTENT_LENGTH", "0")
-
]
-
-
body = b''
-
for name, value in params:
-
body += encode_name_value_pair(name, value)
-
-
header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 1, len(body))
-
return header + body
-
-
def create_params_record_post():
-
"""Create FCGI_PARAMS record for POST request."""
-
params = [
-
("REQUEST_METHOD", "POST"),
-
("SCRIPT_NAME", "/form.cgi"),
-
("REQUEST_URI", "/form.cgi"),
-
("QUERY_STRING", ""),
-
("SERVER_NAME", "localhost"),
-
("SERVER_PORT", "443"),
-
("HTTPS", "on"),
-
("HTTP_HOST", "localhost"),
-
("HTTP_USER_AGENT", "curl/7.68.0"),
-
("CONTENT_TYPE", "application/x-www-form-urlencoded"),
-
("CONTENT_LENGTH", "23")
-
]
-
-
body = b''
-
for name, value in params:
-
body += encode_name_value_pair(name, value)
-
-
header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 1, len(body))
-
return header + body
-
-
def create_empty_params():
-
"""Create empty FCGI_PARAMS record (end of params stream)."""
-
header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 1, 0)
-
return header
-
-
def create_stdin_record():
-
"""Create FCGI_STDIN record with form data."""
-
data = b"name=John&email=john@example.com"
-
header = create_record_header(FCGI_VERSION_1, FCGI_STDIN, 1, len(data))
-
return header + data
-
-
def create_empty_stdin():
-
"""Create empty FCGI_STDIN record (end of stdin stream)."""
-
header = create_record_header(FCGI_VERSION_1, FCGI_STDIN, 1, 0)
-
return header
-
-
def create_stdout_record():
-
"""Create FCGI_STDOUT record with HTTP response."""
-
response = b"Content-Type: text/html\r\n\r\n<html><body>Hello World</body></html>"
-
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(response))
-
return header + response
-
-
def create_empty_stdout():
-
"""Create empty FCGI_STDOUT record (end of stdout stream)."""
-
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, 0)
-
return header
-
-
def create_stderr_record():
-
"""Create FCGI_STDERR record with error message."""
-
error_msg = b"Warning: Configuration file not found\n"
-
header = create_record_header(FCGI_VERSION_1, FCGI_STDERR, 1, len(error_msg))
-
return header + error_msg
-
-
def create_empty_stderr():
-
"""Create empty FCGI_STDERR record (end of stderr stream)."""
-
header = create_record_header(FCGI_VERSION_1, FCGI_STDERR, 1, 0)
-
return header
-
-
def create_get_values():
-
"""Create FCGI_GET_VALUES record."""
-
# Request standard capability variables
-
body = (encode_name_value_pair("FCGI_MAX_CONNS", "") +
-
encode_name_value_pair("FCGI_MAX_REQS", "") +
-
encode_name_value_pair("FCGI_MPXS_CONNS", ""))
-
header = create_record_header(FCGI_VERSION_1, FCGI_GET_VALUES, 0, len(body))
-
return header + body
-
-
def create_get_values_result():
-
"""Create FCGI_GET_VALUES_RESULT record."""
-
body = (encode_name_value_pair("FCGI_MAX_CONNS", "1") +
-
encode_name_value_pair("FCGI_MAX_REQS", "1") +
-
encode_name_value_pair("FCGI_MPXS_CONNS", "0"))
-
header = create_record_header(FCGI_VERSION_1, FCGI_GET_VALUES_RESULT, 0, len(body))
-
return header + body
-
-
def create_unknown_type():
-
"""Create FCGI_UNKNOWN_TYPE record."""
-
# FCGI_UnknownTypeBody
-
body = struct.pack('B7x', 99) # unknown type 99
-
header = create_record_header(FCGI_VERSION_1, FCGI_UNKNOWN_TYPE, 0, len(body))
-
return header + body
-
-
def create_abort_request():
-
"""Create FCGI_ABORT_REQUEST record."""
-
header = create_record_header(FCGI_VERSION_1, FCGI_ABORT_REQUEST, 1, 0)
-
return header
-
-
def create_data_record():
-
"""Create FCGI_DATA record (for Filter role)."""
-
file_data = b"This is file content that needs to be filtered\nLine 2\nLine 3\n"
-
header = create_record_header(FCGI_VERSION_1, FCGI_DATA, 3, len(file_data))
-
return header + file_data
-
-
def create_empty_data():
-
"""Create empty FCGI_DATA record (end of data stream)."""
-
header = create_record_header(FCGI_VERSION_1, FCGI_DATA, 3, 0)
-
return header
-
-
def create_multiplexed_records():
-
"""Create a sequence showing multiplexed requests."""
-
records = []
-
-
# Request 1 begins
-
records.append(create_begin_request())
-
records.append(create_params_record())
-
records.append(create_empty_params())
-
-
# Request 2 begins (different request ID)
-
body = struct.pack('>HB5x', FCGI_RESPONDER, FCGI_KEEP_CONN)
-
header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 2, len(body))
-
records.append(header + body)
-
-
# Request 1 continues
-
records.append(create_empty_stdin())
-
-
# Request 2 params
-
params = [("REQUEST_METHOD", "POST"), ("SCRIPT_NAME", "/other.cgi")]
-
body = b''
-
for name, value in params:
-
body += encode_name_value_pair(name, value)
-
header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 2, len(body))
-
records.append(header + body)
-
-
# Request 2 empty params
-
header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 2, 0)
-
records.append(header)
-
-
# Request 2 completes first
-
response = b"Content-Type: text/plain\r\n\r\nRequest 2 done"
-
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 2, len(response))
-
records.append(header + response)
-
-
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 2, 0)
-
records.append(header)
-
-
body = struct.pack('>IB3x', 0, FCGI_REQUEST_COMPLETE)
-
header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 2, len(body))
-
records.append(header + body)
-
-
# Request 1 completes
-
response = b"Content-Type: text/html\r\n\r\n<html><body>Request 1 done</body></html>"
-
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(response))
-
records.append(header + response)
-
-
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, 0)
-
records.append(header)
-
-
body = struct.pack('>IB3x', 0, FCGI_REQUEST_COMPLETE)
-
header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 1, len(body))
-
records.append(header + body)
-
-
return b''.join(records)
-
-
def create_large_record():
-
"""Create a record with maximum content size."""
-
# Create a large response (just under 64KB)
-
large_content = b"x" * 65000
-
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(large_content))
-
return header + large_content
-
-
def create_padded_record():
-
"""Create a record with padding for alignment."""
-
data = b"Hello" # 5 bytes
-
padding_length = 3 # to align to 8-byte boundary (5 + 3 = 8)
-
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(data), padding_length)
-
padding = b'\x00' * padding_length
-
return header + data + padding
-
-
# Test case definitions
-
test_cases = {
-
"begin_request_responder.bin": create_begin_request,
-
"begin_request_no_keep.bin": create_begin_request_no_keep_conn,
-
"begin_request_authorizer.bin": create_begin_request_authorizer,
-
"begin_request_filter.bin": create_begin_request_filter,
-
"end_request_success.bin": create_end_request,
-
"end_request_error.bin": create_end_request_error,
-
"params_get.bin": create_params_record,
-
"params_post.bin": create_params_record_post,
-
"params_empty.bin": create_empty_params,
-
"stdin_form_data.bin": create_stdin_record,
-
"stdin_empty.bin": create_empty_stdin,
-
"stdout_response.bin": create_stdout_record,
-
"stdout_empty.bin": create_empty_stdout,
-
"stderr_message.bin": create_stderr_record,
-
"stderr_empty.bin": create_empty_stderr,
-
"get_values.bin": create_get_values,
-
"get_values_result.bin": create_get_values_result,
-
"unknown_type.bin": create_unknown_type,
-
"abort_request.bin": create_abort_request,
-
"data_filter.bin": create_data_record,
-
"data_empty.bin": create_empty_data,
-
"multiplexed_requests.bin": create_multiplexed_records,
-
"large_record.bin": create_large_record,
-
"padded_record.bin": create_padded_record,
-
}
-
-
if __name__ == "__main__":
-
for filename, creator in test_cases.items():
-
with open(filename, 'wb') as f:
-
f.write(creator())
-
print(f"Created {filename}")
-
-
print(f"\nGenerated {len(test_cases)} test case files")
-
print("\nTest case descriptions:")
-
print("- begin_request_*.bin: Various BEGIN_REQUEST records for different roles")
-
print("- end_request_*.bin: END_REQUEST records with different status codes")
-
print("- params_*.bin: PARAMS records with CGI environment variables")
-
print("- stdin_*.bin: STDIN records with request body data")
-
print("- stdout_*.bin: STDOUT records with response data")
-
print("- stderr_*.bin: STDERR records with error messages")
-
print("- get_values*.bin: Management records for capability negotiation")
-
print("- unknown_type.bin: Unknown record type handling")
-
print("- abort_request.bin: Request abortion")
-
print("- data_*.bin: DATA records for Filter role")
-
print("- multiplexed_requests.bin: Multiple concurrent requests")
-
print("- large_record.bin: Maximum size record")
-
print("- padded_record.bin: Record with padding for alignment")
test_cases/get_values.bin

This is a binary file and will not be displayed.

test_cases/get_values_result.bin

This is a binary file and will not be displayed.

test_cases/large_record.bin

This is a binary file and will not be displayed.

test_cases/multiplexed_requests.bin

This is a binary file and will not be displayed.

test_cases/padded_record.bin

This is a binary file and will not be displayed.

test_cases/params_empty.bin

This is a binary file and will not be displayed.

test_cases/params_get.bin

This is a binary file and will not be displayed.

test_cases/params_post.bin

This is a binary file and will not be displayed.

test_cases/stderr_empty.bin

This is a binary file and will not be displayed.

test_cases/stderr_message.bin

This is a binary file and will not be displayed.

test_cases/stdin_empty.bin

This is a binary file and will not be displayed.

test_cases/stdin_form_data.bin

This is a binary file and will not be displayed.

test_cases/stdout_empty.bin

This is a binary file and will not be displayed.

test_cases/stdout_response.bin

This is a binary file and will not be displayed.

-24
test_cases/test_case_sizes.txt
···
-
abort_request.bin 8 bytes
-
begin_request_authorizer.bin 16 bytes
-
begin_request_filter.bin 16 bytes
-
begin_request_no_keep.bin 16 bytes
-
begin_request_responder.bin 16 bytes
-
data_empty.bin 8 bytes
-
data_filter.bin 69 bytes
-
end_request_error.bin 16 bytes
-
end_request_success.bin 16 bytes
-
get_values_result.bin 59 bytes
-
get_values.bin 56 bytes
-
large_record.bin 65008 bytes
-
multiplexed_requests.bin 496 bytes
-
padded_record.bin 16 bytes
-
params_empty.bin 8 bytes
-
params_get.bin 216 bytes
-
params_post.bin 246 bytes
-
stderr_empty.bin 8 bytes
-
stderr_message.bin 46 bytes
-
stdin_empty.bin 8 bytes
-
stdin_form_data.bin 40 bytes
-
stdout_empty.bin 8 bytes
-
stdout_response.bin 72 bytes
-
unknown_type.bin 16 bytes
test_cases/unknown_type.bin

This is a binary file and will not be displayed.

-130
test_cases/validate_test_cases.py
···
-
#!/usr/bin/env python3
-
"""
-
Validate that the generated FastCGI test cases are properly formatted.
-
"""
-
import struct
-
import os
-
import glob
-
-
def parse_record_header(data):
-
"""Parse a FastCGI record header."""
-
if len(data) < 8:
-
return None
-
-
version, record_type, request_id, content_length, padding_length, reserved = struct.unpack('>BBHHBB', data[:8])
-
return {
-
'version': version,
-
'type': record_type,
-
'request_id': request_id,
-
'content_length': content_length,
-
'padding_length': padding_length,
-
'reserved': reserved,
-
'total_length': 8 + content_length + padding_length
-
}
-
-
def validate_file(filename):
-
"""Validate a single test case file."""
-
print(f"\nValidating {filename}:")
-
-
with open(filename, 'rb') as f:
-
data = f.read()
-
-
if len(data) < 8:
-
print(f" ❌ File too short: {len(data)} bytes")
-
return False
-
-
# Parse all records in the file
-
offset = 0
-
record_count = 0
-
-
while offset < len(data):
-
if offset + 8 > len(data):
-
print(f" ❌ Incomplete header at offset {offset}")
-
return False
-
-
header = parse_record_header(data[offset:])
-
if not header:
-
print(f" ❌ Failed to parse header at offset {offset}")
-
return False
-
-
record_count += 1
-
print(f" Record {record_count}:")
-
print(f" Version: {header['version']}")
-
print(f" Type: {header['type']}")
-
print(f" Request ID: {header['request_id']}")
-
print(f" Content Length: {header['content_length']}")
-
print(f" Padding Length: {header['padding_length']}")
-
print(f" Reserved: {header['reserved']}")
-
-
# Validate header fields
-
if header['version'] != 1:
-
print(f" ❌ Invalid version: {header['version']}")
-
return False
-
-
if header['type'] < 1 or header['type'] > 11:
-
print(f" ❌ Invalid record type: {header['type']}")
-
return False
-
-
if header['reserved'] != 0:
-
print(f" ❌ Reserved field not zero: {header['reserved']}")
-
return False
-
-
# Check if we have enough data for content and padding
-
expected_end = offset + header['total_length']
-
if expected_end > len(data):
-
print(f" ❌ Not enough data: need {header['total_length']}, have {len(data) - offset}")
-
return False
-
-
# Extract content
-
content_start = offset + 8
-
content_end = content_start + header['content_length']
-
content = data[content_start:content_end]
-
-
# Extract padding
-
padding_start = content_end
-
padding_end = padding_start + header['padding_length']
-
padding = data[padding_start:padding_end]
-
-
print(f" Content: {len(content)} bytes")
-
if header['padding_length'] > 0:
-
print(f" Padding: {len(padding)} bytes")
-
-
# Show content preview for small records
-
if len(content) <= 32:
-
print(f" Content hex: {content.hex()}")
-
else:
-
print(f" Content preview: {content[:16].hex()}...")
-
-
print(f" ✅ Record valid")
-
-
offset = expected_end
-
-
print(f" ✅ File valid: {record_count} record(s), {len(data)} total bytes")
-
return True
-
-
def main():
-
"""Validate all test case files."""
-
test_files = glob.glob("*.bin")
-
test_files.sort()
-
-
print(f"Found {len(test_files)} test case files")
-
-
valid_count = 0
-
for filename in test_files:
-
if validate_file(filename):
-
valid_count += 1
-
-
print(f"\n{'='*50}")
-
print(f"Validation complete: {valid_count}/{len(test_files)} files valid")
-
-
if valid_count == len(test_files):
-
print("✅ All test cases are valid!")
-
else:
-
print("❌ Some test cases failed validation")
-
return 1
-
-
return 0
-
-
if __name__ == "__main__":
-
import sys
-
sys.exit(main())
-438
test_cases/validation_results.txt
···
-
Found 24 test case files
-
-
Validating abort_request.bin:
-
Record 1:
-
Version: 1
-
Type: 2
-
Request ID: 1
-
Content Length: 0
-
Padding Length: 0
-
Reserved: 0
-
Content: 0 bytes
-
Content hex:
-
✅ Record valid
-
✅ File valid: 1 record(s), 8 total bytes
-
-
Validating begin_request_authorizer.bin:
-
Record 1:
-
Version: 1
-
Type: 1
-
Request ID: 2
-
Content Length: 8
-
Padding Length: 0
-
Reserved: 0
-
Content: 8 bytes
-
Content hex: 0002000000000000
-
✅ Record valid
-
✅ File valid: 1 record(s), 16 total bytes
-
-
Validating begin_request_filter.bin:
-
Record 1:
-
Version: 1
-
Type: 1
-
Request ID: 3
-
Content Length: 8
-
Padding Length: 0
-
Reserved: 0
-
Content: 8 bytes
-
Content hex: 0003010000000000
-
✅ Record valid
-
✅ File valid: 1 record(s), 16 total bytes
-
-
Validating begin_request_no_keep.bin:
-
Record 1:
-
Version: 1
-
Type: 1
-
Request ID: 1
-
Content Length: 8
-
Padding Length: 0
-
Reserved: 0
-
Content: 8 bytes
-
Content hex: 0001000000000000
-
✅ Record valid
-
✅ File valid: 1 record(s), 16 total bytes
-
-
Validating begin_request_responder.bin:
-
Record 1:
-
Version: 1
-
Type: 1
-
Request ID: 1
-
Content Length: 8
-
Padding Length: 0
-
Reserved: 0
-
Content: 8 bytes
-
Content hex: 0001010000000000
-
✅ Record valid
-
✅ File valid: 1 record(s), 16 total bytes
-
-
Validating data_empty.bin:
-
Record 1:
-
Version: 1
-
Type: 8
-
Request ID: 3
-
Content Length: 0
-
Padding Length: 0
-
Reserved: 0
-
Content: 0 bytes
-
Content hex:
-
✅ Record valid
-
✅ File valid: 1 record(s), 8 total bytes
-
-
Validating data_filter.bin:
-
Record 1:
-
Version: 1
-
Type: 8
-
Request ID: 3
-
Content Length: 61
-
Padding Length: 0
-
Reserved: 0
-
Content: 61 bytes
-
Content preview: 546869732069732066696c6520636f6e...
-
✅ Record valid
-
✅ File valid: 1 record(s), 69 total bytes
-
-
Validating end_request_error.bin:
-
Record 1:
-
Version: 1
-
Type: 3
-
Request ID: 1
-
Content Length: 8
-
Padding Length: 0
-
Reserved: 0
-
Content: 8 bytes
-
Content hex: 0000000100000000
-
✅ Record valid
-
✅ File valid: 1 record(s), 16 total bytes
-
-
Validating end_request_success.bin:
-
Record 1:
-
Version: 1
-
Type: 3
-
Request ID: 1
-
Content Length: 8
-
Padding Length: 0
-
Reserved: 0
-
Content: 8 bytes
-
Content hex: 0000000000000000
-
✅ Record valid
-
✅ File valid: 1 record(s), 16 total bytes
-
-
Validating get_values.bin:
-
Record 1:
-
Version: 1
-
Type: 9
-
Request ID: 0
-
Content Length: 48
-
Padding Length: 0
-
Reserved: 0
-
Content: 48 bytes
-
Content preview: 0e00464347495f4d41585f434f4e4e53...
-
✅ Record valid
-
✅ File valid: 1 record(s), 56 total bytes
-
-
Validating get_values_result.bin:
-
Record 1:
-
Version: 1
-
Type: 10
-
Request ID: 0
-
Content Length: 51
-
Padding Length: 0
-
Reserved: 0
-
Content: 51 bytes
-
Content preview: 0e01464347495f4d41585f434f4e4e53...
-
✅ Record valid
-
✅ File valid: 1 record(s), 59 total bytes
-
-
Validating large_record.bin:
-
Record 1:
-
Version: 1
-
Type: 6
-
Request ID: 1
-
Content Length: 65000
-
Padding Length: 0
-
Reserved: 0
-
Content: 65000 bytes
-
Content preview: 78787878787878787878787878787878...
-
✅ Record valid
-
✅ File valid: 1 record(s), 65008 total bytes
-
-
Validating multiplexed_requests.bin:
-
Record 1:
-
Version: 1
-
Type: 1
-
Request ID: 1
-
Content Length: 8
-
Padding Length: 0
-
Reserved: 0
-
Content: 8 bytes
-
Content hex: 0001010000000000
-
✅ Record valid
-
Record 2:
-
Version: 1
-
Type: 4
-
Request ID: 1
-
Content Length: 208
-
Padding Length: 0
-
Reserved: 0
-
Content: 208 bytes
-
Content preview: 0e03524551554553545f4d4554484f44...
-
✅ Record valid
-
Record 3:
-
Version: 1
-
Type: 4
-
Request ID: 1
-
Content Length: 0
-
Padding Length: 0
-
Reserved: 0
-
Content: 0 bytes
-
Content hex:
-
✅ Record valid
-
Record 4:
-
Version: 1
-
Type: 1
-
Request ID: 2
-
Content Length: 8
-
Padding Length: 0
-
Reserved: 0
-
Content: 8 bytes
-
Content hex: 0001010000000000
-
✅ Record valid
-
Record 5:
-
Version: 1
-
Type: 5
-
Request ID: 1
-
Content Length: 0
-
Padding Length: 0
-
Reserved: 0
-
Content: 0 bytes
-
Content hex:
-
✅ Record valid
-
Record 6:
-
Version: 1
-
Type: 4
-
Request ID: 2
-
Content Length: 43
-
Padding Length: 0
-
Reserved: 0
-
Content: 43 bytes
-
Content preview: 0e04524551554553545f4d4554484f44...
-
✅ Record valid
-
Record 7:
-
Version: 1
-
Type: 4
-
Request ID: 2
-
Content Length: 0
-
Padding Length: 0
-
Reserved: 0
-
Content: 0 bytes
-
Content hex:
-
✅ Record valid
-
Record 8:
-
Version: 1
-
Type: 6
-
Request ID: 2
-
Content Length: 42
-
Padding Length: 0
-
Reserved: 0
-
Content: 42 bytes
-
Content preview: 436f6e74656e742d547970653a207465...
-
✅ Record valid
-
Record 9:
-
Version: 1
-
Type: 6
-
Request ID: 2
-
Content Length: 0
-
Padding Length: 0
-
Reserved: 0
-
Content: 0 bytes
-
Content hex:
-
✅ Record valid
-
Record 10:
-
Version: 1
-
Type: 3
-
Request ID: 2
-
Content Length: 8
-
Padding Length: 0
-
Reserved: 0
-
Content: 8 bytes
-
Content hex: 0000000000000000
-
✅ Record valid
-
Record 11:
-
Version: 1
-
Type: 6
-
Request ID: 1
-
Content Length: 67
-
Padding Length: 0
-
Reserved: 0
-
Content: 67 bytes
-
Content preview: 436f6e74656e742d547970653a207465...
-
✅ Record valid
-
Record 12:
-
Version: 1
-
Type: 6
-
Request ID: 1
-
Content Length: 0
-
Padding Length: 0
-
Reserved: 0
-
Content: 0 bytes
-
Content hex:
-
✅ Record valid
-
Record 13:
-
Version: 1
-
Type: 3
-
Request ID: 1
-
Content Length: 8
-
Padding Length: 0
-
Reserved: 0
-
Content: 8 bytes
-
Content hex: 0000000000000000
-
✅ Record valid
-
✅ File valid: 13 record(s), 496 total bytes
-
-
Validating padded_record.bin:
-
Record 1:
-
Version: 1
-
Type: 6
-
Request ID: 1
-
Content Length: 5
-
Padding Length: 3
-
Reserved: 0
-
Content: 5 bytes
-
Padding: 3 bytes
-
Content hex: 48656c6c6f
-
✅ Record valid
-
✅ File valid: 1 record(s), 16 total bytes
-
-
Validating params_empty.bin:
-
Record 1:
-
Version: 1
-
Type: 4
-
Request ID: 1
-
Content Length: 0
-
Padding Length: 0
-
Reserved: 0
-
Content: 0 bytes
-
Content hex:
-
✅ Record valid
-
✅ File valid: 1 record(s), 8 total bytes
-
-
Validating params_get.bin:
-
Record 1:
-
Version: 1
-
Type: 4
-
Request ID: 1
-
Content Length: 208
-
Padding Length: 0
-
Reserved: 0
-
Content: 208 bytes
-
Content preview: 0e03524551554553545f4d4554484f44...
-
✅ Record valid
-
✅ File valid: 1 record(s), 216 total bytes
-
-
Validating params_post.bin:
-
Record 1:
-
Version: 1
-
Type: 4
-
Request ID: 1
-
Content Length: 238
-
Padding Length: 0
-
Reserved: 0
-
Content: 238 bytes
-
Content preview: 0e04524551554553545f4d4554484f44...
-
✅ Record valid
-
✅ File valid: 1 record(s), 246 total bytes
-
-
Validating stderr_empty.bin:
-
Record 1:
-
Version: 1
-
Type: 7
-
Request ID: 1
-
Content Length: 0
-
Padding Length: 0
-
Reserved: 0
-
Content: 0 bytes
-
Content hex:
-
✅ Record valid
-
✅ File valid: 1 record(s), 8 total bytes
-
-
Validating stderr_message.bin:
-
Record 1:
-
Version: 1
-
Type: 7
-
Request ID: 1
-
Content Length: 38
-
Padding Length: 0
-
Reserved: 0
-
Content: 38 bytes
-
Content preview: 5761726e696e673a20436f6e66696775...
-
✅ Record valid
-
✅ File valid: 1 record(s), 46 total bytes
-
-
Validating stdin_empty.bin:
-
Record 1:
-
Version: 1
-
Type: 5
-
Request ID: 1
-
Content Length: 0
-
Padding Length: 0
-
Reserved: 0
-
Content: 0 bytes
-
Content hex:
-
✅ Record valid
-
✅ File valid: 1 record(s), 8 total bytes
-
-
Validating stdin_form_data.bin:
-
Record 1:
-
Version: 1
-
Type: 5
-
Request ID: 1
-
Content Length: 32
-
Padding Length: 0
-
Reserved: 0
-
Content: 32 bytes
-
Content hex: 6e616d653d4a6f686e26656d61696c3d6a6f686e406578616d706c652e636f6d
-
✅ Record valid
-
✅ File valid: 1 record(s), 40 total bytes
-
-
Validating stdout_empty.bin:
-
Record 1:
-
Version: 1
-
Type: 6
-
Request ID: 1
-
Content Length: 0
-
Padding Length: 0
-
Reserved: 0
-
Content: 0 bytes
-
Content hex:
-
✅ Record valid
-
✅ File valid: 1 record(s), 8 total bytes
-
-
Validating stdout_response.bin:
-
Record 1:
-
Version: 1
-
Type: 6
-
Request ID: 1
-
Content Length: 64
-
Padding Length: 0
-
Reserved: 0
-
Content: 64 bytes
-
Content preview: 436f6e74656e742d547970653a207465...
-
✅ Record valid
-
✅ File valid: 1 record(s), 72 total bytes
-
-
Validating unknown_type.bin:
-
Record 1:
-
Version: 1
-
Type: 11
-
Request ID: 0
-
Content Length: 8
-
Padding Length: 0
-
Reserved: 0
-
Content: 8 bytes
-
Content hex: 6300000000000000
-
✅ Record valid
-
✅ File valid: 1 record(s), 16 total bytes
-
-
==================================================
-
Validation complete: 24/24 files valid
-
✅ All test cases are valid!