Protecting a download using a unique URL
Update 6/25/09: I’ve updated the script to include a number of suggestions made in the comments. The new script supports multiple files, up to 20 URLs can be created at a time, and a brief note can be attached to each key. If these features sound useful, please check out the post Protecting multiple downloads using unique URLs.
A client asked me to develop a simple method for protecting a download (or digital product) by generating a unique URL that can be distributed to authorized users via email. The URL would contain a key that would be valid for a certain amount of time and number of downloads. The key will become invalid once the first of those conditions is exceeded. The idea is that distributing the unique URL will limit unauthorized downloads resulting from the sharing of legitimate download links.
In addition, once the key has been validated, the download starts immediately, preventing the visitor from seeing the actual location of the download file. What’s more, the filename of the download in the “Save as” dialogue box isn’t necessarily the same as the filename of the file on the server, making the file itself pretty much undiscoverable.
How it works
There are five main components to this system:
- the MySQL database that holds each key, the key creation time, and the number of times the key has been used
- the downloadkey.php page that generates the unique keys and corresponding URLs
- the download.php page that accepts the key, verifies its validity, and either initiates the download or rejects the key as invalid
- a dbconnect.php file that contains the link to the database and which is included into both of the other PHP files
- the download .zip file that is to be protected
Place all three PHP scripts and the .zip file into the same directory on your server.
The MySQL database
Using whatever method you’re comfortable with, create a new MySQL database named “download” and add the following table:
CREATE TABLE `downloadkey` (
`uniqueid` varchar(255) NOT NULL default '',
`timestamp` varchar(255) NOT NULL default '',
`downloads` varchar(255) NOT NULL default '0',
PRIMARY KEY (uniqueid)
);
The downloadkey.php page
This page generates the key, creates a URL containing the key, and writes the key to the database. Never give out the location of this page – this is for only you to access.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Download Key Generator</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<meta name="author" content="http://www.ardamis.com/" />
<style type="text/css">
#wrapper {
font: 15px Verdana, Arial, Helvetica, sans-serif;
margin: 40px 100px 0 100px;
}
.box {
border: 1px solid #e5e5e5;
padding: 6px;
background: #f5f5f5;
}
</style>
</head>
<body>
<div id="wrapper">
<h2>Download Key Generator</h2>
<?php
// A script to generate unique download keys for the purpose of protecting downloadable goods
require ('dbconnect.php');
if(empty($_SERVER['REQUEST_URI'])) {
$_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME'];
}
// Strip off query string so dirname() doesn't get confused
$url = preg_replace('/\?.*$/', '', $_SERVER['REQUEST_URI']);
$folderpath = 'http://'.$_SERVER['HTTP_HOST'].'/'.ltrim(dirname($url), '/').'/';
// Generate the unique download key
$key = uniqid(md5(rand()));
// echo "key: " . $key . "<br />";
// Get the activation time
$time = date('U');
// echo "time: " . $time . "<br />";
// Generate the link
echo "<p>Here's a new download link:</p>";
echo "<p><span class=\"box\">" . $folderpath . "download.php?id=" . $key . "</span></p>";
// Write the key and activation time to the database as a new row
$registerid = mysql_query("INSERT INTO downloadkey (uniqueid,timestamp) VALUES(\"$key\",\"$time\")") or die(mysql_error());
?>
<p> </p>
<p>Each time you refresh this page, a unique download key is generated and saved to a database. Copy and paste the download link into an email to allow the recipient access to the download.</p>
<p>This key will be valid for a certain amount of time and number of downloads, which can be set in the download.php script. The key will expire and no longer be usable when the first of these conditions is exceeded.</p>
<p>The download page has been written to force the browser to begin the download immediately. This will prevent the recipient of the email from discovering the location of the actual download file.</p>
</div>
</body>
</html>
The download.php page
The URL generated by downloadkey.php points to this page. It contains the key validation script and then forces the browser to begin the download if it finds the key is valid.
<?php
// Set the maximum number of downloads (actually, the number of page loads)
$maxdownloads = "2";
// Set the key's viable duration in seconds (86400 seconds = 24 hours)
$maxtime = "86400";
require ('dbconnect.php');
if(get_magic_quotes_gpc()) {
$id = stripslashes($_GET['id']);
}else{
$id = $_GET['id'];
}
// Get the key, timestamp, and number of downloads from the database
$query = sprintf("SELECT * FROM downloadkey WHERE uniqueid= '%s'",
mysql_real_escape_string($id, $link));
$result = mysql_query($query) or die(mysql_error());
$row = mysql_fetch_array($result);
if (!$row) {
echo "The download key you are using is invalid.";
}else{
$timecheck = date('U') - $row['timestamp'];
if ($timecheck >= $maxtime) {
echo "This key has expired (exceeded time allotted).<br />";
}else{
$downloads = $row['downloads'];
$downloads += 1;
if ($downloads > $maxdownloads) {
echo "This key has expired (exceeded allowed downloads).<br />";
}else{
$sql = sprintf("UPDATE downloadkey SET downloads = '".$downloads."' WHERE uniqueid= '%s'",
mysql_real_escape_string($id, $link));
$incrementdownloads = mysql_query($sql) or die(mysql_error());
// Debug echo "Key validated.";
// Force the browser to start the download automatically
/*
Variables:
$file = real name of actual download file on the server
$filename = new name of local download file - this is what the visitor's file will actually be called when he/she saves it
*/
ob_start();
$mm_type="application/octet-stream";
$file = "actual_download.zip";
$filename = "bogus_download_name.zip";
header("Cache-Control: public, must-revalidate");
header("Pragma: no-cache");
header("Content-Type: " . $mm_type);
header("Content-Length: " .(string)(filesize($file)) );
header('Content-Disposition: attachment; filename="'.$filename.'"');
header("Content-Transfer-Encoding: binary\n");
ob_end_clean();
readfile($file);
}
}
}
?>
The dbconnect.php script (database connection)
This is the PHP include referenced by both scripts that contains the database link.
<?php
// Connect to database "download" using: dbname , username , password
$link = mysql_connect('localhost', 'root', '') or die("Could not connect: " . mysql_error());
mysql_select_db("download") or die(mysql_error());
?>
This file will almost certainly require some editing. You will need to specify a host name for your MySQL server and a MySQL username and password in that file at mysql_connect('localhost', 'root', '') so that you can connect to the database you’ve set up. It’s extremely unlikely that your production MySQL database will be installed on localhost with a user “root” and no password.
That’s all there is to it. Whenever you want to give someone access to the download, visit the downloadkey.php page. It will generate a unique key code, save it to a database, and print out a URL that you can copy and paste into an email or whatever. The page at that URL checks to see if the key code is legit, 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.















