3

I'm trying to create a secure document retrieval system for logged in users.

First I check if the user is logged in. If they are I progress to copy the file from the protected directory on the server to the web accessible space within the users current directory.

I then redirect them to the URL the file is now within. Tell the code to sleep for 10 seconds to allow slow connections time to download the file and then to delete it so the link is no longer usable by others.

My problem is that after the header the sleep function is not working. I tried removing the if statement and this made the script sleep for 10 seconds and then unlink the file before redirecting the user so the link as already broken.

I'm struggling to find a way to make the script sleep for 10 seconds and still execute the code AFTER the redirect has happened.

    <?php
if(!isset($_SESSION['id'])){
header("location:../../../");
}
else {
    echo copy('C:\directorypath\test.xls','test.xls');
    if(header("location:../docs/test.xls")){
    sleep("10");
    unlink("test.xls");
    }
}
    ?>
James
  • 190
  • 2
  • 4
  • 13
  • Possible duplicate of [Php, wait 5 seconds before executing an action](https://stackoverflow.com/questions/6730855/php-wait-5-seconds-before-executing-an-action) – Hamza Zafeer Jan 19 '18 at 11:25
  • Thanks Hamza, I have no problem with using a sleep function as is in my code, the problem is when this is happening. I want it to sleep AFTER the if statement is returned as true but instead it is starting before any actions take place. – James Jan 19 '18 at 11:27
  • 1
    PHP is single threaded by nature, This is not possible. When the `header` line is executed, the rest of the script is terminated. There is no way to both return the file, and at the same time also delete it. This is not how PHP works. – OptimusCrime Jan 19 '18 at 11:27
  • Hi Optimus, code after headers can continue to run. In fact is that not why we explicitly terminate scripts at the point we want them to stop running? Please correct me if I'm wrong but I'd always assumed this. for example: https://stackoverflow.com/questions/14847010/will-php-script-be-executed-after-header-redirect – James Jan 19 '18 at 11:30
  • 1
    You should serve the file through php instead of copying it to a public directory. – jeroen Jan 19 '18 at 11:30
  • 1
    If the files are that small, then it would make much more sense IMHO to handle the download via the PHP script to begin with (instead of copying files around on the HDD.) – CBroe Jan 19 '18 at 11:30
  • Yes, you can continue to execute code after the header line, but the file will not be served to the user before the script is finished executing. This means that once the entire request (and the sleep) is finished, the file is served. This happens after the file is deleted, so there is no file there. – OptimusCrime Jan 19 '18 at 11:31
  • OK understood thanks. These documents are stored outside of httpdocs because they are sensitive by nature and I want them as offline as possible whilst still being downloadable by the user. Is it possible to download to the user from C:\ directory outside of the webfiles? – James Jan 19 '18 at 11:33
  • Possible duplicate of [continue processing php after sending http response](https://stackoverflow.com/questions/15273570/continue-processing-php-after-sending-http-response) – rlanvin Jan 19 '18 at 11:49

1 Answers1

4

What you are trying to do is in practice impossible to do with PHP, because PHP is single threaded and executed from start-to-finish for each request. This means that the header redirect is not done until all the code in your script is completed. This means that the script will first wait, then delete the file, then attempt to redirect to a non-exiting file.

Instead I purpose another approach; using and storing a unique hash that is usable only once.

Check if the user is logged in. If he or she is logged in, generate a unique hash and insert it in the database e.g. INSERT INTO hashes (hash, used) VALUES ($myUniqueHash, 0). Then redirect the user to a PHP-file with the hash in the request query, e.g.

header("Location: serve_file.php?hash=$myUniqueHash");

This file will check the value of $_GET['hash'] against the table that contains the hashes, and check if the value of used is 0. If this is the case it will update the used column to 1 and serve the file as a proxy. If the hash is not found, or if the value of the used column is not 0 it will return an error.

This way we can serve a (secret) file only once for logged in users.

OptimusCrime
  • 14,662
  • 13
  • 58
  • 96
  • Thank you for this, a very detailed answer, I'll have a go at it now. – James Jan 19 '18 at 11:38
  • "This means that the header redirect is not done until all the code in your script is completed" Wrong! You should look at [flush()](http://php.net/manual/en/function.flush.php) and [this question](https://stackoverflow.com/questions/15273570/continue-processing-php-after-sending-http-response) – rlanvin Jan 19 '18 at 11:42
  • @rlanvin That is true. You can flush the content, but how will you know if the file is finished downloading? The original idea was to use a timeout of 10 seconds to "make sure slow connections would also download the file", which is very error prone approach in my opinion. A single use token for file proxying is a much more common approach to this type of problem. – OptimusCrime Jan 19 '18 at 11:44
  • Of course it is error prone and completely flawed. But starting your answer with "it's impossible to do with PHP" is simply not true. It is possible, but it's not advisable for the reasons you stated. – rlanvin Jan 19 '18 at 11:47
  • Just thinking out loud here...would creating a temporary directory based on a hash of the username that is deleted every x minutes by a scheduled task have the same effect? My point being that I would really like this download window to be time limited. – James Jan 19 '18 at 11:50
  • @Rockhopper Sure, it is just less secure than the purposed idea given here. The benefit of serving the file via a PHP-script (like a proxy) is that the file itself does not have to be accessible to the web, it can be behind the webroot for increased security. This way the user is never given neither the file location nor the file name. This also makes it virtually impossible to download the file without a valid hash, because the file is not accessible. – OptimusCrime Jan 19 '18 at 11:52
  • Edit: If you also want the download time to be limited, you can insert the timestamp for when the hash was added to the table, with a predetermined time to live (for example 30 seconds). Just expand the check done in the `serve_file.php` script to make sure the current timestamp it less or equal the time to live value. – OptimusCrime Jan 19 '18 at 11:53
  • Thanks so much for your help. I'll go with your recommended solution, I really appreciate you taking the time to explain all this. – James Jan 19 '18 at 11:55
  • Just wanted to update you @OptimusCrime - got this working perfectly now with the hashes and everything. Thanks for your help! – James Jan 19 '18 at 14:38