FastCGI implementation in OCaml

Add FastCGI sample applications demonstrating all three roles

- Create examples/ directory with four sample applications
- hello_responder.ml: Simple "Hello World" FastCGI Responder
- echo_responder.ml: Echo server displaying request information
- simple_authorizer.ml: Path-based authorization server
- basic_filter.ml: Text filter converting input to uppercase
- Update interface types to be compatible with Eio's actual types
- Fix TCP address type from Sockaddr to string for simplicity
- All examples type-check successfully against interfaces
- Expected linking errors confirm readiness for implementation phase

This completes STEP 2: sample binaries successfully demonstrate the API design
and prove the interfaces are usable for real applications.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

+1 -1
CLAUDE.md
···
# Software engineering
-
We will go through a multi step process to build this library. We are currently at STEP 1.
+
We will go through a multi step process to build this library. We are currently at STEP 2.
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.
+43
examples/basic_filter.ml
···
+
(* Basic filter that uppercases text content *)
+
+
open Fastcgi
+
+
let uppercase_filter_logic filter_req response =
+
let input = filter_req.Filter.data_stream in
+
let output = response.stdout in
+
+
(* Simple text transformation: read input and write uppercase output *)
+
let buffer = Cstruct.create 4096 in
+
let rec process () =
+
try
+
let n = Eio.Flow.single_read input buffer in
+
if n > 0 then begin
+
let text = Cstruct.to_string ~len:n buffer in
+
let uppercase_text = String.uppercase_ascii text in
+
Eio.Flow.copy_string uppercase_text output;
+
process ()
+
end
+
with
+
| End_of_file -> ()
+
in
+
process ()
+
+
let filter_handler request response =
+
let filter_req = Filter.request_of_fastcgi request in
+
+
(* Write HTTP headers *)
+
Eio.Flow.copy_string "Status: 200 OK\r\n" response.stdout;
+
Eio.Flow.copy_string "Content-Type: text/plain\r\n" response.stdout;
+
Eio.Flow.copy_string "\r\n" response.stdout;
+
+
(* Process the data stream *)
+
uppercase_filter_logic filter_req response;
+
+
{ app_status = 0; protocol_status = Request_complete }
+
+
let () = Eio_main.run @@ fun env ->
+
let net = Eio.Stdenv.net env in
+
Eio.Switch.run @@ fun sw ->
+
Server.run_default ~sw ~net
+
~handler:(Handler.Filter filter_handler)
+
~listen_address:(`Tcp ("127.0.0.1", 9003))
+4
examples/dune
···
+
(executables
+
(public_names hello_responder echo_responder simple_authorizer basic_filter)
+
(names hello_responder echo_responder simple_authorizer basic_filter)
+
(libraries fastcgi eio_main cstruct))
+40
examples/echo_responder.ml
···
+
(* Echo server that shows request information *)
+
+
open Fastcgi
+
+
let echo_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>FastCGI Echo Server</h1>";
+
http_resp.write_body ("<p><strong>Method:</strong> " ^ http_req.method_ ^ "</p>");
+
http_resp.write_body ("<p><strong>URI:</strong> " ^ http_req.uri ^ "</p>");
+
http_resp.write_body ("<p><strong>Query String:</strong> " ^ http_req.query_string ^ "</p>");
+
+
(match http_req.content_type with
+
| Some ct -> http_resp.write_body ("<p><strong>Content-Type:</strong> " ^ ct ^ "</p>")
+
| None -> ());
+
+
(match http_req.content_length with
+
| Some cl -> http_resp.write_body ("<p><strong>Content-Length:</strong> " ^ string_of_int cl ^ "</p>")
+
| None -> ());
+
+
http_resp.write_body "<h2>Headers:</h2><ul>";
+
List.iter (fun (name, value) ->
+
http_resp.write_body ("<li><strong>" ^ name ^ ":</strong> " ^ value ^ "</li>")
+
) http_req.headers;
+
http_resp.write_body "</ul>";
+
+
http_resp.finish ();
+
+
{ app_status = 0; protocol_status = Request_complete }
+
+
let () = Eio_main.run @@ fun env ->
+
let net = Eio.Stdenv.net env in
+
Eio.Switch.run @@ fun sw ->
+
Server.run_default ~sw ~net
+
~handler:(Handler.Responder echo_handler)
+
~listen_address:(`Tcp ("127.0.0.1", 9001))
+22
examples/hello_responder.ml
···
+
(* Simple Hello World FastCGI Responder application *)
+
+
open Fastcgi
+
+
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.write_body "<p>This is a simple FastCGI responder application.</p>";
+
http_resp.finish ();
+
+
{ app_status = 0; protocol_status = Request_complete }
+
+
let () = Eio_main.run @@ fun env ->
+
let net = Eio.Stdenv.net env in
+
Eio.Switch.run @@ fun sw ->
+
Server.run_default ~sw ~net
+
~handler:(Handler.Responder hello_handler)
+
~listen_address:(`Tcp ("127.0.0.1", 9000))
+29
examples/simple_authorizer.ml
···
+
(* Simple authorization server *)
+
+
open Fastcgi
+
+
let auth_logic auth_req =
+
(* Simple authorization: allow only GET requests to /public/* paths *)
+
match auth_req.Authorizer.method_, auth_req.uri with
+
| "GET", uri when String.starts_with ~prefix:"/public/" uri ->
+
Authorizer.Authorized [("USER_AUTHORIZED", "true"); ("ACCESS_LEVEL", "public")]
+
| "GET", "/login" ->
+
Authorizer.Authorized [("LOGIN_PAGE", "true")]
+
| _ ->
+
Authorizer.Unauthorized {
+
status = 403;
+
headers = [("Content-Type", "text/html")];
+
body = "<h1>403 Forbidden</h1><p>Access denied. Only GET requests to /public/* are allowed.</p>";
+
}
+
+
let auth_handler request response =
+
let auth_req = Authorizer.request_of_fastcgi request in
+
let result = auth_logic auth_req in
+
Authorizer.response_of_result result response
+
+
let () = Eio_main.run @@ fun env ->
+
let net = Eio.Stdenv.net env in
+
Eio.Switch.run @@ fun sw ->
+
Server.run_default ~sw ~net
+
~handler:(Handler.Authorizer auth_handler)
+
~listen_address:(`Tcp ("127.0.0.1", 9002))
+6 -6
lib/fastcgi.mli
···
listen_address : [
| `Unix of string (** Unix domain socket path. Common for local
communication with web servers. *)
-
| `Tcp of Eio.Net.Sockaddr.stream * int (** TCP socket address and port.
+
| `Tcp of string * int (** TCP socket address (host) and port.
Used for remote FastCGI servers. *)
];
backlog : int; (** Listen socket backlog size. Controls how
···
@raise Sys_error if unable to bind to the specified address *)
val run :
sw:Eio.Switch.t ->
-
net:'a Eio.Net.t ->
-
'a config ->
+
net:[< `Generic | `Unix ] Eio.Net.ty Eio.Resource.t ->
+
_ config ->
unit
(** Run server with default configuration.
···
@raise Sys_error if unable to bind to the specified address *)
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.Sockaddr.stream * int] ->
+
net:[< `Generic | `Unix ] Eio.Net.ty Eio.Resource.t ->
+
handler:_ Handler.handler ->
+
listen_address:[`Unix of string | `Tcp of string * int] ->
unit
end