Привет, Хабр!
Nginx — это не просто веб‑сервер, а платформа, возможности которой можно расширять своими модулями. Если вам не хватает стандартного функционала и хочется больше контроля над обработкой запросов, кастомные модули могут позволить внедрять свою логику, оптимизировать маршрутизацию, фильтровать контент и реализовывать нестандартные механизмы аутентификации.
Начнем с базового модуля.
Простейший модуль «Hello World»
Когда я только начинал, всегда первым делом создавал модуль, который отвечал простым текстом. Минимальный пример модуля, который отвечает строкой «Hello World from custom Nginx module!».
#include <ngx_config.h> #include <ngx_core.h> #include <ngx_http.h> // Обработчик запроса: возвращает приветственное сообщение static ngx_int_t ngx_http_hello_handler(ngx_http_request_t r) { ngx_int_t rc; ngx_buf_t b; ngx_chain_t out; // Устанавливаем тип контента r->headers_out.content_type.len = sizeof("text/plain") - 1; r->headers_out.content_type.data = (u_char )"text/plain"; // Выделяем буфер ответа из пула памяти b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); if (b == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } // Текст ответа, который мы собираемся отправить u_char response = (u_char )"Hello World from custom Nginx module!"; size_t response_len = ngx_strlen(response); // Заполняем буфер b->pos = response; b->last = response + response_len; b->memory = 1; // Данные находятся в памяти (не на диске) b->last_buf = 1; // Это последний буфер в цепочке out.buf = b; out.next = NULL; // Настраиваем заголовки ответа r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = response_len; rc = ngx_http_send_header(r); if (rc != NGX_OK) { return rc; } return ngx_http_output_filter(r, &out); } // Функция для установки обработчика через директиву "hello" static char ngx_http_hello(ngx_conf_t cf, ngx_command_t cmd, void conf) { // Получаем конфигурацию текущей локации и назначаем наш обработчик ngx_http_core_loc_conf_t clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_hello_handler; return NGX_CONF_OK; } static ngx_command_t ngx_http_hello_commands[] = { { ngx_string("hello"), NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS, ngx_http_hello, 0, 0, NULL }, ngx_null_command }; static ngx_http_module_t ngx_http_hello_module_ctx = { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }; ngx_module_t ngx_http_hello_module = { NGX_MODULE_V1, &ngx_http_hello_module_ctx, ngx_http_hello_commands, NGX_HTTP_MODULE, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NGX_MODULE_V1_PADDING };
В функции‑обработчике выделяем буфер из пула памяти (Nginx работает с памятью весьма бережно, так что никаких malloc‑ов — только пул), устанавливаем тип контента и формируем ответ. Этот модуль можно собрать как статически, так и динамически, а дальше можно расширять его по своему усмотрению.
Модуль с кастомной конфигурацией
Когда первый модуль уже прошел проверку временем, хочется добавить гибкости. Допустим, стоит задача задавать приветственное сообщение через конфигурацию Nginx — ну, чтобы не перекомпилировать сервер при каждом изменении идеи.
Рассмотрим модуль, который читает директиву hello_msg и использует её для формирования ответа. В этой версии создаем структуру конфигурации, регистрируем её и затем используем в обработчике.
#include <ngx_config.h> #include <ngx_core.h> #include <ngx_http.h> typedef struct { ngx_str_t greeting; } ngx_http_custom_conf_t; // Создание конфигурации для локации static void ngx_http_custom_create_loc_conf(ngx_conf_t cf) { ngx_http_custom_conf_t conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_custom_conf_t)); if (conf == NULL) { return NGX_CONF_ERROR; } // По умолчанию приветствие не задано conf->greeting.len = 0; conf->greeting.data = NULL; return conf; } // Обработчик запроса, который использует кастомное сообщение static ngx_int_t ngx_http_custom_handler(ngx_http_request_t r) { ngx_int_t rc; ngx_buf_t b; ngx_chain_t out; ngx_http_custom_conf_t conf; conf = ngx_http_get_module_loc_conf(r, ngx_http_custom_module); if (conf->greeting.len == 0) { // Если сообщение не задано, используем стандартное conf->greeting.data = (u_char )"Default custom greeting!"; conf->greeting.len = ngx_strlen(conf->greeting.data); } r->headers_out.content_type.len = sizeof("text/plain") - 1; r->headers_out.content_type.data = (u_char )"text/plain"; b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); if (b == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } b->pos = conf->greeting.data; b->last = conf->greeting.data + conf->greeting.len; b->memory = 1; b->last_buf = 1; out.buf = b; out.next = NULL; r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = conf->greeting.len; rc = ngx_http_send_header(r); if (rc != NGX_OK) { return rc; } return ngx_http_output_filter(r, &out); } // Функция установки директивы "hello_msg" static char ngx_http_custom_set_greeting(ngx_conf_t cf, ngx_command_t cmd, void conf) { ngx_http_custom_conf_t custom_conf = conf; ngx_str_t value = cf->args->elts; custom_conf->greeting = value[1]; return NGX_CONF_OK; } static ngx_command_t ngx_http_custom_commands[] = { { ngx_string("hello"), NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS, ngx_http_custom_handler, 0, 0, NULL }, { ngx_string("hello_msg"), NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, ngx_http_custom_set_greeting, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_custom_conf_t, greeting), NULL }, ngx_null_command }; static ngx_http_module_t ngx_http_custom_module_ctx = { NULL, NULL, NULL, NULL, NULL, NULL, ngx_http_custom_create_loc_conf, NULL }; ngx_module_t ngx_http_custom_module = { NGX_MODULE_V1, &ngx_http_custom_module_ctx, ngx_http_custom_commands, NGX_HTTP_MODULE, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NGX_MODULE_V1_PADDING };
Определили структуру ngx_http_custom_conf_t для хранения строки приветствия. Функция ngx_http_custom_create_loc_conf выделяет память под эту структуру, а функция установки директивы hello_msg позволяет пользователю задавать своё сообщение через конфигурационный файл. Таким образом, в конфигурации Nginx можно написать:
location /custom { hello; hello_msg "Привет, мир! Это мое кастомное приветствие."; }
Так можно менять поведение модуля можно на ходу, без перекомпиляции.
Фильтрующий модуль
Бывает, хочется не просто отдавать ответ, а немного пофантазировать с ним — добавить подпись, изменить формат, обогащать контент. Фильтрующие модули Nginx позволяют вмешиваться в цепочку формирования ответа. Я, например, люблю добавлять в конец HTML‑страницы комментарий с подписью «»:
#include <ngx_config.h> #include <ngx_core.h> #include <ngx_http.h> static ngx_int_t ngx_http_filter_header_filter(ngx_http_request_t r); static ngx_int_t ngx_http_filter_body_filter(ngx_http_request_t r, ngx_chain_t in); // Указатели на следующие фильтры в цепочке static ngx_http_output_header_filter_pt next_header_filter; static ngx_http_output_body_filter_pt next_body_filter; // Строка, которую мы добавляем в конец ответа static ngx_str_t custom_footer = ngx_string("<!-- Powered by custom filter -->"); // Фильтр заголовков: пропускаем дальше без изменений static ngx_int_t ngx_http_filter_header_filter(ngx_http_request_t r) { return next_header_filter(r); } // Фильтр тела: добавляем наш footer к последнему буферу static ngx_int_t ngx_http_filter_body_filter(ngx_http_request_t r, ngx_chain_t in) { ngx_chain_t cl; if (in == NULL) { return next_body_filter(r, in); } // Ищем последний буфер в цепочке for (cl = in; cl; cl = cl->next) { if (cl->buf->last_buf) { size_t footer_size = custom_footer.len; // Создаем новый временный буфер для footer ngx_buf_t b = ngx_create_temp_buf(r->pool, footer_size); if (b == NULL) { return NGX_ERROR; } ngx_memcpy(b->pos, custom_footer.data, footer_size); b->last = b->pos + footer_size; b->last_buf = 1; // Снимаем флаг последнего буфера у старого буфера cl->buf->last_buf = 0; // Создаем новый элемент цепочки для footer ngx_chain_t new_cl = ngx_alloc_chain_link(r->pool); if (new_cl == NULL) { return NGX_ERROR; } new_cl->buf = b; new_cl->next = NULL; cl->next = new_cl; break; } } return next_body_filter(r, in); } static ngx_int_t ngx_http_filter_init(ngx_conf_t cf) { // Сохраняем указатели на существующие фильтры и заменяем их нашими функциями next_header_filter = ngx_http_top_header_filter; ngx_http_top_header_filter = ngx_http_filter_header_filter; next_body_filter = ngx_http_top_body_filter; ngx_http_top_body_filter = ngx_http_filter_body_filter; return NGX_OK; } static ngx_http_module_t ngx_http_filter_module_ctx = { NULL, ngx_http_filter_init, NULL, NULL, NULL, NULL, NULL, NULL }; ngx_module_t ngx_http_filter_module = { NGX_MODULE_V1, &ngx_http_filter_module_ctx, NULL, NGX_HTTP_MODULE, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NGX_MODULE_V1_PADDING };
Когда приходит последний буфер, создаём дополнительный буфер с нашим комментарием и подставляем его вместо исходного последнего. Таким образом, к каждому ответу добавляется наш footer.
Модуль аутентификации
А теперь перейдем к более серьёзной задаче — базовой аутентификации. Допустим, хочется ограничить доступ к определённому ресурсу, проверяя наличие специального HTTP‑заголовка (например, X‑Auth‑Token). Если его нет — сервер сразу возвращает 403 Forbidden. Вот как это можно реализовать:
#include <ngx_config.h> #include <ngx_core.h> #include <ngx_http.h> static ngx_int_t ngx_http_auth_handler(ngx_http_request_t r) { ngx_table_elt_t *h; ngx_list_part_t part; ngx_uint_t i; ngx_str_t header_name = ngx_string(«X‑Auth‑Token»); part = &r→headers_in.headers.part; h = part→elts; // Перебираем все заголовки, пока не найдем нужный for (i = 0;; i++) { if (i >= part→nelts) { if (part→next == NULL) { break; } part = part→next; h = part→elts; i = 0; } if (ngx_strcasecmp(h[i]→key.data, header_name.data) == 0) { // Наш заголовок найден — пропускаем дальше return NGX_DECLINED; } } // Заголовок не найден — отдаем 403 return NGX_HTTP_FORBIDDEN; } static ngx_int_t ngx_http_auth_init(ngx_conf_t cf) { ngx_http_handler_pt h; ngx_http_core_main_conf_t cmcf; cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); h = ngx_array_push(&cmcf→phases[NGX_HTTP_PREACCESS_PHASE].handlers); if (h == NULL) { return NGX_ERROR; } *h = ngx_http_auth_handler; return NGX_OK; } static ngx_http_module_t ngx_http_auth_module_ctx = { NULL, ngx_http_auth_init, NULL, NULL, NULL, NULL, NULL, NULL }; ngx_module_t ngx_http_auth_module = { NGX_MODULE_V1, &ngx_http_auth_module_ctx, NULL, NGX_HTTP_MODULE, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NGX_MODULE_V1_PADDING };
Обработчик перебирает все входящие заголовки, и если нужный заголовок не найден — сервер мгновенно отказывает в доступе.
Сборка и интеграция модулей
Какой бы модуль вы ни написали, он должен быть интегрирован в Nginx. Есть два варианта: статическая или динамическая сборка.
Статическая сборка
Скачиваем исходники Nginx, добавляем ваш модуль (например, в каталог /path/to/your/module), и конфигурируем сборку:
/configure ‑add‑module=/path/to/your/module make sudo make install
Динамическая сборка
Если вы хотите иметь возможность менять модули без перекомпиляции всего сервера (начиная с версии 1.9.11):
/configure ‑add‑dynamic‑module=/path/to/your/module make modules
После сборки добавляем в конфигурационный файл nginx.conf строку:
load_module modules/ngx_http_your_module.so;
Это позволит серверу подгружать модуль при запуске.
А какие модули вы уже писали или планируете написать? Делитесь своим опытом в комментариях.
Больше актуальных навыков по IT-инфраструктуре вы можете получить в рамках практических онлайн-курсов от экспертов отрасли. В календаре мероприятий можно ознакомиться со списком открытых уроков.
