PHP DevCenter
oreilly.comSafari Books Online.Conferences.

advertisement


Generating One-Time URLs with PHP

by Daniel Solin
12/05/2002

Imagine that you're selling a digital product online. Maybe you've written an article or a book and want to sell it on your site as a PDF. There are many ways one could do this, but one of the more convenient is to provide the user with a unique URL that only will work a limited number of times. This URL could, for example, be presented to the user (your client) on the last page of an orderflow, after payment has been made. We will look at code to generate a unique URL that will work a single time.

Creating the URL

The script generate_url.php will generate our URLs. It uses PHP's md5() and uniqid() functions to create a unique, long and complicated token which will act as the key to the file to protect. Tokens are 32 random characters long, so to figure this out by mental arithmetic would be impossible. The listing below shows you the implementation of generate_url.php.

<?
/*
* generate_url.php
*
* Script for generating URLs that can be accessed one single time.
*
*/

/* Generate a unique token: */
$token = md5(uniqid(rand(),1));

/* This file is used for storing tokens. One token per line. */
$file = "/tmp/urls.txt";
if( !($fd = fopen($file,"a")) )
        die("Could not open $file!");

if( !(flock($fd,LOCK_EX)) )
        die("Could not aquire exclusive lock on $file!");

if( !(fwrite($fd,$token."\n")) )
        die("Could not write to $file!");

if( !(flock($fd,LOCK_UN)) )
        die("Could not release lock on $file!");

if( !(fclose($fd)) )
        die("Could not close file pointer for $file!");

/* Parse out the current working directory for this script. */
$cwd = substr($_SERVER['PHP_SELF'],0,strrpos($_SERVER['PHP_SELF'],"/"));

/* Report the one-time URL to the user: */
print "Use this URL to download the secret file:<br><br>\n";
print "<a href='http://".$_SERVER['HTTP_HOST']. 
      "$cwd/get_file.php?q=$token'>\n";
print "http://".$_SERVER['HTTP_HOST']."/get_file.php?q=$token</a>\n";

?>

In short terms, md5() calculates a 32-character long hexadecimal number by using the RSA Data Security, Inc. MD5 Message-Digest Algorithm out of the unique string generated by uniqid(). Note that we also feed uniqid() with a random value. You see, uniqid() generates a unique id based on the current time in microseconds. Although small, there's always a possibility that the script is executed two or more times simultaneously, which could result in the same id being generated more than once. By feeding uniqid() with a random value generated by rand(), we push the risk of not getting a really unique id closer to zero. Or, as stated in the PHP reference documentation, this generates a token "that is extremely difficult to predict". If you need more documentation on md5() and uniqid(), please consult the PHP reference documentation at http://www.php.net/manual/en/function.md5.php and http://www.php.net/manual/en/function.uniqid.php.

After generating the unique id, we open the file /tmp/urls.txt for writing. This file is used to store the unique tokens, one per line. When writing to this file, it's very important the we do real file-locking to prevent two processes writing the file at the same time. We do this by using PHP's flock() function. See http://www.php.net/manual/en/function.flock.php for more information about file-locking with PHP.

Finally, on the last three lines, the newly generated URL is presented to the client. It should show something like Figure 1.


Figure 1 -- A new and unique id is generated.

Sending Out the Secret File

As you saw in the previous section, there's reference to a file called get_file.php. This script takes care of the verification of a generated token, and, if the token is valid, sends out the secret file to the user. This file also deletes a token from /tmp/urls.txt after it has been used. See the listing below.

<?
/*
* get_file.php
*
* Script for validating a request through a secret token, passing a file
* to the user, and ensuring the token can not be used again.
*
*/

/* Retrive the given token: */
$token = $_GET['q'];

if( strlen($token)<32 )
{
        die("Invalid token!");
}

/* Define the secret file: */
$secretfile = "/tmp/secret_file.txt";

/* This variable is used to determine if the token is valid or not: */
$valid = 0;

/* Define what file holds the ids. */
$file = "/tmp/urls.txt";

