[Решено] Как полностью скрыть URL на внешний файл от пользователей?

Главные вкладки

Аватар пользователя meloff meloff 17 июня 2013 в 11:57

В общем ситуация следующая - разрабатываю некий интернет магазин цифровых товаров. В каждом товаре предусмотрено поле fied_link, которое становится доступно пользователю только после покупки товара (сделано это через entity reference и функцию, тема тут: http://www.drupal.ru/node/100863). Продавцом в этом поле будет размещаться прямая ссылка на внешний файл. Соответственно после совершения покупки, пользователи видят ссылку и могут скачивать файлы.

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

Попробовал, установил модуль redirect и с помощью вот этого мануала настроил его так, что при обновлении или создании нод, для ссылки из поля field_link автоматически создается редирект. К примеру для ссылки "http://files1.freesoft.ru/rep/2014/7z920-x64.msi" автоматически создается редирект вида "drupal/node/download/38". Ну и соответственно во вьюхах у меня выводится ссылка с нужным паттерном. При клике на замаскированную ссылку (если ссылка прямая) никуда не перебрасывает и сразу же начинается скачивание файла.

Вроде бы и хорошо.. и ссылку не видно (хотя наверное если у пользователя будет какая нибудь качалка он все же увидит ссылку..), и редирект создается на автомате, и скачивание файла происходит без перезагрузки страницы (хотя если по ссылке будет картинка, то она откроется, а не скачается)... НО ссылка "drupal/node/download/38" доступна всем - и зарегистрированным и незарегестрированным и покупателям и продавцам.. а мне нужно чтобы скачивание было доступно только тем кто купил товар (сделать это можно тем же entity referece). Пробовал я как-то подцепить rules с редиректом (ну к примеру на страницу "У вас нет прав на скачивание") к страницам /node/download/% , но у меня ничего не получается, т.к. страница "drupal/node/download/38" никак не рендерится и вообще её не существует и правила content is viewed тут не работают. Искал какие-нибудь триггеры редиректа, но ничего не нашел, так что не знаю даже что делать. Еще есть мысля как-то сделать промежуточную страницу редиректа, которая рендерилась бы и далее редиректила на файл, чтобы можно было присосать к ней rules. Но пока не знаю как это можно сделать.

В общем прошу совета, может быть я изобретаю велосипед и кто-то уже что-то подобное реализовывал. Основной вопрос конечно - Как полностью скрыть URL на внешний файл от пользователей? И может быть редирект тут вовсе и не при чём.

Комментарии

Аватар пользователя Chyvakoff Chyvakoff 17 июня 2013 в 13:13

В начале и в конце сообщения у вас

"meloff" wrote:
Как полностью скрыть URL на внешний файл от пользователей?

А между ними демагогия из друпальских модулей и редиректов.

Имхо, лучше заморочиться с файлом, а не с модулями.
Продавцы большие файлы продают?В мегабайтах всмысле.
Если мелкие - после покупки можешь скидывать их себе, отдавать покупателю,а потом удалять.
Я когда то давно делал нечто подобное, я вроде-бы делал редирект на самого себя, насколько я помню.

Аватар пользователя meloff meloff 17 июня 2013 в 22:04

Chyvakoff wrote:
между ними демагогия из друпальских модулей и редиректов

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

Размер файлов может быть от нескольких килобайт до нескольких гигабайт, думаю в основном в размер dvd диска, поэтому я и не хочу хранить файлы на сервере, тем более по началу. У меня денег не хватит на такой серв Smile

Аватар пользователя meloff meloff 19 июня 2013 в 10:39

В общем, отпишусь о своих успехах.
Теперь у меня при нажатии на ссылку пользователи фильтруются по признаку купил продукт или нет, те кто не купил - идут лесом, для тех кто купил - срабатывает скрипт, который парсит внешний файл и выдает его пользователю. Smile URL ссылки не видно абсолютно нигде, кроме как на странице редактирования материала(!). Менеджер загрузок говорит, что файл скачан с "node/download/38"(!).

Как это сделано.
В файле темизации views я прописал вывод ссылки: <a href="node/download/<?=$nid?>">Скачать</а>, который выдает адрес - "node/download/38".
(страницы node/download/38 не существует, поэтому с рулсами придется поизвращаться))
Далее, я создал правило rules:
Событие - "Drupal is initializing"
Условия - Оператор AND
Условие первое - "Check path"(нужен модуль pathrules), "PATH TO CHECK: node/download/", "COMPARISON OPERATION: contains"
Условие второе - "Execute custom PHP code":
<?php
$url = current_path(); //берем url
$nid_rules = basename($url); //отрезаем всё, кроме цифр после последнего слеша - получаем id ноды
global $user;
$profile = user_load($user->uid);
$node_purchased = false;
$products = field_get_items('user', $profile, 'field_purchased_products'); //в это поле записываются купленные ноды, как его сделать читать тут: http://www.drupal.ru/node/100863
if ($products) {
foreach ($products as $delta => $item) {
if ($item['target_id'] == $nid_rules) {
$node_purchased = true;
break;
}
}
}
return $node_purchased; //возвращаем true, если нода покупалась, т.е. записана в профиле пользователя в поле field_purchased_products
?>
Действие - "Execute custom PHP code":
<?php
$url = current_path();
$nid_rules = basename($url); //извлекаем id ноды из урла
$node = node_load($nid_rules);
$fieldlink_arr = field_get_items('node', $node, 'field_link');
$fieldlink_arr = field_view_value('node', $node, 'field_link', $fieldlink_arr[0]);//выцепляем url из поля field_link ноды
$telic_url = $fieldlink_arr['#element']['url']; //думаю можно получше написать, но я в php не силен
file_download_url($telic_url); //выполняем функцию из файла template.php темы друпала (не знаю правильно ли так делать, но зато работает)
?>
В содержимое файла template.php я добавил функции:
<?php
//Функция берет урл файла, считывает с него заголовки, вставляет свои, скачивает его во временный файл порциями по 1мб и отдает пользователю
//Таким образом для пользователя скачка идет с того же сайта где и наш сайт с нодами и затирается любая информация о внешнем url, но перегружается сервер если файлы большие...
//Если адрес внешней ссылки ya.ru - скачается файл ya.ru, в моём случае это жирный плюс.
function file_download_url($filename, $mimetype='application/octet-stream') {
if ($filename) {
header($_SERVER["SERVER_PROTOCOL"] . ' 200 OK');
header('Content-Type: ' . $mimetype);
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
header('Content-Length: ' . length_url($filename)); // provide file size
header('Connection: close');
header('Content-Disposition: attachment; filename="' . basename($filename) . '";');
// Открываем искомый файл
$f=fopen($filename, 'r');
while(!feof($f)) {
// Читаем килобайтный блок, отдаем его в вывод и сбрасываем в буфер
echo fread($f, 1024);
flush();
}
// Закрываем файл
fclose($f);
} else {
header($_SERVER["SERVER_PROTOCOL"] . ' 404 Not Found');
header('Status: 404 Not Found');
}
exit;
}
//Это вспомогательная функция, которая вычисляет размер скачиваемого файла. Без данных о размере файл будет весить 0 байт. Эту функцию нужно заменить чем то другим, т.к. она тяжеловата..
function length_url($url) {
$curl = curl_init($url);
curl_setopt( $curl, CURLOPT_NOBODY, true );
curl_setopt( $curl, CURLOPT_HEADER, true );
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $curl, CURLOPT_FOLLOWLOCATION, true );
curl_setopt( $curl, CURLOPT_CONNECTTIMEOUT, 1);
$data = curl_exec( $curl );
curl_close( $curl );
if($data) {
$content_length = "unknown";
$status = "unknown";

if( preg_match( "/^HTTP\/1\.[01] (\d\d\d)/", $data, $matches ) ) {
$status = (int)$matches[1];
}

if( preg_match( "/Content-Length: (\d+)/", $data, $matches ) ) {
$content_length = (int)$matches[1];
}

// http://en.wikipedia.org/wiki/List_of_HTTP_status_codes
if( $status == 200 || ($status > 300 && $status <= 308) ) {
return $content_length;
}
}
}
?>
Вроде бы все хорошо, ссылки закрылись и даже если пользователь будет очень стараться узнать откуда файл, у него ничего не получится, НО функция file_download_url в реальных условиях будет вешать сайт, это сто процентов. Поэтому нужно переделать её под nginx. Если честно, давно догадывался о его существовании, но никогда не знал что это такое)) Так что теперь буду пытаться установить на свой виртуальный сервер nginx, стараясь не уронить сайт.. Если вы знаете как можно улучшить вышеописанное или как переписать функцию file_download_url под nginx, буду премного благодарен за помощь.

