Если вам нужно адаптировать контент, авторизировать, фильтровать, изменять поведение Nginx, то njs
может стать решением. Nginx предлагает бэкенд на JavaScript для манипулирования запросами и ответами и даже потоками. Это njs
и он очень мощный.
Nginx — это очень хороший HTTP-сервер и обратный прокси. Его легко настроить, легко запустить, и он «делает свою работу». Мы часто используем его в качестве прокси для бэкендов. Но иногда вы можете почувствовать некоторые ограничения. Например, если вам нужно авторизовать пользователей на нескольких бэкендах с помощью определенного провайдера идентификации со странным API. Или, иногда, это бэкенд, который является проблемным.
Например, я обслуживаю бэкенд Rancher, и Rancher предоставляет свой YAML-файл kubeconfig
(динамически создаваемый для каждого пользователя) с сертификатом внутри, который мне нужно удалить.
Короче говоря, мне нужно «фильтровать» содержимое, потому что я не могу изменить поведение бэкенда.
И это именно тот случай, когда
njs
может быть использован!
Что я мог бы сделать, так это просто обнаружить, что пользователь требует файл KubeConfig, удалить запись о сертификате в YAML и обслужить файл ответа. Это одна из огромного количества манипуляций, которые вы можете сделать с помощью njs
.
Что такое njs
?
njs
— это JavaScript движок, интегрированный в Nginx. njs
обеспечивает другой подход, который запускает JS VM на каждом необходимом процессе. Он действительно может использовать обычные модули JavaScript (например, fs
), и он совместим с ES6. Это означает, что вы будете кодировать скрипты без необходимости менять свои привычки.
Что вы можете сделать
Есть много вещей, которые вы можете сделать с помощью njs
:
- Делать авторизацию (с бэкендом или без)
- Манипулировать содержимым вывода, заголовками, статусом…
- Взаимодействовать с потоками
- Использовать криптографию
- И так далее…
Вы должны помнить, что njs
не предназначен для создания приложения. Nginx — это не сервер приложений, это веб-сервер и обратный прокси. Поэтому он не заменит «настоящий» веб-фреймворк. Но он поможет исправить некоторые вещи, которые трудно сделать.
Прочтите это прежде!
Перейдите на страницу документации здесь, чтобы проверить глобальные переменные, которые уже определены Nginx/njs, вы увидите, что есть много методов для отслеживания, шифрования, декодирования или манипулирования запросами.
Не тратьте слишком много времени на чтение страницы, просто взгляните, чтобы знать о возможностях.
По умолчанию он не активирован
njs
не активирован по умолчанию. Это модуль, который необходимо загрузить при запуске.
Традиционный способ его активации — добавить load_module modules/ngx_http_js_module.so;
поверх файла nginx.conf
.
Для docker вы можете изменить «command», чтобы заставить модуль быть загруженным:
docker run --rm -it nginx:alpine
nginx -g "daemon off; load_module modules/ngx_http_js_module.so;"
Теперь в ваших http
сервисах вы можете загружать скрипт и вызывать функции для различных ситуаций.
Подготовьте тесты
Чтобы следовать этой статье и иметь возможность тестировать, мы запустим службу Nginx и движок блога «Ghost».
Это поможет при запуске:
mkdir -p ~/Projects/nginx-tests
cd ~/Projects/nginx-tests
mkdir -p nginx/conf.d
touch nginx/conf.d/default.conf
cat << EOF | tee docker-compose.yaml
version: "3"
services:
# our nginx reverse proxy
# with njs activated
http:
image: nginx:alpine
command: nginx -g "daemon off; load_module modules/ngx_http_js_module.so;"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:z
ports:
- 80:80
# a backend
ghost:
image: ghost
depends_on:
- http
environment:
url: http://example.localhost
EOF
Отладка
Иногда ваш скрипт не возвращает ничего, вы получаете ошибку 500 и ничего не отображается в логах nginx.
Что вы можете сделать, так это добавить это в «расположение»:
error_log /var/log/nginx/error.log debug;
Для пользователей docker/podman используйте:
# see the logs in real time
docker-compose logs -f http
И вы можете использовать в своем скрипте :
r.log("something here")
r.log(njs.dump(variable))
Когда вы, наконец, нашли проблемы, вы можете удалить суффикс «debug» из директивы «error_log
«.
Есть много способов, много вариантов, много ситуаций
Nginx управляет 2 типами конфигураций: http и stream. По случайности, njs
можно использовать для обоих.
В этой статье мы будем говорить только о режиме «http».
Когда мы работаем с режимом «http», мы можем сказать Nginx использовать JavaScript для манипуляций:
- содержимым (это означает, что мы будем генерировать весь запрос до вызова)
- Только заголовки (после запроса)
- Только телом (после запроса)
- Установить переменные
- …
Конвейер не сложный: импортируйте файл javascript, затем используйте директиву Nginx, чтобы использовать функцию для ситуации. Ничего больше.
Первый пример, создание контента
Для начала создадим файл с именем example.js
. Для удобства тестирования поместите его в nginx/conf.d/example.js
— в производственной среде лучше изменить расположение ваших скриптов.
Итак, вот содержание:
function information(r) {
r.return(200, JSON.stringify({
"hello": "world",
}));
}
// make the function(s) available
export default {information}
Переменная r
(аргумент функции) — это объект «request», предоставляемый Nginx. Существует множество методов и свойств, которые мы можем использовать, здесь мы используем только r.return()
.
Теперь пришло время создать функцию JavaScript, которая будет вызываться в качестве создателя контента. В nginx/conf.d/default.conf
добавьте следующее:
js_import /etc/nginx/conf.d/example.js;
server {
listen 80;
server_name example.localhost;
location / {
js_content example.information;
}
}
Да, это все. Мы импортируем скрипт, и используем функцию как js_content
. Это означает, что Nginx передаст запрос скрипту, а скрипт сможет выдать содержимое.
Запустите (или перезапустите) контейнер и зайдите в домен «example.localhost»:
$ docker-compose up -d
$ curl example.localhost
{"hello": "world"}
Это первый пример. Здесь мы генерируем только вывод JSON.
Теперь мы можем делать некоторые приятные вещи, например, заменять содержимое.
Замена содержимого
njs
предлагает несколько API fetch
для получения, подзапроса или внутреннего перенаправления запроса.
В этом примере мы заменим содержимое страницы «/about/», вставив внутрь сообщение.
Следующая конфигурация не идеальна (на самом деле мы могли бы подобрать location /about
и вызвать наш JavaScript, но я хочу показать вам внутреннее перенаправление) — но она покажет вам некоторые классные вещи, которые позволяет njs
.
Измените файл nginx/conf.d/default.conf
следующим образом:
js_import /etc/nginx/conf.d/example.js;
upstream ghost_backend {
server ghost:2368;
}
server {
listen 80;
server_name example.localhost;
gunzip on;
# call our js for /exemple url
location = /exemple {
js_content example.information;
}
# match everything
location / {
js_content example.replaceAboutPage;
}
# call the "ghost blog" backend
# note the "arobase" that creates a named location
location @ghost_backend {
# important !
subrequest_output_buffer_size 16k;
proxy_pass http://ghost_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
}
В этом файле вы должны обратить внимание вот на что:
Мы заставляем «gunzip» распаковывать проксированные ответы. Это важно, если бэкенд использует
gzip
ответы, а вы хотите сделать замену.
Мы также установили размер «выходного буфера подзапроса» равным 16k, при необходимости вы можете установить его на 128k (например, для больших CSS-файлов при подзапросе).Это важно, потому что позже мы будем делать подзапросы, и буфер будет заполняться слишком быстро.
Затем в файл example.js
добавьте функцию replaceAboutPage
и экспортируйте ее :
function information(r) {
r.return(
200,
JSON.stringify({
hello: "world",
})
);
}
async function replaceAboutPage(r) {
if (r.uri == "/about/") {
r.headersOut["content-type"] = "text/html";
r.return(200, "Changed");
} else {
r.internalRedirect("@ghost_backend");
}
}
export default { information, replaceAboutPage };
Потратьте минуту, чтобы прочитать функцию replaceAboutPage
. В этом примере мы только:
- возвращаем страницу с надписью «Changed» внутри, если URI является «/about»
- либо мы используем
@ghost_backend
местоположение для проксирования блога.
Перезапустите контейнер nginx:
docker-compose restart http
Зайдите на сайт http://example.locahost. Затем перейдите на страницу «/about», используя ссылку «About» сверху.
Отлично, теперь мы можем сделать более качественную замену.
Нам понадобится «подзапрос» страницы. Но для подзапроса нужен «реальный путь URI». Поэтому, давайте сначала добавим местоположение в default.conf
.
# a reversed uri.
# We remove the prefix and use the @ghost_backend
location /__reversed {
internal;
rewrite ^/__reversed/(.*)$ /$1 break;
try_files $uri @ghost_backend;
}
Затем перейдем в example.js
и заменим функцию replaceAboutPage
на эту:
//...
async function replaceAboutPage(r) {
if (r.uri == "/about/") {
r.subrequest(`/__reversed${r.uri}`) // call the reversed url
.then((res) => {
// copy the response headersOut
Object.keys(res.headersOut).forEach((key) => {
r.headersOut[key] = res.headersOut[key];
});
// replace the end of "header" tag to append a title
const responseBuffer = res.responseBuffer
.toString()
.replace("</header>", "</header><h1>Reversed</h1>");
r.return(res.status, responseBuffer);
})
.catch((err) => {
r.log(err);
r.return(500, "Internal Server Error");
});
} else {
// in any other case
r.internalRedirect("@ghost_backend");
}
}
//...
Еще раз перезапустите контейнер http
:
docker-compose restart http
Затем посетите сайт http://example.locahost/about/ — вы должны увидеть:
Второй способ, используйте js_body_filter
.
Возможно, лучшим решением будет использование js_body_filter
вместо js_content
. Это позволит Nginx передать прокси, а затем мы сможем манипулировать телом.
Итак, давайте изменим файл default.conf
на этот:
js_import /etc/nginx/conf.d/example.js;
upstream ghost_backend {
server ghost:2368;
}
server {
listen 80;
server_name example.localhost;
gunzip on;
# call our js for /exemple url
location = /exemple {
js_content example.information;
}
# call the "ghost blog" backend
location / {
js_body_filter example.replaceAboutPage;
subrequest_output_buffer_size 16k;
proxy_pass http://ghost_backend;
proxy_set_header Host $host;
proxy_set_header Accept-Encoding "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
}
Очень важно, что здесь мы заставим Accept-Encoding
быть пустым, потому что Ghost вернет gzipped содержимое, которое невозможно (на данный момент) распаковать из javascript.
Затем измените функцию JavaScript replaceAboutPage
на эту:
async function replaceAboutPage(r, data, flags) {
if (r.uri == "/about/") {
r.sendBuffer(data.toString().replace("</header>", "</header>Reversed"), flags);
} else {
r.sendBuffer(data, flags);
}
}
Функция должна использовать sendBuffer()
для отправки данных клиенту.
Конечно, есть много других вещей, которые нужно сделать, например, изменить заголовок «Content-Length», но это работает.
Это очень эффективно, и это хороший метод для замены, проверки содержимого или исправления ответа без необходимости делать subrequest
.
Выполните fetch
для внешнего ответа
njs
предоставляет глобальную переменную ngx
. Этот объект предлагает API fetch
, который более или менее похож на тот, что вы можете найти в современных браузерах.
Давайте добавим метод для вызова API Dev.to.
Пожалуйста, не злоупотребляйте API, это простой пример, и вы рады не перегружать серверы
Следующая функция получит список моих статей.
async function getMetal3D(r) {
ngx
.fetch("https://dev.to/api/articles?username=metal3d", {
headers: {
"User-Agent": "Nginx",
},
})
.then((res) => res.json())
.then((response) => {
const titles = response.map((article) => article.title);
r.headersOut["Content-Type"] = "application/json";
r.return(200, JSON.stringify(titles));
})
.catch((error) => {
r.log(error);
});
}
// don't forget to export functions
export default { information, replaceAboutPage, getMetal3D };
Хорошо, теперь вы думаете, что вам достаточно добавить вызов js_content
в файл default.conf
. Но… возникнут некоторые проблемы.
- Также, вам нужно определить, где он может найти центры сертификации.
Но это не так сложно сделать:
location /metal3d {
resolver 9.9.9.9;
js_fetch_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
js_content example.getMetal3D;
}
Итак, еще раз перезапустите контейнер http
и вызовите URI /metal3d
:
docker-compose restart http
curl http://example.localhost/metal3d
# response:
[
"Python, the usefulness of "dataclass"",
"Fixing a Promise return object to be typed in Typescript",
"Flask / Quart - manage module loading and splitting",
"Change local container http(s) reverse proxy to Pathwae"
]
Заключение
Nginx предлагает очень полезную систему расширений JavaScript. Легко осознать большие возможности, которые можно реализовать с ее помощью.
Можно создать собственную систему кэширования, манипулировать заголовками, читать токен, проверять авторизацию, изменять содержимое, создавать прокси на API и многое другое.
Мы рассмотрели только манипуляции с контентом, но можно также манипулировать «потоками» с помощью событий.
Перейдите на страницы документации:
- NJS doc: https://nginx.org/en/docs/njs/
- JS объект, который вы можете использовать: https://nginx.org/en/docs/njs/reference.html
- Директива модуля:
- http https://nginx.org/en/docs/http/ngx_http_js_module.html
- поток https://nginx.org/en/docs/stream/ngx_stream_js_module.html
- Примеры на GitHub: https://github.com/nginx/njs-examples/