(* netcgi_scgi.ml Copyright (C) 2005-2006 Christophe Troestler email: Christophe.Troestler@umh.ac.be WWW: http://math.umh.ac.be/an/ This library is free software; see the file LICENSE for more information. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the file LICENSE for more details. *) (* The protocol is described at http://python.ca/nas/scgi/protocol.txt *) open Netcgi_common open Printf (* This channel reads from the descriptor, but does not forward the * close request (fd is kept open) *) class prim_in_channel fd = object(self) inherit Netchannels.input_descr fd method close_in() = () (* don't close fd *) end (* This channel writes to the descriptor, but does not forward the * close request (fd is kept open) *) class prim_out_channel fd = object(self) inherit Netchannels.output_descr fd method close_out() = () (* don't close fd *) end (* A bidirectional buffered channel for I/O *) class scgi_channel fd = let prim_in_ch = new prim_in_channel fd in let prim_out_ch = new prim_out_channel fd in object(self) inherit Netchannels.buffered_raw_in_channel ~eol:[ "\000" ] ~buffer_size:8192 prim_in_ch (* A buffered input channel. Additionally, we set that lines end with 0 bytes, so we can use enhanced_input_line to read 0-terminated strings *) inherit Netchannels.augment_raw_in_channel (* reduces additional methods like really_input to the more primitive ones from Netchannels.buffered_raw_in_channel. Note that input_line is slow, so we override it. *) method input_line = self # enhanced_input_line method input_until_colon() = (* Slow, but only used for a very short string *) let b = Buffer.create 100 in let c = ref 'X' in while !c <> ':' do c := self # input_char(); if !c <> ':' then Buffer.add_char b !c done; Buffer.contents b inherit Netchannels.buffered_raw_out_channel ~buffer_size:8192 prim_out_ch inherit Netchannels.augment_raw_out_channel (* reduces additional methods like really_output to the more primitive ones from Netchannels.buffered_raw_out_channel *) method safe_close_out() = try self # close_out() with Netchannels.Closed_channel -> () end (************************************************************************) let scgi_log_error msg = let zone = Netdate.localzone (* log local time *) in let date = Netdate.format "%c" (Netdate.create ~zone (Unix.gettimeofday())) in prerr_endline ("[" ^ date ^ "] [Netcgi_scgi] " ^ msg) (* [input_props_inheader in_obj] reads the netstring [len]":"[string]"," from the input object [in_obj] and chunk it into the key-value pairs representing the properties. *) let rec input_props_inheader_loop in_obj len props_inheader = if len < 0 then raise(HTTP(`Bad_request, "Netcgi_scgi: Bad header length")); if len = 0 then begin (* The netstring must finish with a comma *) if in_obj#input_char() <> ',' then raise(HTTP(`Bad_request, "Netcgi_scgi: The header must end with ','")); (* Add some compulsory CGI properties *) let (props, inheader) = props_inheader in (("GATEWAY_INTERFACE", "CGI/1.1") :: props, inheader) end else begin (* Note that input_line is redefined so the lines are assumed to be 0-terminated strings *) let name = in_obj#input_line() in let value = in_obj#input_line() in let len = len - String.length name - String.length value - 2 (* \000 *) in let props_inheader = update_props_inheader (name, value) props_inheader in input_props_inheader_loop in_obj len props_inheader end let input_props_inheader in_obj = (* length of the "netstring": *) let len = try int_of_string(in_obj#input_until_colon()) with End_of_file | Failure _ -> let msg = "Netcgi_scgi: Incorrect length of netstring header" in raise(HTTP(`Bad_request, msg)) in try input_props_inheader_loop in_obj len ([],[]) with | End_of_file-> raise(HTTP(`Bad_request, "EOF while reading header")) class scgi_env ?log_error ~config ~properties ~input_header out_obj : Netcgi_common.cgi_environment = object inherit cgi_environment ~config ~properties ~input_header out_obj (* Override to use the correct channel *) method log_error msg = match log_error with | None -> scgi_log_error msg | Some f -> f msg end let handle_request config output_type arg_store exn_handler f ~log fd = let scgi_ch = new scgi_channel fd in let (properties, input_header) = input_props_inheader scgi_ch in let env = new scgi_env ?log_error:log ~config ~properties ~input_header (scgi_ch :> Netchannels.out_obj_channel) in (* Now that one knows the environment, one can warn about exceptions *) try exn_handler_default env ~exn_handler (fun () -> let cgi = cgi_with_args (new cgi) env output_type (scgi_ch :> Netchannels.in_obj_channel) arg_store in (try f (cgi:Netcgi.cgi); cgi#out_channel#commit_work(); cgi#finalize() with e when config.default_exn_handler -> cgi#finalize(); raise e); None (* no "special" internal exception *) ) ~finally:(fun () -> scgi_ch#safe_close_out() (* => flush buffer; it is the user responsability to commit his work. *) ); `Conn_close_linger with | Unix.Unix_error(Unix.EPIPE,_,_) -> `Conn_close_linger | error -> `Conn_error error (* [handle_connection fd .. f] handle an accept()ed connection, reading incoming records on the file descriptor [fd] and running [f] for each incoming request. *) let handle_connection config output_type arg_store exn_handler f fd = Netlog.Debug.track_fd ~owner:"Netcgi_scgi" ~descr:("connection from " ^ try Netsys.string_of_sockaddr(Netsys.getpeername fd) with _ -> "(noaddr)") fd; let log = Some scgi_log_error in let cdir = handle_request config output_type arg_store exn_handler f ~log fd in match cdir with | `Conn_close_linger -> Unix.setsockopt_optint fd Unix.SO_LINGER (Some 15); Unix.shutdown fd Unix.SHUTDOWN_ALL; Netlog.Debug.release_fd fd; Unix.close fd | `Conn_error e -> Netlog.Debug.release_fd fd; Unix.close fd; raise e (* Othe cdirs not possible *) let run ?(config=Netcgi.default_config) ?(allow=fun _ -> true) ?(output_type=(`Direct "":Netcgi.output_type)) ?(arg_store=(fun _ _ _ -> `Automatic)) ?(exn_handler=(fun _ f -> f())) ?socket ?sockaddr ?port (* no default in the spec *) f = (* Socket to listen to *) let sock = match socket with | Some s -> s | None -> let saddr = match (sockaddr, port) with | (Some sa, _) -> sa | (None, Some p) -> Unix.ADDR_INET(Unix.inet_addr_loopback, p) | (None, None) -> invalid_arg "Netcgi_scgi.run: \ neither socket nor sockaddr or port passed" in let sock = Unix.socket (Unix.domain_of_sockaddr saddr) Unix.SOCK_STREAM 0 in Unix.setsockopt sock Unix.SO_REUSEADDR true; Unix.bind sock saddr; Unix.listen sock 5; Netlog.Debug.track_fd ~owner:"Netcgi_scgi" ~descr:("master " ^ Netsys.string_of_sockaddr saddr) sock; sock in while true do let (fd, server) = Unix.accept sock in try if allow server then handle_connection config output_type arg_store exn_handler f fd; with | e when config.default_exn_handler -> () done