Создание расширений Nginx на JavaScript

Если вам нужно адаптировать контент, авторизировать, фильтровать, изменять поведение 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))
Enter fullscreen mode Выйти из полноэкранного режима

Когда вы, наконец, нашли проблемы, вы можете удалить суффикс «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/

Оцените статью
devanswers.ru
Добавить комментарий