Protecting multiple downloads using unique URLs

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 to 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 direct access to a file on the server. Sharing the URL/key allows the recipient 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 seven main components to this system:

  1. the MySQL database that holds each key, the key creation date and time, the number of times the key has been used, the file associated with the key, and the note attached to the key, if any
  2. the downloadkey.php page that generates the unique keys and outputs the corresponding URLs
  3. the download.php page that accepts the key, checks its validity, and either initiates the download or rejects the key as invalid
  4. the downloadreport.php page that returns all of the data in the database
  5. a dbconnect.php file that contains the link to the database
  6. a config.php file that contains variables such as number of downloads allowed and the filenames of the downloads
  7. the .zip file(s) to be protected

The files

Download the protecting multiple downloads PHP script

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(12) NOT NULL default '',
  `timestamp` INT 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 dbconnect.php and config.php in your text editor of choice.

Change the $link line in dbconnect.php to point to your MySQL database.

Change the variables in config.php to specify how many times the keys can be used before expiring and how long the keys should remain valid.

You can also set a variable to be the filename of the download you’re protecting. If you leave the variable blank, the form will display an empty input box as the Filename field. 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.

I would strongly recommend renaming downloadkey.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 downloadkey.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.

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.

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

25 Responses to “Protecting multiple downloads using unique URLs”

  1. Vinyl Candy says:

    This is great! ONE THING: I have 5 parts (or 5 different zip files) I want to generate (1 single) download key that will start the download process for all 5 zips.
    a. Is that possible?
    b. If not than generating 5 different download codes based on 5 different zips. they will all have to same “save-as” file name.

  2. Vinyl Candy says:

    Also: There seems to be a LIMIT to the file size of MAX 33554432 bytes
    Fatal error: Allowed memory size of 33554432 bytes exhausted”
    How can I bump this limit up to and beyond 130MB?

  3. jonathan says:

    Not working for me ! always sending me a 0byte file except if i change the value of $file
    error log :

    [08-Jul-2009 17:04:41] PHP Warning: filesize() [function.filesize]: stat failed for download.zip in /home/commint/public_html/dl/download.php on line 43
    [08-Jul-2009 17:04:41] PHP Warning: readfile() [function.readfile]: Filename cannot be empty in /home/commint/public_html/dl/download.php on line 48

  4. Ian says:

    Hello ardamis,

    I can’t tell you how much this script is appreciated, thank you!

    I have run into a slight problem, however. If you have a moment, I do hope you’ll be able to help.

    I am able to reach the generator page. Once I click ‘Generate Key’, I am brought to a new page that shows an error in addition to the ‘x keys successfully created’. There is no download link displayed.

    The error reads as follows:

    
    http://xxxxx.com/xxxx/xxxxxx/download.php?id=26928bcefb67d878dbe9f2f2421f20274a5f3189c7e40
    
    Warning: mysql_real_escape_string() expects parameter 2 to be resource, null given in /home/content/i/n/d/indecisiveian1/html/blog/somethingnew/downloadkey.php on line 84
    
    Warning: mysql_real_escape_string() expects parameter 2 to be resource, null given in /home/content/i/n/d/indecisiveian1/html/blog/somethingnew/downloadkey.php on line 85
    
    

    Any ideas?

  5. ardamis says:

    I would first make sure the correct MySQL database is specified in dbconnect.php.

    I’ve written a script to help test and troubleshoot PHP / MySQL connections.

    Let me know if I can be of further help.

  6. Ian says:

    Hello again ardamis,

    Thanks for the quick reply.

    I tested out my connection info with the testing script your provided and indeed, my information is correct. I was able to successfully connect to my DB, so my dbconnect.php does contain the correct information. That being said, the generator does work, in terms of producing keys (I currently have some 6 or more according to the report). However, there is no download link created.

    Any other ideas about what it could be?

  7. Ian says:

    Ok, sorry if this is a double post.

    I spoke to a friend and he noticed I was missing the $link infront of the mysql_connect statement (D’oh!). So, the errors are gone now (hooray!).

    However, when visiting the generated link, the download does not begin. Instead, I view a blank screen.

    Do you know why this could be?

  8. ardamis says:

    I’m glad to help. Try uncommenting line 33 in download.php , changing:

    
    // Debug		echo "Key validated.";
    


    to

    
    echo "Key validated.";
    


    and let’s see if that text echos when you use a newly generated key.

  9. Ian says:

    Hi again ardamis,

    I have uncommented line 33, as you have suggested. Now, upon visiting the generated link, “Key validated.” appears on the screen (as expected?). The download, however, does not begin.

  10. ardamis says:

    Ok, well, that helps narrow it down. It could be the part of the script that initiates the download, or maybe the download file is missing, or perhaps your browser is blocking this file transmission. Can you tell me what OS/browser/version you are using?

    I’ll be able to test this more thoroughly tomorrow.

  11. Ian says:

    Thanks again for keeping at this with me.

    Vista Home Premium.

    I’ve tried the generated links in both IE v.8.0.6 and Chrome v.2.0.172.37. Neither browser brings up a download prompt when visiting the link.

    For information’s sake, all the files are uploaded in a single directory; they are not separated (was hoping to have it working before rearranging for security).

    Also, should I be referencing the file by name only (e.g. actual_file.zip) or by path (/xxxx/actual_file.zip) in the config file?

    Have a good night!

  12. ardamis says:

    Thanks, Jonathan. This has been fixed, although it comes way too late, I admit.

  13. ardamis says:

    Crap. I changed a variable name throughout the script, but missed one instance. Even when it the script would initiate the download, you’d only get a 0KB file, as Jonathan pointed out some time ago. I’ve uploaded the fixed script. Please download it again from this page.

    If your download file is in the same directory as download.php, you don’t need to include the /path/to/folder/ as part of the $realfilename variable in config.php. I haven’t tried putting these things in different folders yet, so I don’t know that including the path will work.

    Let me know what happens.

  14. Ian says:

    Afternoon!

    I’ve re-downloaded the files and upped them on my server. Now, when visiting the generated link, a page full of various characters appears (still no download prompt).

    I tested the generated link in both IE and Chrome and received the same results (page full of various characters).

    Also, thanks for the path info!

  15. Andrew Devenish-Meares says:

    Hi,

    Just a quick note to say thanks for this code. It did exactly what I needed and saved me a bunch of time.

  16. Tom says:

    Thank you Ardamis for wonderful script!

    Would it be possible to add input fields for $realfilename and $fakefilename in to the downloadkey.php so that it would be easier to create download keys for multiple files?

    Also where could I change the location for the downloadable file so that I could locate the file outside of the www (in the server root)?

  17. lascaux21 says:

    I’ve also had an issue with large file sized ZIP files ending too soon. I’ve managed to get around the problem by replacing the “readfile” with a header(“Location”), but this isn’t optimum. Anyone figured out a way to solve this? My research indicates it may have something to do with “chunked transfer encoding”, and someone suggested using blobs to calculate size. Over my head, unfortunately…

  18. Chad says:

    I downloaded and installed this with no problem. It works great but only for 1 file. Whatever file I have set as the default file ($realfilename) will always download fine. if i have multiple files in the same directory, and enter a different file name in the “downloadkey.php” form, the default file still downloads. How can I use this and choose which file to generate the download key for? Please help…I love this!!

  19. ardamis says:

    OK, I’ve updated the script to fix the issue Chad reported. I’ve also reduced the size of the keys to 12 characters, and made some changes to the database.

    It’s now possible to add a list of download files to the $realfilenames variable in config.php, so that the form generates a <select> menu of all the files (instead of making you type the name in to an <input> field each time).

    I’ve also added a number of comments to download.php to better explain step-by-step the process by which each key is validated.

  20. Ruark says:

    ardamis, what a great piece of code :)

    My 1st question is regarding the real file URL. I would like to structure where the files are kept. I dont really like leaving all files in one folder (security and ease of keeping it neat)

    Is there anyway to do this? the dropdown should be able to show directory structure should it not?

    My 2nd question is regarding the keylimit. Why is it limited to 20? is this a code limit or a hash generation limit? If I want to use this to send multiple (say 50) people unique links to the same file, would I need to increase the hash character amount?

    thanks again for your reply and this great piece of php

  21. Tom says:

    Ardamis – I love this script.
    Would it be possible to update the download counter after successful download?
    Now the download counter is always updated even when download is canceled before it is finished or when the download is somehow not finished successfully.

  22. Meg says:

    Hey, this script is awesome. But I have a question: How do I go about removing the option that creates the $fakefilename? I just want it to be the original one.

    I have a 100 or some files I want to send to a friend using this and they all will appear with the same name except the ( (1), (2) copy tags) behind them. I want all to have a unique name.

    So I tried to delete this line: header(‘Content-Disposition: attachment; filename=”‘.$fakefilename.’”‘);

    But it only gave me the download.php instead of the zip file. What do I have to modify to make this work?

    Thanks, Meg

  23. Meg says:

    By the way, also this line in config.php

    // Set the name of local download file – this is what the visitor’s file will actually be called when he/she saves it
    $fakefilename = “bogus_download_name.zip”;

  24. Meg says:

    Nevermind. I think I found a solution. I just change the line:

    header(‘Content-Disposition: attachment; filename=”‘.$fakefilename.’”‘);
    to
    header(‘Content-Disposition: attachment; filename=”‘.$realfilename.’”‘);

    Thanks for the script!

    Meg

  25. amirul says:

    resume download not possible by our script.if i cancel download then nest time i can not download.so no resume capacity of our code.

Leave a Reply

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