···
1
+
# OCaml FastCGI Specification
3
+
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.
7
+
1. [Overview](#overview)
8
+
2. [Core Types](#core-types)
9
+
3. [Protocol Constants](#protocol-constants)
10
+
4. [Record Types](#record-types)
11
+
5. [Request/Response Model](#requestresponse-model)
12
+
6. [Application Interface](#application-interface)
13
+
7. [Connection Management](#connection-management)
14
+
8. [Role Implementations](#role-implementations)
15
+
9. [Error Handling](#error-handling)
16
+
10. [Resource Management](#resource-management)
17
+
11. [Examples](#examples)
21
+
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.
23
+
### Key Design Principles
25
+
- **Capability-based**: All external resources (network, filesystem) are explicitly passed as capabilities
26
+
- **Structured concurrency**: Use Eio switches for automatic resource cleanup
27
+
- **Type safety**: Leverage OCaml's type system to prevent protocol errors
28
+
- **Performance**: Support connection multiplexing and keep-alive for efficiency
29
+
- **Extensibility**: Support all three FastCGI roles with room for future extensions
34
+
(** FastCGI protocol version *)
37
+
(** FastCGI record types *)
51
+
(** FastCGI roles *)
53
+
| Responder (** Handle HTTP requests and generate responses *)
54
+
| Authorizer (** Perform authorization decisions *)
55
+
| Filter (** Process data streams with filtering *)
57
+
(** Request ID for multiplexing *)
58
+
type request_id = int
60
+
(** Application status code *)
61
+
type app_status = int
63
+
(** Protocol status codes *)
64
+
type protocol_status =
65
+
| Request_complete (** Normal completion *)
66
+
| Cant_mpx_conn (** Cannot multiplex connection *)
67
+
| Overloaded (** Application overloaded *)
68
+
| Unknown_role (** Unknown role requested *)
70
+
(** Connection flags *)
71
+
type connection_flags = {
72
+
keep_conn : bool; (** Keep connection open after request *)
76
+
## Protocol Constants
79
+
module Constants = struct
80
+
(** Protocol version *)
83
+
(** Standard file descriptor for FastCGI listening socket *)
84
+
let listensock_fileno = 0
86
+
(** Record type constants *)
87
+
let fcgi_begin_request = 1
88
+
let fcgi_abort_request = 2
89
+
let fcgi_end_request = 3
95
+
let fcgi_get_values = 9
96
+
let fcgi_get_values_result = 10
97
+
let fcgi_unknown_type = 11
99
+
(** Role constants *)
100
+
let fcgi_responder = 1
101
+
let fcgi_authorizer = 2
102
+
let fcgi_filter = 3
104
+
(** Flag constants *)
105
+
let fcgi_keep_conn = 1
107
+
(** Maximum sizes *)
108
+
let max_content_length = 65535
109
+
let max_padding_length = 255
110
+
let header_length = 8
112
+
(** Management record variables *)
113
+
let fcgi_max_conns = "FCGI_MAX_CONNS"
114
+
let fcgi_max_reqs = "FCGI_MAX_REQS"
115
+
let fcgi_mpxs_conns = "FCGI_MPXS_CONNS"
122
+
(** FastCGI record header *)
123
+
type record_header = {
125
+
record_type : record_type;
126
+
request_id : request_id;
127
+
content_length : int;
128
+
padding_length : int;
131
+
(** Begin request record body *)
132
+
type begin_request_body = {
134
+
flags : connection_flags;
137
+
(** End request record body *)
138
+
type end_request_body = {
139
+
app_status : app_status;
140
+
protocol_status : protocol_status;
143
+
(** Complete FastCGI record *)
145
+
header : record_header;
147
+
padding : bytes option;
150
+
(** Name-value pair for parameters *)
151
+
type name_value_pair = {
157
+
## Request/Response Model
160
+
(** Request context containing all information for a FastCGI request *)
161
+
type 'a request = {
162
+
request_id : request_id;
164
+
flags : connection_flags;
165
+
params : (string * string) list;
166
+
stdin : 'a Eio.Flow.source;
167
+
data : 'a Eio.Flow.source option; (** Only for Filter role *)
170
+
(** Response builder for constructing FastCGI responses *)
171
+
type 'a response = {
172
+
stdout : 'a Eio.Flow.sink;
173
+
stderr : 'a Eio.Flow.sink;
176
+
(** Complete response with status *)
177
+
type response_result = {
178
+
app_status : app_status;
179
+
protocol_status : protocol_status;
183
+
## Application Interface
186
+
(** Application handler signature *)
187
+
module Handler = struct
188
+
(** Responder handler: process HTTP request and generate response *)
189
+
type 'a responder = 'a request -> 'a response -> response_result
191
+
(** Authorizer handler: make authorization decision *)
192
+
type 'a authorizer = 'a request -> 'a response -> response_result
194
+
(** Filter handler: process data stream with filtering *)
195
+
type 'a filter = 'a request -> 'a response -> response_result
197
+
(** Generic handler that can handle any role *)
199
+
| Responder of 'a responder
200
+
| Authorizer of 'a authorizer
201
+
| Filter of 'a filter
204
+
(** Application configuration *)
205
+
type 'a app_config = {
206
+
max_connections : int; (** Maximum concurrent connections *)
207
+
max_requests : int; (** Maximum concurrent requests *)
208
+
multiplex_connections : bool; (** Support connection multiplexing *)
209
+
handler : 'a Handler.handler; (** Application request handler *)
213
+
## Connection Management
216
+
(** Connection manager for handling FastCGI protocol *)
217
+
module Connection = struct
218
+
(** Opaque connection type *)
221
+
(** Create a connection from a network flow *)
222
+
val create : sw:Eio.Switch.t -> 'a Eio.Flow.two_way -> 'a t
224
+
(** Accept and process a single request on the connection *)
225
+
val process_request : 'a t -> 'a Handler.handler -> response_result Eio.Promise.t
227
+
(** Get connection statistics *)
228
+
val stats : 'a t -> {
229
+
active_requests : int;
230
+
total_requests : int;
232
+
bytes_received : int;
235
+
(** Close the connection gracefully *)
236
+
val close : 'a t -> unit
239
+
(** FastCGI server for accepting and managing connections *)
240
+
module Server = struct
241
+
(** Server configuration *)
243
+
app : 'a app_config;
245
+
| `Unix of string (** Unix domain socket path *)
246
+
| `Tcp of Eio.Net.Ipaddr.t * int (** TCP address and port *)
248
+
backlog : int; (** Listen backlog size *)
249
+
max_connections : int; (** Maximum concurrent connections *)
252
+
(** Run a FastCGI server *)
255
+
net:'a Eio.Net.t ->
259
+
(** Run server with default configuration *)
262
+
net:'a Eio.Net.t ->
263
+
handler:'a Handler.handler ->
264
+
listen_address:[`Unix of string | `Tcp of Eio.Net.Ipaddr.t * int] ->
269
+
## Role Implementations
274
+
(** Responder role implementation *)
275
+
module Responder = struct
276
+
(** CGI-style environment variables *)
277
+
type cgi_env = (string * string) list
279
+
(** HTTP request information *)
280
+
type http_request = {
281
+
method_ : string; (** HTTP method (GET, POST, etc.) *)
282
+
uri : string; (** Request URI *)
283
+
query_string : string; (** Query string parameters *)
284
+
content_type : string option; (** Content-Type header *)
285
+
content_length : int option; (** Content-Length header *)
286
+
headers : (string * string) list; (** Additional HTTP headers *)
287
+
body : 'a Eio.Flow.source; (** Request body stream *)
290
+
(** HTTP response builder *)
291
+
type 'a http_response = {
292
+
write_status : int -> unit; (** Set HTTP status code *)
293
+
write_header : string -> string -> unit; (** Add response header *)
294
+
write_body : string -> unit; (** Write response body *)
295
+
write_body_chunk : bytes -> unit; (** Write body chunk *)
296
+
finish : unit -> unit; (** Complete response *)
299
+
(** Convert FastCGI request to HTTP request *)
300
+
val request_of_fastcgi : 'a request -> http_request
302
+
(** Create HTTP response writer from FastCGI response *)
303
+
val response_of_fastcgi : 'a response -> 'a http_response
305
+
(** Convenience handler wrapper for HTTP-style handlers *)
307
+
(http_request -> 'a http_response -> unit) ->
308
+
'a Handler.responder
315
+
(** Authorizer role implementation *)
316
+
module Authorizer = struct
317
+
(** Authorization result *)
319
+
| Authorized of (string * string) list (** Authorized with variable bindings *)
320
+
| Unauthorized of { (** Unauthorized with error response *)
322
+
headers : (string * string) list;
326
+
(** Authorization request information *)
327
+
type auth_request = {
330
+
remote_addr : string option;
331
+
remote_user : string option;
332
+
auth_type : string option;
333
+
headers : (string * string) list;
336
+
(** Convert FastCGI request to authorization request *)
337
+
val request_of_fastcgi : 'a request -> auth_request
339
+
(** Convert authorization result to FastCGI response *)
340
+
val response_of_result : auth_result -> 'a response -> response_result
342
+
(** Convenience handler wrapper for authorization handlers *)
344
+
(auth_request -> auth_result) ->
345
+
'a Handler.authorizer
352
+
(** Filter role implementation *)
353
+
module Filter = struct
354
+
(** Filter request with data stream *)
355
+
type 'a filter_request = {
356
+
request : 'a request; (** Base FastCGI request *)
357
+
data_stream : 'a Eio.Flow.source; (** File data to filter *)
358
+
data_last_modified : float option; (** File modification time *)
359
+
data_length : int option; (** Expected data length *)
362
+
(** Convert FastCGI request to filter request *)
363
+
val request_of_fastcgi : 'a request -> 'a filter_request
365
+
(** Convenience handler wrapper for filter handlers *)
366
+
val filter_handler :
367
+
('a filter_request -> 'a response -> unit) ->
375
+
(** FastCGI specific errors *)
376
+
module Error = struct
378
+
| Protocol_error of string (** Protocol violation *)
379
+
| Invalid_record of string (** Malformed record *)
380
+
| Unsupported_version of int (** Unsupported protocol version *)
381
+
| Unknown_record_type of int (** Unknown record type *)
382
+
| Request_id_conflict of request_id (** Duplicate request ID *)
383
+
| Connection_closed (** Connection unexpectedly closed *)
384
+
| Application_error of string (** Application-specific error *)
386
+
exception Fastcgi_error of t
388
+
(** Convert error to string description *)
389
+
val to_string : t -> string
391
+
(** Raise a FastCGI error *)
392
+
val raise : t -> 'a
396
+
## Resource Management
399
+
(** Resource management utilities *)
400
+
module Resource = struct
401
+
(** Request context with automatic cleanup *)
402
+
type 'a request_context = {
403
+
request : 'a request;
404
+
response : 'a response;
405
+
switch : Eio.Switch.t; (** Switch for request-scoped resources *)
408
+
(** Create a request context with automatic resource management *)
409
+
val with_request_context :
412
+
('a request_context -> 'b) ->
415
+
(** Buffer management for efficient I/O *)
416
+
module Buffer = struct
419
+
val create : size:int -> t
420
+
val read_into : t -> 'a Eio.Flow.source -> int
421
+
val write_from : t -> 'a Eio.Flow.sink -> int -> unit
422
+
val clear : t -> unit
429
+
### Basic Responder Application
434
+
let hello_handler request response =
435
+
let http_req = Responder.request_of_fastcgi request in
436
+
let http_resp = Responder.response_of_fastcgi response in
438
+
http_resp.write_status 200;
439
+
http_resp.write_header "Content-Type" "text/html";
440
+
http_resp.write_body "<h1>Hello, FastCGI!</h1>";
441
+
http_resp.finish ();
443
+
{ app_status = 0; protocol_status = Request_complete }
445
+
let run_server env =
446
+
let net = Eio.Stdenv.net env in
449
+
max_connections = 10;
451
+
multiplex_connections = true;
452
+
handler = Handler.Responder hello_handler;
454
+
listen_address = `Tcp (Eio.Net.Ipaddr.V4.loopback, 9000);
456
+
max_connections = 10;
459
+
Switch.run @@ fun sw ->
460
+
Server.run ~sw ~net config
462
+
let () = Eio_main.run run_server
465
+
### File-serving Responder
468
+
let file_server_handler ~cwd request response =
469
+
let http_req = Responder.request_of_fastcgi request in
470
+
let http_resp = Responder.response_of_fastcgi response in
472
+
let path = Eio.Path.(cwd / String.drop_prefix http_req.uri 1) in
474
+
match Eio.Path.load path with
476
+
http_resp.write_status 200;
477
+
http_resp.write_header "Content-Type" "text/html";
478
+
http_resp.write_body content;
479
+
http_resp.finish ()
480
+
| exception (Eio.Io (Eio.Fs.E (Not_found _), _)) ->
481
+
http_resp.write_status 404;
482
+
http_resp.write_header "Content-Type" "text/plain";
483
+
http_resp.write_body "File not found";
484
+
http_resp.finish ()
486
+
http_resp.write_status 500;
487
+
http_resp.write_header "Content-Type" "text/plain";
488
+
http_resp.write_body ("Server error: " ^ Printexc.to_string ex);
489
+
http_resp.finish ();
491
+
{ app_status = 0; protocol_status = Request_complete }
493
+
let run_file_server env =
494
+
let net = Eio.Stdenv.net env in
495
+
let cwd = Eio.Stdenv.cwd env in
496
+
let handler = file_server_handler ~cwd in
498
+
Switch.run @@ fun sw ->
499
+
Server.run_default ~sw ~net ~handler:(Handler.Responder handler)
500
+
~listen_address:(`Tcp (Eio.Net.Ipaddr.V4.loopback, 9000))
503
+
### Authorization Application
506
+
let auth_handler auth_req =
507
+
match auth_req.remote_user with
508
+
| Some user when String.starts_with user "admin_" ->
509
+
Authorizer.Authorized [("AUTH_USER", user); ("AUTH_LEVEL", "admin")]
511
+
Authorizer.Authorized [("AUTH_USER", user); ("AUTH_LEVEL", "user")]
513
+
Authorizer.Unauthorized {
515
+
headers = [("WWW-Authenticate", "Basic realm=\"FastCGI\"")];
516
+
body = "Authentication required";
519
+
let run_auth_server env =
520
+
let net = Eio.Stdenv.net env in
521
+
let handler = Authorizer.auth_handler auth_handler in
523
+
Switch.run @@ fun sw ->
524
+
Server.run_default ~sw ~net ~handler:(Handler.Authorizer handler)
525
+
~listen_address:(`Unix "/tmp/auth.sock")
528
+
### Filter Application
531
+
let markdown_filter filter_req response =
532
+
let data = Eio.Flow.read_all filter_req.data_stream in
533
+
let html = Markdown.to_html data in (* Assuming markdown library *)
535
+
Eio.Flow.copy_string "Content-Type: text/html\r\n\r\n" response.stdout;
536
+
Eio.Flow.copy_string html response.stdout;
538
+
{ app_status = 0; protocol_status = Request_complete }
540
+
let run_filter_server env =
541
+
let net = Eio.Stdenv.net env in
542
+
let handler = Filter.filter_handler markdown_filter in
544
+
Switch.run @@ fun sw ->
545
+
Server.run_default ~sw ~net ~handler:(Handler.Filter handler)
546
+
~listen_address:(`Tcp (Eio.Net.Ipaddr.V4.loopback, 9001))
549
+
### Connection Management Example
552
+
let custom_connection_handler env =
553
+
let net = Eio.Stdenv.net env in
555
+
Switch.run @@ fun sw ->
556
+
let server_socket = Eio.Net.listen net ~sw ~reuse_addr:true ~backlog:5
557
+
(`Tcp (Eio.Net.Ipaddr.V4.loopback, 9000)) in
559
+
let handler = Handler.Responder hello_handler in
561
+
Eio.Net.run_server server_socket (fun flow _addr ->
562
+
Switch.run @@ fun conn_sw ->
563
+
let conn = Connection.create ~sw:conn_sw flow in
565
+
let rec process_loop () =
566
+
match Connection.process_request conn handler with
567
+
| response -> process_loop ()
568
+
| exception End_of_file -> ()
570
+
Eio.traceln "Connection error: %s" (Printexc.to_string ex)
576
+
## Implementation Notes
578
+
### Wire Protocol Details
580
+
The implementation must handle:
582
+
1. **Binary record format**: 8-byte headers with version, type, request ID, content length, and padding
583
+
2. **Name-value pair encoding**: Variable-length encoding for parameter names and values
584
+
3. **Stream management**: Proper handling of stdin, stdout, stderr, and data streams
585
+
4. **Connection multiplexing**: Support multiple concurrent requests over single connection
586
+
5. **Error propagation**: Convert protocol errors to appropriate OCaml exceptions
588
+
### Performance Considerations
590
+
1. **Buffer management**: Reuse buffers to minimize allocations
591
+
2. **Streaming I/O**: Process requests in streaming fashion for large payloads
592
+
3. **Connection pooling**: Support keep-alive connections for efficiency
593
+
4. **Concurrent processing**: Use Eio fibers for handling multiple requests
595
+
### Security Considerations
597
+
1. **Input validation**: Validate all protocol fields and reject malformed records
598
+
2. **Resource limits**: Enforce limits on request size, connection count, etc.
599
+
3. **Capability restriction**: Use Eio's capability system to limit access
600
+
4. **Error information**: Avoid leaking sensitive information in error messages
602
+
### Testing Strategy
604
+
The implementation should include:
606
+
1. **Protocol conformance tests**: Verify correct implementation of FastCGI protocol
607
+
2. **Interoperability tests**: Test with real web servers (nginx, Apache)
608
+
3. **Performance tests**: Measure throughput and latency under load
609
+
4. **Error handling tests**: Verify graceful handling of error conditions
610
+
5. **Mock testing**: Use Eio_mock for deterministic unit tests
612
+
This specification provides a comprehensive foundation for implementing a robust, efficient, and type-safe FastCGI library for OCaml using the Eio effects system.