FastCGI implementation in OCaml

add claude generated spec

Changed files
+708 -2
spec
+96 -2
CLAUDE.md
···
-
This branch exists to figure out an OCaml specification for the FastCGI interface.
+
This repository exists to implement an OCaml specification for the FastCGI interface.
The spec for the FastCGI protocol is in spec/FastCGI_Specification.html.
The Go lang implementation of FastCGI is in spec/fcgi.go.
+
An OCaml specification exists in spec/OCAML.md.
-
Both of these are intended to act as a reference implementation, for us to figure out what the ideal OCaml interface should look like for FastCGI.
+
These are intended to act as a reference implementation for us to figure out what the ideal OCaml interface should look like for FastCGI.
Our target language is OCaml, using the Eio library. The README for Eio is in OCaml-EIO-README.md to give you a reference.
+
The parsing and serialisation should be done using Eio's Buf_read and Buf_write modules.
+
+
You can run commands with:
+
+
- clean: `opam exec -- dune clean`
+
- build: `opam exec -- dune build @check`
+
- docs: `opam exec -- dune build @doc`
+
- build while ignoring warnings: add `--profile=release` to the CLI to activate the profile that ignores warnings
+
+
# Tips on fixing bugs
+
+
If you see errors like this:
+
+
```
+
File "../../.jmap.objs/byte/jmap.odoc":
+
Warning: Hidden fields in type 'Jmap.Email.Identity.identity_create'
+
```
+
+
Then examine the HTML docs built for that module. You will see that there are module references with __ in them, e.g. "Jmap__.Jmap_email_types.Email_address.t" which indicate that the module is being accessed directly instead of via the module aliases defined.
+
+
## Documentation Comments
+
+
When adding OCaml documentation comments, be careful about ambiguous documentation comments. If you see errors like:
+
+
```
+
Error (warning 50 [unexpected-docstring]): ambiguous documentation comment
+
```
+
+
This usually means there isn't enough whitespace between the documentation comment and the code element it's documenting. Always:
+
+
1. Add blank lines between consecutive documentation comments
+
2. Add a blank line before a documentation comment for a module/type/value declaration
+
3. When documenting record fields or variant constructors, place the comment after the field with at least one space
+
+
Example of correct documentation spacing:
+
+
```ocaml
+
(** Module documentation. *)
+
+
(** Value documentation. *)
+
val some_value : int
+
+
(** Type documentation. *)
+
type t =
+
| First (** First constructor *)
+
| Second (** Second constructor *)
+
+
(** Record documentation. *)
+
type record = {
+
field1 : int; (** Field1 documentation *)
+
field2 : string (** Field2 documentation *)
+
}
+
```
+
+
If in doubt, add more whitespace lines than needed - you can always clean this up later with `dune build @fmt` to get ocamlformat to sort out the whitespace properly.
+
+
# Module Structure Guidelines
+
+
IMPORTANT: For all modules, use a nested module structure with a canonical `type t` inside each submodule. This approach ensures consistent type naming and logical grouping of related functionality.
+
+
1. Top-level files should define their main types directly (e.g., `jmap_identity.mli` should define identity-related types at the top level).
+
+
2. Related operations or specialized subtypes should be defined in nested modules within the file:
+
```ocaml
+
module Create : sig
+
type t (* NOT 'type create' or any other name *)
+
(* Functions operating on creation requests *)
+
+
module Response : sig
+
type t
+
(* Functions for creation responses *)
+
end
+
end
+
```
+
+
3. Consistently use `type t` for the main type in each module and submodule.
+
+
4. Functions operating on a type should be placed in the same module as the type.
+
+
5. When a file is named after a concept (e.g., `jmap_identity.mli`), there's no need to have a matching nested module inside the file (e.g., `module Identity : sig...`), as the file itself represents that namespace.
+
+
This structured approach promotes encapsulation, consistent type naming, and clearer organization of related functionality.
+
+
# Software engineering
+
+
We will go through a multi step process to build this library. We are currently at STEP 1.
+
+
1) we will generate OCaml interface files only, and no module implementations. The purpose here is to write and document the necessary type signatures. Once we generate these, we can check that they work with "dune build @check". Once that succeeds, we will build HTML documentation with "dune build @doc" in order to ensure the interfaces are reasonable.
+
+
2) once these interface files exist, we will build a series of sample binaries that will attempt to implement the library for some sample usecases. This binary will not fully link, but it should type check. The only linking error that we get should be from the missing library implementation that we are currently buildign.
+
+
3) we will calculate the dependency order for each module in our library, and work through an implementation of each one in increasing dependency order (that is, the module with the fewest dependencies should be handled first). For each module interface, we will generate a corresponding module implementation. We will also add test cases for this specific module, and update the dune files. Before proceeding to the next module, a build should be done to ensure the implementation builds and type checks as far as is possible.
+
+612
spec/OCAML.md
···
+
# OCaml FastCGI Specification
+
+
This document specifies an OCaml interface for the FastCGI protocol using the Eio effects-based IO library. The design follows Eio conventions for structured concurrency, capability-based security, and resource management.
+
+
## Table of Contents
+
+
1. [Overview](#overview)
+
2. [Core Types](#core-types)
+
3. [Protocol Constants](#protocol-constants)
+
4. [Record Types](#record-types)
+
5. [Request/Response Model](#requestresponse-model)
+
6. [Application Interface](#application-interface)
+
7. [Connection Management](#connection-management)
+
8. [Role Implementations](#role-implementations)
+
9. [Error Handling](#error-handling)
+
10. [Resource Management](#resource-management)
+
11. [Examples](#examples)
+
+
## Overview
+
+
The OCaml FastCGI implementation provides a high-level, type-safe interface for building FastCGI applications that can handle multiple concurrent requests efficiently. It leverages OCaml 5's effects system through Eio for structured concurrency and resource safety.
+
+
### Key Design Principles
+
+
- **Capability-based**: All external resources (network, filesystem) are explicitly passed as capabilities
+
- **Structured concurrency**: Use Eio switches for automatic resource cleanup
+
- **Type safety**: Leverage OCaml's type system to prevent protocol errors
+
- **Performance**: Support connection multiplexing and keep-alive for efficiency
+
- **Extensibility**: Support all three FastCGI roles with room for future extensions
+
+
## Core Types
+
+
```ocaml
+
(** FastCGI protocol version *)
+
type version = int
+
+
(** FastCGI record types *)
+
type record_type =
+
| Begin_request
+
| Abort_request
+
| End_request
+
| Params
+
| Stdin
+
| Stdout
+
| Stderr
+
| Data
+
| Get_values
+
| Get_values_result
+
| Unknown_type
+
+
(** FastCGI roles *)
+
type role =
+
| Responder (** Handle HTTP requests and generate responses *)
+
| Authorizer (** Perform authorization decisions *)
+
| Filter (** Process data streams with filtering *)
+
+
(** Request ID for multiplexing *)
+
type request_id = int
+
+
(** Application status code *)
+
type app_status = int
+
+
(** Protocol status codes *)
+
type protocol_status =
+
| Request_complete (** Normal completion *)
+
| Cant_mpx_conn (** Cannot multiplex connection *)
+
| Overloaded (** Application overloaded *)
+
| Unknown_role (** Unknown role requested *)
+
+
(** Connection flags *)
+
type connection_flags = {
+
keep_conn : bool; (** Keep connection open after request *)
+
}
+
```
+
+
## Protocol Constants
+
+
```ocaml
+
module Constants = struct
+
(** Protocol version *)
+
let version_1 = 1
+
+
(** Standard file descriptor for FastCGI listening socket *)
+
let listensock_fileno = 0
+
+
(** Record type constants *)
+
let fcgi_begin_request = 1
+
let fcgi_abort_request = 2
+
let fcgi_end_request = 3
+
let fcgi_params = 4
+
let fcgi_stdin = 5
+
let fcgi_stdout = 6
+
let fcgi_stderr = 7
+
let fcgi_data = 8
+
let fcgi_get_values = 9
+
let fcgi_get_values_result = 10
+
let fcgi_unknown_type = 11
+
+
(** Role constants *)
+
let fcgi_responder = 1
+
let fcgi_authorizer = 2
+
let fcgi_filter = 3
+
+
(** Flag constants *)
+
let fcgi_keep_conn = 1
+
+
(** Maximum sizes *)
+
let max_content_length = 65535
+
let max_padding_length = 255
+
let header_length = 8
+
+
(** Management record variables *)
+
let fcgi_max_conns = "FCGI_MAX_CONNS"
+
let fcgi_max_reqs = "FCGI_MAX_REQS"
+
let fcgi_mpxs_conns = "FCGI_MPXS_CONNS"
+
end
+
```
+
+
## Record Types
+
+
```ocaml
+
(** FastCGI record header *)
+
type record_header = {
+
version : int;
+
record_type : record_type;
+
request_id : request_id;
+
content_length : int;
+
padding_length : int;
+
}
+
+
(** Begin request record body *)
+
type begin_request_body = {
+
role : role;
+
flags : connection_flags;
+
}
+
+
(** End request record body *)
+
type end_request_body = {
+
app_status : app_status;
+
protocol_status : protocol_status;
+
}
+
+
(** Complete FastCGI record *)
+
type record = {
+
header : record_header;
+
content : bytes;
+
padding : bytes option;
+
}
+
+
(** Name-value pair for parameters *)
+
type name_value_pair = {
+
name : string;
+
value : string;
+
}
+
```
+
+
## Request/Response Model
+
+
```ocaml
+
(** Request context containing all information for a FastCGI request *)
+
type 'a request = {
+
request_id : request_id;
+
role : role;
+
flags : connection_flags;
+
params : (string * string) list;
+
stdin : 'a Eio.Flow.source;
+
data : 'a Eio.Flow.source option; (** Only for Filter role *)
+
}
+
+
(** Response builder for constructing FastCGI responses *)
+
type 'a response = {
+
stdout : 'a Eio.Flow.sink;
+
stderr : 'a Eio.Flow.sink;
+
}
+
+
(** Complete response with status *)
+
type response_result = {
+
app_status : app_status;
+
protocol_status : protocol_status;
+
}
+
```
+
+
## Application Interface
+
+
```ocaml
+
(** Application handler signature *)
+
module Handler = struct
+
(** Responder handler: process HTTP request and generate response *)
+
type 'a responder = 'a request -> 'a response -> response_result
+
+
(** Authorizer handler: make authorization decision *)
+
type 'a authorizer = 'a request -> 'a response -> response_result
+
+
(** Filter handler: process data stream with filtering *)
+
type 'a filter = 'a request -> 'a response -> response_result
+
+
(** Generic handler that can handle any role *)
+
type 'a handler =
+
| Responder of 'a responder
+
| Authorizer of 'a authorizer
+
| Filter of 'a filter
+
end
+
+
(** Application configuration *)
+
type 'a app_config = {
+
max_connections : int; (** Maximum concurrent connections *)
+
max_requests : int; (** Maximum concurrent requests *)
+
multiplex_connections : bool; (** Support connection multiplexing *)
+
handler : 'a Handler.handler; (** Application request handler *)
+
}
+
```
+
+
## Connection Management
+
+
```ocaml
+
(** Connection manager for handling FastCGI protocol *)
+
module Connection = struct
+
(** Opaque connection type *)
+
type 'a t
+
+
(** Create a connection from a network flow *)
+
val create : sw:Eio.Switch.t -> 'a Eio.Flow.two_way -> 'a t
+
+
(** Accept and process a single request on the connection *)
+
val process_request : 'a t -> 'a Handler.handler -> response_result Eio.Promise.t
+
+
(** Get connection statistics *)
+
val stats : 'a t -> {
+
active_requests : int;
+
total_requests : int;
+
bytes_sent : int;
+
bytes_received : int;
+
}
+
+
(** Close the connection gracefully *)
+
val close : 'a t -> unit
+
end
+
+
(** FastCGI server for accepting and managing connections *)
+
module Server = struct
+
(** Server configuration *)
+
type 'a config = {
+
app : 'a app_config;
+
listen_address : [
+
| `Unix of string (** Unix domain socket path *)
+
| `Tcp of Eio.Net.Ipaddr.t * int (** TCP address and port *)
+
];
+
backlog : int; (** Listen backlog size *)
+
max_connections : int; (** Maximum concurrent connections *)
+
}
+
+
(** Run a FastCGI server *)
+
val run :
+
sw:Eio.Switch.t ->
+
net:'a Eio.Net.t ->
+
'a config ->
+
unit
+
+
(** Run server with default configuration *)
+
val run_default :
+
sw:Eio.Switch.t ->
+
net:'a Eio.Net.t ->
+
handler:'a Handler.handler ->
+
listen_address:[`Unix of string | `Tcp of Eio.Net.Ipaddr.t * int] ->
+
unit
+
end
+
```
+
+
## Role Implementations
+
+
### Responder
+
+
```ocaml
+
(** Responder role implementation *)
+
module Responder = struct
+
(** CGI-style environment variables *)
+
type cgi_env = (string * string) list
+
+
(** HTTP request information *)
+
type http_request = {
+
method_ : string; (** HTTP method (GET, POST, etc.) *)
+
uri : string; (** Request URI *)
+
query_string : string; (** Query string parameters *)
+
content_type : string option; (** Content-Type header *)
+
content_length : int option; (** Content-Length header *)
+
headers : (string * string) list; (** Additional HTTP headers *)
+
body : 'a Eio.Flow.source; (** Request body stream *)
+
}
+
+
(** HTTP response builder *)
+
type 'a http_response = {
+
write_status : int -> unit; (** Set HTTP status code *)
+
write_header : string -> string -> unit; (** Add response header *)
+
write_body : string -> unit; (** Write response body *)
+
write_body_chunk : bytes -> unit; (** Write body chunk *)
+
finish : unit -> unit; (** Complete response *)
+
}
+
+
(** Convert FastCGI request to HTTP request *)
+
val request_of_fastcgi : 'a request -> http_request
+
+
(** Create HTTP response writer from FastCGI response *)
+
val response_of_fastcgi : 'a response -> 'a http_response
+
+
(** Convenience handler wrapper for HTTP-style handlers *)
+
val http_handler :
+
(http_request -> 'a http_response -> unit) ->
+
'a Handler.responder
+
end
+
```
+
+
### Authorizer
+
+
```ocaml
+
(** Authorizer role implementation *)
+
module Authorizer = struct
+
(** Authorization result *)
+
type auth_result =
+
| Authorized of (string * string) list (** Authorized with variable bindings *)
+
| Unauthorized of { (** Unauthorized with error response *)
+
status : int;
+
headers : (string * string) list;
+
body : string;
+
}
+
+
(** Authorization request information *)
+
type auth_request = {
+
method_ : string;
+
uri : string;
+
remote_addr : string option;
+
remote_user : string option;
+
auth_type : string option;
+
headers : (string * string) list;
+
}
+
+
(** Convert FastCGI request to authorization request *)
+
val request_of_fastcgi : 'a request -> auth_request
+
+
(** Convert authorization result to FastCGI response *)
+
val response_of_result : auth_result -> 'a response -> response_result
+
+
(** Convenience handler wrapper for authorization handlers *)
+
val auth_handler :
+
(auth_request -> auth_result) ->
+
'a Handler.authorizer
+
end
+
```
+
+
### Filter
+
+
```ocaml
+
(** Filter role implementation *)
+
module Filter = struct
+
(** Filter request with data stream *)
+
type 'a filter_request = {
+
request : 'a request; (** Base FastCGI request *)
+
data_stream : 'a Eio.Flow.source; (** File data to filter *)
+
data_last_modified : float option; (** File modification time *)
+
data_length : int option; (** Expected data length *)
+
}
+
+
(** Convert FastCGI request to filter request *)
+
val request_of_fastcgi : 'a request -> 'a filter_request
+
+
(** Convenience handler wrapper for filter handlers *)
+
val filter_handler :
+
('a filter_request -> 'a response -> unit) ->
+
'a Handler.filter
+
end
+
```
+
+
## Error Handling
+
+
```ocaml
+
(** FastCGI specific errors *)
+
module Error = struct
+
type t =
+
| Protocol_error of string (** Protocol violation *)
+
| Invalid_record of string (** Malformed record *)
+
| Unsupported_version of int (** Unsupported protocol version *)
+
| Unknown_record_type of int (** Unknown record type *)
+
| Request_id_conflict of request_id (** Duplicate request ID *)
+
| Connection_closed (** Connection unexpectedly closed *)
+
| Application_error of string (** Application-specific error *)
+
+
exception Fastcgi_error of t
+
+
(** Convert error to string description *)
+
val to_string : t -> string
+
+
(** Raise a FastCGI error *)
+
val raise : t -> 'a
+
end
+
```
+
+
## Resource Management
+
+
```ocaml
+
(** Resource management utilities *)
+
module Resource = struct
+
(** Request context with automatic cleanup *)
+
type 'a request_context = {
+
request : 'a request;
+
response : 'a response;
+
switch : Eio.Switch.t; (** Switch for request-scoped resources *)
+
}
+
+
(** Create a request context with automatic resource management *)
+
val with_request_context :
+
sw:Eio.Switch.t ->
+
'a request ->
+
('a request_context -> 'b) ->
+
'b
+
+
(** Buffer management for efficient I/O *)
+
module Buffer = struct
+
type t
+
+
val create : size:int -> t
+
val read_into : t -> 'a Eio.Flow.source -> int
+
val write_from : t -> 'a Eio.Flow.sink -> int -> unit
+
val clear : t -> unit
+
end
+
end
+
```
+
+
## Examples
+
+
### Basic Responder Application
+
+
```ocaml
+
open Eio.Std
+
+
let hello_handler request response =
+
let http_req = Responder.request_of_fastcgi request in
+
let http_resp = Responder.response_of_fastcgi response in
+
+
http_resp.write_status 200;
+
http_resp.write_header "Content-Type" "text/html";
+
http_resp.write_body "<h1>Hello, FastCGI!</h1>";
+
http_resp.finish ();
+
+
{ app_status = 0; protocol_status = Request_complete }
+
+
let run_server env =
+
let net = Eio.Stdenv.net env in
+
let config = {
+
app = {
+
max_connections = 10;
+
max_requests = 50;
+
multiplex_connections = true;
+
handler = Handler.Responder hello_handler;
+
};
+
listen_address = `Tcp (Eio.Net.Ipaddr.V4.loopback, 9000);
+
backlog = 5;
+
max_connections = 10;
+
} in
+
+
Switch.run @@ fun sw ->
+
Server.run ~sw ~net config
+
+
let () = Eio_main.run run_server
+
```
+
+
### File-serving Responder
+
+
```ocaml
+
let file_server_handler ~cwd request response =
+
let http_req = Responder.request_of_fastcgi request in
+
let http_resp = Responder.response_of_fastcgi response in
+
+
let path = Eio.Path.(cwd / String.drop_prefix http_req.uri 1) in
+
+
match Eio.Path.load path with
+
| content ->
+
http_resp.write_status 200;
+
http_resp.write_header "Content-Type" "text/html";
+
http_resp.write_body content;
+
http_resp.finish ()
+
| exception (Eio.Io (Eio.Fs.E (Not_found _), _)) ->
+
http_resp.write_status 404;
+
http_resp.write_header "Content-Type" "text/plain";
+
http_resp.write_body "File not found";
+
http_resp.finish ()
+
| exception ex ->
+
http_resp.write_status 500;
+
http_resp.write_header "Content-Type" "text/plain";
+
http_resp.write_body ("Server error: " ^ Printexc.to_string ex);
+
http_resp.finish ();
+
+
{ app_status = 0; protocol_status = Request_complete }
+
+
let run_file_server env =
+
let net = Eio.Stdenv.net env in
+
let cwd = Eio.Stdenv.cwd env in
+
let handler = file_server_handler ~cwd in
+
+
Switch.run @@ fun sw ->
+
Server.run_default ~sw ~net ~handler:(Handler.Responder handler)
+
~listen_address:(`Tcp (Eio.Net.Ipaddr.V4.loopback, 9000))
+
```
+
+
### Authorization Application
+
+
```ocaml
+
let auth_handler auth_req =
+
match auth_req.remote_user with
+
| Some user when String.starts_with user "admin_" ->
+
Authorizer.Authorized [("AUTH_USER", user); ("AUTH_LEVEL", "admin")]
+
| Some user ->
+
Authorizer.Authorized [("AUTH_USER", user); ("AUTH_LEVEL", "user")]
+
| None ->
+
Authorizer.Unauthorized {
+
status = 401;
+
headers = [("WWW-Authenticate", "Basic realm=\"FastCGI\"")];
+
body = "Authentication required";
+
}
+
+
let run_auth_server env =
+
let net = Eio.Stdenv.net env in
+
let handler = Authorizer.auth_handler auth_handler in
+
+
Switch.run @@ fun sw ->
+
Server.run_default ~sw ~net ~handler:(Handler.Authorizer handler)
+
~listen_address:(`Unix "/tmp/auth.sock")
+
```
+
+
### Filter Application
+
+
```ocaml
+
let markdown_filter filter_req response =
+
let data = Eio.Flow.read_all filter_req.data_stream in
+
let html = Markdown.to_html data in (* Assuming markdown library *)
+
+
Eio.Flow.copy_string "Content-Type: text/html\r\n\r\n" response.stdout;
+
Eio.Flow.copy_string html response.stdout;
+
+
{ app_status = 0; protocol_status = Request_complete }
+
+
let run_filter_server env =
+
let net = Eio.Stdenv.net env in
+
let handler = Filter.filter_handler markdown_filter in
+
+
Switch.run @@ fun sw ->
+
Server.run_default ~sw ~net ~handler:(Handler.Filter handler)
+
~listen_address:(`Tcp (Eio.Net.Ipaddr.V4.loopback, 9001))
+
```
+
+
### Connection Management Example
+
+
```ocaml
+
let custom_connection_handler env =
+
let net = Eio.Stdenv.net env in
+
+
Switch.run @@ fun sw ->
+
let server_socket = Eio.Net.listen net ~sw ~reuse_addr:true ~backlog:5
+
(`Tcp (Eio.Net.Ipaddr.V4.loopback, 9000)) in
+
+
let handler = Handler.Responder hello_handler in
+
+
Eio.Net.run_server server_socket (fun flow _addr ->
+
Switch.run @@ fun conn_sw ->
+
let conn = Connection.create ~sw:conn_sw flow in
+
+
let rec process_loop () =
+
match Connection.process_request conn handler with
+
| response -> process_loop ()
+
| exception End_of_file -> ()
+
| exception ex ->
+
Eio.traceln "Connection error: %s" (Printexc.to_string ex)
+
in
+
process_loop ()
+
)
+
```
+
+
## Implementation Notes
+
+
### Wire Protocol Details
+
+
The implementation must handle:
+
+
1. **Binary record format**: 8-byte headers with version, type, request ID, content length, and padding
+
2. **Name-value pair encoding**: Variable-length encoding for parameter names and values
+
3. **Stream management**: Proper handling of stdin, stdout, stderr, and data streams
+
4. **Connection multiplexing**: Support multiple concurrent requests over single connection
+
5. **Error propagation**: Convert protocol errors to appropriate OCaml exceptions
+
+
### Performance Considerations
+
+
1. **Buffer management**: Reuse buffers to minimize allocations
+
2. **Streaming I/O**: Process requests in streaming fashion for large payloads
+
3. **Connection pooling**: Support keep-alive connections for efficiency
+
4. **Concurrent processing**: Use Eio fibers for handling multiple requests
+
+
### Security Considerations
+
+
1. **Input validation**: Validate all protocol fields and reject malformed records
+
2. **Resource limits**: Enforce limits on request size, connection count, etc.
+
3. **Capability restriction**: Use Eio's capability system to limit access
+
4. **Error information**: Avoid leaking sensitive information in error messages
+
+
### Testing Strategy
+
+
The implementation should include:
+
+
1. **Protocol conformance tests**: Verify correct implementation of FastCGI protocol
+
2. **Interoperability tests**: Test with real web servers (nginx, Apache)
+
3. **Performance tests**: Measure throughput and latency under load
+
4. **Error handling tests**: Verify graceful handling of error conditions
+
5. **Mock testing**: Use Eio_mock for deterministic unit tests
+
+
This specification provides a comprehensive foundation for implementing a robust, efficient, and type-safe FastCGI library for OCaml using the Eio effects system.