Hands-on How-to is a service mark of Brass Cannon LLC
 

More About CGI

A Hands-on How-toSM

from Brass Cannon Consulting

A little vague handwaving can often save hours of tedious explanation.

Example Number Two - a CGI File Downloader

In the previous example, we talked about passing a value to a script that lives in the cgi-bin directory. That script then generated html code, just like the text with tags that you would type by hand to create a web page, except that the CGI script can plug in the values you give it and thus change the page that is displayed.

The first example picked different image files based on the URL, so one small routine could serve an unlimited number of different image files.

Here's a more ambitious example. Let's say we wanted to make a Web-based file server, which would download files to the user's hard disk automatically instead of displaying them on screen.

Now, in practice you can just tell the user how to save a file, right? You right-click on the link in your browser and choose "Save link as". Or is it "Save File as"? Of course if it's a Mac you hold down the single mouse button instead of right-clicking.... hmm, forcing the browser to pop up that "save file" window may be a good idea after all!

So without further ado, here's my second CGI script. Again, it's written in Perl, sometimes called the "Swiss Army chainsaw" of the Web developer.


#!/usr/bin/perl
@params=split(/=/,$ENV{'QUERY_STRING'});
my $p0 = $params[0];
my $p1 = $params[1];

## Handle URLencodings:
$p0 =~ tr/+/ /;
$p0 =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C",hex($1))/eg;
$p0 =~ s/,/ /eg;
$p1 =~ tr/+/ /;
$p1 =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C",hex($1))/eg;
$p1 =~ s/,/ /eg;

## Eat /.. attacks:
$p0 =~ s/\/\.\.//eg;
$p1 =~ s/\/\.\.//eg;

## Force a known starting directory:
my $DD = "/home/www";

## Validate username:
my $user_dir = "$DD/$p0";
if (!(-d $user_dir)) {
 print "Content-type: text/html\r\n\r\n";
 print "<html><body bgcolor=#FFFF00>";
 print ("<br>User $p0 is not set up to download files.<br>");
 print ("Please ask admin\@example.com to set up User $p0.<br>");
 print "P0: ", $params[0], "+", $p0, " P1: ", $params[1], "+", $p1,"\r\n";
 print "</body></html>";
 die("Unknown user $p0");
}

## Kill Unix shell escape characters:
$p1 =~ s/([;<>\*\|'\$!#\(\)\[\]\{\}:'"])/\\$1/g;
## Fix up for embedded spaces in filenames:
$p1 =~ s/ /\ /;

my $user_file = $p1;
my $send_file = "$user_dir/$user_file";
if (!(-f $send_file)) {
 print "Content-type: text/html\r\n\r\n";
 print "<html><body bgcolor=#FFFF00>";
 print ("<br>No such file ($user_file)<br>");
 print "P0: ", $params[0], "+", $p0, " P1: ", $params[1], "+", $p1,"\r\n";
 print "</body></html>";
 die("No such file $p1");
}

##    Debug:
#print "Content-type: text/html\r\n\r\n";
#print "<html><body>";
#print "P0: ", $params[0], "=", $p0, " P1: ", $params[1], "=", $p1,"\r\n";
#print "<br>SF: ", $send_file, "\r\n";
#if (!(-f $send_file)) {print ("No such file ($!)<br>")}
#print "";
#1;
#}
##

my $data = "";
open(AFILE, "<$send_file");
while() { $data .= $_; }
close(AFILE);
print "Content-type: application/x-unknown\r\n";
print "Content-disposition: inline; filename= ", $user_file, "\r\n";
print "Content-length: ", length($data), "\r\n\r\n$data";

1;

Whee, wasn't that fun! Okay, what's going on here? The beginning lines should look just a little bit familiar -- we are accepting parameters, but this time we want two of them. The first will be a username, for a special directory we have set aside to hold the files we want to send. The second will be the actual filename to send. We use both of them to construct a full path to the file that will be sent, but we also save the name portion alone so when the file arrives at the user's PC, we know what to call it.

I'm not the world's greatest developer, so in order to make sure this script understood the parameters I was entering, I ran this script a few times just echoing the input back to my browser. The lines to do that are now marked as "Debug to browser" and rather than remove them, I left them in as comments, in case I ever need them again.

(There is a Perl module called CARP that includes the ability to redirect runtime errors to the browser; I'm not getting into that here, but it's worth mentioning that there are a lot of Perl and CGI resources out there. As always, the purpose of a Hands-on How-to is not to reinvent those; it's to "level the playing field" by providing some of the background material they must take for granted.)

Once I was confident that the script did indeed agree with me about which parameter was which, I turned on the next chunk of code. As in the prior example, we have to provide a MIME header, something which a web server would normally provide; but this time instead of "text/html" we tell the browser that it's about to receive something of an "unknown" type. That usually makes it save the file to disk instead of displaying it on screen. (Big thanks to Glenn Hunt for pointing out this trick!) We also have to tell the browser what filename to use -- it's up to the user to pick a local directory. Finally, we count the number of bytes because that's what the rules say we should do, and we shove the whole file down the pipe to the waiting user.

As in our previous example, I've added some traps to keep rude strangers from trying to send arbitrary files from directories other than the one we set up for this user. (I have another way to confirm the username which I'm not going to elaborate on here, but it's pretty essential to making this a "production quality" script.)

To use this in practice, I have a fancy PHP page that looks in the directory I've set up for the user, and generates links "on the fly." Each of those links is in the form "/cgi-bin/download.cgi?user=file" and clicking on one of them sends the corresponding file to the user.

There is one major flaw in this script. See the part in red? Perl is going to read in the entire file to get its size and then send it from the server's main memory. What happens if you try to send a two-hundred-megabyte file from a server with 256MB of main memory?

Nothing good, that's what.

So, I wrote a PHP version that is a little bit smarter, and uses a PHP function to get the filesize, then streams it from disk. It's also quite a bit shorter:

<?php
function sss($string)
{
  $pattern = '/(;|\||`|>|<|&|^|"|'."\n|\r|'".'|{|}|[|]|\)|\()/i';
  $string = preg_replace($pattern, '', $string);
  return $string;
}

$user = '';
$file = '';
if (isset($_GET['user'])) {
	$u = sss($_GET['user']);
}
if (isset($_GET['file'])) {
	$f = sss(addslashes(urldecode($_GET['file'])));
}
if (!$f) {
	echo "No file given";
	exit;
}
if (!$u) {
	echo "No user defined";
	exit;
}

$filename = "/home/www/".$u."/".$f;
if (! file_exists("$filename")) {
	echo "No such file $filename";
	exit;
}
header("Content-Type: application/force-download");
header("Content-Type: application/octet-stream");
header("Content-Type: application/download");
header("Content-disposition: inline; filename=\"$f\"");
header("Content-Transfer-Encoding: binary");
header("Content-Length: ".filesize($filename));
readfile("$filename"); 
exit();

Quick overview -- function "sss(string)" will strip any nasty characters out of the parameters being passed to our page. The form that calls this page must provide a couple of values as "GET" variables, those being a user and a filename. We apply the sss function to those, then make sure the user is valid (has a home directory) and that the file exists. If all is well, we tell the user's browser to download the file (as opposed to streaming it). With the right HTML headers, this could be a poor man's streaming server, something I hope to try soon.

Another interesting thing about CGI or PHP -- see where we are going to "/home/www" in the code? We can change that to put our files somewhere outside the webserver "document root," so they don't even exist as far as our webserver knows. Even if someone knows or guesses a filename, they still have to ask our download manager for it. We can add code that requires them to login, give us a credit card, whatever.


Our next installment may not be immediately useful, but it's important; it provides a warning about the darker side of CGI scripting.

You can discuss this article with the author in the Feedback section of the Brass Cannon webboard!


Google
 
Web handsonhowto.com



Hands-On 
How-To Index