Аватар пользователя meloff meloff 21 июня 2013 в 0:12

Итак, мне удалось подружить nginx со своим виртуальным хостингом и я разобрался, как перевесить проксирование файлов на nginx. Теперь самое главное чтобы то же самое можно было проделать у реального хостера. Smile

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

Обе моих функции в template.php теперь превратились в довольно миниатюрный код:
<?php
function file_download_url($url) {
if ($url) {
header('X-Accel-Redirect: /internal_redirect/'.$url.'filenameis:'.basename($url));
exit; //может exit тут лишний, остался от предыдущего варианта
}
}
?>

Что тут происходит. Функция отправляет заголовок "X-Accel-Redirect", чем сообщает nginx'у, что надо бы что-то сделать со ссылкой. Полностью ссылка в моем примере выглядит как-то так: "/internal_redirect/http://files1.freesoft.ru/rep/2014/7z920-x64.msifilenameis:7z920-x64.msi"

nginx.conf сконфигурирован у меня следующим образом (инфа в комментах):
<?php
user www-data;
worker_processes 4;
pid /var/run/nginx.pid;

events {
worker_connections 768;
}

http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;

include /etc/nginx/mime.types;
default_type application/octet-stream;

access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;

gzip on;
gzip_disable "msie6";

proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

server {
listen *:80;
server_name sait.ru;
access_log /var/log/nginx/access.log;

location / {
proxy_pass http://127.0.0.1:88/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_connect_timeout 120;
proxy_send_timeout 120;
proxy_read_timeout 180;
}

# Proxy download
location ~* /internal_redirect/(.*)filenameis:(.*) { //В основном нас волнует именно эта секция, от этой строки, до "}". Эта строка задаёт regex
//regex - это паттерн, который реагирует на мою ссылку, которую я сформировал в функции
//в этом регэксе говорится - среагировать на ссылку, начинающуюся на "/internal_redirect/",
//вычленить в первую переменную весь следующий код, до "filenameis:", то что после "filenameis:"
//засунуть во вторую переменную. Если в ссылке не будет "/internal_redirect/", символов, "filenameis:",
//символов, до данная секция не сработает. Проверить свой паттерн можно с помощью онлайн сервиса
//"http://regex.tacticalvim.com/photobooks/index.html"

//кстати такие комментарии, как этот, conf воспримет как код, так что не забудьте стереть

internal; //работает и с этой строкой и без нее
resolver 8.8.8.8; //эта строка отправляет адрес на иденификацию гугловскому прокси, чтобы понять на какой адрес стучаться
//без resolver валится, кажется ошибка 502

access_log logs/internal_redirect.access.log; //куда писать логи доступа
error_log logs/internal_redirect.error.log; //куда писать логи ошибок

set $name $2; //засовываем в переменную $name, все что во вторых скобках regex'а
set $url $1; //засовываем в переменную $url, все что в первых скобках regex'а
//можно просто использовать переменные $1 и $2

proxy_set_header Authorization ''; //без этой строки лезут ошибки
proxy_hide_header Content-Disposition; //убирал строку, ниче не происходило, но она у всех, так что оставил
add_header Content-Disposition 'attachment; filename="$name"'; //че-то у меня все слезло, так что строкой ниже..
//в этой строке добавляется заголовок, который говорит nginx'у, что файл надо бы скачать,
//а не открывать, и скачать его нужно под именем $name, которое берется из вторых скобок
//regex'а.. В php функции это имя задает "basename($url);", и в данном примере
//выглядит как "7z920-x64.msi"

proxy_max_temp_file_size 0; //не проверял, но по идее говорит, что файл скачивать не надо никуда, надо его
//сразу отдать клиенту.. и на самом деле так и происходит

proxy_pass $url; //проксируем файл пользователю
} //ну и все, не так уж и много тут кода

//хочу предупредить, если ссылка оканчивается на любой из форматов, прописанных
//в следующем location, то данная локаль должна стоять выше следующей, если же она
//будет стоять ниже следующей локали, то сработает regex из следующей локали........
//короче, если следующая локаль будет выше нашей локали, и ссыль будет заканчиваться на .jpg
//то наша локаль не сработает и полезут ошибки в логах.. так вроде понятней Smile

location ~* \.(jpg|jpeg|gif|png|ico|css|bmp|swf|js|html|txt)$ {
root /home/racord/www/sait.ru/htdocs;
}
}

