FastCGI implementation in OCaml
1 2(** {1 Error Handling Helpers} *) 3 4(** Helper functions for consistent error handling *) 5let invalid_version version = 6 invalid_arg (Printf.sprintf "Unsupported FastCGI version: %d" version) 7 8let unknown_record_type n = 9 invalid_arg (Printf.sprintf "Unknown FastCGI record type: %d" n) 10 11let buffer_underflow msg = 12 failwith (Printf.sprintf "Unexpected end of buffer while %s" msg) 13 14let check_bounds buf pos len context = 15 if pos + len > String.length buf then 16 buffer_underflow context 17 18type version = int 19 20type record = 21 | Begin_request 22 | Abort_request 23 | End_request 24 | Params 25 | Stdin 26 | Stdout 27 | Stderr 28 | Data 29 | Get_values 30 | Get_values_result 31 | Unknown_type 32 33let record_to_int = function 34 | Begin_request -> 1 35 | Abort_request -> 2 36 | End_request -> 3 37 | Params -> 4 38 | Stdin -> 5 39 | Stdout -> 6 40 | Stderr -> 7 41 | Data -> 8 42 | Get_values -> 9 43 | Get_values_result -> 10 44 | Unknown_type -> 11 45 46let record_of_int = function 47 | 1 -> Begin_request 48 | 2 -> Abort_request 49 | 3 -> End_request 50 | 4 -> Params 51 | 5 -> Stdin 52 | 6 -> Stdout 53 | 7 -> Stderr 54 | 8 -> Data 55 | 9 -> Get_values 56 | 10 -> Get_values_result 57 | 11 -> Unknown_type 58 | n -> unknown_record_type n 59 60let pp_record ppf = function 61 | Begin_request -> Format.pp_print_string ppf "Begin_request" 62 | Abort_request -> Format.pp_print_string ppf "Abort_request" 63 | End_request -> Format.pp_print_string ppf "End_request" 64 | Params -> Format.pp_print_string ppf "Params" 65 | Stdin -> Format.pp_print_string ppf "Stdin" 66 | Stdout -> Format.pp_print_string ppf "Stdout" 67 | Stderr -> Format.pp_print_string ppf "Stderr" 68 | Data -> Format.pp_print_string ppf "Data" 69 | Get_values -> Format.pp_print_string ppf "Get_values" 70 | Get_values_result -> Format.pp_print_string ppf "Get_values_result" 71 | Unknown_type -> Format.pp_print_string ppf "Unknown_type" 72 73type request_id = int 74 75type t = { 76 version : version; 77 record_type : record; 78 request_id : request_id; 79 content : string; 80} 81 82let pp ?(max_content_len=100) ppf record = 83 let truncated_content = 84 let content = record.content in 85 let len = String.length content in 86 if len <= max_content_len then content 87 else String.sub content 0 max_content_len ^ "..." ^ Printf.sprintf " (%d more bytes)" (len - max_content_len) 88 in 89 Format.fprintf ppf 90 "@[<2>{ version = %d;@ record_type = %a;@ request_id = %d;@ content = %S }@]" 91 record.version 92 pp_record record.record_type 93 record.request_id 94 truncated_content 95 96(* FastCGI constants *) 97let fcgi_version_1 = 1 98let fcgi_header_len = 8 99 100let read buf_read = 101 Printf.eprintf "[DEBUG] Fastcgi_record.read: Starting to read record\n%!"; 102 (* Read the 8-byte header *) 103 Printf.eprintf "[DEBUG] Fastcgi_record.read: Reading %d-byte header\n%!" fcgi_header_len; 104 let header = Eio.Buf_read.take fcgi_header_len buf_read in 105 106 (* Parse header fields *) 107 let version = Char.code header.[0] in 108 let record_type_int = Char.code header.[1] in 109 let request_id = (Char.code header.[2] lsl 8) lor (Char.code header.[3]) in 110 let content_length = (Char.code header.[4] lsl 8) lor (Char.code header.[5]) in 111 let padding_length = Char.code header.[6] in 112 let _reserved = Char.code header.[7] in 113 114 Printf.eprintf "[DEBUG] Fastcgi_record.read: Header parsed - version=%d, type=%d, id=%d, content_len=%d, padding=%d\n%!" 115 version record_type_int request_id content_length padding_length; 116 117 (* Validate version *) 118 if version <> fcgi_version_1 then invalid_version version; 119 120 (* Convert record type *) 121 let record_type = record_of_int record_type_int in 122 Printf.eprintf "[DEBUG] Fastcgi_record.read: Record type = %s\n%!" 123 (Format.asprintf "%a" pp_record record_type); 124 125 (* Read content *) 126 let content = 127 if content_length = 0 then ( 128 Printf.eprintf "[DEBUG] Fastcgi_record.read: No content to read (length=0)\n%!"; 129 "" 130 ) else ( 131 Printf.eprintf "[DEBUG] Fastcgi_record.read: Reading %d bytes of content\n%!" content_length; 132 let c = Eio.Buf_read.take content_length buf_read in 133 Printf.eprintf "[DEBUG] Fastcgi_record.read: Successfully read %d bytes\n%!" (String.length c); 134 c 135 ) 136 in 137 138 (* Skip padding *) 139 if padding_length > 0 then ( 140 Printf.eprintf "[DEBUG] Fastcgi_record.read: Skipping %d bytes of padding\n%!" padding_length; 141 ignore (Eio.Buf_read.take padding_length buf_read) 142 ); 143 144 let record = { version; record_type; request_id; content } in 145 Printf.eprintf "[DEBUG] Fastcgi_record.read: Complete record = %s\n%!" 146 (Format.asprintf "%a" (pp ~max_content_len:50) record); 147 record 148 149let write buf_write record = 150 let content_length = String.length record.content in 151 152 (* Calculate padding for 8-byte alignment *) 153 let padding_length = (8 - (content_length land 7)) land 7 in 154 155 (* Create and write header *) 156 let header = Bytes.create fcgi_header_len in 157 Bytes.set_uint8 header 0 record.version; 158 Bytes.set_uint8 header 1 (record_to_int record.record_type); 159 Bytes.set_uint16_be header 2 record.request_id; 160 Bytes.set_uint16_be header 4 content_length; 161 Bytes.set_uint8 header 6 padding_length; 162 Bytes.set_uint8 header 7 0; (* reserved *) 163 164 Eio.Buf_write.string buf_write (Bytes.to_string header); 165 166 (* Write content *) 167 if content_length > 0 then 168 Eio.Buf_write.string buf_write record.content; 169 170 (* Write padding *) 171 if padding_length > 0 then 172 Eio.Buf_write.string buf_write (String.make padding_length '\000') 173 174let create ~version ~record ~request_id ~content = 175 { version; record_type = record; request_id; content } 176 177module KV = struct 178 type t = (string * string) list 179 180 let empty = [] 181 182 let add key value kvs = (key, value) :: kvs 183 184 let remove key kvs = List.filter (fun (k, _) -> k <> key) kvs 185 186 let cardinal kvs = List.length kvs 187 188 let find key kvs = 189 try List.assoc key kvs 190 with Not_found -> raise Not_found 191 192 let find_opt key kvs = 193 try Some (List.assoc key kvs) 194 with Not_found -> None 195 196 let to_seq kvs = List.to_seq kvs 197 198 let of_seq seq = List.of_seq seq 199 200 (** Helper functions for length encoding/decoding *) 201 let is_long_length first_byte = first_byte land 0x80 <> 0 202 203 let decode_short_length first_byte = first_byte 204 205 let decode_long_length buf pos = 206 check_bounds buf pos 4 "reading 4-byte length"; 207 let first_byte = Char.code buf.[pos] in 208 ((first_byte land 0x7f) lsl 24) lor 209 ((Char.code buf.[pos + 1]) lsl 16) lor 210 ((Char.code buf.[pos + 2]) lsl 8) lor 211 (Char.code buf.[pos + 3]) 212 213 let encode_length len = 214 if len <= 127 then 215 String.make 1 (Char.chr len) 216 else 217 let b = Bytes.create 4 in 218 Bytes.set_int32_be b 0 (Int32.logor (Int32.of_int len) 0x80000000l); 219 Bytes.to_string b 220 221 let decode_length buf pos = 222 check_bounds buf pos 1 "reading length"; 223 let first_byte = Char.code buf.[pos] in 224 if is_long_length first_byte then 225 (* Four byte length *) 226 let len = decode_long_length buf pos in 227 (len, pos + 4) 228 else 229 (* Single byte length *) 230 (decode_short_length first_byte, pos + 1) 231 232 let encode kvs = 233 let buf = Buffer.create 256 in 234 List.iter (fun (key, value) -> 235 let key_len = String.length key in 236 let value_len = String.length value in 237 Buffer.add_string buf (encode_length key_len); 238 Buffer.add_string buf (encode_length value_len); 239 Buffer.add_string buf key; 240 Buffer.add_string buf value 241 ) kvs; 242 Buffer.contents buf 243 244 (** Extract key-value pair from buffer at given position *) 245 let extract_kv_pair content pos name_len value_len = 246 check_bounds content pos (name_len + value_len) "reading key-value data"; 247 let name = String.sub content pos name_len in 248 let value = String.sub content (pos + name_len) value_len in 249 (name, value) 250 251 let decode content = 252 let len = String.length content in 253 let rec loop pos acc = 254 if pos >= len then 255 List.rev acc 256 else 257 (* Read name length *) 258 let name_len, pos = decode_length content pos in 259 (* Read value length *) 260 let value_len, pos = decode_length content pos in 261 262 (* Extract name and value *) 263 let name, value = extract_kv_pair content pos name_len value_len in 264 265 loop (pos + name_len + value_len) ((name, value) :: acc) 266 in 267 loop 0 [] 268 269 let read buf_read = 270 (* For reading from a stream, we need to determine how much data to read. 271 Since we're in a record context, we should read all available data 272 until we hit the end of the record content. *) 273 let content = Eio.Buf_read.take_all buf_read in 274 decode content 275 276 let write buf_write kvs = 277 let encoded = encode kvs in 278 Eio.Buf_write.string buf_write encoded 279 280 let pp ppf kvs = 281 Format.fprintf ppf "@[<2>[@["; 282 let first = ref true in 283 List.iter (fun (key, value) -> 284 if not !first then Format.fprintf ppf ";@ "; 285 first := false; 286 Format.fprintf ppf "(%S, %S)" key value 287 ) kvs; 288 Format.fprintf ppf "@]]@]" 289end