views:

45

answers:

2

For the past 3 months my site has been using a PHP file handler in combination with htaccess. Users accessing the uploads folder of the site would be redirected to the handler as such:

RewriteRule  ^(.+)\.*$ downloader.php?f=%{REQUEST_FILENAME} [L]

The purpose of the file handler is pseudo coded below, followed by actual code.

//Check if file exists and user is downloading from uploads directory; True.
//Check against a file type white list and set the mime type(); $ctype = mime type;
header("Pragma: public"); // required
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Cache-Control: private",false); // required for certain browsers
header("Content-Type: $ctype");
header("Content-Disposition: attachment; filename=\"".basename($filename)."\";" );
header("Content-Transfer-Encoding: binary");
header("Content-Length: ".filesize($filename));
readfile("$filename");

As of yesterday, the handler started returning garbled files, unreadable images, and had to be bypassed. I'm wondering what settings could have gone awry to cause this.

-EDIT-

Problem found, but not resolved. Including a path to a php library I was using for integrating with Wordpress was corrupting the files. Removing that block of code solves the corruption issue but leaves the files accessible without the desired authentication.

  @include_once($_SERVER['DOCUMENT_ROOT'].'/wp-blog-header.php');
  if(!is_user_logged_in()) 
  {
    auth_redirect(); //Kicks the user to a login page.
  }

  //resume download script
+1  A: 

Maybe more tests will reveal the problem...

if ( !isset($filename) ) {
  die('parameter "filename" not set');
}
else if ( !file_exists($filename) ) {
  die('file does not exist');
}
else if ( !is_readable($filename) ) {
  die('file not readable');
}
else if ( false===($size=filesize($filename)) ) {
  die('stat failed');
}
else if ( headers_sent() || ob_get_length()>0) {
    die('something already sent output.');
}
else {
  $basename = basename($filename);

  header("Pragma: public");
  header("Expires: 0");
  header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
  header("Cache-Control: private",false); // required for certain browsers
  header("Content-Type: $ctype");
  header("Content-Disposition: attachment; filename=\"".$basename."\";" );
  header("Content-Transfer-Encoding: binary");
  header("Content-Length: ".$size);
  readfile($filename);
}
VolkerK
Looking at this code (thanks btw), it seems that a header has already been sent. Investigating further.
A: 

How are the files corrupted? Truncated? 0-byte? Completely different content? Random sections replaced with garbage?

Is it possible the server's PHP memory limit has been lowered? readfile() will buffer the whole file in memory before outputing it. Therefore a 40meg file will fail is the memory limit is 39.9999, kind of thing.

For streaming a file to the user, it's best to NOT use php's own "dump file to browser" functions, as they're all subject to the memory limit. It's best to do an fopen/fwrite/fclose loop and spit the file out in small manageable chunks (4k, 16k, etc...).

Marc B
readfile() either uses memory mapped files or a buffer of 8k (which is not subject to memory_limit), see `PHPAPI size_t _php_stream_passthru(php_stream * stream STREAMS_DC TSRMLS_DC)` in main/stream/streams.c, http://cvs.php.net/viewvc.cgi/php-src/main/streams/streams.c?view=log
VolkerK