[Из песочницы] После прочтения сжечь. Делаем одноразовые ссылки на голом Nginx
Для начала нужно уточнить, что я настоятельно не рекомендую использовать это решение в боевых условиях. Лучше вовсе так не делать никогда. Всё, что вы делаете, вы делаете на свой страх и риск. Причины, которые заставляют дать такой совет, будут приведены в содержании статьи. Если это предупреждение вас не отпугнуло, то добро пожаловать под кат.
Под «голым Nginx» понимается пакет для Ubuntu 16.04 из mainline ветки официального репозитория, который уже собран с ключом --with-http_dav_module.
Предполагается, что у вас уже есть настроенный nginx в такой же «комплектации», следовательно, ниже будет описываться лишь настройка двух location, которые вы добавите в свою секцию server конфига nginx
В моём случае все временные файлы будут храниться в папке /var/www/upload, ссылки на файл будут иметь вид example.com/upload/random_folder_name/filename, где в качестве random_folder_name будет рандомная строка из нужного нам количества байт, потому создаём location вида
location ~ ^/upload/([\w]+)/([^/]*)?$ {
root /var/www;
if ($request_method !~ ^(GET|PUT|DELETE)$) {
return 444;
}
client_body_buffer_size 2M;
client_max_body_size 1G;
dav_methods PUT DELETE;
dav_access group:rw all:r;
create_full_put_path on;
}
Проверяем, что загрузка, получение и удаление файлов и папок работает командами в консоли
curl -X PUT -T test.txt https://example.com/upload/random_folder_name/
curl https://example.com/upload/random_folder_name/test.txt
curl -X DELETE https://example.com/upload/random_folder_name/
Если тесты прошли успешно, то начинаем делать магию, а именно:
- Если единожды попросить у nginx файл, то он его закеширует и будет снова и снова его отдавать, даже если файл удалить с диска. Это не укладывается в нашу концепцию одноразовых ссылок, потому необходимо, следуя инструкции привести директиву open_file_cache к значению off
open_file_cache off;
- Для того, чтобы оградить свой сервер от неконтролируемого потока загружаемых файлов, добавим проверку токена, который мы будем передавать заголовком Token. В конфиге это будет выглядеть следующим образом
set $token "cb110ef4c4165e495001e297feae7092"; ... if ($http_token != $token) { return 444; }
Сам токен можно сгенерировать в консоли командой видаcat /dev/urandom | head -c16 | xxd -ps | tr -d "\n"; echo ""
- Для того, чтобы все файлы отдавались как аттачи, в том числе и html, необходимо их отдавать с заголовками Content-Type: application/octet-stream и Content-Disposition: attachment. А также, чтобы «умные» браузеры, например Internet Explorer, не могли переопределить content type на основе содержимого файла,
нужен заголовок X-Content-Type-Options: nosniff. В конфиге это будет выглядеть следующим образомtypes { } default_type application/octet-stream; add_header Content-Disposition "attachment"; add_header X-Content-Type-Options "nosniff";
- Для удаления файла после отдачи мы заведём именованный location
location @delete { if ($request_uri ~ ^/upload/([\w]+)/([^/]*)$) { set $path $1; set $token "cb110ef4c4165e495001e297feae7092"; } proxy_method DELETE; proxy_set_header Token $token; proxy_pass https://example.com/upload/$path/; }
И это первое место, из-за которого эта схема просто не имеет права на существование. Из-за того, что здесь используется if, а он в свою очередь is evil. Так делать нельзя, хотя в данном случае оно работает.Итак, если вы готовы продолжать, то стоит описать, что происходит в данном location — мы принимаем запрос, проверяем что путь соответствует паттерну /upload/random_folder_name/filename, из него получаем random_folder_name, и отправляем запрос на удаление этой папки в себя же
- Единственный нераскрытый вопрос — как сделать так, чтобы файл, после того как был отдан, был удалён. На него ответит очередная магия, вторая по счёту, из-за которой эта реализация одноразовых ссылок не должна существовать:
if ($request_method = GET) { set $http_token $token; post_action @delete; }
Выглядит вполне прилично, но не нужно использовать то, что непонятно как работает и не задокументировано
Теперь всё это объединяем в один конфиг, который, в моём случае, выглядит вот так:
location ~ ^/upload/([\w]+)/([^/]*)?$ {
root /var/www;
open_file_cache off;
if ($request_method !~ ^(GET|PUT|DELETE)$) {
return 444;
}
set $token "cb110ef4c4165e495001e297feae7092";
types { }
default_type application/octet-stream;
add_header Content-Disposition "attachment";
add_header X-Content-Type-Options "nosniff";
if ($request_method = GET) {
set $http_token $token;
post_action @delete;
}
if ($http_token != $token) {
return 444;
}
client_body_buffer_size 2M;
client_max_body_size 1G;
dav_methods PUT DELETE;
dav_access group:rw all:r;
create_full_put_path on;
}
location @delete {
if ($request_uri ~ ^/upload/([\w]+)/([^/]*)$) {
set $path $1;
set $token "cb110ef4c4165e495001e297feae7092";
}
proxy_method DELETE;
proxy_set_header Token $token;
proxy_pass https://example.com/upload/$path/;
}
Всё отлично, но пользоваться этим крайне неудобно, ибо придётся вбивать в консоли длинные команды на загрузку файла. Чтобы этого избежать и максимально упростить загрузку файлов и папок, я набросал в .zshrc (предполагаю, что будет работать и в .bashrc)
upload() {
if [ $# -eq 0 ]; then
echo "Usage:
upload [file|folder] [option]
cat file | upload [name] [option]
Options:
gpg - Encrypt file. The folder is pre-packed to tar
gzip - Pack to gzip archive. The folder is pre-packed to tar
"
return 1
fi
uri="https://example.com/upload"
token="cb110ef4c4165e495001e297feae7092"
random=$(cat /dev/urandom | head -c8 | xxd -ps | tr -d "\n")
if tty -s; then
name=$(basename "$1")
if [ "$2" = "gpg" ]; then
passphrase=$(cat /dev/urandom | tr -dc "[:graph:]" | head -c16)
echo "$passphrase"
if [ "$1" = "-" ]; then
name=$(basename $(pwd))
tar cf - `ls -1 $(pwd)` | gpg --passphrase-file <(echo -n "$passphrase") --batch -ac -o- | curl -I --progress-bar -H "Token: $token" -T "-" "$uri/$random/$name.tar.gpg" | grep "Location: " | cut -d " " -f2
elif [ -d "$1" ]; then
tar cf - `ls -1 "$1"` | gpg --passphrase-file <(echo -n "$passphrase") --batch -ac -o- | curl -I --progress-bar -H "Token: $token" -T "-" "$uri/$random/$name.tar.gpg" | grep "Location: " | cut -d " " -f2
elif [ -f "$1" ]; then
gpg --passphrase-file <(echo -n "$passphrase") --batch -ac -o- "$1" | curl -I --progress-bar -H "Token: $token" -T "-" "$uri/$random/$name.gpg" | grep "Location: " | cut -d " " -f2
fi
elif [ "$2" = "gzip" ]; then
if [ "$1" = "-" ]; then
name=$(basename $(pwd))
tar czf - `ls -1 $(pwd)` | curl -I --progress-bar -H "Token: $token" -T "-" "$uri/$random/$name.tar.gz" | grep "Location: " | cut -d " " -f2
elif [ -d "$1" ]; then
tar czf - `ls -1 "$1"` | curl -I --progress-bar -H "Token: $token" -T "-" "$uri/$random/$name.tar.gz" | grep "Location: " | cut -d " " -f2
elif [ -f "$1" ]; then
gzip -c "$1" | curl -I --progress-bar -H "Token: $token" -T "-" "$uri/$random/$name.gz" | grep "Location: " | cut -d " " -f2
fi
else
if [ "$1" = "-" ]; then
name=$(basename $(pwd))
tar cf - `ls -1 $(pwd)` | curl -I --progress-bar -H "Token: $token" -T "-" "$uri/$random/$name.tar" | grep "Location: " | cut -d " " -f2
elif [ -d "$1" ]; then
tar cf - `ls -1 "$1"` | curl -I --progress-bar -H "Token: $token" -T "-" "$uri/$random/$name.tar" | grep "Location: " | cut -d " " -f2
elif [ -f "$1" ]; then
curl -I --progress-bar -H "Token: $token" -T "$1" "$uri/$random/$name" | grep "Location: " | cut -d " " -f2
fi
fi
else
if [ "$2" = "gpg" ]; then
passphrase=$(cat /dev/urandom | tr -dc "[:graph:]" | head -c16)
echo "$passphrase"
gpg --passphrase-file <(echo -n "$passphrase") --batch -ac -o- | curl -I --progress-bar -H "Token: $token" -T "-" "$uri/$random/$1.gpg" | grep "Location: " | cut -d " " -f2
elif [ "$2" = "gzip" ]; then
gzip | curl -I --progress-bar -H "Token: $token" -T "-" "$uri/$random/$1.gz" | grep "Location: " | cut -d " " -f2
else
curl -I --progress-bar -H "Token: $token" -T "-" "$uri/$random/$1" | grep "Location: " | cut -d " " -f2
fi
fi
}
Минусы реализации, помимо того, что так делать не стоит:
- Нет докачки. Если оборвалось соединение, то nginx исполнит директиву post_action и удалит файл
- Если передавать ссылку, например, через Telegram, то он попытается получить превью этого файла, и, следовательно, файл тут же удалится, и адресат увидит вместо файла 404. Для того, чтобы этого избежать, можно добавить условие вида
if ($http_user_agent ~* (bot)) { return 444; }
в первый location. Это спасёт как минимум от Telegram, но не факт, что спасёт от кого-то другого
Надеюсь эта статья будет кому-то полезна, он почерпнёт из неё новые знания и не будет делать так никогда