Blog. Just Blog

Защита файлов на сервере от прямых ссылок (antileech)

Версия для печати Добавить в Избранное Отправить на E-Mail | Категория: Web-мастеру и не только | Автор: ManHunter
Защита от скачивания файлов по прямым ссылкам, или как ее еще называют "антилич-защита" - задача не менее важная, чем защита остального контента сайта. Грамотно сделанная система защиты от прямых ссылок позволит разграничивать доступ к файлам разным категориям пользователей, вести подробную статистику скачиваний, скрывать истинное место размещения файлов, а также привлечет новых посетителей на ваш сайт. А то обычно получается так, что сторонние сайты публикуют прямую ссылку на файл, размещенный на вашем сервере, они получают посетителей, рейтинги, стригут купоны с рекламы, а вы получаете только счета за трафик. Справедливое разделение? Нет. В последнее время появилось огромное количество файлообменных сервисов, и на каждом из них используется своя система антилич-защиты, где-то более удачно реализованная, где-то менее. За счет этого они имеют возможность регулировать скорость отдачи файлов премиум-пользователям и халявщиками, определять лимит на скачивание по времени, поддерживать или не поддерживать докачку и т.д. В этой статье я расскажу о самом принципе построения антилич-системы, а также приведу пример простейшего, но вполне работоспособного скрипта.

Сперва начнем с изучения предмета. Посмотрим лог работы какого-нибудь менеджера закачки, который скачивает обычный файл с обычного web-сервера. Конкретно нас интересуют служебные заголовки, которые сервер отдает менеджеру закачки или браузеру:

HTTP/1.1 200 OK
Date: Sat, 15 Aug 2009 17:24:40 GMT
Server: Apache/1.3.41 (Win32)
Connection: close
Content-Type: application/octet-stream
Accept-Ranges: bytes
Content-Disposition: Attachment; filename=filename.rar
Content-Length: 2676708

В заголовках есть вся нужная информация: тип файла, его имя, размер файла. После этого идет непосредственно содержимое файла. Если сервер отдает такие заголовки, значит их точно так же может отдавать и наш PHP-скрипт через функцию header.

Напишем простейший скрипт и попробуем открыть его в браузере или добавим ссылку на него в менеджер закачек:
  1. <?
  2. Header("HTTP/1.1 200 OK");
  3. Header("Connection: close");
  4. Header("Content-Type: application/octet-stream");
  5. Header("Accept-Ranges: bytes");
  6. Header("Content-Disposition: Attachment; filename=filename.dat");
  7. Header("Content-Length: 50000");
  8.  
  9. echo str_repeat('!',50000);
  10. ?>
В любом случае начнется скачивание файла filename.dat размером 50000 байт, состоящего из восклицательных знаков. Мы только что "на лету" сгенерировали файл, которого физически на сервере не существует. Таким образом мы можем генерировать "виртуальные" файлы и отдавать их пользователю. Это могут быть, например, созданные скриптами CSV-файлы, картинки, архивы, сгенерированные PDF-файлы и другой контент. Тут главное правильно указывать имя файла и размер передаваемых данных, тип рекомендуется всегда оставлять application/octet-stream. Главный недостаток этого способа в том, что не поддерживается докачка в случае обрыва соединения, но он лучше всего подходит для небольших, до нескольких сот килобайт, файлов.

Теперь немного усложним задачу и модифицируем скрипт для работы с реальными файлами. Принцип тут тот же самый, только данные будут создаваться не из воздуха, а читаться из файла. Пусть сами файлы лежат в подпапке \secret_data, а имя файла передается в качестве параметра вызова скрипта.
  1. <?
  2. $fname=$_GET['fname'];
  3. $fsize=filesize('secret_data/'.$fname);
  4.  
  5. Header("HTTP/1.1 200 OK");
  6. Header("Connection: close");
  7. Header("Content-Type: application/octet-stream");
  8. Header("Accept-Ranges: bytes");
  9. Header("Content-Disposition: Attachment; filename=".$fname);
  10. Header("Content-Length: ".$fsize);
  11.  
  12. // Открыть файл для чтения и отдавать его частями
  13. $f=fopen('secret_data/'.$fname,'r');
  14. while (!feof($f)) {
  15.   // Если соединение оборвано, то остановить скрипт
  16.   if (connection_aborted()) {
  17.     fclose($f);
  18.     break;
  19.   }
  20.   echo fread($f,10000);
  21.   // Пазуа в 1 секунду. Скорость отдачи 10000 байт/сек
  22.   sleep(1);
  23. }
  24. fclose($f);
  25. ?>