Very, very helpful. Clear and concise, just what I needed! Thanks!
How would I allow resume through a download manager? I use a similar system and allow my tickets to be used for up to a week but I cannot get it to resume.
Hmm. That’s a good question. I’m not at all sure. I’ll look into it, though.
Clear and concise. Good snippet. Thx!
This is really useful – was looking for something like that for ages. Thanks a lot for sharing! Much appreciated.
You are THE MAN! This is the best script I have have ever found relating to digital content. If you combine this script with a solid chunk of .htaccess and .htpassword security you got yourself a top notch setup!
Thank you so much!
Listen my friend! This is great!!!
Hi,
I have tried the above code but I get the error message below:-
——————————————————————
Warning: mysql_connect() [function.mysql-connect]: Access denied for user ‘root’@'localhost’ (using password: NO) in /home/alanpotts93/public_html/hiddenfiles/dbconnect.php on line 3
Could not connect: Access denied for user ‘root’@'localhost’ (using password: NO)
——————————-
I have already created a mysql table as was instructed with the default value for the third field being a zero.
I am a novice at website building and only know html, but any help would be welcome.
Thanks in advance
Great tutorial. I have a question if you can help: I have an ebook I am selling through clickbank. How can I make it so that each time a visitor buys the ebook, when they are returned to the THANK YOU Page, the link for download is already there, unique, so they can only download the file after payment and not be able to pass it forward to others or guess the download link?
I wish I knew how to do that. I wanted to do something similar with Paypal, where a completed transaction would send the buyer to a download page, but I never figured it out.
If you figure it out, I’d be interested in hearing about how you did it.
Many thanks!
I have just enough html knowledge to maintain my site and even less php.
I was able to create the five main components and apply them to 3 zip files I need to protect. A little help with the database settings from the support folks at Host Rocket and I was up and running.
Thanks again, JP
To Val:
You can integrate your thankyou.php page with the downloadkey.php so that each successful payment will redirected them to your thank you page where the unique id is already created.
I tested this on my test server running lighttpd and it works perfectly. Now I will find ways how to make it an expiring URL with embedded details to be use on my email marketing campaigns…
i am also facing the same problem as joe
please do help me
please some reply
I suspect that you need to edit the “dbconnect.php” file so that it uses YOUR database information.
You will need to specify a host name, a username and a password in that file so that you can connect to the database you’ve set up. It’s extremely unlikely that your production MySQL database will be installed on localhost with a user root and no password.
ardamis
i have already done this
i have write my dbname,my login or pass in the position of * but nothing happen
now what reply
Nice work mate works nicely… I would like to be able to work with multiple zip file with a unique URL. So I can have different directories of zip files then when the user click on the link it will point to a particular zip file… how can I associate a link to a particular zip file. Would this be easy to achieve…?
Thanks…
This piece of code is a work of art. Thanks for sharing.
This is a LIFESAVER. I have been trying to figure something like this out for months if not years now and you present it in a simple php script that I easily edited to suit my needs.
Thanks!
Remy
Just what i needed but i need the ability to have one code generator with the ability for multiple files. IE enter the URL to be scrambled and it creates one for that URL only. If you get my drift. How can i edit it to do that? Can someone help? email me at webdev (at) chemical-productions (dot) co (dot) uk if anyone knows how to do that.
I have a mobile download site where user can download without login or register but i wan to protect all downloadable files that only registered users can download. Anyone please help with thanks.
This is a fantastic solution for unique links. I do have a question. I got this error
Access denied for user ‘XXXXX’@'XXXXX’ to database ‘download’. I have the dbconnect.php file with “localhost” still. Should that be changed?
It sounds like localhost is the correct location of the database, but maybe the username or password is incorrect.
I’ve written a simple script that will help you test these things:
http://www.ardamis.com/2008/05/26/a-php-script-for-testing-a-mysql-database-connection/
nice i ll use it
Are there issues with signed files? I’m trying this with a couple of downloads–one is a zip file and one is a Windows .MSI. In both cases, the resulting download file shows as corrupt. Not sure what is happening here.
Hmm, I’m not aware of any issues. Can you provide a link to a file that I can use to test?
I had the same problem as Manu and Joe Black until I realized that I needed to change:
mysql_select_db("download")the download part in the dbconnect. I changed it to the name of my database and it now works. Seems obvious in hindsight. Maybe others are making the same mistake I did. Great Script!
Very cool! One question – how do I protect the file directory where the download resides so that no one can access it unless they have the download code? Obviously legitimate downloads should be possible, but were someone to figure out the directory where the files live they could just get them directly from there, no?
I’m glad you like it.
The script allows you to use anything as the file name on the server, and then show something different to the user in the download dialogue box. This prevents even legitimate users from knowing the location of the file on the server.
For example, the file on the server may be named
k6Fw9Ik.zipwhile the file name presented to the user may bedownload.zip.If you use a suitably long string of random alpha-numeric characters (I use https://www.grc.com/passwords.htm) as the file name, the chances of someone successfully guessing it are astronomical.
I would strongly recommend renaming downloadkey.php, though, as knowing the location of that page allows unlimited access to the file.
If you want to prevent people from viewing the folder contents, this is most easily done by just dropping an index file into the folder. It’s possible to restrict the listing of folder contents via a server setting, too, and this is done by default by many commercial hosting companies.
If you’re on Apache, you could also use htpassword to limit access to a folder, but this level of security probably isn’t necessary.
An example folder structure that should work for most people:
/
/index.html
/download.php
/downloadkey-00.php
/the-download-file.zip
Hey, I absolutely love your code. I have a question though, and I have a feeling other people might be able to use this.
What I need is a way to generate a long list of codes, that way I can enter them all into an excel file and hand them out individually. So, without reloading the page every time I need a new code, I wonder if you could help me modify the download key generator so that it repeats its process X number of times.
That way, I could get a big block of download keys to work with.
Nice Script…. can we do something like email this link to the visitor????
Right, that’s the idea. You can then email the link to someone.
I have this script functioning perfectly, it is very easy to install and use. Thank you!
I would love for their to be a way to mark each key created with a simple text note, so when I create a key for a special download and email it to ten different people, I can mark the key with the person’s name or email address for tracking it easily.
I am using this script to keep track of a new album release for a band, and we are emailing to specific industry contacts. We need to track who has downloaded it and how many times. Currently, the database does show how many times it has been downloaded.
However, in order to keep track of whose link is whose, I am maintaining a google spreadsheet where I have three columns: unique id, person’s name, email address. I am updating this google doc manually but I’m sure there’s a more elegant way to do this with the database, but I am fairly new to SQL commands, and still messing around on how to write the SQL code to do this with your script.
I was thinking about adding another column to the downloadkey database called ‘note’ and manually insert the information from my phpAdmin page provided by my hosting provider. Then I could export the table to PDF and see my notes next to each key.
Is there an easier way to do this or am I pretty close?
Thanks again for anyone’s feedback, I think that others may find this answer useful as well as the only thing this snappy scipt seems to be missing is any sort of tracking/reporting.
I am getting a FATAL ERROR only at larger sized downloads.
If I replace the file with a small zip with the same name, than it works perfect.
THIS IS THE TEXT ERROR MESSAGE:
Fatal error: Allowed memory size of 33554432 bytes exhausted (tried to allocate 41400320 bytes) in /hermes/web08/b2683/pow.leberj/htdocs/store/Land/Land_p1/download.php on line 60
What am I doing wrong?
Can I edit the code to reflect a larger download?
Best Regards,
Jim
[...] 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 [...]