I'm having a download abuse issue with php Content-Disposition: attachment and readfile. It seems that my problem is with readfile, because although this script works, whether or not the client closes their browser, readfile reads the entire contents of the mp4, setting up the possibility of abuse with scripts initiating the download and immediately closing the progress. Something, somewhere, is running a script which clicks this link hundreds of times per second, running my php script and immediately cancelling their download, but my server is preparing that entire file to be offloaded each time.
Here's the script I'm running, when the user/abuser clicks a download link:
<?php
// get MP4 address
$MP4Address = $_GET["MP4Address"];
// We'll be outputting a MOV
header( 'Content-Type: application/octet-stream' );
$filename = basename($MP4Address);
// Name file
header('Content-Disposition: attachment; filename="'.$filename.'"');
// Source file
readfile($MP4Address);
?>
I suspect that readfile is the culprit here, but without it, the client will receive an empty file. There must be a more modern, proper way of doing this, but I'm not sure what it could be.
Unless you've called ignore_user_abort(true) PHP should get the signal that the connection has been aborted and cease execution. But it's possible that once you're inside the readfile() call PHP is not able to watch for that signal since it's busy doing low-level IO.
I would say you've got 2 options, the first being to simply write some code to detect and block the person that's abusing your service. You downloads are already backed by a PHP script, so adding in a bit of checking and filtering should be relatively simple.
The other would be to replace the readfile() call with a bit of [admittedly less efficient] code that should give PHP some breathing space to look for user aborts.
function read_file($filename, $chunksize=4096) {
if( ! $fh = fopen($filename, 'rb') ) {
throw new \Exception('Failed to open file');
}
while($chunk = fread($fh, $chunksize)) {
echo $chunk;
}
fclose($fh);
}
I want to organize live streaming from recording files over HTTP based on PHP. INTRODUCTION: On the streaming server I writing video to local file(local_file.mpg) and when received a request from client then start streaming to it from $start_byte = filesize("local_file.mpg")-10MB; The local_file.mpg is still writing and PHP script continue reading it and flushing. PROBLEM: I streaming it via HTTP Range with the following headers:
header('HTTP/1.1 206 Partial Content');
header("Content-Type: video/mpeg");
header('Content-Length: '.($seek_end - $seek_start));
header('Content-Range: bytes '.$seek_start.'-'.$seek_end.'/'.$size);
And flushing as follows:
while(!feof($fp)){
$buf_size = 1024*8;
$pos = ftell($fp);
if ($pos >= $item["to_byte"]){
fclose($fp);
break;
}
if ($pos + $buf_size > $item["to_byte"]){
$buf_size = $item["to_byte"] - $pos;
}
if ($buf_size > 0){
echo fread($fp, $buf_size);
}
flush();
ob_flush();
}
I open it via VLC or FFplay, but it played until the time moment when the stream was requested. This is to be expected, because we determine the size of the file and provide it to requested side. But if we artificially increase a file size, for example $size = filesize("local_file.mpg")+999999999999; it also not help, because video players requesting new data too early when it is not recorded. And also stopped play at the time moment when the stream was requested.
1. Please advice how to correct organize live streaming from recording files over HTTP based on PHP. 2. Is it possible to do it with HTTP RANGE mechanism or I should use another way?
UPDATE: Based on this question I tried the next code:
<?php
$file = "online.mpg";
function flush_buffers(){
ob_end_flush();
ob_flush();
flush();
ob_start();
}
header('Content-Type: video/mpeg');
$stream = fopen( $file, "rb" );
fseek($stream, (filesize($file)-10000000), SEEK_SET);
while(1){
$response = fread($stream, 8192);
echo $response;
flush_buffers();
}
fclose( $stream );
exit();
?>
And it works well via ffplay, but via VLC it played no more then 1 minute and stoped then. Please advice how to make it work on VLC also?
Do you have a time limit for php execution ?
if yes , change it to unlimited with :
set_time_limit(0);
I'm downloading a large file like that:
$fd = fopen($url, "r");
while(!feof($fd))
{
echo fread($fd, 4096);
ob_flush();
flush();
}
But I have one problem - the file is downloading only to 11,6 MB and stop...
Where is a problem? I'm using ob_flush and flush so I think - it should work.
Thanks.
You don't need the fread() loop if you just want to output a remote file. You can use:
readfile($url);
That's it. However, the script you showed should work as well. The reason must be on the remote server.
If the download takes long you should consider to set the execution time to unlimited:
set_time_limit(0);
... on top of your script.
I have a php script on a server to send files to recipents: they get a unique link and then they can download large files. Sometimes there is a problem with the transfer and the file is corrupted or never finishes. I am wondering if there is a better way to send large files
Code:
$f = fopen(DOWNLOAD_DIR.$database[$_REQUEST['fid']]['filePath'], 'r');
while(!feof($f)){
print fgets($f, 1024);
}
fclose($f);
I have seen functions such as
http_send_file
http_send_data
But I am not sure if they will work.
What is the best way to solve this problem?
Regards
erwing
Chunking files is the fastest / simplest method in PHP, if you can't or don't want to make use of something a bit more professional like cURL, mod-xsendfile on Apache or some dedicated script.
$filename = $filePath.$filename;
$chunksize = 5 * (1024 * 1024); //5 MB (= 5 242 880 bytes) per one chunk of file.
if(file_exists($filename))
{
set_time_limit(300);
$size = intval(sprintf("%u", filesize($filename)));
header('Content-Type: application/octet-stream');
header('Content-Transfer-Encoding: binary');
header('Content-Length: '.$size);
header('Content-Disposition: attachment;filename="'.basename($filename).'"');
if($size > $chunksize)
{
$handle = fopen($filename, 'rb');
while (!feof($handle))
{
print(#fread($handle, $chunksize));
ob_flush();
flush();
}
fclose($handle);
}
else readfile($path);
exit;
}
else echo 'File "'.$filename.'" does not exist!';
Ported from richnetapps.com / NeedBee. Tested on 200 MB files, on which readfile() died, even with maximum allowed memory limit set to 1G, that is five times more than downloaded file size.
BTW: I tested this also on files >2GB, but PHP only managed to write first 2GB of file and then broke the connection. File-related functions (fopen, fread, fseek) uses INT, so you ultimately hit the limit of 2GB. Above mentioned solutions (i.e. mod-xsendfile) seems to be the only option in this case.
EDIT: Make yourself 100% that your file is saved in utf-8. If you omit that, downloaded files will be corrupted. This is, because this solutions uses print to push chunk of a file to a browser.
If you are sending truly large files and worried about the impact this will have, you could use the x-sendfile header.
From the SOQ using-xsendfile-with-apache-php, an howto blog.adaniels.nl : how-i-php-x-sendfile/
Best solution would be to rely on lighty or apache, but if in PHP, I would use PEAR's HTTP_Download (no need to reinvent the wheel etc.), has some nice features, like:
Basic throttling mechanism
Ranges (partial downloads and resuming)
See intro/usage docs.
We've been using this in a couple of projects and it works quite fine so far:
/**
* Copy a file's content to php://output.
*
* #param string $filename
* #return void
*/
protected function _output($filename)
{
$filesize = filesize($filename);
$chunksize = 4096;
if($filesize > $chunksize)
{
$srcStream = fopen($filename, 'rb');
$dstStream = fopen('php://output', 'wb');
$offset = 0;
while(!feof($srcStream)) {
$offset += stream_copy_to_stream($srcStream, $dstStream, $chunksize, $offset);
}
fclose($dstStream);
fclose($srcStream);
}
else
{
// stream_copy_to_stream behaves() strange when filesize > chunksize.
// Seems to never hit the EOF.
// On the other handside file_get_contents() is not scalable.
// Therefore we only use file_get_contents() on small files.
echo file_get_contents($filename);
}
}
For downloading files the easiest way I can think of would be to put the file in a temporary location and give them a unique URL that they can download via regular HTTP.
As part generating these links you could also remove files that were more than X hours old.
Create a symbolic link to the actual file and make the download link point at the symbolic link. Then, when the user clicks on the DL link, they'll get a file download from the real file but named from the symbolic link. It takes milliseconds to create the symbolic link and is better than trying to copy the file to a new name and download from there.
For example:
<?php
// validation code here
$realFile = "Hidden_Zip_File.zip";
$id = "UserID1234";
if ($_COOKIE['authvalid'] == "true") {
$newFile = sprintf("myzipfile_%s.zip", $id); //creates: myzipfile_UserID1234.zip
system(sprintf('ln -s %s %s', $realFile, $newFile), $retval);
if ($retval != 0) {
die("Error getting download file.");
}
$dlLink = "/downloads/hiddenfiles/".$newFile;
}
// rest of code
?>
<a href="<?php echo $dlLink; ?>Download File</a>
That's what I did because Go Daddy kills the script from running after 2 minutes 30 seconds or so....this prevents that problem and hides the actual file.
You can then setup a CRON job to delete the symbolic links at regular intervals....
This whole process will then send the file to the browser and it doesn't matter how long it runs since it's not a script.
When I have done this in the past I've used this:
set_time_limit(0); //Set the execution time to infinite.
header('Content-Type: application/exe'); //This was for a LARGE exe (680MB) so the content type was application/exe
readfile($fileName); //readfile will stream the file.
These 3 lines of code will do all the work of the download readfile() will stream the entire file specified to the client, and be sure to set an infinite time limit else you may be running out of time before the file is finished streaming.
If you are using lighttpd as a webserver, an alternative for secure downloads would be to use ModSecDownload. It needs server configuration but you'll let the webserver handle the download itself instead of the PHP script.
Generating the download URL would look like that (taken from the documentation) and it could of course be only generated for authorized users:
<?php
$secret = "verysecret";
$uri_prefix = "/dl/";
# filename
# please note file name starts with "/"
$f = "/secret-file.txt";
# current timestamp
$t = time();
$t_hex = sprintf("%08x", $t);
$m = md5($secret.$f.$t_hex);
# generate link
printf('%s',
$uri_prefix, $m, $t_hex, $f, $f);
?>
Of course, depending on the size of the files, using readfile() such as proposed by Unkwntech is excellent. And using xsendfile as proposed by garrow is another good idea also supported by Apache.
header("Content-length:".filesize($filename));
header('Content-Type: application/zip'); // ZIP file
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="downloadpackage.zip"');
header('Content-Transfer-Encoding: binary');
ob_end_clean();
readfile($filename);
exit();
I'm not sure this is a good idea for large files. If the thread for your download script runs until the user has finished the download, and you're running something like Apache, just 50 or more concurrent downloads could crash your server, because Apache isn't designed to run large numbers of long-running threads at the same time. Of course I might be wrong, if the apache thread somehow terminates and the download sits in a buffer somewhere whilst the download progresses.
I have used the following snippet found in the comments of the php manual entry for readfile:
function _readfileChunked($filename, $retbytes=true) {
$chunksize = 1*(1024*1024); // how many bytes per chunk
$buffer = '';
$cnt =0;
// $handle = fopen($filename, 'rb');
$handle = fopen($filename, 'rb');
if ($handle === false) {
return false;
}
while (!feof($handle)) {
$buffer = fread($handle, $chunksize);
echo $buffer;
ob_flush();
flush();
if ($retbytes) {
$cnt += strlen($buffer);
}
}
$status = fclose($handle);
if ($retbytes && $status) {
return $cnt; // return num. bytes delivered like readfile() does.
}
return $status;
}
I have had same problem,
my problem solved by adding this before starting session
session_cache_limiter('none');
This is tested on files of a size 200+ MB on a server that has 256MB memory limit.
header('Content-Type: application/zip');
header("Content-Disposition: attachment; filename=\"$file_name\"");
set_time_limit(0);
$file = #fopen($filePath, "rb");
while(!feof($file)) {
print(#fread($file, 1024*8));
ob_flush();
flush();
}
I can download remote files using PHP but how do you download from a link that pushes headers out? I mean, you can click on some links and it will force a download and present you with dialog box to save the file. How can I download and save this sort of thing using PHP?
Any examples or links to tutorials would be great since I couldn't find anything useful on this topic.
Thank you for any help
Updated and [SOLVED]
<?php
set_time_limit(300);
// File to download
$remoteFile = $_GET['url'];
$file = fopen($remoteFile, "r");
if (!$file) {
echo "<p>Unable to open remote file.\n";
exit;
}
$line = '';
while (!feof ($file)) {
$line .= fgets ($file, 4096);
}
//readfile($line);
file_put_contents('here2.mp4', $line);
fclose($file);
?>
Just tried to reproduce situation. Gubmo is right, this download method works for me with Content-Type: application/octet-stream and Content-type: application/force-download headers.
As explained here, HTTP 410 means that URL requested by the client is no longer available from that system. This is not a 'never heard of it' response, but a 'does not live here any more' response. Maybe they have some kind of antileach system.
This should be investigated. If they need cookies -- stream-context-create can help. Or maybe they check referer. But I am almost sure that problem is not in headers.
Hope this helps.
UPD Sample code you've asked about.
// file to download -- application/octet-stream
$remoteFile = 'http://dev/test/remote/send.php';
// file to download -- application/force-download
$remoteFile = 'http://chtyvo.org.ua/authors/Skriabin_Kuzma/Ya_Pobieda_i_Berlin.rtf.zip';
// file to store
$localFile = 'kuzma.zip';
$fin = fopen($remoteFile, "r");
if (!$fin) {
die("Unable to open remote file");
}
$fout = fopen($localFile, "w");
if (!$fout) {
die("Unable to open local file");
}
while (!feof($fin)) {
$line = fgets($fin, 1024);
fwrite($fout, $line, 1024);
}
fclose($fout);
fclose($fin);
Same as yours.
You can do it the same way as you download your remote files. Those “force download” header values just tell user agents that want to display the data inline to download them instead. But it makes no difference for your script as it cannot display the data.