Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to return an image to an https request with PHP

After many hours,

I nailed down my problem concerning downloading an image with an https request. When I access an image with the hard path (https://mysite/tmp/file.jpg), apache returns it with success and I can view it in the browser, without any extra manipulation. When I try to access it with another path (https://mysite/files/file.jpg), in order to control its access with php, I get a response from my php code, but I cannot view the image in the browser.

  • VirtualHost defined; mysite: set to /var/www/mysite
  • $app['controllers']->requireHttps();

Description of the environment:

mysite/tmp/file.jpg          
mysite/files/.htaccess
            //... no files; handled by Silex router. here is the .htaccess:
            ---
            <IfModule mod_rewrite.c>
                RewriteEngine On
                RewriteRule ^ ../web/index.php [L]
            </IfModule>
            ---
  • https:// mysite/tmp/file.jpg, served with https:200 and viewed in browser; ok
  • https:// mysite/files/file.jpg served with https:200 but not viewed in browser; ?

Here are the 3 methods tried:

Method 1: Silex sendFile() direct method >

$app->get('/files/{onefile}', function ($onefile) use ($app) {    
    // Validate authorization;  if ok, then
    return $app->sendFile('/var/www/mysite/tmp/' . $onefile);
});

Method 2: Silex Streaming >

$app->get('/files/{onefile}', function ($onefile) use ($app) {   
    // Validate authorization;  if ok, then
   $stream = function () use ($file) {
       readfile($file);
   };
   return $app->stream($stream, 200, array('Content-Type' => 'image/jpeg'));

Method 3: Symfony2 style >

$app->get('/files/{onefile}', function ($onefile) use ($app) {   
    // Validate authorization;  if ok, then
    $exactFile="/var/www/mysite/tmp/" . $onefile;
    $response = new StreamedResponse();
    $response->setCallback(function () use ($exactFile) {
        $fp = fopen($exactFile, 'rb');
        fpassthru($fp);
    });
    $response->headers->set('Content-Type', 'image/jpg');
    $response->headers->set('Content-length', filesize($exactFile));
    $response->headers->set('Connection', 'Keep-Alive');
    $response->headers->set('Accept-Ranges','bytes');
    $response->send();

This is what Chrome presents:

enter image description here

With this Chrome image this is the Http (Https or not, same result) request

Request URL:https://mysite/files/tmpphp0XkXn9.jpg
Request Method:GET
Status Code:200 OK

Request Headers:
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Cache-Control:no-cache
Connection:keep-alive
Cookie:XDEBUG_SESSION=netbeans-xdebug; _MYCOOKIE=u1k1vafhaqik2d4jknko3c94j1
Host:mysite
Pragma:no-cache
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36

Response Headers:
Accept-Ranges:bytes
Cache-Control:no-cache
Connection:Keep-Alive, Keep-Alive
Content-Length:39497
Content-Type:image/jpg
Date:Thu, 13 Jun 2013 13:44:55 GMT
Keep-Alive:timeout=15, max=99
Server:Apache/2.2.16 (Debian)
X-Powered-By:PHP/5.3.3-7+squeeze15

Other tests made to eliminate possible undesired behavior:

  • I checked the BOM and made sure the php code responding to the request is valid and do not have undesired byte-order marks (BOMs) using Emacs (set-buffer-file-coding-system utf-8). Furthermore, to avoid unknown BOM tagged files, I executed the following command in the mysite directory (grep -rl $'\xEF\xBB\xBF' .). Nothing abnormal appeared.

UPDATE:

Looking at the files (image) received by the browser (save as on each image), this is what the tool (Hex Friend) help me to find, but I still do not understand why:

Comparing the two files

  • (one with success: mysite/tmp/file.jpg ; served directly by Apache)
  • (one with NO success: mysite/files/file.jpg; served by a PHP script).

At the beginning of the binary file, I get this difference: enter image description here

At the end of the binary file, I get this difference: enter image description here

Question: How can I return an image (in stream or some other technique) with a php code, and view it in the browser ? All php code methods return the same output, I suspect an environment problem. Is there an environment setting that could produce the error I am experimenting ?

like image 584
Alain Avatar asked Jun 12 '13 22:06

Alain


2 Answers

I am not happy with this solution (A PATCH), but the problem is fixed, by adding the following call:

$app->get('/files/{onefile}', function ($onefile) use ($app) {    
  ob_end_clean();  // ob_clean() gave me the same result.
...
}

Ok, it is fixed, but, can somebody explain to me how to fix it more intelligently ?

UPDATE

What happened ?:

This is what my interpretation:
Extra newlines remains in the orignal php raw files to a unintentionally position! It turns out that when you have a php file

<?php
...
?>

where you left some newlines (or spaces) after the ?>, those newlines will add up in the output buffer. Then trying to stream a file to the output will take these add up newlines and put them where they belong: in the header stream, or at the footer stream. Having a fixed size for the stream (equal to the image size) will take specially the header extra characters and shift accordingly the bytes outputted to the browser. I suspected that I add exactly 5 characters (0A 0A 20 0A 0A) corresponding to (linefeed linefeed space linefeed linefeed) discovered in the received image from the browser. Now the browser do not recognize the image structure, being shift from the offset 0, of 5 non logical character for the binary image. Therefore, the browser can only show a broken image icon.

Also look at: Another helping SO fix

Advise to PHP framework developer:

If you provide a sendFile() equivalent method, maybe you should thrown an Exception, when ob_get_contents() does not return an empty buffer, just before streaming to the output !

For now:

This small linux batch file can at least let you find where your code should be cleaned. After using it, I was able to remove the ob_end_clean()... after analyzing the output of this small script. It tells you suspicious php files that might contain extra space or newline at the end of your php files. Just execute and manually fix files:

#!/bin/bash

for phpfiles in $(ls -1R *.php); do
   hexdump -e '1/1 "%.2x"' $phpfiles | tail -1 >endofphpfile;
   if [  `cat endofphpfile` = "3f3e" ]; then
      echo "OK.................File  $phpfiles"
   else 
      thisout=`cat endofphpfile`
      echo "File  $phpfiles: Suspucious. Check ($thisout) at the EOF; it should end with '?>' (in hex 3f3e) "
   fi
done

It can surely be improved, but that should, at least, help anybody !

like image 131
Alain Avatar answered Nov 15 '22 11:11

Alain


don't use the closing ?> ... it's against symfony coding standards.

Against PSR-2 to be more concrete.

All PHP files MUST end with a single blank line.

The closing ?> tag MUST be omitted from files containing only PHP.

The reason for that is partly what you experienced.

This behavior is sometimes caused by the BOM aswell.

check none of your files contains the byte-order-mark!

like image 27
Nicolai Fröhlich Avatar answered Nov 15 '22 13:11

Nicolai Fröhlich