В разработке nginx
участия я никогда не принимал, так как мой навык работы в Си находится где-то на уровне 1/10. Однако меня не страшит идея скачать исходный код, разобрать его, скомпилировать и запустить. Цель этой статьи помочь и вам преодолеть собственный страх проделать то же самое.
И дело не в том, что вам стоит выполнять в продакшене собственные ответвления программы, а в том, что я вижу многих разработчиков, которых даже не посещала идея познакомиться с исходным кодом привычного им серьезного инструмента или зависимости.
Самое же главное, что изучение зрелых проектов является одним из лучших способов совершенствования навыков программирования.
Исходник и сборка
На верхнем уровне этапы реверс-инжиниринга программных проектов всегда одинаковы:
- Найти/скачать исходный код.
- Установить необходимые библиотеки/компиляторы.
- Начать с
grep
’инга чего-то, наблюдаемого в выводе, или известных вам возможностей программы. - Внести изменения.
- Выполнить вариацию
./configure && make
для сборки. - Запустить программу.
- Возвращаться к шагу 4, пока не получите желаемый результат.
nginx
Давайте проделаем все эти шаги для nginx
. Через поиск в Google по запросу nginx github
находим досутпную только для чтения версию исходного кода на GitHub.
$ mkdir ~/vendor
$ cd ~/vendor
$ git clone https://github.com/nginx/nginx
$ cd nginx
Облом, здесь нет readme
. Снова идем в Google, но теперь с запросом nginx build from source
, и находим это.
Тут мы наблюдаем типичный проект Си, который собирается вполне ожидаемым образом: ./configure && make
. При этом не похоже, чтобы у него были какие-то сторонние зависимости, кроме моего компилятора Си.
Устанавливаем autoconf
, gmake
и компилятор Си. В этом каталоге нет файла ./configure
, но заметьте, что он есть в auto
. Попытка выполнить cd auto && ./configure
проходит безуспешно, так что попробуем ./auto/configure
. Вроде сработало, но вызвало предупреждение:
$ ./auto/configure
...
./auto/configure: error: the HTTP rewrite module requires the PCRE library.
You can either disable the module by using --without-http_rewrite_module
option, or install the PCRE library into the system, or build the PCRE library
statically from the source with nginx by using --with-pcre=<path> option.
Выполняем ./auto/configure --without-http_rewrite_module
и потом еще раз, когда она дает сбой, но уже без http_gzip_module
.
Отлично, автонастройка выполнена. Теперь у нас есть Makefile. Выполняем make -j
для компиляции с использованием всех ядер.
Далее выполняем git status
, чтобы увидеть расположение бинарника. Теперь ls objs
и… вуаля:
$ ls objs
autoconf.err nginx ngx_auto_config.h ngx_modules.c src
Makefile nginx.8 ngx_auto_headers.h ngx_modules.o
Хак
Нам нужна простая команда dump
, которая будет возвращать строковый литерал в блоке location
. Что-то вроде этого:
$ diff --git a/conf/nginx.conf b/conf/nginx.conf
index 29bc085f..e96e817f 100644
--- a/conf/nginx.conf
+++ b/conf/nginx.conf
@@ -41,8 +41,7 @@ http {
#access_log logs/host.access.log main;
location / {
- root html;
- index index.html index.htm;
+ dump 'It was a good Thursday.';
}
#error_page 404 /404.html;
}
Теперь, собрав nginx
, можно использовать флаг -t
для проверки валидности этой конфигурации:
$ ./objs/nginx -t -c $(pwd)/conf/nginx.conf
nginx: [alert] could not open error log file: open() "/usr/local/nginx/logs/error.log" failed (2: No such file or directory)
2021/04/04 21:24:09 [emerg] 1030951#0: unknown directive "dump" in /home/phil/vendor/nginx/conf/nginx.conf:44
nginx: configuration file /home/phil/vendor/nginx/conf/nginx.conf test failed
Вот теперь у нас есть от чего оттолкнуться! Очевидно, что нам нужно зарегистрировать эту директиву, и эта запись дает достаточно информации для начала grep
-инга:
$ git --no-pager grep 'unknown directive'
src/core/ngx_conf_file.c: "unknown directive "%s"", name->data);
Кейс, который содержит этот сбой, находится на строчке 463: rv = cmd->set(cf, cmd, conf)
. Посмотрим, что делает set
. Команда git grep set
здесь не поможет. Так что попробуем выяснить, что такое cmd
, чтобы можно было найти структуру, содержащую set
.
Ага – это ngx_command_t
. Поскольку перед ней нет struct
, это означает, что определена она с помощью typedef
и скорее всего завершается на ;
. Итак, git grep ngx_command_t;
дает:
$ git --no-pager grep ngx_command_t;
src/core/ngx_core.h:typedef struct ngx_command_s ngx_command_t;
И это значит, что реализация скрыта. Тогда ищем ngx_command_s
:
$ git --no-pager grep ngx_command_s
src/core/ngx_conf_file.h:struct ngx_command_s {
src/core/ngx_core.h:typedef struct ngx_command_s ngx_command_t;
Ладно, это ни к чему не ведет. Меняем подход. Посмотрим, какую же команду мы удалили.
$ git --no-pager diff
diff --git a/conf/nginx.conf b/conf/nginx.conf
index 29bc085f..e96e817f 100644
--- a/conf/nginx.conf
+++ b/conf/nginx.conf
@@ -41,8 +41,7 @@ http {
#access_log logs/host.access.log main;
location / {
- root html;
- index index.html index.htm;
+ dump 'It was a good Thursday.';
}
#error_page 404 /404.html;
root
является командой. Попробуем ее скопировать.
$ git --no-pager grep "root"
docs/xml/nginx/changes.xml:in the "root" or "auth_basic_user_file" directives.
docs/xml/nginx/changes.xml:a request was handled incorrectly, if a "root" directive used variables;
docs/xml/nginx/changes.xml:the $document_root variable usage in the "root" and "alias" directives
docs/xml/nginx/changes.xml:the $document_root variable did not support the variables in the "root"
docs/xml/nginx/changes.xml:if a "root" was specified by variable only, then the root was relative
src/http/ngx_http_core_module.c: { ngx_string("root"),
src/http/ngx_http_core_module.c: &cmd->name, clcf->alias ? "alias" : "root");
Это уже интереснее. Скопируем:
$ git --no-pager diff src/http/
diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c index 9b94b328..17a64e80 100644 --- a/src/http/ngx_http_core_module.c +++ b/src/http/ngx_http_core_module.c @@ -331,6 +331,14 @@ static ngx_command_t ngx_http_core_commands[] = {
0,
NULL },
+ { ngx_string("dump"),
+ NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF
+ |NGX_CONF_TAKE1,
+ ngx_http_core_dump,
+ NGX_HTTP_LOC_CONF_OFFSET,
+ 0,
+ NULL },
+
{ ngx_string("alias"),
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_core_root,
Ясно. Значит, вот как регистрируется команда. Очевидно, что сборку без ngx_http_core_dump
мы не сделаем, так что давайте реализуем ее, скопировав/переименовав ngx_http_core_root
:
$ git --no-pager diff src
diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c
index 9b94b328..c184dab5 100644
--- a/src/http/ngx_http_core_module.c
+++ b/src/http/ngx_http_core_module.c
@@ -4402,6 +4410,16 @@ ngx_http_core_root(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
}
+static char *
+ngx_http_core_dump(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
+{
+ ngx_http_core_loc_conf_t *clcf = conf;
+ ngx_str_t *value = cf->args->elts;
+ clcf->dump = value[1];
+ return NGX_CONF_OK;
+}
+
+
static ngx_http_method_name_t ngx_methods_names[] = {
{ (u_char *) "GET", (uint32_t) ~NGX_HTTP_GET },
{ (u_char *) "HEAD", (uint32_t) ~NGX_HTTP_HEAD },
Здесь наша цель просто сохранить строку дампа в этом объекте conf
. Затем в процессе обработки запроса мы сможем проверить, устанавливается ли она, и если да, то ответить на запрос этой строкой.
Понятно, что этот код по-прежнему не соберется, так как мы не изменили объект conf
. Но make
мы все же выполним:
$ make -f objs/Makefile
make[1]: Entering directory '/home/phil/vendor/nginx'
cc -c -pipe -O -W -Wall -Wpointer-arith -Wno-unused-parameter -Werror -g -I src/core -I src/event -I src/event/modules -I src/os/unix -I objs -I src/http -I src/http/modules
-o objs/src/http/ngx_http_core_module.o
src/http/ngx_http_core_module.c
src/http/ngx_http_core_module.c:337:7: error: ngx_http_core_dump undeclared here (not in a function); did you mean ngx_http_core_type?
337 | ngx_http_core_dump,
| ^~~~~~~~~~~~~~~~~~~~~
| ngx_http_core_type
src/http/ngx_http_core_module.c: In function ngx_http_core_dump:
src/http/ngx_http_core_module.c:4418:9: error: ngx_http_core_loc_conf_t {aka struct ngx_http_core_loc_conf_s} has no member named dump
4418 | clcf->dump = value[1];
| ^~
src/http/ngx_http_core_module.c:4418:5: error: statement with no effect [-Werror=unused-value]
4418 | clcf->dump = value[1];
| ^~~~
At top level:
src/http/ngx_http_core_module.c:4414:1: error: ngx_http_core_dump defined but not used [-Werror=unused-function]
4414 | ngx_http_core_dump(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
| ^~~~~~~~~~~~~~~~~~~~~
cc1: all warnings being treated as errors
make[1]: *** [objs/Makefile:834: objs/src/http/ngx_http_core_module.o] Error 1
make[1]: Leaving directory '/home/phil/vendor/nginx'
make: *** [Makefile:10: build] Error 2
Обработчик дампа не объявлен. Когда я копировал ngx_http_core_root
, то выше видел предварительное объявление. Давайте его тоже скопируем и посмотрим, поможет ли.
$ git --no-pager diff
diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c
index 9b94b328..430e1256 100644
--- a/src/http/ngx_http_core_module.c
+++ b/src/http/ngx_http_core_module.c
@@ -56,6 +56,7 @@ static char *ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd,
static char *ngx_http_core_server_name(ngx_conf_t *cf, ngx_command_t *cmd,
void *conf);
static char *ngx_http_core_root(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
+static char *ngx_http_core_dump(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static char *ngx_http_core_limit_except(ngx_conf_t *cf, ngx_command_t *cmd,
void *conf);
static char *ngx_http_core_set_aio(ngx_conf_t *cf, ngx_command_t *cmd,
Теперь сборка:
$ make
make -f objs/Makefile
make[1]: Entering directory '/home/phil/vendor/nginx'
cc -c -pipe -O -W -Wall -Wpointer-arith -Wno-unused-parameter -Werror -g -I src/core -I src/event -I src/event/modules -I src/os/unix -I objs -I src/http -I src/http/modules
-o objs/src/http/ngx_http_core_module.o
src/http/ngx_http_core_module.c
src/http/ngx_http_core_module.c: In function ngx_http_core_dump:
src/http/ngx_http_core_module.c:4419:9: error: ngx_http_core_loc_conf_t {aka struct ngx_http_core_loc_conf_s} has no member named dump
4419 | clcf->dump = value[1];
| ^~
make[1]: *** [objs/Makefile:834: objs/src/http/ngx_http_core_module.o] Error 1
make[1]: Leaving directory '/home/phil/vendor/nginx'
make: *** [Makefile:10: build] Error 2
Отлично. Теперь добавим dump
в этот объект conf
.
$ git --no-pager grep ngx_http_core_loc_conf_t;
src/http/ngx_http_core_module.h:typedef struct ngx_http_core_loc_conf_s ngx_http_core_loc_conf_t;
Далее просто клонируем root
:
$ diff --git a/src/http/ngx_http_core_module.h b/src/http/ngx_http_core_module.h
index 2aadae7f..6b1b178b 100644
--- a/src/http/ngx_http_core_module.h
+++ b/src/http/ngx_http_core_module.h
@@ -333,6 +333,7 @@ struct ngx_http_core_loc_conf_s {
/* location name length for inclusive location with inherited alias */
size_t alias;
ngx_str_t root; /* root, alias */
+ ngx_str_t dump;
ngx_str_t post_action;
ngx_array_t *root_lengths;
Выполняем make
, и все проходит успешно!
Теперь проведем несколько часов за поиском удачного места для добавления хука в процессе запроса.
В конечном итоге на роль такого места, похоже, подходит ngx_http_core_find_config_phase
, так как только в этом случае мы будем работать со структурой, в которую добавили dump
.
Следующим шагом нужно выяснить, как отправить ответ. Поиск response
с помощью grep
здесь не особо поможет, как и использование write
. Однако send
обладает некоторым низкоуровневым, но при этом наглядным поведением.
$ git --no-pager grep send(
src/mail/ngx_mail.h:void ngx_mail_send(ngx_event_t *wev);
src/mail/ngx_mail_auth_http_module.c: n = ngx_send(c, ctx->request->pos, size);)
...
Второй результат выглядит обещающе. Судя по этому файлу, я думаю, что нам нужен объект, содержащий ->data
. Ранее в src/http/ngx_http_core_module.c
я заметил, что объект запроса содержит интересный элемент: r->connection->write->data
. Исходя из его сигнатуры, нужно просто также передать в ngx_send
строку и длину.
Хорошо. Эти данные у нас уже есть из элемента dump
, так что пробуем простой вариант:
$ git --no-pager diff
diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c
index 9b94b328..bd58788b 100644
--- a/src/http/ngx_http_core_module.c
+++ b/src/http/ngx_http_core_module.c
@@ -989,6 +996,11 @@ ngx_http_core_find_config_phase(ngx_http_request_t *r,
ngx_http_finalize_request(r, NGX_HTTP_REQUEST_ENTITY_TOO_LARGE);
return NGX_OK;
}
+
+ if (clcf->dump.len) {
+ ngx_send(r->connection->write->data, clcf->dump.data, clcf->dump.len);
+ return NGX_OK;
+ }
Выполняем make
, и все проходит отлично! Давайте отключим демона nginx
и процессы воркеров, чтобы упростить выход программы в течение наших экспериментов.
$ git --no-pager diff conf/
diff --git a/conf/nginx.conf b/conf/nginx.conf
index 29bc085f..7cce7d65 100644
--- a/conf/nginx.conf
+++ b/conf/nginx.conf
@@ -1,4 +1,5 @@
-
+daemon off;
+master_process off;
#user nobody;
worker_processes 1;
Теперь выполняем ./objs/nginx -c $(pwd)/conf/nginx.conf
. Пробуем curl
:
$ curl localhost:2020
curl: (1) Received HTTP/0.9 when not allowed
А вот это неожиданно. Попробуем получить весь необработанный ответ с помощью telnet
:
$ telnet localhost 2020
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /
It was a good Thursday.
Вот это да. Супер круто! К сожалению, это тоже не валидный HTTP. Похоже, если мы используем ngx_send
, то заголовки HTTP-ответа нужно устанавливать вручную.
Если мы собираемся передать в ngx_send
строковый литерал, то нужно преобразовать его в ngx_str_t
. Судя по src/core/ngx_string.h
, с этим должен справиться макрос ngx_string
.
$ git --no-pager diff src
diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c
index 9b94b328..1a1baccd 100644
--- a/src/http/ngx_http_core_module.c
+++ b/src/http/ngx_http_core_module.c
@@ -989,6 +996,13 @@ ngx_http_core_find_config_phase(ngx_http_request_t *r,
ngx_http_finalize_request(r, NGX_HTTP_REQUEST_ENTITY_TOO_LARGE);
return NGX_OK;
}
+
+ static ngx_str_t header = ngx_string("HTTP/1.0 200 OKrnrn");
+ if (clcf->dump.len) {
+ ngx_send(r->connection->write->data, header.data, header.len);
+ ngx_send(r->connection->write->data, clcf->dump.data, clcf->dump.len);
+ return NGX_OK;
+ }
if (rc == NGX_DONE) {
ngx_http_clear_location(r);
}
Компилируем, запускаем и выполняем curl
:
$ curl localhost:2020
Мда. Программа больше не ругается на HTTP/0.9
, зато теперь зависает. Попробуем расширенную версию curl
:
$ curl -vvv localhost:2020
* Trying ::1:2020...
* connect to ::1 port 2020 failed: Connection refused
* Trying 127.0.0.1:2020...
* Connected to localhost (127.0.0.1) port 2020 (#0)
> GET / HTTP/1.1
> Host: localhost:2020
> User-Agent: curl/7.71.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
Очень странно. Но я заметил там функцию ngx_http_request_finalize
, вызываемую из других участков кода. Попробуем ее добавить.
$ git --no-pager diff src
diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c
index 9b94b328..1a1baccd 100644
--- a/src/http/ngx_http_core_module.c
+++ b/src/http/ngx_http_core_module.c
@@ -989,6 +996,14 @@ ngx_http_core_find_config_phase(ngx_http_request_t *r,
ngx_http_finalize_request(r, NGX_HTTP_REQUEST_ENTITY_TOO_LARGE);
return NGX_OK;
}
+
+ static ngx_str_t header = ngx_string("HTTP/1.0 200 OKrnrn");
+ if (clcf->dump.len) {
+ ngx_send(r->connection->write->data, header.data, header.len);
+ ngx_send(r->connection->write->data, clcf->dump.data, clcf->dump.len);
+ ngx_http_finalize_request(r, NGX_DONE);
+ return NGX_OK;
+ }
Собираем, запускаем, выполняем curl
. Опять зависание. Если взглянуть на исходный код ngx_http_finalize_request
, то похоже, что там есть кейс, в котором соединение полностью закрывается при передаче NGX_HTTP_CLOSE
. Попробуем его.
$ curl localhost:2020
It was a good Thursday.
Ну вот. Сработало.
Что я из этого понял
Хороший ли это способ реализации команд в nginx
? Нет. Несмотря на то, что я кое-что знал о модулях nginx
на уровне пользователя, на уровне разработчика эта команду, как и модуль, можно было реализовать гораздо грамотнее.
При этом также необходимы высокоуровневые инструменты, чтобы возвращать создаваемые ответы, а не вводить заголовки вручную.
Автор: Дмитрий Брайт