Ссылка на закачку в этом случае будет выглядеть примерно так:

http://server/getfile.php?fname=secret_file.rar

Обратите внимание, что в этом скрипте не выполняется никаких проверок на наличие файла и корректность его имени и расположения, так что в плане безопасности он вообще никакой. Не вздумайте использовать скрипт в таком виде, он предназначен только для демонстрационных целей! Но теперь мы уже имеем следующее: можно регулировать скорость отдачи, через функцию connection_aborted проверяется состояние соединения, а из ссылки нельзя узнать фактическое расположение файлов, причем файл может располагаться вообще на другом сервере. Недостаток с отсутствием возможности докачки сохраняется. Также имейте в виду, что при маленькой скорости отдачи и большом объеме файла время выполнения скрипта может превысить максимальное время, установленное в настройках PHP на сервере. В этом случае его надо увеличить до неограниченного, но это возможно только если у вас свой выделенный сервер. Вряд ли какой хостинг-провайдер разрешит такие долгоживущие процессы на своих хостинговых серверах.

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

GET /filename.rar HTTP/1.0
User-Agent: Download Master
Accept: */*
Referer: http://server.com/
Range: bytes=2730210-
Pragma: no-cache
Cache-Control: no-cache
Host: server.com

В поле Range передается смещение от начала файла, с которого начинается или продолжается закачка. На сервере это поле преобразуется в переменную окружения HTTP_RANGE. Чтобы скрипт поддерживал докачку, надо проверять установлена ли эта переменная, и если она имеет непустое значение, то читать файл и отдавать данные с указанной позиции. В этом случае заголовки немного изменятся:

HTTP/1.1 206
Date: Sat, 15 Aug 2009 20:41:27 GMT
Server: Apache/1.3.41 (Win32)
Accept-Ranges: bytes
Connection: close
Content-Disposition: Attachment; filename=filename.rar
Content-Range: bytes 9902214-13807860/13807861
Content-Length: 3905647
Content-Type: application/octet-stream

Добавилось поле Content-Range, показывающее нужный интервал и общий размер файла, а Content-Length содержит размер фрагмента файла от начала запрошенной позиции докачки до конца файла. Продублируем их в нашем скрипте:
  1. <?
  2. $fname=$_GET['fname'];
  3. $fsize=filesize('secret_data/'.$fname);
  4. $fdown='secret_data/'.$fname;
  5.  
  6. // Установлена или нет переменная HTTP_RANGE
  7. if (getenv('HTTP_RANGE')=="") {
  8.   // Читать и отдавать файл от самого начала
  9.   $f=fopen($fdown'r');
  10.  
  11.   header("HTTP/1.1 200 OK");
  12.   header("Connection: close");
  13.   header("Content-Type: application/octet-stream");
  14.   header("Accept-Ranges: bytes");
  15.   header("Content-Disposition: Attachment; filename=".$fname);
  16.   header("Content-Length: ".$fsize); 
  17.  
  18.   while (!feof($f)) {
  19.     if (connection_aborted()) {
  20.       fclose($f);
  21.       break;
  22.     }
  23.     echo fread($f10000);
  24.     sleep(1);
  25.   }
  26.   fclose($f);
  27. }
  28. else {
  29.   // Получить значение переменной HTTP_RANGE
  30.   preg_match ("/bytes=(\d+)-/"getenv('HTTP_RANGE'), $m);
  31.   $csize=$fsize-$m[1];  // Размер фрагмента
  32.   $p1=$fsize-$csize;    // Позиция, с которой начинать чтение файла
  33.   $p2=$fsize-1;         // Конец фрагмента
  34.  
  35.   // Установить позицию чтения в файле
  36.   $f=fopen($fdown'r');
  37.   fseek ($f$p1);
  38.  
  39.   header("HTTP/1.1 206 Partial Content");
  40.   header("Connection: close");
  41.   header("Content-Type: application/octet-stream");
  42.   header("Accept-Ranges: bytes");
  43.   header("Content-Disposition: Attachment; filename=".$fname);
  44.   header("Content-Range: bytes ".$p1."-".$p2."/".$fsize);
  45.   header("Content-Length: ".$csize);
  46.  
  47.   while (!feof($f)) {
  48.     if (connection_aborted()) {
  49.       fclose($f);
  50.       break;
  51.     }
  52.     echo fread($f10000);
  53.     sleep(1);
  54.   }
  55.   fclose($f);
  56. }
  57. ?>
Теперь скрипт поддерживает докачку в случае обрыва связи и закачку файла в несколько потоков. Можно начинать добавлять к нему всякие навороты: например, проверять регистрацию или права доступа пользователя, регулировать количество соединений с одного ip-адреса, поддерживать докачку, изменять скорость отдачи для премиум-пользователей, проверять cookies после посещения основного сайта и все, что только можно придумать. Это я оставляю целиком на вашу фантазию. Последний штрих - преобразование ссылок через файл .htaccess и модуль Apache mod_rewrite. Например, ссылка будет иметь вид:

http://server/download/secret_file.rar

В файле .htaccess прописано правило для mod_rewrite:

RewriteEngine on
Options +FollowSymlinks
RewriteRule ^download\/(.*)$ getfile.php?fname=$1 [L]

И ссылка преобразуется в уже знакомую нам

http://server/getfile.php?fname=secret_file.rar

В исходную ссылку можно включить хэш от ip, идентификатор сессии, идентификатор файла в базе данных и любые другие данные, закодировав их известным только нам образом, а потом разбирать их соответствующими правилами mod_rewrite. Большой плюс таких преобразований в том, что никто не сможет узнать не только фактическое расположение файлов, но и имя скрипта антилич-системы. Дополнительно можно защитить секретную папку с файлами, изменив права доступа к ней на сервере или поместив в нее файл .htaccess следующего содержания:

Options -Indexes
Deny from all

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

В приложении рабочий скрипт антилича из этой статьи с настроенной структурой папок. Запускать его, естественно, надо только на локальном или удаленном web-сервере с поддержкой mod_rewrite и .htaccess

Antileech DemoAntileech Demo

Antileech.Demo.zip (59,564 bytes)


Поделиться ссылкой ВКонтакте
Просмотров: 33245 | Комментариев: 40

Внимание! Статья опубликована больше года назад, информация могла устареть!

Комментарии

Отзывы посетителей сайта о статье
ManHunter (23.01.2012 в 17:29):
Любые консультации - через кассу. Надоело.
Евгений (20.03.2011 в 14:49):
Для чего, тогда они предоставляют так много пространства: 20, 30, 40 Гб?
ManHunter (20.03.2011 в 14:38):
Для файлообменников надо выделенные серверы, а не виртуальный хостинг. Тебя с джино только за один трафик выпрут.
Евгений (20.03.2011 в 14:26):
Здравствуйте.
Я хочу разместить ~ 3.000 файлов, каждый весит ~ 5 мб, на обычный хостинг (джино). Не будет ли придельной нагрузки на хостинг, при скачивании моими посетителями ~ 1500 скачиваний в сутки этих файлов, при использовании вашего скрипта?
Ботаник eGo (17.01.2011 в 13:08):
Спасибо, хорошая статья :) в закладки
Александр (21.11.2010 в 01:34):
Спасибо за отличный скрипт. Использовал уже на нескольких проектах
Андрей (14.11.2010 в 15:53):
Скажите пожалуйста какая ссылка должна прописываться на страницах
http://server/download/secret_file.rar
такая, а потом htaccess ее преобразовывает в
http://server/getfile.php?fnam...ret_file.rar

Или такая http://server/getfile.php?fnam...ret_file.rar а htaccess ее преобразует в простую - http://server/getfile.php?fnam...ret_file.rar
ManHunter (14.11.2010 в 13:28):
Да никак. Превысишь время выполнения - получишь по жопе от хостера, сперва будет предупреждение, а потом без разговоров заблочат аккаунт вылетишь со свистом с сервера.
Solitarius (14.11.2010 в 13:21):
Антилич конечно можно и на уровне htaccess настроить...но вот как сделать поддержку докачки в обход лимита php.ini не понимаю )=
Solitarius (14.11.2010 в 12:00):
ManHunter, спасибо =)
ManHunter (14.11.2010 в 11:52):
Ну а что понимается под "адекватным решением"? Развернуть полноценный файлообменник на пятибаксовом хосте? Так не бывает. Файлообменник - это сервис, который подразумевает определенную нагрузку на сервер и объем хранимых данных, а такие ресурсы виртуальный хостинг не может обеспечить в принципе. Любой нормальный хостер не будет терпеть у себя на хостинговом сервере такого бОрзого "грузчика", после первых же пиковых скачков тебя попросят или переехать на новый тариф, или свалить куда-нибудь подальше. Я сам работал в крупной хостинговой компании, и знаю о чем говорю. Поэтому полноценное решение для раздачи файлов, которые требуют для скачивания более 30 секунд, можно поднять только в локальной сети или на своем сервере/серверах.

Как временный вариант, можно реализовать скачивание с поддержкой докачки, как описано в статье, но на стороне сервера принудительно обрывать соединение каждые 25 секунд. В этом случае качалка у пользователя будет просто возобновлять закачку с места обрыва. Но браузером такие файлы уже не скачать.
Solitarius (14.11.2010 в 11:18):
Если обычный хост, где большинство людей размещают сайты. Или в таком случае средствами php это не сделать?
Solitarius (14.11.2010 в 10:05):
ManHunter, а более адекватное решение?
ManHunter (14.11.2010 в 00:54):
Покупаешь или арендуешь выделенный сервер и там резвишься как хочешь.
Solitarius (13.11.2010 в 22:58):
А если нельзя время выполнения скрипта менять? По умолчанию 30 сек стоит в конфиге.
Вот и качает 30 сек то, что успеет, потом обрыв. Как обойти?
RIPN (30.09.2010 в 23:17):
Таки мой вопрос отпал. Я, вроде, осознал эту глубочайшую истину, а раньше я был слегка туповат. Извините за беспокойство и еще раз благодарю за статью!
RIPN (30.09.2010 в 23:05):
Здравствуйте, ManHunter! Огромное спасибо за статью, решилось много проблем :) Но вот вопрос - как регулировать к-во одновременных закачек на IP? Как это делается, хотя бы в общих словах? Заранее благодарен.
ManHunter (19.05.2010 в 10:29):
И парсер поправил, и статью. Спасибо.
ManHunter (19.05.2010 в 07:25):
Мда, что-то у меня парсер погнулся. Надо поправить.
alex_rov (19.05.2010 в 02:58):
Небольшое дополнение в .htaccess
В место:
RewriteEngine on
Options +FollowSymlinks
RewriteRule ^download\/(.*)$ getfile.php?fname=
Вот так:
RewriteEngine on
Options +FollowSymlinks
RewriteRule ^download/(.*)$ getfile.php?fname=$1 [L]
За статью спасибо.
lugaro (07.05.2010 в 06:45):
В цикле flush нужно добавить, чтоб браузер не ждал пока скрипт весь файл выдаст, а сразу качал, если файл большой окно с запросом "куда сохранить" будет долго не появляться, создаст впечатление что браузер висит, и не каждый браузер захочет долго ждать
ManHunter (03.04.2010 в 17:20):
Отключаешь время максимальной работы скрипта и спокойно раздаешь гигабайты. А $fsize - очень плохое решение.
andy (03.04.2010 в 17:09):
спасибо за статью
а это решение подойдёт для отдачи больших файлов(1гб например)? При том что на сервере могут параллельно качать десятки таких файлов. Не будет ли переполняться память при echo fread($f, 10000); (а при echo fread($f, $fsize);) ?
ManHunter (09.03.2010 в 14:11):
Ставишь Firefox, на него плуг для просмотра заголовков и экспериментируешь до наступления полного прояснения. Код, кстати, дырявый, даже в этом крохотном кусочке.
Марик (07.03.2010 в 14:20):
Ваш скрипт полностью работает, никаких проблем нет.
В моём скрипте я в самом начале немного изменил:

//Получаем номер файла
    $number=$_GET['number'];

//Выбираем из базы название файла вида name_file.jpg
    $result=mysql_query('SELECT name_file FROM file WHERE id_file="'.$number.'"');
    $enter=mysql_fetch_array($result,MYSQL_ASSOC);
    $name_file=$enter['name_file'];

//Превращаем название файла в массив. Последний элемент массива это тип файла, в нашем случае в основном jpg
    $name_array=explode('.',$way_file);
    $last_point=count($name_array)-1;
    $type=$name_array[$last_point];

//Случайное название файла
    $fname=md5(time().$name_array).'.'.$type;
    $fdown='secret_data/'.$name_array;
    $fsize=filesize('secret_data/'.$name_array);

    //Далее идёт скрипт, полностью как у Вас.

Всё работает, но размер файла не хочет показываться. При скачивание размер пишет неизвестен. Так и не могу решить проблему :-(
ManHunter (03.03.2010 в 11:13):
Не мог бы. Я как раз своих телепатов, работающих бесплатно, отправил в отпуск.
Марик (03.03.2010 в 11:09):
У меня есть проблемка:
Вроде все работает как надо, но не получается узнать размер файла.
Файл запускается на скачивание, идет закачка, но не видно сколько осталось, так как размер никак не хочет узнаваться. url верный, так как без скрипта вывод размера получается, а при с скрипте, такое чувство, что его надо как-то по особенному выдавать. Не могли бы Вы помочь?
ManHunter (07.01.2010 в 17:50):
Да, это очень правильное дополнение. Но на практике вполне достаточно полей, указанных в статье.
dervan (07.01.2010 в 17:31):
Спасибо за статью.

Небольшое дополнение: при отправке файла скриптом полезно добавлять еще 2 заголовка HTML - ETag и Last-Modified.

1. ETag: Entity Tag, строка, указывающий состояние данных.
Вычислить ETag для файла можно на основе алгоритма Apache ( http://httpd.apache.org/docs/2...od/core.html ) - как md5 строки, составленной из таких строк: полный путь к файлу на сервере, размер файла, время последнего изменения файла.

2. Last-Modified: время последнего изменения файла на сервере.
axae (24.12.2009 в 12:28):
Толково все расписано, спасибо
ManHunter (21.12.2009 в 13:21):
Когда я подниму свой файлохостинг и напишу под него весь нужный софт, тогда я смогу ответить на подобные вопросы. А до этого момента я ничего не подскажу, и гуглояндексы у меня точно такие же как у всех. К настройке серверов я тоже не имею никакого отношения. Пробуй, экспериментируй, это единственный путь к получению знания.
Макс (21.12.2009 в 13:15):
Добрый день, МэнХантер. Вот выдался у меня свободный денек, можно опять по-изучать разные манулы.  И возник вопрос, возможно вы сможете помочь. У меня сервер Апаче2 на Центос5, если я буду отдавать большие файлы, то сколько будет доступно одновременных подключений? Т.е. я читал что 150 максимум одновременных скачек дает апаче. И теперь я как то в недоумении, что же делать, изучаю мануал апача но пока без результата. Рекомендуют качать мод limitipconn и ставить лимиты через него. Но я так ине понял сколько же одновременно смогут качать людей с моего сервера если сервер такой: Core Duo T2450 2.0GHz, 4GB ram, 100mb (не шаред).  Надеюсь Вы поможете мне хоть как то примерно рассчитать число одновременных скачек, я так понял что за скачку считается и кол-во потоков. Спасибо.
ManHunter (30.11.2009 в 15:25):
Делай "красивые" ссылки с правильными именами файлов через mod_rewrite, как сделано здесь, обычно такой вариант помогает для всяких неправильных хостингов.
Макс (30.11.2009 в 14:00):
Спасибо, теперь понял, возник другой вопрос. На локальной машине при помещенном файле htaccess с параметрами (Options -Indexes Deny from all) файл качается нормально. А на хостингах: пробовал 3 крупных, файл отдается с разрешением .htm вместо .mp3 , если удалить htaccess из папки файлов, то разрешение отдается правильное. Прошу отнестись с пониманием ко мне, я начинающий программист php, Ваши статьи очень помогают в изучении.
ManHunter (29.11.2009 в 22:36):
В качалке Скорость = (Скорость отдачи * Количество потоков), а в браузере всегда 1 поток. Регулируй количество одновременных подключений с уникального ip.
Макс (29.11.2009 в 22:33):
Выставил ограничения 500000 байт на скорость скачивания, так вот через браузер он это ограничение учитывает (500кб/сек), а если ссылку на файл сунуть в менеджер закачек там скорость 5000кб/сек  как такое можно поправить? Спасибо.
1n1t (10.09.2009 в 23:44):
весьма информативно.
спасибо.
ManHunter (28.08.2009 в 01:17):
Я вроде бы все написал: "Обратите внимание, что в этом скрипте не выполняется никаких проверок на наличие файла и корректность его имени и расположения, так что в плане безопасности он вообще никакой." Читай внимательнее. На рабочих скриптах, естественно, выполняются все мыслимые проверки, а тут оставлен только минимально рабочий код, который относится непосредственно к антиличу.
poma (28.08.2009 в 01:15):
error_reporting в 0
и str_replace('..','',$) хотя бы сделал )

./getfile.php?fname[]=test
./getfile.php?fname=../getfile.php

Добавить комментарий

Заполните форму для добавления комментария
Комментарии отключены администратором сайта
Наверх
Powered by PCL's Speckled Band Engine 0.2 RC3
© ManHunter / PCL, 2008-2024
При использовании материалов ссылка на сайт обязательна
Время генерации: 0.08 сек. / MySQL: 2 (0.0037 сек.) / Память: 4.5 Mb
Наверх