FastCGI implementation in OCaml

checkpoint

Changed files
+99 -93
bin
config
lib
+65 -12
bin/fcgi_server.ml
···
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;
-
Eio.Net.run_server server_socket ~on_error:(fun ex -> Eio.traceln "Error: %s" (Printexc.to_string ex))
-
@@ fun flow addr ->
-
Eio.traceln "Accepted connection from %a" Eio.Net.Sockaddr.pp addr;
-
(* Here you would handle the FastCGI protocol, but for simplicity, we just echo a string. *)
-
let req = Fastcgi.read_request_from_flow ~sw flow in
-
match req with
-
| Error msg ->
-
Eio.traceln "Failed to read request: %s" msg;
-
Eio.Flow.close flow
-
| Ok req ->
-
Eio.traceln "Received request: %a" Fastcgi.Request.pp req;
-
Eio.Flow.close flow
+
+
(* Handler function that processes FastCGI requests *)
+
let handler ~sw:_ request output =
+
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
+
let full_response = response_headers ^ response_body in
+
+
(* Write STDOUT content *)
+
Fastcgi.Request.write_stdout_records output request.Fastcgi.Request.request_id full_response;
+
+
(* Write empty STDERR (no errors) *)
+
Fastcgi.Request.write_stderr_records output request.Fastcgi.Request.request_id "";
+
+
(* Write END_REQUEST with success status *)
+
Fastcgi.Request.write_end_request output request.Fastcgi.Request.request_id 0 Fastcgi.Request.Request_complete
+
in
+
+
(* Run the FastCGI server *)
+
Fastcgi.run server_socket
+
~on_error:(fun ex -> Eio.traceln "Error: %s" (Printexc.to_string ex))
+
handler
let port =
let doc = "Port to listen on" in
+7
config/Caddyfile
···
+
{
+
debug
+
}
+
+
localhost:80 {
+
php_fastcgi 127.0.0.1:9000
+
}
+19 -50
lib/fastcgi.ml
···
stderr:Eio.Flow.sink_ty Eio.Resource.t ->
Request.app_status
-
(** [read_request_from_flow ~sw flow] reads a complete FastCGI request from flow.
-
Processes BEGIN_REQUEST, PARAMS, STDIN, and DATA records until complete.
-
Returns the populated request context. *)
-
let read_request_from_flow ~sw:_ flow =
-
let buf_read = Eio.Buf_read.of_flow flow ~max_size:1000000 in
-
Request.read_request buf_read
(** [write_response ~sw request ~stdout ~stderr sink app_status] writes FastCGI response.
Reads from stdout and stderr flows, converts to FastCGI records, and writes to sink.
···
write_response ~sw request ~stdout:stdout_source ~stderr:stderr_source sink app_status
-
(** [process_request_with_flows ~sw request ~stdout ~stderr sink app_status]
-
processes request using provided output flows. *)
-
let process_request_with_flows ~sw request ~stdout ~stderr sink app_status =
-
write_response ~sw request ~stdout ~stderr sink app_status
-
(** {1 Connection Management} *)
-
-
(** [handle_connection ~sw flow handler] handles complete FastCGI connection.
-
Reads requests from flow, processes them with handler, multiplexes responses.
-
Continues until connection is closed. *)
-
let handle_connection ~sw flow handler =
-
let rec loop () =
-
try
-
(* Read next request *)
-
match read_request_from_flow ~sw flow with
-
| Error msg ->
-
(* Log error and continue or close connection *)
-
Printf.eprintf "Error reading request: %s\n%!" msg
-
| Ok request ->
-
(* Process request *)
-
let response_buf = Buffer.create 4096 in
-
let response_sink = Eio.Flow.buffer_sink response_buf in
-
-
process_request ~sw request handler response_sink;
-
-
(* Write response to connection *)
-
let response_data = Buffer.contents response_buf in
-
Eio.Flow.copy (Eio.Flow.string_source response_data) flow;
-
-
(* Continue if keep_conn is true *)
-
if request.Request.keep_conn then
-
loop ()
-
with
-
| End_of_file -> () (* Connection closed *)
-
| exn ->
-
Printf.eprintf "Connection error: %s\n%!" (Printexc.to_string exn)
-
in
-
loop ()
-
-
(** [serve ~sw ~backlog ~port handler] creates FastCGI server.
-
Listens on port, accepts connections, handles each with handler. *)
-
let serve ~sw:_ ~backlog:_ ~port:_ _handler =
-
(* This would typically use Eio.Net to create a listening socket *)
-
(* For now, we'll provide a placeholder implementation *)
-
failwith "serve: Implementation requires Eio.Net integration"
+
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 ->
+
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
+
try begin
+
Eio.Buf_write.with_flow socket @@ fun output ->
+
match Request.read_request input with
+
| Error msg ->
+
Eio.traceln "%a: failed to read request: %s" Eio.Net.Sockaddr.pp peer_address msg;
+
Eio.Flow.close socket
+
| Ok req ->
+
Eio.traceln "%a: read request %a" Eio.Net.Sockaddr.pp peer_address Request.pp req;
+
handler ~sw req output;
+
end
+
with Eio.Io (Eio.Net.E (Connection_reset _), _) ->
+
Eio.traceln "%a: connection reset" Eio.Net.Sockaddr.pp peer_address
+
)
+8 -31
lib/fastcgi.mli
···
stderr:Eio.Flow.sink_ty Eio.Resource.t ->
Request.app_status
-
(** [read_request_from_flow ~sw flow] reads a complete FastCGI request from flow.
-
Processes BEGIN_REQUEST, PARAMS, STDIN, and DATA records until complete.
-
Returns the populated request context. *)
-
val read_request_from_flow : sw:Eio.Switch.t -> 'a Eio.Flow.source -> (Request.t, string) result
-
(** [write_response ~sw request ~stdout ~stderr sink app_status] writes FastCGI response.
Reads from stdout and stderr flows, converts to FastCGI records, and writes to sink.
Automatically handles stream termination and END_REQUEST. *)
···
handler ->
Eio.Flow.sink_ty Eio.Resource.t -> unit
-
(** [process_request_with_flows ~sw request ~stdout ~stderr sink app_status]
-
processes request using provided output flows. *)
-
val process_request_with_flows :
-
sw:Eio.Switch.t ->
-
Request.t ->
-
stdout:'a Eio.Flow.source ->
-
stderr:'a Eio.Flow.source ->
-
'a Eio.Flow.sink ->
-
Request.app_status -> unit
-
-
(** {1 Connection Management} *)
-
(** [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 handle_connection :
-
sw:Eio.Switch.t ->
-
Eio.Flow.two_way_ty Eio.Resource.t ->
-
handler ->
-
unit
-
-
(** [serve ~sw ~backlog ~port handler] creates FastCGI server.
-
Listens on port, accepts connections, handles each with handler. *)
-
val serve :
-
sw:Eio.Switch.t ->
-
backlog:int ->
-
port:int ->
-
handler ->
-
unit
+
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 ->
+
(sw:Eio.Switch.t -> Request.t -> Eio.Buf_write.t -> unit) -> 'a