views:

367

answers:

2

In my website I've a php script that launches a file download without showing the full path of the file, the code look like this:

$path = '../examples/test.zip';
$type = "application/zip";

header("Expires: 0");
header("Pragma: no-cache");
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: pre-check=0, post-check=0, max-age=0');
header("Content-Description: File Transfer");
header("Content-Type: " . $type);
header("Content-Length: " .(string)(filesize($path)) );
header('Content-Disposition: attachment; filename="'.basename($path).'"');
header("Content-Transfer-Encoding: binary\n");

readfile($path); // outputs the content of the file

exit();

There is a way to add an HTTP authentication before to launch the download?

Thanks in advance!


EDIT 1: Thanks to André Hoffmann, I've solved the problem by using an HTTP Basic Authentication! But If I would like to use an HTTP Digest Authentication like the following, how can I do? I've tried to write the code above, after the line: "echo 'Your are logged in as: ' . $data['username'];" ... but I get an error saying that I can not modify the header twice!

<?php

$realm = 'Restricted area';

//user => password
$users = array('admin' => 'mypass', 'guest' => 'guest');


if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
    header('HTTP/1.1 401 Unauthorized');
    header('WWW-Authenticate: Digest realm="'.$realm.
           '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');

    die('Text to send if user hits Cancel button');
}


// analyze the PHP_AUTH_DIGEST variable
if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) ||
    !isset($users[$data['username']]))
    die('Wrong Credentials!');


// generate the valid response
$A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]);
$A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
$valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);

if ($data['response'] != $valid_response){
    die('Wrong Credentials!');
}

// ok, valid username & password
echo 'Your are logged in as: ' . $data['username'];


// function to parse the http auth header
function http_digest_parse($txt)
{
    // protect against missing data
    $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
    $data = array();
    $keys = implode('|', array_keys($needed_parts));

    preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);

    foreach ($matches as $m) {
        $data[$m[1]] = $m[3] ? $m[3] : $m[4];
        unset($needed_parts[$m[1]]);
    }

    return $needed_parts ? false : $data;
}

?>


SOLUTION:

Thanks to André and Anthony, I can write the solution:

<?php

$realm = 'Restricted area';

//user => password
$users = array('admin' => 'mypass', 'guest' => 'guest');


if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
    header('HTTP/1.1 401 Unauthorized');
    header('WWW-Authenticate: Digest realm="'.$realm.
           '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');

    die('Text to send if user hits Cancel button');
}


// analyze the PHP_AUTH_DIGEST variable
if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) ||
    !isset($users[$data['username']]))
    die('Wrong Credentials!');


// generate the valid response
$A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]);
$A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
$valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);

if ($data['response'] != $valid_response){
    die('Wrong Credentials!');
}

// ok, valid username & password ... start the download
$path = '../examples/test.zip';
$type = "application/zip";

header("Expires: 0");
header("Pragma: no-cache");
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: pre-check=0, post-check=0, max-age=0');
header("Content-Description: File Transfer");
header("Content-Type: " . $type);
header("Content-Length: " .(string)(filesize($path)) );
header('Content-Disposition: attachment; filename="'.basename($path).'"');
header("Content-Transfer-Encoding: binary\n");

readfile($path); // outputs the content of the file

exit();


// function to parse the http auth header
function http_digest_parse($txt)
{
    // protect against missing data
    $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
    $data = array();
    $keys = implode('|', array_keys($needed_parts));

    preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);

    foreach ($matches as $m) {
        $data[$m[1]] = $m[3] ? $m[3] : $m[4];
        unset($needed_parts[$m[1]]);
    }

    return $needed_parts ? false : $data;
}

?>
+1  A: 

Sure see the manual for this.

<?php
if (!isset($_SERVER['PHP_AUTH_USER'])) {
    header('WWW-Authenticate: Basic realm="My Realm"');
    header('HTTP/1.0 401 Unauthorized');
    echo 'Text to send if user hits Cancel button';
    exit;
} else {
    //check $_SERVER['PHP_AUTH_USER'] and $_SERVER['PHP_AUTH_PW']
    if ($valid) {
        //start download

        $path = '../examples/test.zip';
        $type = "application/zip";

        header("Expires: 0");
        header("Pragma: no-cache");
        header('Cache-Control: no-store, no-cache, must-revalidate');
        header('Cache-Control: pre-check=0, post-check=0, max-age=0');
        header("Content-Description: File Transfer");
        header("Content-Type: " . $type);
        header("Content-Length: " .(string)(filesize($path)) );
        header('Content-Disposition: attachment; filename="'.basename($path).'"');
        header("Content-Transfer-Encoding: binary\n");

        readfile($path); // outputs the content of the file

        exit();
    } else {
        //show error
    }
}

UPDATE:

A .htaccess based authentication acutally allows to be used by more than one user. Put this in your .htaccess:

AuthType Basic
AuthName "Password Required"
AuthUserFile passwords.file
AuthGroupFile groups.file

The file passwords.file containing the passwords can be created using the htpasswd tool that came with apache. The file groups.file should like similar to this:

GroupName: rbowen dpitts sungo rshersey

Here you basically just list the users that should have access to the directory.

Please also see this tutorial.

André Hoffmann
Thanks a lot!Before I have tried to use the HTTP Digest Authentication but I get an error saying that I can not modify the header twice!
BitDrink
Thanks again André! I have still a lot of things to learn ... so thanks!
BitDrink
+1  A: 

Are you trying to initiate the download after having the script echos "You have successfully logged in as ..."?

You can't set any headers after you have output anything to screen. As soon as you echo or print or what-have-you, you have started the body part of the HTTP response, which means the headers have been set.

By the way, if you try setting the headers and THEN giving the "you've logged in" bit, that will get stuck in the file, not output to screen.

What you want to do is have the script output "you are logged in" and the redirect to the script that sends the download headers. The user won't see that second page, as the header is set to "Attachment". This is how your typical "Your download will begin momentarily" pages work.

Anthony
You are right Anthony! Thanks!
BitDrink