Accepting HTML <form> data sent with POST
How to write a CGI script (in Ruby) to accept files uploaded with POST.
~user/cgi-bin
The first thing that I needed to do in this case was enable CGI processing for the cgi-bin directory in my public_html directory.
Something like the following added to /etc/apache/httpd.conf seemed to do the trick:
<DirectoryMatch ^/home/.*/public_html/cgi-bin>
AllowOverride None
Options ExecCGI
Order allow,deny
allow from all
SetHandler cgi-script
</DirectoryMatch>
The upload form
Before the user can upload a file, they’ll need to use an HTML form to specify the file to upload. This, again, is quite easy:
<HTML>
<HEAD><TITLE>Upload File</TITLE>
<LINK href="/~roger/upload.css" rel="stylesheet" type="text/css">
</HEAD>
<BODY>
<H1>Upload File</H1>
<P>
<FORM METHOD="post" ENCTYPE="multipart/form-data" ACTION="cgi-bin/upload2">
<FIELDSET><LEGEND>Filename</LEGEND>
<TABLE>
<TR><TD>
<INPUT name="upload_file" size="40" TYPE="file">
</TD></TR>
<TR><TD align="right">
<INPUT TYPE="submit" value="Upload File">
</TD></TR>
</TABLE>
</FIELDSET>
</FORM>
</P>
</BODY>
</HTML>
The interesting part in here is the <FORM METHOD="post" ENCTYPE="multipart/form-data" ACTION="cgi-bin/upload2">. Normally, when you specify “GET” in METHOD, the values in the form are appended to the URL, after a question mark.
When you’re sending a file, it’ll no longer fit, so we need to use POST, which packages the values into the request body. Note that the HTML documentation states that you should only use GET for idempotent queries; that is: issuing the same query twice will not have a lasting effect on the state of the universe.
Because we’re sending multiple values, we need to specify ENCTYPE="multipart/form-data", which prescribes a certain formatting of the data. The ACTION tells the browser which URL to POST the data to. Note that the form is outside the cgi-bin directory, otherwise Apache will try to run it, so we’ve had to specify a relative path to the upload script.
A common tactic is to have the upload script respond differently depending on whether it’s being accessed with GET or with POST. Maybe I’ll come back to this in a later article.
Using CGI.rb to handle the POST data
Something like this:
#!/usr/bin/env ruby
require "cgi"
require "ftools"
require "socket"
def showFileStats(html, tmp)
html.table {
html.tr {
html.td { "Original Filename:" } +
html.td { tmp.original_filename }
} +
html.tr {
html.td { "Local Path:" } +
html.td { tmp.local_path }
} +
html.tr {
html.td { "Local Path:" } +
html.td { tmp.content_type }
} +
html.tr {
html.td { "File Size:" } +
html.td { tmp.stat.size.to_s }
}
}
end
def showFileContents(html, tmp)
html.pre { tmp.read }
end
def copyFile(html, tmp)
toName = "/tmp/%d.M%dP%d.%s" %
[ Time.now.to_i, Time.now.usec, $$, Socket.gethostname ]
if File.syscopy(tmp.local_path, toName) then
html.p { "File " + html.code { tmp.local_path } +
" successfully copied to " + html.code { toName } }
else
html.p { "Failed to copy " + html.code { tmp.local_path } +
" to " + html.code { toName } }
end
end
def showHead(html)
html.head {
html.title { "Uploaded File" } +
html.link("rel"=>"stylesheet",
"type"=>"text/css",
"href"=>"/upload.css")
}
end
def showBody(query, html)
html.body {
value = query['upload_file'][0]
if value
html.h1 { "Uploaded File" } +
showFileStats(html, value) +
html.hr +
# showFileContents(html, value) +
# html.hr +
copyFile(html, value)
else
html.h1 { "Error" } +
html.p { "You must specify a filename." } +
html.p { "Press the Back button " +
"on your browser and try again" }
end
}
end
query = CGI.new
html = CGI.new("html4Tr")
html.out {
CGI.pretty (
html.html {
showHead(html) +
showBody(query, html)
}
)
}
This script accepts the file data attached to the upload_file field and saves it in /tmp, generating a unique filename for it.
Security Considerations
There’s nothing in this script stopping anyone from uploading anything to your /tmp directory. They can easily cause a denial of service attack with this. Resolving this is outside the scope of this article.