include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
?>
Довольно мало информации по такого рода настройки, поэтому расписал как можно подробнее, чтобы другие не мучались.. Сложно разобраться в языке, который видишь в первый раз, при том, что в php плохо разбираешься. Кажется это язык ruby.. хотя может и не он.. Smile Ссылка которая позволила мне все это сделать http://kovyrin.net/2010/07/24/nginx-fu-x-accel-redirect-remote/

Работает все шикарно относительно предыдущих вариантов. Ну кроме прямого редиректа, он вообще самый простой и самый удобный, но мне не подходит, так как надо закрыть ссылки. Благодаря x-accel-redirect я добился того чего хотел в самом начале - пользователь никогда не сможет узнать ссылку на файл, все работает быстро и без задержек, если ссылка ссылается на страницу /stranica.php, то скачается файл stranica.php и наверное он даже откроется при желании.

Единственный маленький недостаток, про который я говорил в начале - если ссылка выглядит к примеру так http://google.ru и не содержит дальнейших / и букв, а просто заканчивается именем домена, то редирект ведет себя довольно странно... он начинает ломиться на http://93.158.134.203:80/internal_redirect/http://google.rufilenameis:go... на что гугл в недоумении показывает ошибку 404 и говорит, что такой странице у него никогда не было и быть не может. Пока не понял из-за чего это происходит, но думаю таких ссылок никто указывать не будет. Так что в принципе все отлично Smile