A little over a year ago, I wrote a post about a PHP script I had created for protecting a download using a unique URL. The post turned out to be pretty popular, and many of the comments included requests to extend the script in useful ways. So, I’ve finally gotten around to updating the script to generate multiple URLs (up to 20) at a time, to allow different files to be associated with different keys, and to allow brief notes to be attached to the download key.
I’ve also added a simple page that prints out a list of all of the keys generated the date and time that each key was created, the filename of the download on the server that the key accesses, the number of times the key was used, and any attached note. This should make it easier to generate gobs of keys, drop them into an Excel spreadsheet, and help the files’ owner keep track of who’s getting which file, and how often.
The scripts themselves are a little more involved this time around, but the general idea is the same. A unique key is generated and combined with a URL that allows access to a single file on the server. Share the URL/key instead of the URL to the file itself to allow a visitor to download the file, but not to know the location of the file. The key will be valid for a certain length of time and number of downloads, and will stop working once the first limiting condition is met. This should prevent unauthorized downloading due to people sharing the keys.
How it works
There are six main components to this system:
- the MySQL database that holds each key, the key creation date and time, the maximum age of the key, the number of times the key has been used, the maximum times the key may be used, the file associated with the key, and the note attached to the key, if any
- a generatekey.php page that generates the keys and outputs the corresponding unique URLs
- a download.php page that accepts the key, checks its validity, and either initiates the download or rejects the key as invalid
- a report.php page that returns all of the data in the database
- a config.php file that contains variables such as number of downloads allowed, the maximum allowable age of the key, and the filenames of the downloads, along with the database connection information
- the .zip file(s) to be protected
The files, along with two example downloads, are available for download as a .zip file.
The MySQL database
Using whatever method you’re comfortable with, create a new MySQL database named “download” and add the following table:
CREATE TABLE `downloadkeys` ( `uniqueid` varchar(12) NOT NULL default '', `timestamp` INT UNSIGNED, `lifetime` INT UNSIGNED, `maxdownloads` SMALLINT UNSIGNED, `downloads` SMALLINT UNSIGNED default '0', `filename` varchar(60) NOT NULL default '', `note` varchar(255) NOT NULL default '', PRIMARY KEY (uniqueid) );
How to use the scripts
The scripts require a little setup before they’re ready to be used, so open config.php in your text editor of choice.
Change the values for
$db_name to point to your database.
Set the variable
$maxdownloads equal to the maximum number of downloads (actually, the number of page loads).
Set the variable
$lifetime equal to the keys’ viable duration in seconds (86400 seconds = 24 hours).
Set the variable
$realfilenames to the real names of actual download files on the server as a comma-separated list (this is optional; you can also use a single filename or just leave it as empty double-quotes: “”). If you have more than one file to protect, enter the names as a comma-separated list and the script will create a drop-down menu as the Filename field. If you leave the variable blank, the form will display an empty input box as the Filename field.
Set the variable
$fakefilename to anything – this is what the visitor’s file will be named when the download is initiated.
I would strongly recommend renaming generatekey.php, as anyone who can view it will be able to create unlimited numbers of keys, and worse, they’ll be able to see the filenames (if you set them in config.php). I would also recommend that the directory you put these files into, and each directory on your site (/images, /css, /js, etc.), contain an index.html file. This is a simple security measure that will prevent visitors from snooping around a directory and viewing its contents (though access to the directory contents is usually prohibited by a setting on the server).
Place all the PHP scripts and your .zip file(s) into the same directory on your server.
That’s all there is to it. Whenever you want to give someone access to the download, visit the generatekey.php page and fill out the form. It will generate a key code, save it to a database, and print out a unique link that you can copy and paste into an email or whatever. The page that the unique link points to checks to see if the key code is legitimate, then checks to see if the code is less than X hours old, then checks to see if it has been used less than X times. The visitor will get a descriptive message for the first unmet condition and the script will terminate. If all three conditions are met, the download starts automatically.
Errors and issues
Note: The download will not initiate automatically, and will actually be output as text on the page, if the download.php page is changed to send headers or any output to the browser. Be careful when making modifications or incorporating this script into another page.
Check the HTTP headers (Google for an online service that does this, or install the LiveHTTPHeaders Firefox plugin) of the download link. If the script is working correctly, you should see Content-Transfer-Encoding: binary and Content-Type: application/octet-stream in the headers. If you’re getting a page of text instead of the zip file, you’ll probably see Content-Type: text/html.
Example HTTP headers for a correctly working download
If the script is working correctly, the HTTP headers will look something like this:
HTTP/1.1 200 OK Date: Sun, 20 Jun 2010 13:31:50 GMT Server: Apache Cache-Control: must-revalidate, post-check=0, pre-check=0, private Content-Disposition: attachment; filename="bogus_download_name.zip" Content-Transfer-Encoding: binary Pragma: public Content-Length: 132 Keep-Alive: timeout=15, max=100 Connection: Keep-Alive Content-Type: application/octet-stream