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:

  1. the MySQL database that holds each key, the key creation time, and the number of times the key has been used
  2. the downloadkey.php page that generates the unique keys and corresponding URLs
  3. the download.php page that accepts the key, verifies its validity, and either initiates the download or rejects the key as invalid
  4. a dbconnect.php file that contains the link to the database and which is included into both of the other PHP files
  5. 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>&nbsp;</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.

  • RSS
  • email
  • Twitter
  • Facebook
  • Digg
  • StumbleUpon
  • del.icio.us
  • Google Bookmarks
  • Technorati
  • LinkedIn
  • Reddit
  • MySpace
  • Slashdot
  • SphereIt
  • Sphinn
  • Mixx

44 Responses to “Protecting a download using a unique URL”

  1. Tim says:

    Very, very helpful. Clear and concise, just what I needed! Thanks!

  2. Mike says:

    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.

  3. ardamis says:

    Hmm. That’s a good question. I’m not at all sure. I’ll look into it, though.

  4. Rajiv Totlani says:

    Clear and concise. Good snippet. Thx!

  5. Steve says:

    This is really useful – was looking for something like that for ages. Thanks a lot for sharing! Much appreciated.

  6. Hunter says:

    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!

  7. Ahi says:

    Listen my friend! This is great!!!

  8. Joe Black says:

    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

  9. Val says:

    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?

  10. ardamis says:

    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.

  11. 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

  12. Cliffordx says:

    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…

  13. manu says:

    i am also facing the same problem as joe

    
    ____________________________________
    Warning: mysql_connect() [function.mysql-connect]: Access denied for user 'root'@'localhost' (using password: NO) in /www/oxyhost.com/m/y/h/myhotspot/htdocs/dbconnect.php on line 3
    Could not connect: Access denied for user 'root'@'localhost' (using password: NO)
    _____________________________________________
    

    please do help me
    please some reply

  14. ardamis says:

    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.

  15. manu says:

    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

  16. Phil says:

    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…

  17. Serge says:

    This piece of code is a work of art. Thanks for sharing.

  18. RLBO says:

    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

  19. Callum says:

    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.

  20. manbros says:

    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.

  21. Rick Billings says:

    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?

  22. ardamis says:

    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/

  23. vaseem says:

    nice i ll use it

  24. Darren says:

    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.

  25. ardamis says:

    Hmm, I’m not aware of any issues. Can you provide a link to a file that I can use to test?

  26. Jarad says:

    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!

  27. Sean says:

    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?

  28. ardamis says:

    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.zip while the file name presented to the user may be download.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

  29. RLBO says:

    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.

  30. chetan says:

    Nice Script…. can we do something like email this link to the visitor????

  31. ardamis says:

    Right, that’s the idea. You can then email the link to someone.

  32. scraps232 says:

    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.

  33. Vinyl Candy says:

    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

  34. [...] 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 [...]

  35. Nate says:

    I found the following code to be able to limit the download speed:

    
    <?php
      
     $file = "Windows_Vista_Ultimate_x86_OEM.iso"; // file to be send to the client
     $speed = 500;  // 8,5 kb/s download rate limit
      
     if(file_exists($file) && is_file($file)) {
      
        header("Cache-control: private");
        header("Content-Type: application/octet-stream"); 
        header("Content-Length: ".filesize($file));
        header("Content-Disposition: filename=$file" . "%20"); 
      
        flush();
      
        $fd = fopen($file, "r");
        while(!feof($fd)) {
              echo fread($fd, round($speed*1024));
            flush();
            sleep(1);
        }
        fclose ($fd);
      
     }
     ?>
    

    How can I incorporate this code into download.php? I’m not very PHP savvy but I’ve tried several ways to include it. Two ways I did it didn’t cause any script errors but when when trying to access the random link, it just tries and tries. Browser says “waiting website.com” on the bottom left corner. Eventually, it gives a download prompt but it just downloads a blank file. Any ideas?

    Thanks in advance.

  36. Nate says:

    Actually, it eventually does download the file, not a blank file. It takes probably 2 minutes though after pressing “Go” for the download link. Also, please ignore the download rate comment in the code above; I meant to change the 8.5kb/s.

  37. Gerard says:

    Thank you very much for your code.

    However, I have the following problem: When I put the URL in the address bar to get the file, it is open as text instead of appearing the menu to save it. I’m using Firefox on Debian Lenny and I have verified that the file is a standard zip file.

    I would appreciate any suggestion.

  38. Ann says:

    Great script! Thank you!

    I have an issue with the download file. Everything works perfectly until I open up the downloaded file. The zip file attached is empty.

    Does it support other file formats (e.g. PDFs)?

    Thanks for your help.

  39. This is an incredible piece of code, and thanks so much, as a client was looking for just this very thing.

    Once the download key is generated, instead of posting this, can we redirect the output from a form to an email, so the user gets an email with the unique download link?

    Thanks!

    ~ Michael

  40. Muthuraman says:

    hello ardamis

    its really Good script for beginner

    $url = preg_replace(‘/\?.*$/’, ”, $_SERVER['REQUEST_URI']);
    $folderpath = ‘http://’.$_SERVER['HTTP_HOST'].’/’.ltrim(dirname($url), ‘/’).’/';

    can you tell me how to give the path url to file

  41. Tim Roijers says:

    Cool script, but when i open the link de browser opens the zipfile in de browser instead of presenting a save as window. Any idea how to fix this?

  42. Vincent says:

    RLBO, i found this to work for your purpose. i am using it to create download cards for my music and other stuff.

    it can generate a code with however many you need

    <?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), '/').'/';

    // Function to generate a 10 digit alpha-numeric code

    function randomPrefix($length)
    {
    $random= "";

    srand((double)microtime()*1000000);

    $data = "AbcDE123IJKLMN67QRSTUVWXYZ";
    $data .= "aBCdefghijklmn123opq45rs67tuv89wxyz";
    $data .= "0FGH45OP89";

    for($i = 0; $i < $length; $i++)
    {
    $random .= substr($data, (rand()%(strlen($data))), 1);
    }

    return $random;
    }

    // Generate the link
    echo "Here are the download keys”;

    $i = 1;
    while ($i <= 10) {
    $i++;

    // $random = randomPrefix(MUMBER OF CHARACTERS DESIRED);
    $random = randomPrefix(10);
    $time = date('U');

    //print the codes to page
    echo "”. $random. “”;

    //sends codes to Database
    $registerid = mysql_query(“INSERT INTO downloadkey (uniqueid,timestamp) VALUES(\”$random\”,\”$time\”)”) or die(mysql_error());

    }

    ?>

  43. Vincent says:

    i also forgot.. after you run that gen.php file

    you can go to your database manager and export it for excel. then use that for mail merge in Microsoft office publisher on say a business card. :)

  44. Vincent says:

    hey everyone i just wanted to show my implementation of this code for my bands music.

    Site: http://download.soundmilitia.com/index3.html
    Download Code: ardamis

    *note the code will only work on the above URL and not on the base site.

    and also try using a fake code such as: Apples or 15d6as17f8afd178a6

Leave a Reply

Wrap code snippets in <code></code> tags.