Я люблю Луа. Я также люблю NGINX. Мы втроем прекрасно ладим. Как и в любых отношениях, у нас были взлеты и падения (да, я смотрю на ваши Lua-паттерны), но в целом жизнь была идеальной. Затем появился JavaScript-модуль NGINX (сокращенно NJS).
JavaScript-модуль NGINX был впервые представлен в 2015 году, но недавно с обновлением 0.5.x его функциональность значительно расширилась. Поскольку я обожаю JS, я решил протестировать его, создав простой (читай, наивный и не готовый к работе) модуль защиты от ботов 🤖.
Настройка Nginx
Прежде чем погрузиться в борьбу с ботами, нам нужно настроить NGINX для поддержки модуля JavaScript. Инструкции ниже относятся к моей установке (Ubuntu 20.4/Nginx 1.18), поэтому YMMV, но общая идея должна быть одинаковой для большинства установок.
- Начните с добавления ключа NGINX PPA, выполнив:
curl -s https://nginx.org/keys/nginx_signing.key | sudo apt-key add -
2. Настройте ключ репозитория, выполнив:
sudo sh -c 'echo "deb http://nginx.org/packages/ubuntu/ focal nginx" >> /etc/apt/sources.list.d/nginx.list'
3. Обновите список репозиториев, запустивsudo apt update
.
4. Установите NJS, запустив sudo apt install nginx-module-njs
.
Если все прошло хорошо, на этом этапе вы должны получить это прекрасное сообщение на своем терминале:
5. Включите NJS, добавив следующее в начало основного файла nginx.conf:
load_module modules/ngx_http_js_module.so;
6. Перезапустите NGINX, чтобы загрузить NJS в работающий экземпляр:
sudo nginx -s reload
Теперь ваш NGINX готов к любви к JS, так что давайте двигаться дальше и создать нашу первую линию защиты — IP-фильтрацию!
Вступительный акт — Создание проекта
Наш проект по защите от ботов будет написан на TypeScript. Для этого нам нужно создать проект, который будет транспилировать TypeScript в JavaScript ES5, понятный NJS. Как вы уже догадались, NodeJS здесь обязателен, поэтому убедитесь, что вы все настроили, прежде чем продолжить.
- Создайте новую папку проекта и инициализируйте ее:
mkdir njs-bot-protection && cd njs-bot-protection npm init -y
2. Установите необходимые пакеты:
npm i -D @rollup/plugin-typescript @types/node njs-types rollup typescript
3. Добавьте скрипт build в раздел scripts файла package.json:
{ ... "scripts": { "build": "rollup -c" }, ... }
4. Чтобы скомпилировать проект, вам нужно указать компилятору TypeScript, как это сделать с помощью файла tsconfig.json. Создайте новый файл tsconfig.json в корне проекта и добавьте в него следующее содержимое:
5. Наконец, давайте добавим конфигурацию свертки, которая завершит все и создаст конечный js-файл, который будет читать NJS.
Создайте новый файл rollup.config.js в корне проекта и добавьте в него следующее содержимое:
Итак, наш шаблон загружен и готов к работе. Это значит, что пришло время выгнать некоторых ботов!
Раунд 1 — IP-фильтрация
Наша первая линия защиты от ботов — блокировка IP; мы сравниваем IP-адрес входящего запроса со списком известных IP-адресов с плохой репутацией и, если находим совпадение, перенаправляем запрос на «заблокированную» страницу.
Мы начнем с создания модуля JavaScript:
- В корневой папке проекта создайте новую папку с именем src, а затем внутри нее создайте новый файл bot.ts.
- Добавьте следующий фрагмент кода в bot.ts:
💡 Итак, что у нас тут?
- Строка 1: импортирует встроенный модуль для файловой системы (например, fs). Этот модуль имеет дело с файловой системой, позволяя нам читать и записывать файлы, среди прочего.
- Строка 2: вызывает функцию
loadFile
, передавая ей имя файла, который мы хотим загрузить. - Строки 4–12: реализация
loadFile
. Сначала мы инициализируем переменнуюdata
массивом пустых строк (строка 5), затем пытаемся прочитать и разобрать текстовый файл, содержащий список неверных IP-адресов, в объектdata
(строка 7) и, наконец, возвращаем объектdata
(строка 11). - Строки 14–21: реализация
verifyIP
— сердцевины нашего модуля (на данный момент). Это функция, которую мы предоставим NGINX для проверки IP. Сначала мы проверяем, содержит ли массив IP-адресов с плохой репутацией текущий IP-адрес клиента запроса (строка 15). Если да, перенаправьте запрос на страницу блокировки и завершите обработку (строки 16 и 17). Если нет, перенаправьте внутренне в расположениеpages
(строка 20). - Строка 23: экспортирует (читай, раскрывает)
verifyIP
вовне.
3. Соберите модуль, запустив npm run build
в терминале. Если все пойдет хорошо, вы должны найти скомпилированный файл bot.js в папке dist 🎉
Имея файл в руках, давайте настроим NGINX, чтобы он мог его использовать:
- В папке NGINX (в моем случае /etc/nginx) создайте папку с именем njs и скопируйте в нее bot.js из предыдущего раздела. .
- Создайте новую папку с именем njs в папке /var/lib, создайте в ней файл с именем ips.txt, и заполните его списком IP-адресов с плохой репутацией (по одному IP-адресу в строке). Вы можете либо добавить свой собственный список IP-адресов, либо использовать что-то вроде https://github.com/stamparm/ipsum.
- В файле nginx.conf в разделе
http
добавьте следующее:
js_path "/etc/nginx/njs/"; js_import bot.js;
💡 Итак, что у нас тут?
- js_path — задает путь к папке модулей NJS.
- js_import — импортирует модуль из папки модулей NJS. Если не указано, пространство имён импортируемого модуля будет определяться именем файла (в нашем случае
bot
)
4. В разделе server
(мой находится в /etc/nginx/conf.d/default.conf) измените местоположение /
следующим образом:
location / { js_content bot.verifyIP; }
Вызывая verifyIP
с помощью директивы js_content
, мы устанавливаем его в качестве обработчика контента, что означает, что verifyIP
может управлять контентом, который мы отправляем обратно вызывающей стороне (в нашем случае либо показывать страницу блокировки, либо передавать запрос источнику).
5. По-прежнему в разделе server
добавьте местоположение block.html
и именованное местоположение pages
:
location @pages { root /usr/share/nginx/html; proxy_pass http://localhost:8080; } location /block.html { root /usr/share/nginx/html; }
(Местоположение namedpages
будет использоваться нашим модулем NJS для внутреннего перенаправления запроса, если его не следует блокировать. Скорее всего, у вас есть собственная логика для этого перенаправления, поэтому измените ее в соответствии с вашими потребностями)
6. Внизу файла добавьте блок server для порта 8080:
server { listen 8080; location / { root /usr/share/nginx/html; index index.html index.htm; } }
7. В папке /usr/share/nginx/html добавьте файл block.html следующим образом:
На этом наша защита интеллектуальной собственности готова! Добавьте свой собственный IP-адрес в файл ips.txt и перезапустите NGINX (sudo nginx -s reload
). Перейдите к своему экземпляру, и вас должно приветствовать следующее:
Раунд 2 — Обнаружение JavaScript
Наш второй уровень защиты — обнаружение JavaScript. Мы используем это обнаружение, чтобы определить, использует ли посетитель, заходящий на наш сайт, JavaScript (что должен делать каждый нормальный браузер) или нет (предупреждающий знак о том, что этот посетитель может быть незаконным пользователем). Начнем с внедрения фрагмента кода JavaScript на страницы, которые будут создавать куки по корневому пути:
- Добавьте следующие фрагменты кода в bot.ts:
💡 Итак, что у нас тут?
- Строка 1: импортирует встроенный модуль Crypto. Этот модуль посвящен криптографии, и вскоре мы будем использовать его для создания HMAC.
- Строки 5–18: реализация
getCookiePayload
. Функция устанавливает объектdate
на один час раньше текущего времени (строки 6–8), затем использует объектdate
для HMAC (используя модульcrypto
) сигнатуру, которую мы передали функции (объектvalue
) с объектомdate
. (строки 10–14). Наконец, функция возвращает информацию о файлах cookie в строковом формате (имя, значение, срок действия и т. д.). Вы можете заметить, что значение cookie содержит не только хешированную подпись, но и объектdate
, который мы использовали для HMAC подписи. Вскоре вы поймете, почему мы это делаем. - Строки 20–30: реализация
addSnippet
. Функция буферизует данные запроса, и после завершения (строка 23) она:
— создает подпись на основе IP-адреса клиента и заголовка User-Agent (строка 24).
— заменяет закрывающийhead
тег с разделомscript
, который вставляет файл cookie (из функцииgetCookiePayload
) на стороне браузера, используя свойство JavaScriptdocument.cookie
. (строки 25–28).
— отправляет измененный ответ обратно клиенту (строка 29).
2. Экспортируйте новую функцию addSnippet
, обновив оператор экспорта внизу файла:
export default { verifyIP, addSnippet };
3. В блоке местоположения @pages
измените местоположение /
следующим образом:
location @pages { js_body_filter bot.addSnippet; proxy_pass http://localhost:8080; }
В отличие от verifyIP
, мы не хотим, чтобы addSnippet
управлял содержимым ответа, мы хотим, чтобы он вставлял содержимое (тег script
в нашем случае) в любой ответ, возвращаемый из источника. Здесь в игру вступает js_body_filter
. Используя директиву js_body_filter
, мы сообщаем NJS, что предоставляемая нами функция изменит исходный ответ от источника и вернет его после завершения.
4. Перезапустите NGINX и перейдите на страницу своего экземпляра. Вы должны увидеть наш новый скрипт, добавленный непосредственно перед закрывающим тегом head
:
Если клиент использует JavaScript, будет создан новый файл cookie с именем njs. Далее, давайте создадим проверку для этого файла cookie/отсутствия файла cookie:
- Добавьте функцию
verifyCookie
(и ее вспомогательные функции/переменные) в bot.ts:
💡 Итак, что у нас тут?
- Строки 5–11: реализация функции
updateFile
, которая использует модульfs
для сохранения массива строк в файл. - Строки 13–52: реализация материнской нагрузки. При проверке файла cookie njs у нас есть процесс проверки и последствия, которым мы должны следовать:
а. Начнем с извлечения файла cookie njs из заголовка запроса Cookie (строки 14–20).
б. Если у нас нет файла cookie (или он у нас есть, но он искажен), мы сравниваем IP-адрес клиента с нашим списком клиентских IP-адресов, которые достигли нас без файла cookie. Если мы находим совпадение в течение последнего часа, мы теряем запрос (возвращая false, строки 26–27). Если нет, то удаляем IP (если он есть в списке, но прошло больше часа) и пропускаем запрос (строки 29–34).
в. Если у нас есть файл cookie, мы разделяем его на метку времени и полезную нагрузку и используем метку времени для создания собственного хэша HMAC на основе заголовка User-Agent запроса и IP-адреса клиента. Если наш собственный HMAC совпадает с HMAC файла cookie njs, мы передаем запрос. В противном случае мы не справимся (строки 38–45).
д. Если что-то пойдет не так во время проверки, мы не сможем открыть (то есть пройти) запрос (строки 48–51).
2. Добавьте новую функцию verify
, которая вызывает новую функцию verifyCookie
, и действуйте в соответствии с ее результатом:
🔥 В этот момент вы можете подумать про себя, что эта функция verify
выглядит жутко похожей на функцию verifyIP
из предыдущего — вы абсолютно правы, и я коснусь этого через минуту!
3. Чтобы протестировать нашу новую функцию проверки файлов cookie, откройте файл конфигурации (мой находится в /etc/nginx/conf.d/default.conf) и измените директиву js_content с verifyIP
на verify
:
location / { js_content bot.verify; }
4. Перезапустите NGINX и попробуйте дважды зайти на сайт без куки njs — ✋ 🎤- вы заблокированы!
Финальный раунд — собираем все вместе
Итак, теперь у нас есть проверка файлов cookie, но мы отключили проверку IP, потому что у нас может быть только одна директива js_content
, как нам это исправить?
Возможно, вы помните, что несколько минут назад мы создали функцию verify
(которая, возможно, заметила зоркие читатели, ОЧЕНЬ похожа на функцию verifyIP
, которую мы использовали ранее). Если мы обновим нашу функцию verifyIP
, чтобы она возвращала логический ответ в качестве проверки, и добавим эту проверку в verify
, мы получим лучшее из обоих миров с помощью одной большой функции, которая проверяет запросы как для IP-адресов, так и для файлов cookie!
- Рефакторинг функции
verifyIP
следующим образом:
2. Обновите функцию verify
для вызова verifyIP
следующим образом:
3. Обновите оператор export
, так как нам больше не нужно выставлять verifyIP
:
export default { addSnippet, verify };
4. Перезапустите NGINX и наслаждайтесь самодельной защитой от ботов с помощью NJS и TypeScript 🎉
🍾 Исходный код модуля доступен на GitHub!