--- /dev/null
+(* $Id$
+ * ----------------------------------------------------------------------
+ *
+ *)
+
+exception Resources_exceeded
+
+type argument_processing = Memory | File | Automatic;;
+
+type argument =
+ { mutable arg_name : string;
+ mutable arg_processing : argument_processing;
+ mutable arg_buf_value : Buffer.t;
+ mutable arg_mem_value : string option;
+ (* Here, the value is stored if it must be kept in memory *)
+ mutable arg_disk_value : string Weak.t;
+ (* This component is used iff arg_mem_value = None. The
+ * weak array has a length of 1, and the single element stores
+ * the value (if any).
+ *)
+ mutable arg_file : string option;
+ (* The filename of the temporary file storing the value *)
+ mutable arg_fd : out_channel option;
+ (* The file descriptor of the temp file (if open) *)
+ mutable arg_mimetype : string;
+ mutable arg_filename : string option;
+ mutable arg_header : (string * string) list;
+ (* For the last three components, see the description of the
+ * corresponding functions in the mli file.
+ *)
+ }
+;;
+
+type workaround =
+ Work_around_MSIE_Content_type_bug
+ | Work_around_backslash_bug
+;;
+
+type config =
+ { maximum_content_length : int;
+ how_to_process_arguments : argument -> argument_processing;
+ tmp_directory : string;
+ tmp_prefix : string;
+ workarounds : workaround list;
+ }
+;;
+
+
+let print_argument arg =
+ Format.printf
+ "<CGIARG name=%s filename=%s mimetype=%s store=%s>"
+ arg.arg_name
+ (match arg.arg_filename with None -> "*" | Some n -> n)
+ arg.arg_mimetype
+ (match arg.arg_file with None -> "Memory" | Some n -> n)
+;;
+
+
+let encode = Netencoding.Url.encode ;;
+let decode = Netencoding.Url.decode ;;
+
+
+
+let url_split_re =
+ Str.regexp "[&=]";;
+
+
+let mk_url_encoded_parameters nv_pairs =
+ String.concat "&"
+ (List.map
+ (fun (name,value) ->
+ let name_encoded = Netencoding.Url.encode name in
+ let value_encoded = Netencoding.Url.encode value in
+ name_encoded ^ "=" ^ value_encoded
+ )
+ nv_pairs
+ )
+;;
+
+
+let dest_url_encoded_parameters parstr =
+
+ let rec parse_after_amp tl =
+ match tl with
+ Str.Text name :: Str.Delim "=" :: Str.Text value :: tl' ->
+ (Netencoding.Url.decode name,
+ Netencoding.Url.decode value) :: parse_next tl'
+ | Str.Text name :: Str.Delim "=" :: Str.Delim "&" :: tl' ->
+ (Netencoding.Url.decode name, "") :: parse_after_amp tl'
+ | Str.Text name :: Str.Delim "=" :: [] ->
+ [Netencoding.Url.decode name, ""]
+ | _ ->
+ failwith "Cgi.dest_url_encoded_parameters"
+ and parse_next tl =
+ match tl with
+ [] -> []
+ | Str.Delim "&" :: tl' ->
+ parse_after_amp tl'
+ | _ ->
+ failwith "Cgi.dest_url_encoded_parameters"
+ in
+ let toklist = Str.full_split url_split_re parstr in
+ match toklist with
+ [] -> []
+ | _ -> parse_after_amp toklist
+;;
+
+
+let mk_form_encoded_parameters ntv_triples =
+ failwith "Cgi.mk_form_encoded_parameters: not implemented";;
+
+
+let dest_parameter_header header options =
+ let get_name s =
+ (* s is: form-data; ... name="fieldname" ...
+ * Extract "fieldname"
+ *)
+ try
+ let tok, params = Mimestring.scan_value_with_parameters s options in
+ List.assoc "name" params
+ with
+ Not_found ->
+ failwith "Cgi.dest_form_encoded_parameters"
+ | Failure "Mimestring.scan_value_with_parameters" ->
+ failwith "Cgi.dest_form_encoded_parameters"
+ in
+
+ let get_filename s =
+ (* s is: form-data; ... filename="fieldname" ...
+ * Extract "fieldname"
+ *)
+ try
+ let tok, params = Mimestring.scan_value_with_parameters s options in
+ Some(List.assoc "filename" params)
+ with
+ Not_found ->
+ None
+ | Failure "Mimestring.scan_value_with_parameters" ->
+ failwith "Cgi.dest_form_encoded_parameters"
+ in
+
+ let mime_type =
+ try List.assoc "content-type" header
+ with Not_found -> "text/plain" in (* the default *)
+
+ let content_disposition =
+ try List.assoc "content-disposition" header
+ with
+ Not_found ->
+ failwith "Cgi.dest_form_encoded_parameters: no content-disposition"
+ in
+
+ let name = get_name content_disposition in
+ let filename = get_filename content_disposition in
+
+ name, mime_type, filename
+;;
+
+
+let dest_form_encoded_parameters parstr ~boundary config =
+ let options =
+ if List.mem Work_around_backslash_bug config.workarounds then
+ [ Mimestring.No_backslash_escaping ]
+ else
+ []
+ in
+ let parts =
+ Mimestring.scan_multipart_body_and_decode
+ parstr 0 (String.length parstr) boundary in
+ List.map
+ (fun (params, value) ->
+
+ let name, mime_type, filename = dest_parameter_header params options in
+ { arg_name = name;
+ arg_processing = Memory;
+ arg_buf_value = Buffer.create 1;
+ arg_mem_value = Some value;
+ arg_disk_value = Weak.create 1;
+ arg_file = None;
+ arg_fd = None;
+ arg_mimetype = mime_type;
+ arg_filename = filename;
+ arg_header = params;
+ }
+
+ )
+ parts
+;;
+
+
+let make_temporary_file config =
+ (* Returns (filename, out_channel). *)
+ let rec try_creation n =
+ try
+ let fn =
+ Filename.concat
+ config.tmp_directory
+ (config.tmp_prefix ^ "-" ^ (string_of_int n))
+ in
+ let fd =
+ open_out_gen
+ [ Open_wronly; Open_creat; Open_excl; Open_binary ]
+ 0o666
+ fn
+ in
+ fn, fd
+ with
+ Sys_error m ->
+ (* This does not look very intelligent, but it is the only chance
+ * to limit the number of trials.
+ *)
+ if n > 1000 then
+ failwith ("Cgi: Cannot create temporary file: " ^ m);
+ try_creation (n+1)
+ in
+ try_creation 0
+;;
+
+
+let dest_form_encoded_parameters_from_netstream s ~boundary config =
+ let parts = ref [] in
+ let options =
+ if List.mem Work_around_backslash_bug config.workarounds then
+ [ Mimestring.No_backslash_escaping ]
+ else
+ []
+ in
+
+ let create header =
+ (* CALLBACK for scan_multipart_body_from_netstream *)
+ let name, mime_type, filename = dest_parameter_header header options in
+ let p0 =
+ { arg_name = name;
+ arg_processing = Memory;
+ arg_buf_value = Buffer.create 80;
+ arg_mem_value = None;
+ arg_disk_value = Weak.create 1;
+ arg_file = None;
+ arg_fd = None;
+ arg_mimetype = mime_type;
+ arg_filename = filename;
+ arg_header = header;
+ }
+ in
+ let pr = config.how_to_process_arguments p0 in
+ let p = { p0 with arg_processing = pr } in
+ if pr = File then begin
+ let fn, fd = make_temporary_file config in
+ p.arg_file <- Some fn;
+ p.arg_fd <- Some fd;
+ p.arg_mem_value <- None;
+ end;
+ p
+ in
+
+ let add p s k n =
+ (* CALLBACK for scan_multipart_body_from_netstream *)
+ if (p.arg_processing = Automatic) &&
+ (Buffer.length (p.arg_buf_value) >= Netstream.block_size s) then begin
+ (* This is a LARGE argument *)
+ p.arg_processing <- File;
+ let fn, fd = make_temporary_file config in
+ p.arg_file <- Some fn;
+ p.arg_fd <- Some fd;
+ p.arg_mem_value <- None;
+ output_string fd (Buffer.contents p.arg_buf_value);
+ p.arg_buf_value <- Buffer.create 1;
+ end;
+
+ match p.arg_processing with
+ (Memory|Automatic) ->
+ Buffer.add_substring
+ p.arg_buf_value
+ (Netbuffer.unsafe_buffer (Netstream.window s))
+ k
+ n
+ | File ->
+ let fd = match p.arg_fd with Some fd -> fd | None -> assert false in
+ output
+ fd
+ (Netbuffer.unsafe_buffer (Netstream.window s))
+ k
+ n;
+ in
+
+ let stop p =
+ (* CALLBACK for scan_multipart_body_from_netstream *)
+ begin match p.arg_processing with
+ (Memory|Automatic) ->
+ p.arg_mem_value <- Some (Buffer.contents p.arg_buf_value);
+ p.arg_buf_value <- Buffer.create 1;
+ | File ->
+ let fd = match p.arg_fd with Some fd -> fd | None -> assert false in
+ close_out fd;
+ p.arg_mem_value <- None
+ end;
+ parts := p :: !parts
+ in
+
+ Mimestring.scan_multipart_body_from_netstream
+ s
+ boundary
+ create
+ add
+ stop;
+
+ List.rev !parts
+;;
+
+
+let getenv name =
+ try Sys.getenv name with Not_found -> "";;
+
+(* getenv:
+ * We use this getenv instead of Sys.getenv. The CGI specification does not
+ * say anything about what should happen if a certain environment variable
+ * is not set.
+ * Some servers initialize the environment variable to the empty string if
+ * it is not applicable, some servers do not set the variable at all. Because
+ * of this, unset variables are always reported as empty variables.
+ *
+ * This is especially a problem with QUERY_STRING.
+ *)
+
+let mk_simple_arg ~name v =
+ { arg_name = name;
+ arg_processing = Memory;
+ arg_buf_value = Buffer.create 1;
+ arg_mem_value = Some v;
+ arg_disk_value = Weak.create 0;
+ arg_file = None;
+ arg_fd = None;
+ arg_mimetype = "text/plain";
+ arg_filename = None;
+ arg_header = [];
+ }
+;;
+
+let mk_memory_arg ~name ?(mime = "text/plain") ?filename ?(header = []) v =
+ { arg_name = name;
+ arg_processing = Memory;
+ arg_buf_value = Buffer.create 1;
+ arg_mem_value = Some v;
+ arg_disk_value = Weak.create 0;
+ arg_file = None;
+ arg_fd = None;
+ arg_mimetype = mime;
+ arg_filename = filename;
+ arg_header = header;
+ }
+;;
+
+let mk_file_arg
+ ~name ?(mime = "text/plain") ?filename ?(header = []) v_filename =
+ let v_abs_filename =
+ if Filename.is_relative v_filename then
+ Filename.concat (Sys.getcwd()) v_filename
+ else
+ v_filename
+ in
+ { arg_name = name;
+ arg_processing = File;
+ arg_buf_value = Buffer.create 1;
+ arg_mem_value = None;
+ arg_disk_value = Weak.create 0;
+ arg_file = Some v_abs_filename;
+ arg_fd = None;
+ arg_mimetype = mime;
+ arg_filename = filename;
+ arg_header = header;
+ }
+;;
+
+
+let get_content_type config =
+ (* Get the environment variable CONTENT_TYPE; if necessary apply
+ * workarounds for browser bugs.
+ *)
+ let content_type = getenv "CONTENT_TYPE" in
+ let user_agent = getenv "HTTP_USER_AGENT" in
+ let eff_content_type =
+ if Str.string_match (Str.regexp ".*MSIE") user_agent 0 &&
+ List.mem Work_around_MSIE_Content_type_bug config.workarounds
+ then begin
+ (* Microsoft Internet Explorer: When used with SSL connections,
+ * this browser sometimes produces CONTENT_TYPEs like
+ * "multipart/form-data; boundary=..., multipart/form-data; boundary=..."
+ * Workaround: Throw away everything after ", ".
+ *)
+ if Str.string_match (Str.regexp "\\([^,]*boundary[^,]*\\), .*boundary")
+ content_type 0
+ then
+ Str.matched_group 1 content_type
+ else
+ content_type
+ end
+ else
+ content_type
+ in
+ eff_content_type
+;;
+
+
+let really_parse_args config =
+ let make_simple_arg (n,v) = mk_simple_arg n v in
+
+ match getenv "REQUEST_METHOD" with
+ ("GET"|"HEAD") ->
+ List.map
+ make_simple_arg
+ (dest_url_encoded_parameters(getenv "QUERY_STRING"))
+
+ | "POST" ->
+ let n =
+ try
+ int_of_string (getenv "CONTENT_LENGTH")
+ with
+ _ -> failwith "Cgi.parse_arguments"
+ in
+ if n > config.maximum_content_length then
+ raise Resources_exceeded;
+ begin
+ let mime_type, params =
+ Mimestring.scan_mime_type(get_content_type config) [] in
+ match mime_type with
+ "application/x-www-form-urlencoded" ->
+ let buf = String.create n in
+ really_input stdin buf 0 n;
+ List.map
+ make_simple_arg
+ (dest_url_encoded_parameters buf)
+ | "multipart/form-data" ->
+ let boundary =
+ try
+ List.assoc "boundary" params
+ with
+ Not_found ->
+ failwith "Cgi.parse_arguments"
+ in
+ (* -------------------------------------------------- DEBUG
+ let f = open_out "/tmp/cgiout" in
+ output_string f buf;
+ close_out f;
+ * --------------------------------------------------
+ *)
+ dest_form_encoded_parameters_from_netstream
+ (Netstream.create_from_channel stdin (Some n) 4096)
+ boundary
+ config
+ | _ ->
+ failwith ("Cgi.parse_arguments: unknown content-type " ^ mime_type)
+ end
+ | _ ->
+ failwith "Cgi.parse_arguments: unknown method"
+
+let parsed = ref None;; (* protected by lock/unlock *)
+
+let lock = ref (fun () -> ());;
+let unlock = ref (fun () -> ());;
+
+let init_mt new_lock new_unlock =
+ lock := new_lock;
+ unlock := new_unlock
+;;
+
+let protect f =
+ !lock();
+ try
+ let r = f() in
+ !unlock();
+ r
+ with
+ x ->
+ !unlock();
+ raise x
+;;
+
+let parse_arguments config =
+ protect
+ (fun () ->
+ match !parsed with
+ Some _ -> ()
+ | None ->
+ parsed := Some (List.map
+ (fun arg -> arg.arg_name, arg)
+ (really_parse_args config))
+ )
+;;
+
+let arguments () =
+ protect
+ (fun () ->
+ match !parsed with
+ Some plist -> plist
+ | None ->
+ failwith "Cgi.arguments"
+ )
+;;
+
+let set_arguments arglist =
+ protect
+ (fun () ->
+ parsed := Some (List.map
+ (fun arg -> arg.arg_name, arg)
+ arglist)
+ )
+;;
+
+let default_config =
+ { maximum_content_length = max_int;
+ how_to_process_arguments = (fun _ -> Memory);
+ tmp_directory = "/var/tmp";
+ tmp_prefix = "cgi-";
+ workarounds = [ Work_around_MSIE_Content_type_bug;
+ Work_around_backslash_bug;
+ ]
+ }
+;;
+
+let arg_value arg =
+ match arg.arg_mem_value with
+ None ->
+ begin
+ match Weak.get arg.arg_disk_value 0 with
+ None ->
+ begin
+ match arg.arg_file with
+ None ->
+ failwith "Cgi.arg_value: no value present"
+ | Some filename ->
+ let fd = open_in_bin filename in
+ try
+ let len = in_channel_length fd in
+ let s = String.create len in
+ really_input fd s 0 len;
+ Weak.set arg.arg_disk_value 0 (Some s);
+ close_in fd;
+ s
+ with
+ any -> close_in fd; raise any
+ end
+ | Some v -> v
+ end
+ | Some s ->
+ s
+;;
+
+let arg_name arg = arg.arg_name;;
+let arg_file arg = arg.arg_file;;
+let arg_mimetype arg = arg.arg_mimetype;;
+let arg_filename arg = arg.arg_filename;;
+let arg_header arg = arg.arg_header;;
+
+let cleanup () =
+ protect
+ (fun () ->
+ match !parsed with
+ None -> ()
+ | Some plist ->
+ List.iter
+ (fun (name, arg) ->
+ match arg.arg_file with
+ None -> ()
+ | Some filename ->
+ (* We do not complain if the file does not exist anymore. *)
+ if Sys.file_exists filename then
+ Sys.remove filename;
+ arg.arg_file <- None
+ )
+ plist
+ )
+;;
+
+let argument name = List.assoc name (arguments());;
+let argument_value name = arg_value (argument name);;
+
+module Operators = struct
+ let ( !% ) = argument
+ let ( !$ ) = argument_value
+end;;
+
+
+let parse_args() =
+ parse_arguments default_config;
+ List.map
+ (fun (name, arg) -> name, arg_value arg)
+ (arguments())
+;;
+
+let parse_args_with_mimetypes() =
+ parse_arguments default_config;
+ List.map
+ (fun (name, arg) -> name, arg_mimetype arg, arg_value arg)
+ (arguments())
+;;
+
+let header s =
+ let t =
+ match s with
+ "" -> "text/html"
+ | _ -> s
+ in
+ print_string ("Content-type: " ^ t ^ "\n\n");
+ flush stdout
+;;
+
+
+let this_url() =
+ "http://" ^ (getenv "SERVER_NAME") ^ (getenv "SCRIPT_NAME")
+;;
+
+
+(* ======================================================================
+ * History:
+ *
+ * $Log$
+ * Revision 1.1 2000/11/17 09:57:27 lpadovan
+ * Initial revision
+ *
+ * Revision 1.8 2000/06/25 22:34:43 gerd
+ * Added labels to arguments.
+ *
+ * Revision 1.7 2000/06/25 21:40:36 gerd
+ * Added printer.
+ *
+ * Revision 1.6 2000/06/25 21:15:48 gerd
+ * Checked thread-safety.
+ *
+ * Revision 1.5 2000/05/16 22:29:36 gerd
+ * Added support for two common file upload bugs.
+ *
+ * Revision 1.4 2000/04/15 16:47:27 gerd
+ * Last minor changes before releasing 0.6.
+ *
+ * Revision 1.3 2000/04/15 13:09:01 gerd
+ * Implemented uploads to temporary files.
+ *
+ * Revision 1.2 2000/03/02 01:15:30 gerd
+ * Updated.
+ *
+ * Revision 1.1 2000/02/25 15:21:12 gerd
+ * Initial revision.
+ *
+ *
+ *)