Защищенная от идиотов, кроссбраузерная принудительная загрузка на PHP

StackOverflow https://stackoverflow.com/questions/2222955

  •  19-09-2019
  •  | 
  •  

Вопрос

Я использую принудительную загрузку для загрузки в основном zip-файлов и mp3-файлов с сайта, который я сделал (http://pr1pad.kissyour.net) - отслеживать загрузки в Google analytics, в базе данных и скрывать реальный путь загрузки.:

Это все:

extending CI model

... - bunch of code

function _fullread ($sd, $len) {
 $ret = '';
 $read = 0;
 while ($read < $len && ($buf = fread($sd, $len - $read))) {
  $read += strlen($buf);
  $ret .= $buf;
 }
 return $ret;
}

function download(){    
    /* DOWNLOAD ITSELF */

    ini_set('memory_limit', '160M');
    apache_setenv('no-gzip', '1');
    ob_end_flush();

    header("Pragma: public");
    header("Expires: 0");
    header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
    header("Cache-Control: public",FALSE);
    header("Content-Description: File Transfer");
    header("Content-type: application/octet-stream");
     if (isset($_SERVER['HTTP_USER_AGENT']) && 
      (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== false))
      header('Content-Type: application/force-download'); //IE HEADER
    header("Accept-Ranges: bytes");
    header("Content-Disposition: attachment; filename=\"" . basename("dir-with-    files/".$filename) . "\";");
    header("Content-Transfer-Encoding: binary");
    header("Content-Length: " . filesize("dir-with-files/".$filename));

    // Send file for download
    if ($stream = fopen("dir-with-files/$filename", 'rb')){
     while(!feof($stream) && connection_status() == 0){
      //reset time limit for big files
      set_time_limit(0);
      print($this->_fullread($stream,1024*16));
      flush();
     }
     fclose($stream);
    }
}

Это на LAMP с CI 1.7.2 - это мой собственный метод, собранный из различных инструкций по всему Интернету, потому что во время разработки возникли эти проблемы:- ограничение сервера. ini_set не помогло, поэтому я использовал буферизованный _fullread вместо обычного fread, который был использован вместо @readonly - ob_end_flush(), потому что сайт выполнен в CI1.7.2, и мне нужно было очистить буфер

Сейчас же...Это не работает.Это произошло, затем он перестал показывать ожидаемый размер / время загрузки - я попытался очистить его, и пока я очищал код, что-то произошло, я не знаю, что и в Любой предыдущая версия - она не работала (никаких изменений в настройках вообще) - Редактировать:не работает = выводит все в окно браузера.

Поэтому я сказал, к черту все, я посмотрю здесь.

Итак, я в основном ищу скрипт или функцию, которые я могу поместить в свою выходную модель и буду делать:

  • Вызовите принудительную загрузку (в Chrome запустите загрузку, в IE, FF, Safari откройте режим открытия / сохранения / отмены)
  • Показывать размер файла и расчетное время dl (это зависит от браузера, я знаю, но сначала браузер должен знать размер файла
  • РАБОТАЕТ (протестировано и подтверждено!) в IE6, 7, 8, FF3, Opera, Chrome и safari на ПК + Mac (Linux...На самом деле мне все равно) - это для заголовочной части
  • на сервере у меня также есть что-то вроде ограничения памяти в 56 МБ, которое я не могу увеличить, так что это тоже важно

Заранее благодарю вас.

Редактировать:Теперь я чувствую себя более облажавшимся, чем когда-либо / прежде, так как я пытался принудительно загрузить с помощью .htaccess - хотя это работало, у него было несколько мелких / серьезных (выбирайте сами) проблем

  • он показал полный путь (незначительный для меня)
  • он ожидает завершения полной загрузки (отображается как "подключение"), а затем просто показывает, что он загружается - и загружается за одну секунду (для меня это важно)

Теперь, хотя я удалил .htaccess, он все еще ожидает завершения загрузки (точно так же, как если бы он сначала загружался в кэш), и он просто получает connected и покажите диалоговое окно открытия / сохранения.

Это было полезно?

Решение

Итак, я использовал этот код (это модифицированная версия возобновляемой http-загрузки, найденная в Интернете).

function _output_file($file, $path)
{
    $size = filesize($path.$file);

    @ob_end_clean(); //turn off output buffering to decrease cpu usage

    // required for IE, otherwise Content-Disposition may be ignored
    if(ini_get('zlib.output_compression'))
    ini_set('zlib.output_compression', 'Off');

    header('Content-Type: application/force-download');
    header('Content-Disposition: attachment; filename="'.basename($file).'"');
    header("Content-Transfer-Encoding: binary");
    header('Accept-Ranges: bytes');

    /* The three lines below basically make the 
    download non-cacheable */
    header("Cache-control: no-cache, pre-check=0, post-check=0");
    header("Cache-control: private");
    header('Pragma: private');
    header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");

    // multipart-download and download resuming support
    if(isset($_SERVER['HTTP_RANGE']))
    {
        list($a, $range) = explode("=",$_SERVER['HTTP_RANGE'],2);
        list($range) = explode(",",$range,2);
        list($range, $range_end) = explode("-", $range);
        $range=intval($range);
        if(!$range_end) {
            $range_end=$size-1;
        } else {
            $range_end=intval($range_end);
        }

        $new_length = $range_end-$range+1;
        header("HTTP/1.1 206 Partial Content");
        header("Content-Length: $new_length");
        header("Content-Range: bytes $range-$range_end/$size");
    } else {
        $new_length=$size;
        header("Content-Length: ".$size);
    }

    /* output the file itself */
    $chunksize = 1*(1024*1024); //you may want to change this
    $bytes_send = 0;
    if ($file = fopen($path.$file, 'rb'))
    {
        if(isset($_SERVER['HTTP_RANGE']))
        fseek($file, $range);

        while
            (!feof($file) && 
             (!connection_aborted()) && 
             ($bytes_send<$new_length) )
        {
            $buffer = fread($file, $chunksize);
            print($buffer); //echo($buffer); // is also possible
            flush();
            $bytes_send += strlen($buffer);
        }
    fclose($file);
    } else die('Error - can not open file.');

die();
}

а затем в модели:

function download_file($filename){
    /*
        DOWNLOAD
    */
    $path = "datadirwithmyfiles/"; //directory

    //track analytics

    include('includes/Galvanize.php'); //great plugin
    $GA = new Galvanize('UA-XXXXXXX-7');
    $GA->trackPageView();

    $this->_output_file($filename, $path);

}

Он работает, как и ожидалось, во всех упомянутых браузерах на Win / MAC - пока с ним никаких проблем.

Другие советы

Ладно, это старый вопрос, и Адам уже принял свой собственный ответ, так что, по-видимому, у него это получилось само собой, но он не объяснил, почему это сработало.Одна вещь, которую я заметил, заключалась в том, что в вопросе он использовал заголовки:

header("Pragma: public");
header("Cache-Control: public",FALSE);

Принимая во внимание, что в решении, которое он использовал:

header("Cache-control: private");
header('Pragma: private');

Он не объяснил, почему он изменил их, но я подозреваю, что это связано с использованием SSL.Недавно я решил аналогичную проблему в программном обеспечении, которому необходимо включить загрузку как по HTTP, так и по HTTPS, используя следующее для добавления правильного заголовка:

if(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) {
    header("Cache-control: private");
    header('Pragma: private');
} else {
    header('Pragma: public');
}

Надеюсь, кто-нибудь сочтет информацию, содержащуюся в этом ответе, полезным дополнением к вышесказанному.

Есть одна вещь, которую я нахожу странной:Ты зовешь ob_end_flush() в начале выполнения функции.Это фактически очищает выходной буфер, но сначала он также выводит все клиенту (я предполагаю, включая заголовки содержимого, установленные CodeIgniter).Измените вызов на ob_end_clean(), он очищает буфер и отбрасывает его.Это даст вам чистое начало для создания ваших собственных заголовков.

Еще один совет:

Вместо того чтобы читать файл в виде потока и передавать его по блокам, вы могли бы попробовать эту функцию:

// ...
if (file_exists("dir-with-files/$filename")) {
   readfile($file);
}

Это заботится почти обо всем.

print($this->_fullread($stream,1024*16));

Я предполагаю, что _fullread находится внутри класса?Если код выглядит как приведенный выше, то $this-> не сработало бы.

Выводит ли он содержимое файла на экран, если вы закомментировали все содержимое заголовка?

Просто выстрел в темноте...каждый заголовок, который я отправляю в своем коде принудительной загрузки (который не так хорошо протестирован, как ваш), совпадает с вашим, за исключением того, что я вызываю:заголовок("Cache-Control:частный", ложный);

вместо того , чтобы:заголовок("Cache-Control:общедоступный", ЛОЖНЫЙ);

Я не знаю, поможет это или нет.

Если вы собираетесь использовать такой метод "Echo it out with php", то вы не сможете показать своим пользователям оставшееся время или ожидаемый размер.Почему?Потому что, если браузер попытается возобновить вашу загрузку в середине, у вас нет способа обработать этот случай в PHP.

Если у вас обычная загрузка файла, Apache способен поддерживать возобновленные загрузки по HTTP, но в случае приостановки загрузки у Apache нет способа выяснить, где в вашем скрипте выполнялись действия, когда клиент запрашивает следующий фрагмент.

По сути, когда браузер приостанавливает загрузку, он полностью прерывает соединение с веб-сервером.Когда вы возобновляете загрузку, соединение снова открывается, и запрос содержит флаг с надписью "Начинать с байта номер X".Но для веб-сервера, просматривающего ваш PHP выше, откуда берется байт X?

Хотя теоретически сервер мог бы определить, где возобновить ваш скрипт в случае прерванной загрузки, Apache не пытается выяснить, где возобновить.В результате в заголовке, отправляемом браузеру, указывается, что сервер не поддерживает resume, что отключает элементы ожидаемого размера файла и ограничения по времени в большинстве основных браузеров.

Редактировать:Кажется, вы могли бы справиться с этим случаем, но с вашей стороны потребуется МНОГО кода.Видишь http://www.php.net/manual/en/function .fread.php#84115. .

Вместо того чтобы пытаться скрыть свой путь к загрузке от всего мира, сделайте его недоступным извне и получайте доступ к файлам только с помощью вышеупомянутого скрипта.для этого вы помещаете файл htaccess (текстовый файл с именем '.htaccess', не забудьте начальную точку) в каталог.Содержимое htaccess будет таким:

order deny,allow
deny from all
allow from localhost

Теперь попытка получить доступ к пути из * world приведет к тому, что веб-сервер создаст 401 запрещенный.

Безопасность через неизвестность - это не то, чего вы хотите.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top