Как контролировать скачивание больших файлов, проверяя права доступа или считая количество закачек? Как сделать, чтобы при проксировании на Apache работала докачка? Как вообще работает докачка, почему она не работает с nginx в IE 9 и как она работает в других браузерах?
1. Мы хотим контролировать скачивание больших файлов, проверяя права доступа или считая количество закачек. Мы используем nginx и PHP (или другой серверный язык).
Это реализуется очень просто через заголовок Accel-Redirect. nginx получает запрос, передает его скрипту, скрипт в заголовки ответа выдает Accel-Redirect с ссылкой на файл, который нужно выдать пользователю. Если файл выдавать нельзя (например, нет прав), скрипт просто выдаст ошибку (например, 403).
В данном случае не важно, работает ваш PHP через проксирование из nginx на Apache, либо на *CGI. Предположим, что вы хотите отдавать на скачку файлы, которые находятся в каталоге /var/files. Тогда в секции server в nginx добавьте:
location /_download_files { root /var/files; internal; }
В скрипте, который контролирует закачку, в том месте, где мы хотим отдать пользователю файл, должен быть следующий код:
header("X-Accel-Redirect: /_download_files/filename.zip");
Никакой выдачи (echo) делать не надо, после отдачи заголовка можно завершить работу скрипта. В этом случае будет скачан файл /var/files/filename.zip. Вместо filename.zip может быть любой длинный путь с каталогами, который должен повторять путь к файлу в /var/files/.
В принципе этого достаточно, чтобы переложить выдачу файла на плечи nginx. Если нужно обязательно показать диалог закачки (даже если файл такого типа, который браузер может показать сам), нужно добавить выдачу заголовка:
header("Content-Type: application/x-force-download");
Обратите так же внимание на то, что в случае проксирования на Apache последний обязательно установит Content-Type (по умолчанию обычно text/html) в случае PHP-скриптов, и этот тип отправится пользователю, так как nginx его не перепишет при выполнении Accel-Redirect. Т.е. в этом случае эту строку нужно писать обязательно. Как будет в случаях с *CGI, не знаю — не проверял.
Чтобы предложить пользователю имя файла для сохранения, отличное от того, что в адресной строке (это особо актуально в случаях «download.php&file_id=12332»), отправим такой заголовок:
Content-Disposition: attachment; filename="Super File Name.zip";
2. В такой схеме при проксировании на Apache не работает докачка. Исправляем.
Докачка в HTTP работает через HTTP-заголовок Range. В нем указывается с какого байта начать передачу. Можно так же указать, сколько байт нужно передать. Это передается в запросе. Например:
Range: bytes=33- Range: bytes=33-100
Первый вариант просит отдать весь файл, начиная с 33 байта (включительно). Второй вариант просит отдать с 33 байта по 100. Чтобы браузер понял, что докачка поддерживается, сервер отдает в ответе заголовок:
Accept-Ranges: bytes
Докачка не работает из-за того, что HTTP-заголовок Range попадает в Apache после просирования и последний думает, что его просят отдать кусок PHP-файла. А он это делать не хочет и отдает ошибку: HTTP/1.1 416 Requested Range Not Satisfiable.
Исправляется это довольно легко — просто зануляйте при проксировании Range (это нужно добавить туда, где у вас описано проксирование на Apache в конфиге nginx):
proxy_set_header Range "";
Тогда Apache (или PHP-скрипт) ничего знать про него не будут и спокойно сделают свое дело. А nginx после Accel-Redirect отдаст ответ в соответствии с запрошенным Range.
Если жизненно необходимо видеть в скрипте запрошенный Range, то его можно либо передать через дополнительный заголовок при проксировании (proxy_add_header), либо последовать совету отсюда.
3. Даже так не работает докачка в IE 9.
Да. На сколько я понял методом простого тыка, IE требует поддержки ETag сервером для работы докачки. Игорь Сысоев (разработчик nginx) отказался внедрять в nginx поддержку ETag. Поэтому из коробки работать докачка в IE 9 не будет точно. Если это жизненно необходимо, пробуйте смотреть сюда (Ctrl+F Etag).
4. А как работает докачка в других браузерах?
Везде есть свои особенности. В общих чертах случай докачки выглядит так. Запрос/ответ на скачку (начало скачки):
GET /file.zip HTTP/1.1 Host: example.com HTTP/1.1 200 OK Server: nginx/1.0.3 Date: Tue, 06 Mar 2012 18:09:47 GMT Content-Type: application/x-force-download Content-Length: 13686314 Last-Modified: Thu, 23 Feb 2012 13:50:26 GMT Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0 Content-Disposition: attachment; filename="Supre File Name.zip"; Accept-Ranges: bytes
Запрос/ответ на докачку:
GET /file.zip HTTP/1.1 Host: example.com Range: bytes=5393683- HTTP/1.1 206 Partial Content Server: nginx/1.0.3 Date: Tue, 06 Mar 2012 18:10:11 GMT Content-Type: application/x-force-download Content-Length: 8292631 Last-Modified: Thu, 23 Feb 2012 13:50:26 GMT Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0 Content-Disposition: attachment; filename="Super File Name.zip"; Content-Range: bytes 5393683-13686313/13686314
На практике ни один браузер не ограничивается таким набором заголовков. Все хотят защитить пользователя от скачки битого файла. Это может получиться так: начал закачку, поставил на паузу; в это время на сервере скачиваемый файл обновился; продолжил закачку с нужно места и получил кусок обновленного файла. В результате скачается «битый» файл — половина старая версия, половина — новая.
Opera. Она шлет указание на то, что «если файл не менялся со времени Last-Modified в первом ответе, дай мне указанный кусок; иначе — отдай мне весь файл заново». Делается это с помощью заголовка If-Range в запросе:
GET /file.zip HTTP/1.1 User-Agent: Opera/9.80 (Windows NT 6.1; U; ru) Presto/2.10.229 Version/11.61 Host: example.com Accept text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, ..., */*;q=0.1 Accept-Language: ru-RU,ru;q=0.9,en;q=0.8 Accept-Encoding: gzip, deflate Connection: Keep-Alive If-Range: Thu, 23 Feb 2012 13:50:26 GMT Range: bytes=5393683-
Firefox. Он делает немного иначе. Используется заголовок If-Unmodified-Since с указанием того же времени из Last-Modified. В этом случае сервер либо вернет запрошенный кусок (если файл не менялся), либо отдаст ошибку 412 Precondition Failed. Выглядит это так:
GET /file.zip HTTP/1.1 Host: example.com User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:10.0.2) Gecko/20100101 Firefox/10.0.2 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Connection: keep-alive Range: bytes=5878651- If-Unmodified-Since: Thu, 23 Feb 2012 13:50:26 GMT
Chrome. Этот «пильмень» — самый хитрый. Он вообще не поддерживает докачку, хотя делает вид, что поддерживает. При постановке закачка на паузу он просто шлет некие TCP-пакеты для поддержания соединения. Когда сервер понимает, что его дурят, он разрывает соединение. Если дождаться этого момента и нажать в Хроме «Продолжить», он сделает умный вид, будто файл полностью скачался и все ОК, но при этом он останется в том размере, в котором успел скачаться до паузы.
Вся эта информация получена методом наблюдения. Возможно в какой-то другой ситуации Хром повел бы себя иначе, но я через WireShark наблюдал именно такую картину.
IE. Про версию 9 уже сказано выше. Она требует работы Etag для поддержки докачки. В данном контексте ETag — это просто контрольная сумма файла. Когда Etag файла передается в первом ответе от сервера, IE даст возможность прервать скачку. При попытке докачки он отдаст заголовки:
GET /file.zip HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0) Host: falcongaze.com Range: bytes=3494051- Unless-Modified-Since: Mon, 05 Mar 2012 08:31:06 GMT If-Range: "1448453-f77f31-4ba7abf5a8680" Connection: Keep-Alive
В добавок к If-Range он передает нестадартный заголовок Unless-Modified-Since — вероятено IIS его обрабатывает (только не понятно, зачем он, если есть стандартный If-Unmodified-Since; может для поддержки старых версий IIS?).
Собственно в первом ответе Etag выглядит примерно так (если он поддерживается сервером, например, Apache):
... ETag: "1448453-f77f31-4ba7abf5a8680"
Дополнительная информация: