How to securely control access to file downloads using php
Posted: Sun Feb 28, 2010 11:27 pm
PHP can be used to securely control access to file downloads. This tutorial will show how you can send file through a PHP script and limit the download rate. The function we will write accepts the path to the file to send and optionally a rate in kB/s to limit the transfer speed. The function should also be able to handle range headers from clients that allow stopping and resuming downloads.
Sending Files
First, we will set up our function:
The first part of the functions needs to make sure the file exists before continuing:
If the file does not exists, the script exits with an error. You can replace die with any other error handling methods.
Now let's collect some important info about the file:16
Here, we found the file name, extension and size, which will be useful later.
We should also determine the most accurate MIME type to send based on the extension:
These are just some common files that may be downloaded, you can add any other types in there. If no matches are found a generic force-download type is used, which does the job just fine. You can also add file types you do not want downloaded before the exit line.
Before we send the file, we need to send the appropriate response headers:
These basically tell the browser that we are sending a file, along with the name, type and size.
Now we need to open the file, send it, then close it:
We send the file in chunks, which will make limiting the speed possible.
Speed Limit
Now let's add the code for the speed limit:
The limit is accomplished by sending the number of kB specified, waiting 1 second, then sending the next piece.
Here is what the code looks like at this point:
Download Ranges
Now we need to add support for download managers that allow resuming partial downloads.
First we need to tell the client that we accept ranges:
We also removed the Content-Length header for now since it might change.
After we open the file, we need to check for the HTTP_RANGE request header and figure out where to start the download:
If the range header exists, we parse the value (ex. bytes=100-4564) and seek to the part of the file requested. We also send the appropriate headers, including the new Content-Lenght and Content-Range headers. If a range was not requested, we just send the full Content-Length.
Final Product
Sending Files
First, we will set up our function:
Code: Select all
<?php
/*
send_file( string $file [, int $rate ] )
param $file - Path to the file to send
param $rate - Speed limit of download in kB/s
*/
function send_file($file, $rate = 0) {
// Send the file
}
?>
Code: Select all
// Check if the file exists
if (!is_file($file))
{
die('404 File Not Found');
}
Now let's collect some important info about the file:16
Code: Select all
// Get the filename, extension, and size
$filename = basename($file);
$file_extension = strtolower(substr(strrchr($filename, '.'), 1));
$size = filesize($file);
We should also determine the most accurate MIME type to send based on the extension:
Code: Select all
// Set the mime type based on the extension
switch($file_extension)
{
case 'exe':
$ctype = 'application/octet-stream';
break;
case 'zip':
$ctype = 'application/zip';
break;
case 'mp3':
$ctype = 'audio/mpeg';
break;
case 'mpg':
$ctype = 'video/mpeg';
break;
case 'avi':
$ctype = 'video/x-msvideo';
break;
// Block access to sensitive file types
case 'php':
case 'inc':
exit;
break;
default:
$ctype='application/force-download';
}
Before we send the file, we need to send the appropriate response headers:
Code: Select all
// Begin writing headers
header('Cache-Control: private');
header('Content-Type: ' . $ctype);
header('Content-Disposition: attachment; filename=' . $filename);
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . $size);
These basically tell the browser that we are sending a file, along with the name, type and size.
Now we need to open the file, send it, then close it:
Code: Select all
// Open the file for reading
$fp = fopen($file, 'rb');
// Set up the size of each piece of data we send
$block_size = 1024;
// Prevent the script from timing out
set_time_limit(0);
// Start sending the file
while(!feof($fp))
{
// Output data
print(fread($fp, $block_size));
flush();
}
// Close the file
fclose($fp);
We send the file in chunks, which will make limiting the speed possible.
Speed Limit
Now let's add the code for the speed limit:
Code: Select all
// Open the file for reading
$fp = fopen($file, 'rb');
// Set up the size of each piece of data we send
$block_size = 1024;
if($rate > 0)
{
// Multiply by rate if specified
$block_size *= $rate;
}
// Prevent the script from timing out
set_time_limit(0);
// Start sending the file
while(!feof($fp))
{
// Output data
print(fread($fp, $block_size));
flush();
if($rate > 0)
{
// Wait one second before next block if rate is specified
sleep(1);
}
}
// Close the file
fclose($fp);
The limit is accomplished by sending the number of kB specified, waiting 1 second, then sending the next piece.
Here is what the code looks like at this point:
Code: Select all
<?php
/*
send_file( string $file [, int $rate ] )
param $file - Path to the file to send
param $rate - Speed limit of download in kB/s
*/
function send_file($file, $rate = 0) {
// Check if the file exists
if (!is_file($file))
{
die('404 File Not Found');
}
// Get the filename, extension, and size
$filename = basename($file);
$file_extension = strtolower(substr(strrchr($filename, '.'), 1));
$size = filesize($file);
// Set the mime type based on the extension
switch($file_extension)
{
case 'exe':
$ctype = 'application/octet-stream';
break;
case 'zip':
$ctype = 'application/zip';
break;
case 'mp3':
$ctype = 'audio/mpeg';
break;
case 'mpg':
$ctype = 'video/mpeg';
break;
case 'avi':
$ctype = 'video/x-msvideo';
break;
// Block access to sensitive file types
case 'php':
case 'inc':
exit;
break;
default:
$ctype='application/force-download';
}
// Begin writing headers
header('Cache-Control: private');
header('Content-Type: ' . $ctype);
header('Content-Disposition: attachment; filename=' . $filename);
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . $size);
// Open the file for reading
$fp = fopen($file, 'rb');
// Set up the size of each piece of data we send
$block_size = 1024;
if($rate > 0)
{
// Multiply by rate if specified
$block_size *= $rate;
}
// Prevent the script from timing out
set_time_limit(0);
// Start sending the file
while(!feof($fp))
{
// Output data
print(fread($fp, $block_size));
flush();
if($rate > 0)
{
// Wait one second before next block if rate is specified
sleep(1);
}
}
// Close the file
fclose($fp);
}
?>
Now we need to add support for download managers that allow resuming partial downloads.
First we need to tell the client that we accept ranges:
Code: Select all
header('Accept-Ranges: bytes');
We also removed the Content-Length header for now since it might change.
After we open the file, we need to check for the HTTP_RANGE request header and figure out where to start the download:
Code: Select all
// Check if http_range was sent by client
if(isset($_SERVER['HTTP_RANGE']))
{
// If so, calculate the range to use
$seek_range = substr($_SERVER['HTTP_RANGE'], 6);
$range = explode('-', $seek_range);
if($range[0] > 0){
$seek_start = intval($range[0]);
}
if($range[1] > 0){
$seek_end = intval($range[1]);
}
// Seek to the requested position in the file
fseek($fp, $seek_start);
// Set the range response headers
header('HTTP/1.1 206 Partial Content');
header('Content-Length: ' . ($seek_end - $seek_start + 1));
header(sprintf('Content-Range: bytes %d-%d/%d', $seek_start, $seek_end, $size));
}
else
{
// Set default response headers
header('Content-Length: ' . $size);
}
If the range header exists, we parse the value (ex. bytes=100-4564) and seek to the part of the file requested. We also send the appropriate headers, including the new Content-Lenght and Content-Range headers. If a range was not requested, we just send the full Content-Length.
Final Product
Code: Select all
<?php
/*
send_file( string $file [, int $rate ] )
param $file - Path to the file to send
param $rate - Speed limit of download in kB/s
*/
function send_file($file, $rate = 0) {
// Check if the file exists
if (!is_file($file))
{
die('404 File Not Found');
}
// Get the filename, extension, and size
$filename = basename($file);
$file_extension = strtolower(substr(strrchr($filename, '.'), 1));
$size = filesize($file);
// Set the mime type based on the extension
switch($file_extension)
{
case 'exe':
$ctype = 'application/octet-stream';
break;
case 'zip':
$ctype = 'application/zip';
break;
case 'mp3':
$ctype = 'audio/mpeg';
break;
case 'mpg':
$ctype = 'video/mpeg';
break;
case 'avi':
$ctype = 'video/x-msvideo';
break;
// Block access to sensitive file types
case 'php':
case 'inc':
exit;
break;
default:
$ctype='application/force-download';
}
// Begin writing headers
header('Cache-Control: private');
header('Content-Type: ' . $ctype);
header('Content-Disposition: attachment; filename=' . $filename);
header('Content-Transfer-Encoding: binary');
header('Accept-Ranges: bytes');
// Open the file for reading
$fp = fopen($file, 'rb');
// Check if http_range was sent by client
if(isset($_SERVER['HTTP_RANGE']))
{
// If so, calculate the range to use
$seek_range = substr($_SERVER['HTTP_RANGE'], 6);
$range = explode('-', $seek_range);
if($range[0] > 0){
$seek_start = intval($range[0]);
}
if($range[1] > 0){
$seek_end = intval($range[1]);
}
// Seek to the requested position in the file
fseek($fp, $seek_start);
// Set the range response headers
header('HTTP/1.1 206 Partial Content');
header('Content-Length: ' . ($seek_end - $seek_start + 1));
header(sprintf('Content-Range: bytes %d-%d/%d', $seek_start, $seek_end, $size));
}
else
{
// Set default response headers
header('Content-Length: ' . $size);
}
// Set up the size of each piece of data we send
$block_size = 1024;
if($rate > 0)
{
// Multiply by rate if specified
$block_size *= $rate;
}
// Prevent the script from timing out
set_time_limit(0);
// Start sending the file
while(!feof($fp))
{
// Output data
print(fread($fp, $block_size));
flush();
if($rate > 0)
{
// Wait one second before next block if rate is specified
sleep(1);
}
}
// Close the file
fclose($fp);
}
?>