Apache does not work with HTTP_RANGE? - php

I wanted to create a .php file, that streams a video!
Now, the problem is, that it works, only if i use a normal readfile(), but then, you can not go back and forward in the video, so i searched on google, to find this code:
(basically, the HTTP_RANGE does not work, NEVER, i do not know why, when testing it, it always fires my die("lol?");, so it clearly does not support it for some reason)
(the die() function is left there on purpose, it will be taken out if it would work..)
(note that i changed "$size = filesize($file);" to "$size = filesize(".".$file);", because someone mentioned that this is required, and "filesize($file);" does not work for me anyways, it always fires an error)!
(and, the $file, shows the actual path for my file, nothing replaced, its how it looks in the original php of me!)
<?php
// Clears the cache and prevent unwanted output
ob_clean();
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
apache_setenv('no-gzip', 1);
ini_set('zlib.output_compression', 'Off');
$file = "/cdn4-e663/zw4su8jiy8skgvizihsjehj/2038tkusi9u848sui7zh/2q3z6hjk97ujduz/a1-cdn/9zw35jbmhkk47wi63uu7.mp4"; // The media file's location
$mime = "application/octet-stream"; // The MIME type of the file, this should be replaced with your own.
$size = filesize(".".$file); // The size of the file
// Send the content type header
header('Content-type: ' . $mime);
// Check if it's a HTTP range request
if(isset($_SERVER['HTTP_RANGE'])){
// Parse the range header to get the byte offset
$ranges = array_map(
'intval', // Parse the parts into integer
explode(
'-', // The range separator
substr($_SERVER['HTTP_RANGE'], 6) // Skip the `bytes=` part of the header
)
);
// If the last range param is empty, it means the EOF (End of File)
if(!$ranges[1]){
$ranges[1] = $size - 1;
}
// Send the appropriate headers
header('HTTP/1.1 206 Partial Content');
header('Accept-Ranges: bytes');
header('Content-Length: ' . ($ranges[1] - $ranges[0])); // The size of the range
// Send the ranges we offered
header(
sprintf(
'Content-Range: bytes %d-%d/%d', // The header format
$ranges[0], // The start range
$ranges[1], // The end range
$size // Total size of the file
)
);
// It's time to output the file
$f = fopen($file, 'rb'); // Open the file in binary mode
$chunkSize = 8192; // The size of each chunk to output
// Seek to the requested start range
fseek($f, $ranges[0]);
die("working?");
// Start outputting the data
while(true){
// Check if we have outputted all the data requested
if(ftell($f) >= $ranges[1]){
break;
}
// Output the data
echo fread($f, $chunkSize);
// Flush the buffer immediately
ob_flush();
flush();
}
}
else {
die("lol?");
header('Content-Length: ' . $size);
// Read the file
readfile($file);
// and flush the buffer
ob_flush();
flush();
}
?>
so, the die("lol?"); was added by me to see if the
if(isset($_SERVER['HTTP_RANGE'])){
/*function fires or not, and no, as it seems it returns FALSE every time..8/
}
so i wanted to ask you all, how can i fix this? i really want to use php to stream my video, because of security reasons, and because i like it, i already use this methode with images but its a different code(and working)!
I am using Apache 2.4 (Windows 10 - 64bit PC) with the latest version of PHP7, but it seems that apache does not support HTTP_RANGE? am i missing something, is there something i need to enable inside either the php.ini or the httpd.conf??
Thank you in advance, i hope someone can tells me what to do, because i really am stuck here, and i tried ALL examples of mp4 video streaming i could find on google, and none worked for me :/

There are 2 parts to this:
The request made by the browser/client. This must send appropriate request headers.
The response given by your server. This is done by your PHP script and must also send the appropriate response headers
When you try and stream your video (or whatever the content is) open the Network tab in your browser.
Look at the Request Headers (in Chrome this is under the Network tab). I've posted a screenshot below. Note that in the request there is a Range: parameter. If this is not present in the request, you'll have problems. This is what tells the PHP script on the server that you are doing a range request in the first place. If the server does not see this header in the request then it will just bypass the if statement and go into the die.
Note that the Range: request header is not normally included in requests by default, so unless you are specifying this, it will never work. If you don't see it in the Request Headers on your Network tab, it is not present, and you need to fix that.
You may also want to examine the response headers - which are totally different from the request headers. Again, these can be seen in the Network tab in your browser. See below for the appropriate headers that must be set:
Going back to the original question, none of it has anything to do with the response (which is what you were describing). The initial problem you are having is all to do with how you're making the request and the fact it does not contain a Range: header, when it must do so.

Related

Is it possible to send a header from a PHP file, that does absolutely nothing

On a page where I offer music sample downloads, I have several <a> tags whose href points to a PHP file. Various data included as GET vars allow the proper file to be downloaded. Normally the PHP will respond with typical download headers followed by a readfile(). (the code for that is below, FYI). This results in a clean download (or download / play dialog box on some browsers). By "clean", I mean the download is completed with no disturbance in the visitors page.
However, in the unlikely event that the requested file is unavailable, I don't know what to do. I know it should not happen, but if it does I would like the download link to simply do NOTHING. Unfortunately since it is an <a> tag referencing a PHP file, doing nothing results in the browser clearing the page, with the URL of the PHP file in the address bar. Not a good visitor experience! So I'd like way to avoid disturbing the page and doing NOTHING if there is is an errant request. I'll use javascript to alert the visitor about what went wrong, but I can't have the errant file request clear the page!
I thought I'd had a solution by issuing a header('Location: #'); when the script detected an impossible file download. But after a few seconds the browser cleared the page and put up a message indicating the page "redirected you too many times." (indeed, my script log fills up with over 100 entries, even though i only clicked the tag once.)
So far the only solution I have that works (works in the sense of NOT disturbing the visitors page if an "unavailable" file is requested) is to point my download headers at a "dummy" file. An actual "silence.mp3" or "nosong.mp3" file. But is there a way to call a header() that does nothing to the calling page? Simply calling exit or exit() won't work (the visitor page is redirected a blank.)
Not that it matters, but this is the code I normally call in response to the d/l request...
function downloadFile($path) {
$path_parts = pathinfo($path);
$ext = strtolower($path_parts["extension"]); // don't need this.
$fsize =fileExists($path);
if ($fsize == 0)
{
header('Location: #'); // this doesn't work!!! (too many redirectcts)
exit;
}
//$dlname = $path_parts['filename'] . "." . strtolower($path_parts["extension"]);
header("Cache-Control: public");
header("Content-Description: File Transfer");
header("Content-Disposition: filename=\"" . $path_parts["basename"]."\"");
header("Content-Type: application/x-file-to-save");
header("Content-Transfer-Encoding: binary");
if($fsize) header("Content-length: $fsize");
$bytesRead = readfile($path);
return $bytesRead;
}
If you are using HTTP/1.x with a standard anchor tag, without JavaScript or other client-side interception. An HTTP/1.0 204 No Content status header will cause the user-agent to simply seem like nothing happened when clicking a link that returns a 204 status header.
HTTP/1.0 204 No Content
The server has fulfilled the request but there is no new information
to send back. If the client is a user agent, it should not change its
document view from that which caused the request to be generated. This
response is primarily intended to allow input for scripts or other
actions to take place without causing a change to the user agent's
active document view. The response may include new metainformation in
the form of entity headers, which should apply to the document
currently in the user agent's active view.
Source: https://www.w3.org/Protocols/HTTP/1.0/spec.html#Code204
This is also compatible with the HTTP/1.1 protocol.
I recommend using output buffering to ensure no other content is being sent by your application by mistake. Additionally there should be no need to send a Content-Length header.
function downloadFile($path) {
if (!is_file($path) || !($fsize = filesize($path))) {
header('HTTP/1.0 204 No Content');
exit;
}
$path_parts = pathinfo($path);
header('Cache-Control: public');
header('Content-Description: File Transfer');
header('Content-Disposition: filename="' . $path_parts['basename'] . '"');
header('Content-Type: application/x-file-to-save');
header('Content-Transfer-Encoding: binary');
header('Content-length: ' . $fsize); //fsize already validated above.
return readfile($path);
}
Performing the file checks before creating the links is the simplest way to do this.
If I understand your request correctly you have files that you wish to allow a client to download, and links to PHP scripts that download certain files.
The problem with your implementation is that when the file is empty, the PHP script still must load and change the content of the clients page(from the action of loading the script), which is the incorrect behavior (correct being no action at all).
Since you are using tags on the main download page, really the only way to not change the content of the page in the case of a missing file is to compute the content of the tags in advance. With a simple PHP function you could check the contents of a list of files and their directories, and then generate links for the ones that exist, and blank links for the ones that do not.
Overall, I think separating the functionality of checking whether a file exists and actually downloading the file to a client is the only way to allow the functionality you desire.

Unable to download a file with example script

I want to create xlsx file and send to the browser as an attachment, so the user downloads it immediately. I literally did copy-paste the code from this example.
But it doesn't work. I fixed the path in require_once, but the issue is somewhere else.
The xlsx file is generated corerctly - when I save ot on the server, I can open it. It is also sent to the browser - firebug's console shows some funny characters in output window. The headers are also correct.
But no Save as... dialog is shown. I did some basic checks based on google search results - I have no extra white space after ?>.
The only difference in my code is that I call php script from jQuery's $.post function with some additional arguments.
Could it be the reason why I can't download this file?
Any help would be greatly appreciated!
PHP version: 5.4.20
PHPExcel version: 1.8.0
Server: Apache/2.4.6 (Linux/SUSE)
This question is also posted on codeplex.
You can't download files via an ajax request such as $.post for security reasons.
You could use a link that opens in a new window instead.
<?php
// If user click the download link
if(isset($_GET['filename'])){
// The directory of downloadable files
// This directory should be unaccessible from web
$file_dir="";
// Replace the slash and backslash character with empty string
// The slash and backslash character can be dangerous
$file_name=str_replace("/", "", $_GET['filename']);
$file_name=str_replace("\\", "", $file_name);
// If the requested file is exist
if(file_exists($file_dir.$file_name)){
// Get the file size
$file_size=filesize($file_dir.$file_name);
// Open the file
$fh=fopen($file_dir.$file_name, "r");
// Download speed in KB/s
$speed=50;
// Initialize the range of bytes to be transferred
$start=0;
$end=$file_size-1;
// Check HTTP_RANGE variable
if(isset($_SERVER['HTTP_RANGE']) &&
preg_match('/^bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $arr)){
// Starting byte
$start=$arr[1];
if($arr[2]){
// Ending byte
$end=$arr[2];
}
}
// Check if starting and ending byte is valid
if($start>$end || $start>=$file_size){
header("HTTP/1.1 416 Requested Range Not Satisfiable");
header("Content-Length: 0");
}
else{
// For the first time download
if($start==0 && $end==$file_size){
// Send HTTP OK header
header("HTTP/1.1 200 OK");
}
else{
// For resume download
// Send Partial Content header
header("HTTP/1.1 206 Partial Content");
// Send Content-Range header
header("Content-Range: bytes ".$start."-".$end."/".$file_size);
}
// Bytes left
$left=$end-$start+1;
// Send the other headers
header("Content-Type: application/octet-stream");
header("Accept-Ranges: bytes");
// Content length should be the bytes left
header("Content-Length: ".$left);
header("Content-Disposition: attachment; filename=".$file_name);
// Read file from the given starting bytes
fseek($fh, $start);
// Loop while there are bytes left
while($left>0){
// Bytes to be transferred
// according to the defined speed
$bytes=$speed*1024;
// Read file per size
echo fread($fh, $bytes);
// Flush the content to client
flush();
// Substract bytes left with the tranferred bytes
$left-=$bytes;
// Delay for 1 second
sleep(1);
}
}
fclose($fh);
}
else
{
// If the requested file is not exist
// Display error message
echo "File not found!";
}
exit();
}
?>
Download.
You can use this code for download you can edit extension and also speed

PHP zip download bug

I have a script that lets users download a zip file on request. It works 100% on computer browsers but does not work on Android/mobile browsers, except for Opera (mobile) only.
Here's my script.
$leads = new Packer($zip_name);
$index = 1;
$count = 0;
foreach($cLeads->lastUnitInfo['leads'] as $lead)
{
// build a request string
$export = 'export_lead_'.$index;
$req = $_POST[$export];
// add it to the zip file
if(isset($req) && $req == '1')
{
// debug only
//echo 'adding lead: '.$lead['file_name'].'<br />';
$leads->addLead('leads/'.$lead['file_name'],$lead['item_name']);
$count++;
//echo 'count: '.$count.'<br/>';
}
$index++;
}
// debug
//exit('count: '.$count); // displays same results on all browsers.
// we got anything packed ?
if($count <= 0) //// <-------- BLOCK OF BUG ON MOBILE PHONE
{
if(file_exists($zip_name))
unlink($zip_name); // delete the zip file created.
exit('<h1>Nothing to export</h1>');
} ///// <---------------------- END BLOCK
// download the leads here.
$leads->purge();
exit;
Here's my purge() function
public function purge($zip_name = 'leads.zip')
{
header('Content-type: application/zip');
header('Content-Disposition: attachment; filename="'.$zip_name.'"');
ob_clean();
flush();
readfile($this->zip_name);
// errors will be disabled here
unlink($this->zip_name);
}
On my Android phone, the zip file gets downloaded but contains <h1>Nothing to export</h1> which renders it as an invalid zip file.
So my problem is, How is it that the block only executes on mobile browsers (except Opera) and then continues to download the zip when it should have exited if $count was zero at all?
I debugged it using Fiddler and the requests are all the same but the output is different, why ?
Is this a bug in PHP? Because if you look at my code the function purge() should output errors saying headers have already been sent but it just continues to download the zip file.
Browsers:
Dolphin (+Beta)
FireFox
Default Android browser
Boat Browser
PHP versions tested:
5.3.13 (Production, shared server)
5.1.4
This is driving me nuts now.
#Alix This is the weirdest bug I've ever seen. I don't seriously see any logical errors here. For the script to initiate a download, there actually has to be files added to the zip. Now on my phone, it says no files are added but there's a zip file in the temp folder. Moreover if there are no files added ($count = 0) then the script should terminate (hence the exit() function) with just a message <h1>Nothing to export</h1>. But it goes on to download the zip file (which at this time does not exist, but in the temp folder it does). The zip file ends up being corrupt as it contains <h1>Nothing to export</h1>
Alix wrote:
*> what happens if you comment out the exit call before serving the file?
It says readfile error then purges the zip file in gibberish UNICODE chars. I can tell it's the zip file because it starts with PK and contains the names of images being exported.
Alix wrote:
If this doesn't work, you may wanna change exit('<h1>Nothing to export</h1>'); to exit(var_dump($_REQUEST));, that way you may check for possible bugs in the form submission by inspecting the Zip file.
Interesting. It prints nothing but the cookie and a $_GET parameter only. If I put the code in your suggestion at the beginning on the script and in the block that adds the files to the zip it prints all the $_POST variables.
There's clearly a bug on PHP's part here. It should terminate when exit is called but it doesn't. And mind you this only happens on mobile browsers except Opera. I'm going to cry blood now.
Are you sure the other browsers are setting $_POST[$export] to the correct value?
Also, you should first ob_[end_]clean() and then output the headers, not the other way around:
public function purge($zip_name = 'leads.zip')
{
ob_end_clean();
flush();
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="'.$zip_name.'"');
readfile($this->zip_name);
// errors will be disabled here
unlink($this->zip_name);
}
Additionally, you may wanna set these headers:
header('Content-Length: ' . intval(filesize($zip_name)));
header('Content-Transfer-Encoding: binary');
If this doesn't work, you may wanna change exit('<h1>Nothing to export</h1>'); to exit(var_dump($_REQUEST));, that way you may check for possible bugs in the form submission by inspecting the Zip file.

How to display an image returned as a bytes in browser without prompting download?

I have written the following PHP function but still get the prompt to download the file:
function navigateToBytes($contentType, $bytes){
header('Content-Type: ' .$contentType);
//header('Content-Transfer-Encoding: binary'); // UPDATE: as pointed out this is not needed, though it does not solve the problem
header('Content-Length: '.strlen($bytes));
ob_clean();
flush();
echo $bytes;
}
An example of calling the function:
navigateToBytes('image/jpeg', $bytes); // UPDATE: turns out this does work, using image/tiff for tiff images is when the browser does not display the image
where $bytes are the bytes as read from the file.
Apologies all - turns out I was having the problem because the images I was testing were TIFF's (with the Content-Type correctly set to image/tiff) when I used a JPEG the browser would display the image!
Ultimately it is up to the browser to decide whether it can display the Content-Type you are sending.
For the record the only headers I needed to change was
Content-Type,
I should set
Content-Length
too unless I set
Transfer-Encoding: chunked
Try the HTTP header "Content-Disposition: Inline", however some browsers may try to save the user from seeing binary data. Here is a random blog article on that HTTP header:
http://dotanything.wordpress.com/2008/05/30/content-disposition-attachment-vs-inline/
That seems like correct behavior to me. The browser is a viewport for humans to view things in. Humans, by and large, don't want to view binary data. What do you think should happen?
Random Advice: If there's a site that's doing what you want to do, use curl to sniff the headers they're sending.
curl -I http://example.com/path/to/binary/file/that/displays/in/browser
and then use the exact same headers in your own script.
As a start, get rid of things that do not exist in HTTP (Content-Transfer-Encoding).
Then get an HTTP tracing tool, such as the Live HTTP headers plugin for Firefox, and compare "your" headers with those received for a working image.
In doubt, post the HTTP trace here.

Is there a good implementation of partial file downloading in PHP?

I'm writing a PHP script that allows the user to download a file. Basically the idea is to prevent the file being downloaded more than X times, since it is paid content, and the link should not be spread around.
Since the files will be pretty large, it should be good to implement resuming. I've read the standard, but it's pretty long and allows for some flexibility. Since I need to get it done quickly, I'd prefer a stable, tested implementation of this feature.
Can anyone point me to such a a script?
Seems that I found what I needed myself. So that other may benefit from this, here is the link: http://www.coneural.org/florian/papers/04_byteserving.php
And just in case the original page stops to work (the script is pretty old already), here is a copy of it:
<?php
/*
The following byte serving code is (C) 2004 Razvan Florian. You may find the latest version at
http://www.coneural.org/florian/papers/04_byteserving.php
*/
function set_range($range, $filesize, &$first, &$last){
/*
Sets the first and last bytes of a range, given a range expressed as a string
and the size of the file.
If the end of the range is not specified, or the end of the range is greater
than the length of the file, $last is set as the end of the file.
If the begining of the range is not specified, the meaning of the value after
the dash is "get the last n bytes of the file".
If $first is greater than $last, the range is not satisfiable, and we should
return a response with a status of 416 (Requested range not satisfiable).
Examples:
$range='0-499', $filesize=1000 => $first=0, $last=499 .
$range='500-', $filesize=1000 => $first=500, $last=999 .
$range='500-1200', $filesize=1000 => $first=500, $last=999 .
$range='-200', $filesize=1000 => $first=800, $last=999 .
*/
$dash=strpos($range,'-');
$first=trim(substr($range,0,$dash));
$last=trim(substr($range,$dash+1));
if ($first=='') {
//suffix byte range: gets last n bytes
$suffix=$last;
$last=$filesize-1;
$first=$filesize-$suffix;
if($first<0) $first=0;
} else {
if ($last=='' || $last>$filesize-1) $last=$filesize-1;
}
if($first>$last){
//unsatisfiable range
header("Status: 416 Requested range not satisfiable");
header("Content-Range: */$filesize");
exit;
}
}
function buffered_read($file, $bytes, $buffer_size=1024){
/*
Outputs up to $bytes from the file $file to standard output, $buffer_size bytes at a time.
*/
$bytes_left=$bytes;
while($bytes_left>0 && !feof($file)){
if($bytes_left>$buffer_size)
$bytes_to_read=$buffer_size;
else
$bytes_to_read=$bytes_left;
$bytes_left-=$bytes_to_read;
$contents=fread($file, $bytes_to_read);
echo $contents;
flush();
}
}
function byteserve($filename){
/*
Byteserves the file $filename.
When there is a request for a single range, the content is transmitted
with a Content-Range header, and a Content-Length header showing the number
of bytes actually transferred.
When there is a request for multiple ranges, these are transmitted as a
multipart message. The multipart media type used for this purpose is
"multipart/byteranges".
*/
$filesize=filesize($filename);
$file=fopen($filename,"rb");
$ranges=NULL;
if ($_SERVER['REQUEST_METHOD']=='GET' && isset($_SERVER['HTTP_RANGE']) && $range=stristr(trim($_SERVER['HTTP_RANGE']),'bytes=')){
$range=substr($range,6);
$boundary='g45d64df96bmdf4sdgh45hf5';//set a random boundary
$ranges=explode(',',$range);
}
if($ranges && count($ranges)){
header("HTTP/1.1 206 Partial content");
header("Accept-Ranges: bytes");
if(count($ranges)>1){
/*
More than one range is requested.
*/
//compute content length
$content_length=0;
foreach ($ranges as $range){
set_range($range, $filesize, $first, $last);
$content_length+=strlen("\r\n--$boundary\r\n");
$content_length+=strlen("Content-type: application/pdf\r\n");
$content_length+=strlen("Content-range: bytes $first-$last/$filesize\r\n\r\n");
$content_length+=$last-$first+1;
}
$content_length+=strlen("\r\n--$boundary--\r\n");
//output headers
header("Content-Length: $content_length");
//see http://httpd.apache.org/docs/misc/known_client_problems.html for an discussion of x-byteranges vs. byteranges
header("Content-Type: multipart/x-byteranges; boundary=$boundary");
//output the content
foreach ($ranges as $range){
set_range($range, $filesize, $first, $last);
echo "\r\n--$boundary\r\n";
echo "Content-type: application/pdf\r\n";
echo "Content-range: bytes $first-$last/$filesize\r\n\r\n";
fseek($file,$first);
buffered_read ($file, $last-$first+1);
}
echo "\r\n--$boundary--\r\n";
} else {
/*
A single range is requested.
*/
$range=$ranges[0];
set_range($range, $filesize, $first, $last);
header("Content-Length: ".($last-$first+1) );
header("Content-Range: bytes $first-$last/$filesize");
header("Content-Type: application/pdf");
fseek($file,$first);
buffered_read($file, $last-$first+1);
}
} else{
//no byteserving
header("Accept-Ranges: bytes");
header("Content-Length: $filesize");
header("Content-Type: application/pdf");
readfile($filename);
}
fclose($file);
}
function serve($filename, $download=0){
//Just serves the file without byteserving
//if $download=true, then the save file dialog appears
$filesize=filesize($filename);
header("Content-Length: $filesize");
header("Content-Type: application/pdf");
$filename_parts=pathinfo($filename);
if($download) header('Content-disposition: attachment; filename='.$filename_parts['basename']);
readfile($filename);
}
//unset magic quotes; otherwise, file contents will be modified
set_magic_quotes_runtime(0);
//do not send cache limiter header
ini_set('session.cache_limiter','none');
$filename='myfile.pdf'; //this is the PDF file that will be byteserved
byteserve($filename); //byteserve it!
?>
You should be using PEAR HTTP_Download. It is pretty easy to use and it allows download resuming just file:
http://pear.php.net/manual/en/package.http.http-download.intro.php
Based on this:
http://w-shadow.com/blog/2007/08/12/how-to-force-file-download-with-php/
(which you also could use)
I've made a small lib that does what PECL http_send_file extension does:
http://php.net/manual/en/function.http-send-file.php
(which you also could use)
The lib resembles the http_send_file, but if you don't have the option of installing the PECL lib, you could use the http-send-file lib:
https://github.com/diversen/http-send-file
See http://us3.php.net/manual/en/function.fread.php
An alternative is to let the web server can handle http by redirecting to the file in question.
A PHP script can do any checks needed (security, authentication, validate the file, incrementing the download count) and any other tasks before calling header("Location $urltofile");
I tested this with apache. Interrupt/resume download works. The server's mime type configuration will determine client behavior. For apache, if defaults in mime.types are not suitable, configuration directives for mod_mime could go in a .htaccess file in the directory of the file to download. If really necessary, these could even by written by the PHP script before it redirects.
Perhaps instead of implementing web server in a web server (yo dawg!) you could use mod trigger before download in lighttpd or mod X-Sendfile available for both lighttpd and Apache2?

Categories