Now: Tutorial for Web and Software Design > PHP > PHP Basic > PHP Content
> Generating One-Time URLs with PHP [Bookmark it]
Generating One-Time URLs with PHP

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.

[Bookmark][Print] [Close][To Top]
  • Prev Article-PHP:

  • Next Article-PHP:
  • Related Materias
    Whats New in ModSecurity
    Important Notice for Apach
    Apaches eXtended Server Si
    Newbies Find Help in OReil
    Industrial-Strength Webcas
    Monitoring Apache Page-Loa
    Getting, Installing, and R
    Implementing MVC in PHP: T
    Implementing MVC in PHP: T
    Simplify PHP Development w
    Topics
    Photoshop Tutorial
     

    Special Effect

      3D Effect
      Photoshop Articles
    Programming Tutorial
     

    C/C++ Tutorial

      Visual Basic
      C# Tutorial
    Database Tutorial
     

    MySQL Tutorial

      MS SQL Tutorial
      Oracle Tutorial
    Graphic Design Tutorial
     

    Coreldraw Tutorial

      Illustrator Tutorial
      3D Graphics Articles
    Webmaster Articles
     

    Domain Service

      Web Hosting
      Site Promotion
    Java Tutorial&Articles
     

    Java Servlets

      JavaEE Tutorial
     

    JavaBeans Tutorial

    XML Tutorial&Articles
     

    XML Style Tutorial

      AJAX Tutorial
      XML Mobile
    Flash Tutorial&Articles
     

    Flash Video

      Action Script
      Flash Articles
    OS Tutorial&Articles
     

    Linux Tutorial

      Symbian Tutorial
      MacOS Tutorial