/* Read the whole token-file into the variable $lines: */
$lines = file($file);

/* Truncate the token-file, and open it for writing: */
if( !($fd = fopen("/tmp/urls.txt","w")) )
        die("Could not open $file for writing!");

/* Aquire exclusive lock on $file. */
if( !(flock($fd,LOCK_EX)) )
        die("Could not equire exclusive lock on $file!");

/* Loop through all tokens in the token-file: */
for( $i = 0; $lines[$i]; $i++ )
{
        /* Is the current token the same as the one defined in $token? */
        if( $token == rtrim($lines[$i]) )
        {
                $valid = 1;
        }
        /* The code below will only get executed if $token does NOT match the
           current token in the token file. The result of this will be that
           a valid token will not be written to the token file, and will
           therefore only be valid once. */
        else
        {
                fwrite($fd,$lines[$i]);
        }
}

/* We're done writing to $file, so it's safe release the lock. */
if( !(flock($fd,LOCK_UN)) )
        die("Could not release lock on $file!");

/* Save and close the token file: */
if( !(fclose($fd)) )
        die("Could not close file pointer for $file!");

/* If there was a valid token in $token, output the secret file: */
if( $valid )
{
        readfile($secretfile);
}
else
{
        print "Invalid URL!";
}

?>

This script requires a token to be passed to it via the GET method. If a valid token is not provided, execution is terminated directly. This is a very important security issue. What would happen if an empty token was sent to the script and /tmp/urls.txt had an empty line on the end? Again, note the use of flock() to accomplish file-locking. This is, as in generate_url.php, an important feature for securing the file-handling.

Every line in urls.txt is then compared to the token that was sent to the script. If the token is actually present in urls.txt, the script marks the request as valid by setting $valid to 1. If the request is not valid, $valid will keep the value 0, and the secret file will not get passed to the user. Also note that all tokens that do not match the request are written to urls.txt again, but tokens that do match the request are skipped and, by that, removed from urls.txt .

Place get_file.php and generate_url.php on your Web server and click on the link shown in Figure 2. Depending on what is in your /tmp/secret_file.txt, you should now see something like Figure 2.


Figure 2 -- the super-secret contents of the super-secret file

As you see, get_file.php approved the request and presented us with the contents of /tmp/secret_file.txt. However, if you now try to reload the page, get_file.php will block the request. See Figure 3.


Figure 3 -- secret files without secret keys

Viola. Mission accomplished.

Summary

This article has presented a quick tip of how you could generate URLs that only can be used one time. It should be said, however, that for a real implementation of this function in a business environment, there are a few additional considerations to take. When the number of active keys grows past a few hundred, is it a good idea to read the whole file directly into memory? Is it wise even to store them in a plain-text file in the first place? Maybe a MySQL database would be a better choice? Additionally, consider the possibility of getting tokens "hijacked" directly from the urls.txt file. For getting this 100 percent secure, you either need to protect the file by setting very restrictive permission on it, only allowing the user executing your PHP scripts to read it. Or, you can simply dump the plain-text solution, and convert to a SQL-database instead.

The code in generate_url.php, of course has to be protected, too. The easiest way of doing that is probably to include it in the "thank-you-for-ordering-page" of your orderflow and make this file accessible only for clients coming from the secure-payment-complete-page. The core concept, though, will probably work very well in any production environment.

Daniel Solin is a freelance writer and Linux consultant whose specialty is GUI programming. His first book, SAMS Teach Yourself Qt Programming in 24 hours, was published in May, 2000.


Return to the PHP DevCenter.


Valuable Online Certification Training

Online Certification for Your Career
Earn a Certificate for Professional Development from the University of Illinois Office of Continuing Education upon completion of each online certificate program.

PHP/SQL Programming Certificate — The PHP/SQL Programming Certificate series is comprised of four courses covering beginning to advanced PHP programming, beginning to advanced database programming using the SQL language, database theory, and integrated Web 2.0 programming using PHP and SQL on the Unix/Linux mySQL platform.

Enroll today!


Sponsored by: