Я люблю Луа. Я также люблю NGINX. Мы втроем прекрасно ладим. Как и в любых отношениях, у нас были взлеты и падения (да, я смотрю на ваши Lua-паттерны), но в целом жизнь была идеальной. Затем появился JavaScript-модуль NGINX (сокращенно NJS).

JavaScript-модуль NGINX был впервые представлен в 2015 году, но недавно с обновлением 0.5.x его функциональность значительно расширилась. Поскольку я обожаю JS, я решил протестировать его, создав простой (читай, наивный и не готовый к работе) модуль защиты от ботов 🤖.

Настройка Nginx

Прежде чем погрузиться в борьбу с ботами, нам нужно настроить NGINX для поддержки модуля JavaScript. Инструкции ниже относятся к моей установке (Ubuntu 20.4/Nginx 1.18), поэтому YMMV, но общая идея должна быть одинаковой для большинства установок.

  1. Начните с добавления ключа 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 здесь обязателен, поэтому убедитесь, что вы все настроили, прежде чем продолжить.

  1. Создайте новую папку проекта и инициализируйте ее:
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:

  1. В корневой папке проекта создайте новую папку с именем src, а затем внутри нее создайте новый файл bot.ts.
  2. Добавьте следующий фрагмент кода в 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, чтобы он мог его использовать:

  1. В папке NGINX (в моем случае /etc/nginx) создайте папку с именем njs и скопируйте в нее bot.js из предыдущего раздела. .
  2. Создайте новую папку с именем njs в папке /var/lib, создайте в ней файл с именем ips.txt, и заполните его списком IP-адресов с плохой репутацией (по одному IP-адресу в строке). Вы можете либо добавить свой собственный список IP-адресов, либо использовать что-то вроде https://github.com/stamparm/ipsum.
  3. В файле 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 на страницы, которые будут создавать куки по корневому пути:

  1. Добавьте следующие фрагменты кода в 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) на стороне браузера, используя свойство JavaScript document.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:

  1. Добавьте функцию 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!

  1. Рефакторинг функции verifyIP следующим образом:

2. Обновите функцию verify для вызова verifyIP следующим образом:

3. Обновите оператор export, так как нам больше не нужно выставлять verifyIP:

export default { addSnippet, verify };

4. Перезапустите NGINX и наслаждайтесь самодельной защитой от ботов с помощью NJS и TypeScript 🎉

🍾 Исходный код модуля доступен на GitHub!