php file download: weird http header

advertisements

i'm builing this php script which displays a given folder 's children folders and files. Files ought to be downloaded.

In order to disclose as little information as possible, each item in the directory is a form with a submit button that contains the html markup. The send method is POST.

IF i click on a directory, it opens that directory and displays the content just fine. If i click on a file, it shows a download prompt and the file downloads just fine. But the click after that, i get to see such thing:

0 65535 f
0000000000 65535 f
0000000000 65535 f
0000000000 65535 f
0000000000 65535 f
0000000000 65535 f
0000000000 65535 f
0000000000 65535 f
0000000000 65535 f
0000000000 65535 f
0000000000 65535 f
0000000000 65535 f
0000000000 65535 f
0000000000 65535 f
trailer
<</Size 34>>
startxref
116
%%EOF
HTTP/1.1 200 OK
Date: Mon, 31 Aug 2009 21:01:36 GMT
Server: Apache
X-Powered-By: PHP/5.2.5
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Keep-Alive: timeout=15, max=93
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html

2822
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">

and the rest of the html code source of the display page is listed.

What is going on here?

UPDATE: my "force download" code:

function offerToDownloadFile($filename, $access_type='url') {

// converting url to local path so Apache can find the file.
// force download:
// required for IE, otherwise Content-disposition is ignored
    if (ini_get('zlib.output_compression'))
        ini_set('zlib.output_compression', 'Off');

    if($access_type === 'url') {
    // access type is via the file 's url
        $parsed_url = parse_url($filename);
        $fileinfo = pathinfo($filename);
        $parsed_url['extension'] = $fileinfo['extension'];
        $parsed_url['filename'] = $fileinfo['basename'];

        $parsed_url['localpath'] = LOCALROOT . $parsed_url['path'];
    }
    else {
    // access type is the local file path
        $fileinfo = pathinfo($filename);
        $parsed_url['localpath'] = $filename;
        $parsed_url['filename'] = basename($filename);
        $parsed_url['extension'] = $fileinfo['extension'];
    }

    // just in case there is a double slash created when joining document_root and path
    $parsed_url['localpath'] = preg_replace('/\/\//', '/', $parsed_url['localpath']);

    if (!file_exists($parsed_url['localpath'])) {
        die('File not found: ' . $parsed_url['localpath']);
    }
    $allowed_ext = array('ics','pdf', 'png', 'jpg', 'jpeg', 'zip', 'doc', 'xls', 'gif', 'exe', 'ppt');
    if (!in_array($parsed_url['extension'], $allowed_ext)) {
        die('This file type is forbidden.');
    }

    switch ($parsed_url['extension']) {
        case "ics": $ctype="text/calendar";
            break;
        case "pdf": $ctype = "application/pdf";
            break;
        case "exe": $ctype = "application/octet-stream";
            break;
        case "zip": $ctype = "application/zip";
            break;
        case "doc": $ctype = "application/msword";
            break;
        case "xls": $ctype = "application/vnd.ms-excel";
            break;
        case "ppt": $ctype = "application/vnd.ms-powerpoint";
            break;
        case "gif": $ctype = "image/gif";
            break;
        case "png": $ctype = "image/png";
            break;
        case "jpeg":
        case "jpg": $ctype = "image/jpg";
            break;
        default: $ctype = "application/force-download";
    }
    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=\"" . $parsed_url['filename'] . "\";");
    header("Content-Transfer-Encoding: binary");
    header("Content-Length: " . filesize($parsed_url['localpath']));
    readfile($parsed_url['localpath']);
    clearstatcache();
    exit();

}


By the looks of it your file download script may be setting the wrong Content-Length, so that the last few bytes of the file can spill over into the next request, causing all manners of brokenness.

What web server and OS are you using, and how are you invoking PHP? Because if it's Windows I'd have a suspicion that your PHP installation may somehow be writing to stdout as a text instead of a binary stream. This would cause each \n byte to be converted to a \r\n sequence, which would probably make most binary files unreadable as well as making the response body a bit longer than the Content-Length header said it was.

$parsed_url['localpath'] = LOCALROOT . $parsed_url['path'];

That seems rather dangerous. I would hope $filename were checked and validated to within an inch of its life before trying anything like that. (Filename validation is deceptively hard.)