(* Based on code written by Xavier Leroy <Xavier.Leroy@inria.fr> to handle the 2001 ICFP contest submissions. Xavier posted his code on the (now defunct) web-caml mailing list in order to be used as a testbed for an OCaml web framework. The following version differs sensibly from the original one but is still quite low-level -- for example the HTML is part of this file and the validation is done by hand. *) open Netcgi open Printf (*********************************************************************** * CONFIGURATION ***********************************************************************) let submission_dir = "/tmp/icfp-contest" (** Directory where submissions are stored. It must be writable by the owner running the CGI scripts (usually "www-data"). This directory must NOT be under the web root for security reasons. *) let tar_pgm = "/bin/tar" let gunzip_pgm = "/bin/gunzip" let unzip_pgm = "/usr/bin/unzip" let judges_email = "judges@pauillac.inria.fr" let counter_file = Filename.concat submission_dir "NEXT" (*********************************************************************** * HTML PAGES generation ***********************************************************************) let text = Netencoding.Html.encode ~in_enc:`Enc_iso88591 () (* This function encodes "<", ">", "&", double quotes, and Latin 1 characters as character entities -- e.g. text "<" = "<". *) let begin_html (out:string -> unit) ~title = out "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \ \"http://www.w3.org/TR/html4/strict.dtd\">\n"; out "<html>\n<head>\n"; out ("<title>" ^ text title ^ "</title>\n"); (*---------------------------- CSS --------------------------------*) out ("<style type=\"text/css\">\n"); out "body { background: white; color: black; }\n"; out "h1 { text-align: center; background: black; color: white; \ padding: 0.5ex; }\n"; out ".error { background: #dc3333; color: white; }\n"; out "fieldset.submit { margin-left: auto; margin-right: auto; \ width: 40%; border: 2px solid #cccccc; padding: 4px; }\n"; out "</style>\n"; out "</head>\n<body>\n"; out (sprintf "<h1>%s</h1>\n" (text title)) let end_html out = out "</body>\n</html>" (*********************************************************************** * SUBMISSION UPLOAD ***********************************************************************) (* Check if a text entry is valid (in particular non empty). *) let is_valid s = try let valid = ref false in for i = 0 to String.length s - 1 do if Char.code(s.[i]) < Char.code(' ') || s.[i] = '\127' then raise Exit else if s.[i] <> ' ' then valid := true done; !valid with Exit -> false (* Display the upload page (possibly showing errors). If the upload passes the simple validation of the arguments, return the corresponding entry struct. *) let upload_page (cgi:cgi) = let form = Buffer.create 0x1000 in let out = Buffer.add_string form in let submitted = cgi#argument_value "page" = "Submit" in let at_least_one_error = ref false in (* Construct the row with entry description [desc] and CGI parameter name [name]. If the form has been submitted, check the entries and highlight the erroneous ones. *) let tr desc ?(entry="text") ?(mandatory=true) name = let value = try let a = cgi#argument name in match a#filename with Some f -> f | None -> a#value with _ -> "" in let err = if not submitted || not mandatory || is_valid value then "" else ( at_least_one_error := true; "class=\"error\"" ) in out(sprintf " <tr><td>%s</td><td>" (text desc)); out(sprintf "<input type=%s name=%S value=%S %s/></td></tr>\n" entry name (text value) err) in tr "Team name:" "team"; tr "Program name:" "program"; tr "Language:" "language"; tr "Contact email:" "email"; tr "Resubmission?" "resubmission" ~entry:"checkbox" ~mandatory:false; tr "File upload:" "file" ~entry:"file"; tr "Length (bytes):" "size" ~mandatory:false; tr "MD5 checksum:" "md5" ~mandatory:false; if not(submitted) || !at_least_one_error then begin cgi#set_header ~cache:`No_cache (); let out = cgi#out_channel#output_string in begin_html out "ICFP 2001: Submit your entry"; out (sprintf "<form method=\"post\" action=\"%s\" \ enctype=\"multipart/form-data\">\n" (cgi#url())); out " <fieldset class=\"submit\"><legend>Submit your entry</legend>\n\ <table cellspacing=\"0\">"; if !at_least_one_error then out "<tr><td colspan=2>Please fill correctly the <i>required</i> \ fields in <span class=\"error\">in red</span>.</td></tr>\n"; out(Buffer.contents form); out "<tr><td></td><td>\ <input type=submit name=\"page\" value=\"Submit\"/></td>\n"; out "</table></fieldset>\n</form>"; end_html out; cgi#out_channel#commit_work(); raise Exit (* Need to (re)submit the form *) end (* Validate file length and MD5 ***********************************************************************) (* [arg_store] mandates that the "file" argument is stored in [`File] *) let tempfile cgi = match (cgi#argument "file")#store with `File s -> s | _ -> assert false type 'a value = No_value | Parse_error | Value of 'a let validate_file (cgi:cgi) = let tempfile = tempfile cgi in let actual_len = (Unix.stat tempfile).Unix.st_size in let actual_md5 = Digest.to_hex(Digest.file tempfile) in let claimed_len = try let s = (cgi#argument "size")#value in if s = "" then No_value else if Str.string_match (Str.regexp "[ \t]*\\([0-9]+\\)") s 0 then Value(int_of_string (Str.matched_group 1 s)) else Parse_error with | Failure _ -> Parse_error | Not_found -> No_value in let claimed_md5 = try let s = (cgi#argument "md5")#value in if s = "" then No_value else if Str.string_match (Str.regexp "[ \t]*\\([0-9A-Za-z]+\\)") s 0 then ( let md5 = String.lowercase(Str.matched_group 1 s) in if String.length md5 = 32 then Value md5 else Parse_error ) else Parse_error with Not_found -> No_value in if (match claimed_len with Value l -> l <> actual_len | _ -> false) || (match claimed_md5 with Value m -> m <> actual_md5 | _ -> false) then begin cgi#set_header ~cache:`No_cache (); let out = cgi#out_channel#output_string in begin_html out "Error"; out "<p>Error during file transmission:</p>\n"; begin match claimed_len with | Value l when l <> actual_len -> out(sprintf "<p>Actual file length is %d bytes, \ instead of %d as claimed.</P>\n" actual_len l) | _ -> () end; begin match claimed_md5 with | Value m when m <> actual_md5 -> out(sprintf "<p>Actual file MD5 is <code>%s</code>, \ instead of <code>%s</code> as claimed.</p>\n" actual_md5 m) | _ -> () end; out(sprintf "<p>Did you send the wrong file by any chance? Please <a href=\"%s\">try again</a>. If the problem persists, maybe something is wrong in this file upload script; please notify <a href=\"mailto:%s\">the contest judges</a>.</p>" (text (cgi#url ~with_query_string:`Env ())) judges_email); end_html out; cgi#out_channel#commit_work(); raise Exit end; (actual_len, claimed_len, actual_md5, claimed_md5) (* Determine and validate file type ***********************************************************************) type file_type = Tar | Tar_gz | Zip let re_tar_gz = Str.regexp_case_fold ".*\\.\\(tgz\\|tar\\.gz\\|tar\\.z\\)" let re_tar = Str.regexp_case_fold ".*\\.tar" let re_zip = Str.regexp_case_fold ".*\\.zip" let validate_file_type (cgi:cgi) = let tempfile = tempfile cgi in let name = match (cgi#argument "file")#filename with Some n -> n | None -> "(none)" in (* Check extension *) let typ = if Str.string_match re_tar_gz name 0 then Tar_gz else if Str.string_match re_tar name 0 then Tar else if Str.string_match re_zip name 0 then Zip else begin cgi#set_header ~cache:`No_cache (); let out = cgi#out_channel#output_string in begin_html out "Error"; out(sprintf "<p>File type <code>%s</code> is not recognized.</p> This script can only accept the following type of files: <ul> <li>Compressed tar files: <code>.tgz</code>, <code>.tar.gz</code>, <code>.tar.Z</code> <li>Uncompressed tar files: <code>.tar</code> <li>ZIP archives: <code>.zip</code> </ul> <p>Please provide a file of one of these types, with the appropriate extension on the file name.</p>" (text name)); out(sprintf "<a href=\"%s\">Try again</a>\n" (text (cgi#url ~with_query_string:`Env ()))); end_html out; cgi#out_channel#commit_work(); raise Exit end in (* Check the un-archiving command works *) let cmd = match typ with | Tar -> sprintf "%s tf %s >/dev/null 2>/dev/null" tar_pgm tempfile | Tar_gz -> sprintf "%s -c %s 2>/dev/null | %s tf - >/dev/null 2>/dev/null" gunzip_pgm tempfile tar_pgm | Zip -> sprintf "%s -t %s >/dev/null 2>/dev/null" unzip_pgm tempfile in (* FIXME: This is what Xavier Leroy used but it does not seem tp be reliable (tar returns 0 even if the file is not an archive!). Maybe using "file" is better. *) if Sys.command cmd <> 0 then begin cgi#set_header ~cache:`No_cache (); let out = cgi#out_channel#output_string in begin_html out "Error"; out(sprintf "<p>The file <code>%s</code> seems corrupted: <code>%s</code> does not recognize it.</p>" name (match typ with | Tar -> "tar tf" | Tar_gz -> "tar tzf" | Zip -> "unzip -t")); out("<p>Please make sure that you sent the right file and gave it a file extension that matches its contents. If the problem persists, maybe something is wrong in this file upload script; please notify <a href=\"mailto:" ^ judges_email ^ "\">the contest judges</a>.</p>"); end_html out; cgi#out_channel#commit_work(); raise Exit end; typ (* Record the submission ***********************************************************************) (* Return a string representation of the current time *) let now() = let gmt = Unix.gmtime(Unix.time()) in sprintf "%04d-%02d-%02d %02d:%02d:%02d GMT" (1900 + gmt.Unix.tm_year) (1 + gmt.Unix.tm_mon) gmt.Unix.tm_mday gmt.Unix.tm_hour gmt.Unix.tm_min gmt.Unix.tm_sec let record_submission (cgi:cgi) (actual_len, claimed_len, actual_md5, claimed_md5) filetype start_date = (* Open and lock log file -- we use it as the master lock *) let log = open_out_gen [Open_wronly; Open_creat] 0o600 (Filename.concat submission_dir "LOG") in Unix.lockf (Unix.descr_of_out_channel log) Unix.F_LOCK 0; (* Determine next submission number *) let nextid = open_in counter_file in let num = int_of_string (input_line nextid) in close_in nextid; (* Increment submission number *) let nextid = open_out counter_file in fprintf nextid "%d\n" (num + 1); close_out nextid; (* Move temp file to final file name *) let filename = match filetype with | Tar -> sprintf "%d.tar" num | Tar_gz -> sprintf "%d.tar.gz" num | Zip -> sprintf "%d.zip" num in let tempfile = tempfile cgi in Unix.link tempfile (Filename.concat submission_dir filename); (* Record submission info in log *) let current_date = now() in let hostip = cgi#environment#cgi_remote_addr in let hostname = cgi#environment#cgi_remote_host in seek_out log (out_channel_length log); fprintf log "----------------------------------------------------- Submission number: %d Submission began at: %s Submission recorded at: %s Submitted from: %s (%s) Team name: %s Program name: %s Language: %s Resubmission: %b E-mail contact: %s MD5: %s File: %s\n\n" num start_date current_date hostname hostip (cgi#argument_value "team") (cgi#argument_value "program") (cgi#argument_value "language") (cgi#argument_exists "resubmission") (cgi#argument_value "email") actual_md5 filename; close_out log; num (* Print a warm fuzzy acknowlegdement ***********************************************************************) let acknowledge (cgi:cgi) (actual_len, claimed_len, actual_md5, claimed_md5) filetype subm_num = cgi#set_header ~cache:`No_cache (); let out = cgi#out_channel#output_string in begin_html out "Submission acknowledgement"; out (sprintf "<p>Your submission to the ICFP programming contest was received in good order.</p> <blockquote> Submission number: <b>%d</b><br /> Team name: <b>%s</b><br /> Program name: <b>%s</b><br /> Language: <b>%s</b><br /> File length: <b>%d</b><br /> File MD5 checksum: <b><code>%s</code></b><br /> Resubmission? <b>%b</b><br /> E-mail contact: <b>%s</b> </blockquote> %s %s <p>A listing of the contents of the submitted file is appended below so that you can check that everything is OK.</p> <p>Thanks for your submission!</p> <hr /> <pre>" subm_num (text (cgi#argument_value "team")) (text (cgi#argument_value "program")) (text (cgi#argument_value "language")) actual_len actual_md5 (cgi#argument_exists "resubmission") (text (cgi#argument_value "email")) (match claimed_len with | Value _ -> "" | No_value -> "<p>Warning: you did not provide the length of \ the submitted file. Please make sure the length above \ is correct.</p>" | Parse_error -> "<p>Warning: the file length you provided could not be parsed. \ Please make sure the length above is correct.</p>") (match claimed_md5 with | Value _ -> "" | No_value -> "<p>Warning: you did not provide the MD5 checksum of \ the submitted file. Please make sure the checksum above \ is correct.</p>" | Parse_error -> "<p>Warning: the MD5 checksum you provided could not be parsed. \ Please make sure the checksum above is correct.</p>"); ); let cmd = let tempfile = tempfile cgi in match filetype with | Tar -> sprintf "%s tvf %s 2>&1" tar_pgm tempfile | Tar_gz -> sprintf "( %s -c %s | %s tvf - ) 2>&1" gunzip_pgm tempfile tar_pgm | Zip -> sprintf "%s -l %s 2>&1" unzip_pgm tempfile in let toc = Unix.open_process_in cmd in cgi#out_channel#output_channel(new Netchannels.input_channel toc); ignore(Unix.close_process_in toc); out " </pre>\n<hr />"; end_html out; cgi#out_channel#commit_work() let main (cgi:cgi) = let start_date = now() in upload_page cgi; (* raise Exit if entries to set/change *) let fileinfo = validate_file cgi in let filetype = validate_file_type cgi in let subm_num = record_submission cgi fileinfo filetype start_date in acknowledge cgi fileinfo filetype subm_num let () = (* Custom exn handler *) let exn_handler env f = try f() with | Netcgi_common.HTTP _ as e -> raise e (* browser error *) | Exit -> () (* Acceptable way of ending early *) | exn -> let exn = Printexc.to_string exn in env#log_error(sprintf "The script %S raised %S" Sys.argv.(0) exn); (* Send email to the judges *) let msg = Netsendmail.compose ~from_addr:("ICFP 2001", "webmaster@pauillac.inria.fr") ~to_addrs:[("Judges", judges_email)] ~subject:"Erreur script ICFP" (sprintf "Error in CGI script: uncaught exception %s\n" exn) in Netsendmail.sendmail msg; (* Generate error page *) env#send_output_header(); let out = env#out_channel#output_string in begin_html out "Internal error in CGI script"; out (sprintf "\ <p>This CGI script encountered an internal error during processing: <pre>Uncaught exception %s</pre> The judges have been notified. Try resubmitting your entry in a couple of hours. If the deadline is approaching, send it by e-mail to <a href=\"mailto:judges@pauillac.inria.fr\">the contest judges</a> (preferred format: MIME attachment). Thanks for your patience!</p>" exn); end_html out; env#out_channel#close_out() in let config = { default_config with tmp_directory = Filename.concat submission_dir ".tmp" } in (* Setup: create the dirs and files if needed *) (try Unix.mkdir submission_dir 0o770; Unix.mkdir config.tmp_directory 0o770; if not(Sys.file_exists counter_file) then ( let fh = open_out counter_file in fprintf fh "0\n"; close_out fh ) with _ -> ()); let buffered _ ch = new Netchannels.buffered_trans_channel ch in let arg_store _ name _ = if name = "file" then `File else `Memory in Netcgi_cgi.run ~config ~arg_store ~exn_handler ~output_type:(`Transactional buffered) main (* Custom exn handler *) let exn_handler env f = try f() with | Netcgi_common.HTTP _ as e -> raise e (* browser error *) | Exit -> () (* Acceptable way of ending early *) | exn -> let exn = Printexc.to_string exn in env#log_error(sprintf "The script %S raised %S" Sys.argv.(0) exn); (* Send email to the judges *) let msg = Netsendmail.compose ~from_addr:("ICFP 2001", "webmaster@pauillac.inria.fr") ~to_addrs:[("Judges", judges_email)] ~subject:"Erreur script ICFP" (sprintf "Error in CGI script: uncaught exception %s\n" exn) in Netsendmail.sendmail msg; (* Generate error page *) env#send_output_header(); let out = env#out_channel#output_string in begin_html out "Internal error in CGI script"; out (sprintf "\ <p>This CGI script encountered an internal error during processing: <pre>Uncaught exception %s</pre> The judges have been notified. Try resubmitting your entry in a couple of hours. If the deadline is approaching, send it by e-mail to <a href=\"mailto:judges@pauillac.inria.fr\">the contest judges</a> (preferred format: MIME attachment). Thanks for your patience!</p>" exn); end_html out; env#out_channel#close_out() let config = { default_config with tmp_directory = Filename.concat submission_dir ".tmp" } let () = (* Setup: create the dirs and files if needed *) (try Unix.mkdir submission_dir 0o770; Unix.mkdir config.tmp_directory 0o770; if not(Sys.file_exists counter_file) then ( let fh = open_out counter_file in fprintf fh "0\n"; close_out fh ) with _ -> ())