···11open Cmdliner
2233+(* Handler function that processes FastCGI requests *)
44+let handler ~stdout ~stderr request =
55+ Eio.traceln "Processing request: %a" Fastcgi.Request.pp request;
66+77+ (* Get request parameters *)
88+ let params = request.Fastcgi.Request.params in
99+ let method_ = Fastcgi.Record.KV.find_opt "REQUEST_METHOD" params |> Option.value ~default:"GET" in
1010+ let uri = Fastcgi.Record.KV.find_opt "REQUEST_URI" params |> Option.value ~default:"/" in
1111+ let script_name = Fastcgi.Record.KV.find_opt "SCRIPT_NAME" params |> Option.value ~default:"" in
1212+1313+ (* Log request info *)
1414+ Eio.traceln " Method: %s" method_;
1515+ Eio.traceln " URI: %s" uri;
1616+ Eio.traceln " Script: %s" script_name;
1717+1818+ (* Generate simple HTTP response *)
1919+ let response_body =
2020+ Printf.sprintf
2121+ "<!DOCTYPE html>\n\
2222+ <html>\n\
2323+ <head><title>FastCGI OCaml Server</title></head>\n\
2424+ <body>\n\
2525+ <h1>FastCGI OCaml Server</h1>\n\
2626+ <p>Request processed successfully!</p>\n\
2727+ <ul>\n\
2828+ <li>Method: %s</li>\n\
2929+ <li>URI: %s</li>\n\
3030+ <li>Script: %s</li>\n\
3131+ </ul>\n\
3232+ <h2>All Parameters:</h2>\n\
3333+ <pre>%s</pre>\n\
3434+ </body>\n\
3535+ </html>\n"
3636+ method_ uri script_name
3737+ (let params_seq = Fastcgi.Record.KV.to_seq params in
3838+ let params_list = List.of_seq params_seq in
3939+ String.concat "\n" (List.map (fun (k, v) -> Printf.sprintf "%s = %s" k v) params_list))
4040+ in
4141+4242+ (* Write HTTP response using FastCGI STDOUT records *)
4343+ let response_headers =
4444+ Printf.sprintf
4545+ "Status: 200 OK\r\n\
4646+ Content-Type: text/html; charset=utf-8\r\n\
4747+ Content-Length: %d\r\n\
4848+ \r\n"
4949+ (String.length response_body)
5050+ in
5151+ stdout response_headers;
5252+ stderr "stderr stuff";
5353+ stdout response_body
5454+355let run port =
456 Eio_main.run @@ fun env ->
557 Eio.Switch.run @@ fun sw ->
···759 let addr = `Tcp (Eio.Net.Ipaddr.V4.loopback, port) in
860 let server_socket = Eio.Net.listen net ~backlog:10 ~reuse_addr:true ~sw addr in
961 Eio.traceln "FastCGI server listening on port %d" port;
1010-1111- (* Handler function that processes FastCGI requests *)
1212- let handler ~sw:_ request output =
1313- Eio.traceln "Processing request: %a" Fastcgi.Request.pp request;
1414-1515- (* Get request parameters *)
1616- let params = request.Fastcgi.Request.params in
1717- let method_ = Fastcgi.Record.KV.find_opt "REQUEST_METHOD" params |> Option.value ~default:"GET" in
1818- let uri = Fastcgi.Record.KV.find_opt "REQUEST_URI" params |> Option.value ~default:"/" in
1919- let script_name = Fastcgi.Record.KV.find_opt "SCRIPT_NAME" params |> Option.value ~default:"" in
2020-2121- (* Log request info *)
2222- Eio.traceln " Method: %s" method_;
2323- Eio.traceln " URI: %s" uri;
2424- Eio.traceln " Script: %s" script_name;
2525-2626- (* Generate simple HTTP response *)
2727- let response_body =
2828- Printf.sprintf
2929- "<!DOCTYPE html>\n\
3030- <html>\n\
3131- <head><title>FastCGI OCaml Server</title></head>\n\
3232- <body>\n\
3333- <h1>FastCGI OCaml Server</h1>\n\
3434- <p>Request processed successfully!</p>\n\
3535- <ul>\n\
3636- <li>Method: %s</li>\n\
3737- <li>URI: %s</li>\n\
3838- <li>Script: %s</li>\n\
3939- </ul>\n\
4040- <h2>All Parameters:</h2>\n\
4141- <pre>%s</pre>\n\
4242- </body>\n\
4343- </html>\n"
4444- method_ uri script_name
4545- (let params_seq = Fastcgi.Record.KV.to_seq params in
4646- let params_list = List.of_seq params_seq in
4747- String.concat "\n" (List.map (fun (k, v) -> Printf.sprintf "%s = %s" k v) params_list))
4848- in
4949-5050- (* Write HTTP response using FastCGI STDOUT records *)
5151- let response_headers =
5252- Printf.sprintf
5353- "Status: 200 OK\r\n\
5454- Content-Type: text/html; charset=utf-8\r\n\
5555- Content-Length: %d\r\n\
5656- \r\n"
5757- (String.length response_body)
5858- in
5959- let full_response = response_headers ^ response_body in
6060-6161- (* Write STDOUT content *)
6262- Fastcgi.Request.write_stdout_records output request.Fastcgi.Request.request_id full_response;
6363-6464- (* Write empty STDERR (no errors) *)
6565- Fastcgi.Request.write_stderr_records output request.Fastcgi.Request.request_id "";
6666-6767- (* Write END_REQUEST with success status *)
6868- Fastcgi.Request.write_end_request output request.Fastcgi.Request.request_id 0 Fastcgi.Request.Request_complete
6969- in
7070-6262+7163 (* Run the FastCGI server *)
7272- Fastcgi.run server_socket
7373- ~on_error:(fun ex -> Eio.traceln "Error: %s" (Printexc.to_string ex))
6464+ Fastcgi.run server_socket
6565+ ~on_error:(fun ex ->
6666+ Eio.traceln "Error: %s" (Printexc.to_string ex);
6767+ Eio.traceln "bt: %s" (Printexc.get_backtrace ()))
7468 handler
75697670let port =
···8276 let info = Cmd.info "fcgi-server" ~doc in
8377 Cmd.v info Term.(const run $ port)
84788585-let () = exit (Cmd.eval cmd)7979+let () = exit (Cmd.eval cmd)
+32-53
lib/fastcgi.ml
···33(** Request-level state machine and application interface *)
44module Request = Fastcgi_request
5566-(** Request handler function type *)
77-type handler = Request.t ->
88- stdout:Eio.Flow.sink_ty Eio.Resource.t ->
99- stderr:Eio.Flow.sink_ty Eio.Resource.t ->
1010- Request.app_status
1111-1212-1313-(** [write_response ~sw request ~stdout ~stderr sink app_status] writes FastCGI response.
1414- Reads from stdout and stderr flows, converts to FastCGI records, and writes to sink.
1515- Automatically handles stream termination and END_REQUEST. *)
1616-let write_response ~sw:_ request ~stdout ~stderr sink app_status =
1717- (* Read stdout content *)
1818- let stdout_buf = Buffer.create 4096 in
1919- Eio.Flow.copy stdout (Eio.Flow.buffer_sink stdout_buf);
2020- let stdout_content = Buffer.contents stdout_buf in
2121-2222- (* Read stderr content *)
2323- let stderr_buf = Buffer.create 1024 in
2424- Eio.Flow.copy stderr (Eio.Flow.buffer_sink stderr_buf);
2525- let stderr_content = Buffer.contents stderr_buf in
2626-2727- (* Write response using Buf_write *)
2828- Eio.Buf_write.with_flow sink (fun buf_write ->
2929- Request.write_stdout_records buf_write request.Request.request_id stdout_content;
3030- Request.write_stderr_records buf_write request.Request.request_id stderr_content;
3131- Request.write_end_request buf_write request.Request.request_id app_status Request.Request_complete
3232- )
3333-3434-(** [process_request ~sw request handler sink] processes complete request.
3535- Calls handler with flows for stdout/stderr output, then writes response to sink. *)
3636-let process_request ~sw request handler sink =
3737- (* Create in-memory flows for stdout and stderr *)
3838- let stdout_buf = Buffer.create 4096 in
3939- let stderr_buf = Buffer.create 1024 in
4040- let stdout_sink = Eio.Flow.buffer_sink stdout_buf in
4141- let stderr_sink = Eio.Flow.buffer_sink stderr_buf in
4242-4343- (* Call handler *)
4444- let app_status = handler request ~stdout:stdout_sink ~stderr:stderr_sink in
4545-4646- (* Convert buffers to sources and write response *)
4747- let stdout_source = Eio.Flow.string_source (Buffer.contents stdout_buf) in
4848- let stderr_source = Eio.Flow.string_source (Buffer.contents stderr_buf) in
4949-5050- write_response ~sw request ~stdout:stdout_source ~stderr:stderr_source sink app_status
5151-66+(* The lifetime of the handler is that the fiber should return when the
77+ stdout and stderr flows are closed, or an abort request has been received *)
88+let handle req bw cancel fn =
99+ let cancel () =
1010+ Eio.Promise.await cancel;
1111+ Eio.traceln "cancelled TODO"
1212+ in
1313+ let stdout buf = Request.write_stdout_records bw req.Request.request_id buf in
1414+ let stderr buf = Request.write_stderr_records bw req.Request.request_id buf in
1515+ let run () =
1616+ fn ~stdout ~stderr req;
1717+ Request.write_end_request bw req.Request.request_id 0 Request.Request_complete
1818+ in
1919+ Eio.Fiber.first run cancel
52205321let run ?max_connections ?additional_domains ?stop ~on_error socket handler =
5422 Eio.Net.run_server socket ?max_connections ?additional_domains ?stop ~on_error
5523 (fun socket peer_address ->
2424+ let ids = Hashtbl.create 7 in
5625 Eio.Switch.run @@ fun sw ->
5726 Eio.traceln "%a: accept connection" Eio.Net.Sockaddr.pp peer_address;
5827 let input = Eio.Buf_read.of_flow ~max_size:max_int socket in
5959- try begin
6060- Eio.Buf_write.with_flow socket @@ fun output ->
2828+ Eio.Buf_write.with_flow socket @@ fun output ->
2929+ let cont = ref true in
3030+ try while !cont do
6131 match Request.read_request input with
6232 | Error msg ->
6333 Eio.traceln "%a: failed to read request: %s" Eio.Net.Sockaddr.pp peer_address msg;
6464- Eio.Flow.close socket
3434+ failwith "done";
6535 | Ok req ->
6666- Eio.traceln "%a: read request %a" Eio.Net.Sockaddr.pp peer_address Request.pp req;
6767- handler ~sw req output;
6868- end
3636+ cont := req.Request.keep_conn;
3737+ Eio.traceln "%a: %b read request %a" Eio.Net.Sockaddr.pp peer_address !cont Request.pp req;
3838+ Eio.Fiber.fork ~sw (fun () ->
3939+ Eio.Switch.run ~name:"req_handler" @@ fun sw ->
4040+ let cancel, canceler = Eio.Promise.create () in
4141+ Hashtbl.add ids req.Request.request_id canceler;
4242+ Eio.Switch.on_release sw (fun () ->
4343+ Hashtbl.remove ids req.Request.request_id
4444+ );
4545+ handle req output cancel handler;
4646+ );
4747+ done
6948 with Eio.Io (Eio.Net.E (Connection_reset _), _) ->
7049 Eio.traceln "%a: connection reset" Eio.Net.Sockaddr.pp peer_address
7171- )5050+ )
+2-26
lib/fastcgi.mli
···14141515(** {1 High-level Request Processing} *)
16161717-(** Request handler function type *)
1818-type handler = Request.t ->
1919- stdout:Eio.Flow.sink_ty Eio.Resource.t ->
2020- stderr:Eio.Flow.sink_ty Eio.Resource.t ->
2121- Request.app_status
2222-2323-(** [write_response ~sw request ~stdout ~stderr sink app_status] writes FastCGI response.
2424- Reads from stdout and stderr flows, converts to FastCGI records, and writes to sink.
2525- Automatically handles stream termination and END_REQUEST. *)
2626-val write_response :
2727- sw:Eio.Switch.t ->
2828- Request.t ->
2929- stdout:'a Eio.Flow.source ->
3030- stderr:'a Eio.Flow.source ->
3131- 'a Eio.Flow.sink ->
3232- Request.app_status -> unit
3333-3434-(** [process_request ~sw request handler sink] processes complete request.
3535- Calls handler with flows for stdout/stderr output, then writes response to sink. *)
3636-val process_request :
3737- sw:Eio.Switch.t ->
3838- Request.t ->
3939- handler ->
4040- Eio.Flow.sink_ty Eio.Resource.t -> unit
4141-4217(** [handle_connection ~sw flow handler] handles complete FastCGI connection.
4318 Reads requests from flow, processes them with handler, multiplexes responses.
4419 Continues until connection is closed. *)
···4924 ?stop:'a Eio__core.Promise.t ->
5025 on_error:(exn -> unit) ->
5126 [> [> `Generic ] Eio.Net.listening_socket_ty ] Eio.Resource.t ->
5252- (sw:Eio.Switch.t -> Request.t -> Eio.Buf_write.t -> unit) -> 'a2727+ (stdout:(string -> unit) ->
2828+ stderr:(string -> unit) -> Request.t -> unit) -> 'a
+27-11
lib/fastcgi_record.ml
···7777 record_type : record;
7878 request_id : request_id;
7979 content : string;
8080+ offset : int;
8181+ length : int;
8082}
81838284let pp ?(max_content_len=100) ppf record =
8585+ let actual_content = String.sub record.content record.offset record.length in
8386 let truncated_content =
8484- let content = record.content in
8585- let len = String.length content in
8686- if len <= max_content_len then content
8787- else String.sub content 0 max_content_len ^ "..." ^ Printf.sprintf " (%d more bytes)" (len - max_content_len)
8787+ let len = String.length actual_content in
8888+ if len <= max_content_len then actual_content
8989+ else String.sub actual_content 0 max_content_len ^ "..." ^ Printf.sprintf " (%d more bytes)" (len - max_content_len)
8890 in
8991 Format.fprintf ppf
9090- "@[<2>{ version = %d;@ record_type = %a;@ request_id = %d;@ content = %S }@]"
9292+ "@[<2>{ version = %d;@ record_type = %a;@ request_id = %d;@ content = %S;@ offset = %d;@ length = %d }@]"
9193 record.version
9294 pp_record record.record_type
9395 record.request_id
9496 truncated_content
9797+ record.offset
9898+ record.length
959996100(* FastCGI constants *)
97101let fcgi_version_1 = 1
···141145 ignore (Eio.Buf_read.take padding_length buf_read)
142146 );
143147144144- let record = { version; record_type; request_id; content } in
148148+ let record = { version; record_type; request_id; content; offset = 0; length = String.length content } in
145149 Printf.eprintf "[DEBUG] Fastcgi_record.read: Complete record = %s\n%!"
146150 (Format.asprintf "%a" (pp ~max_content_len:50) record);
147151 record
148152149153let write buf_write record =
150150- let content_length = String.length record.content in
154154+ let total_content_length = String.length record.content in
155155+ let content_offset = record.offset in
156156+ let content_length = record.length in
157157+158158+ (* Validate bounds *)
159159+ if content_offset < 0 || content_offset > total_content_length then
160160+ invalid_arg "Fastcgi_record.write: offset out of bounds";
161161+ if content_length < 0 || content_offset + content_length > total_content_length then
162162+ invalid_arg "Fastcgi_record.write: length out of bounds";
151163152164 (* Calculate padding for 8-byte alignment *)
153165 let padding_length = (8 - (content_length land 7)) land 7 in
···163175164176 Eio.Buf_write.string buf_write (Bytes.to_string header);
165177166166- (* Write content *)
178178+ (* Write content with offset and length *)
167179 if content_length > 0 then
168168- Eio.Buf_write.string buf_write record.content;
180180+ Eio.Buf_write.string buf_write record.content ~off:content_offset ~len:content_length;
169181170182 (* Write padding *)
171183 if padding_length > 0 then
172184 Eio.Buf_write.string buf_write (String.make padding_length '\000')
173185174174-let create ~version ~record ~request_id ~content =
175175- { version; record_type = record; request_id; content }
186186+let create ?(version=1) ~record ~request_id ~content ?(offset=0) ?length () =
187187+ let content_length = match length with
188188+ | None -> String.length content - offset
189189+ | Some l -> l
190190+ in
191191+ { version; record_type = record; request_id; content; offset; length = content_length }
176192177193module KV = struct
178194 type t = (string * string) list
+11-7
lib/fastcgi_record.mli
···4343 content and optional padding for alignment. *)
4444type t = {
4545 version : version; (** Protocol version (always 1) *)
4646- record_type : record; (** Type of this record *)
4646+ record_type : record; (** Type of this record *)
4747 request_id : request_id; (** Request identifier *)
4848 content : string; (** Record content data *)
4949+ offset : int; (** Offset within content string (default: 0) *)
5050+ length : int; (** Length to use from content (default: String.length content) *)
4951}
50525153(** [pp ?max_content_len ppf record] pretty-prints a FastCGI record.
···6365(** [write buf_write record] writes a FastCGI record to the output buffer.
6466 The record header is automatically constructed from the record fields,
6567 and appropriate padding is added to align the record on 8-byte boundaries
6666- for optimal performance. *)
6868+ for optimal performance. Uses the record's offset and length fields to
6969+ determine which portion of the content to write. *)
6770val write : Eio.Buf_write.t -> t -> unit
68716969-(** [create ~version ~record ~request_id ~content] creates a new record
7070- with the specified parameters. The content length is automatically
7171- calculated from the content string. *)
7272-val create : version:version -> record:record ->
7373- request_id:request_id -> content:string -> t
7272+(** [create ?version ~record ~request_id ~content ?offset ?length] creates a new record
7373+ with the specified parameters. Version defaults to 1 (the only supported version).
7474+ If offset and length are not provided, the entire content string is used. *)
7575+val create : ?version:version -> record:record ->
7676+ request_id:request_id -> content:string ->
7777+ ?offset:int -> ?length:int -> unit -> t
74787579(** {1 Key-Value Pairs} *)
7680
+9-44
lib/fastcgi_request.ml
···240240 | Overloaded -> 2
241241 | Unknown_role -> 3
242242243243-let stream_records_to_string records =
244244- let buf = Buffer.create 1024 in
245245- List.iter (fun record ->
246246- if not (is_stream_terminator record) then
247247- Buffer.add_string buf record.content
248248- ) records;
249249- Buffer.contents buf
250250-251251-let string_to_stream_records ~request_id ~record_type content =
252252- let max_chunk = 65535 in (* FastCGI max record content length *)
253253- let len = String.length content in
254254- let records = ref [] in
255255-256256- let rec chunk_string pos =
257257- if pos >= len then
258258- () (* Empty terminator will be added separately *)
259259- else
260260- let chunk_len = min max_chunk (len - pos) in
261261- let chunk = String.sub content pos chunk_len in
262262- let record = Fastcgi_record.create ~version:1 ~record:record_type ~request_id ~content:chunk in
263263- records := record :: !records;
264264- chunk_string (pos + chunk_len)
265265- in
266266-267267- chunk_string 0;
268268-269269- (* Add stream terminator *)
270270- let terminator = Fastcgi_record.create ~version:1 ~record:record_type ~request_id ~content:"" in
271271- records := terminator :: !records;
272272-273273- List.rev !records
274274-275243let write_stream_records buf_write request_id record_type content =
276244 let max_chunk = 65535 in (* FastCGI max record content length *)
277245 let len = String.length content in
278278-279246 let rec chunk_string pos =
280280- if pos >= len then
281281- () (* Empty terminator will be added separately *)
282282- else
247247+ if pos < len then begin
283248 let chunk_len = min max_chunk (len - pos) in
284284- let chunk = String.sub content pos chunk_len in
285285- let record = Fastcgi_record.create ~version:1 ~record:record_type ~request_id ~content:chunk in
249249+ let record = Fastcgi_record.create ~record:record_type ~request_id ~content ~offset:pos ~length:chunk_len () in
286250 Fastcgi_record.write buf_write record;
287251 chunk_string (pos + chunk_len)
252252+ end
288253 in
289289-290254 chunk_string 0;
291291-292292- (* Add stream terminator *)
293293- let terminator = Fastcgi_record.create ~version:1 ~record:record_type ~request_id ~content:"" in
255255+ let terminator = Fastcgi_record.create ~record:record_type ~request_id ~content:"" () in
294256 Fastcgi_record.write buf_write terminator
295257296258let write_stdout_records buf_write request_id content =
259259+ Printf.eprintf "[DEBUG] write_stdout_records: Writing %d bytes for request_id=%d\n%!"
260260+ (String.length content) request_id;
297261 write_stream_records buf_write request_id Stdout content
298262299263let write_stderr_records buf_write request_id content =
264264+ Printf.eprintf "[DEBUG] write_stderr_records: Writing %d bytes for request_id=%d\n%!"
265265+ (String.length content) request_id;
300266 write_stream_records buf_write request_id Stderr content
301267302268let write_end_request buf_write request_id app_status protocol_status =
···309275 Bytes.set_uint8 buf 7 0; (* reserved *)
310276 Bytes.to_string buf
311277 in
312312- let record = Fastcgi_record.create ~version:1 ~record:End_request ~request_id ~content in
278278+ let record = Fastcgi_record.create ~record:End_request ~request_id ~content () in
313279 Fastcgi_record.write buf_write record
314314-
+2-19
lib/fastcgi_request.mli
···2525 request_id : Fastcgi_record.request_id; (** Request identifier *)
2626 role : role; (** Application role *)
2727 keep_conn : bool; (** Connection keep-alive flag *)
2828- params : Fastcgi_record.KV.t; (** Environment parameters *)
2828+ params : Fastcgi_record.KV.t; (** Environment parameters *)
2929 stdin_data : string; (** Complete STDIN content *)
3030- data_stream : string option; (** DATA stream for Filter role *)
3030+ data_stream : string option; (** DATA stream for Filter role *)
3131}
32323333(** [pp ppf request] pretty-prints a request context *)
···78787979(** [write_end_request buf_write request_id app_status protocol_status] writes END_REQUEST record. *)
8080val write_end_request : Eio.Buf_write.t -> Fastcgi_record.request_id -> app_status -> protocol_status -> unit
8181-8282-8383-(** {1 Utilities} *)
8484-8585-(** [is_stream_terminator record] returns true if record terminates a stream *)
8686-val is_stream_terminator : Fastcgi_record.t -> bool
8787-8888-(** [stream_records_to_string records] concatenates content from stream records *)
8989-val stream_records_to_string : Fastcgi_record.t list -> string
9090-9191-(** [string_to_stream_records ~request_id ~record_type content] converts string to stream records *)
9292-val string_to_stream_records :
9393- request_id:Fastcgi_record.request_id ->
9494- record_type:Fastcgi_record.record ->
9595- string -> Fastcgi_record.t list
9696-9797-