Осваиваем новую базу кода: анализируем программу nginx

В разработке nginxучастия я никогда не принимал, так как мой навык работы в Си находится где-то на уровне 1/10. Однако меня не страшит идея скачать исходный код, разобрать его, скомпилировать и запустить. Цель этой статьи помочь и вам преодолеть собственный страх проделать то же самое.

И дело не в том, что вам стоит выполнять в продакшене собственные ответвления программы, а в том, что я вижу многих разработчиков, которых даже не посещала идея познакомиться с исходным кодом привычного им серьезного инструмента или зависимости.

Самое же главное, что изучение зрелых проектов является одним из лучших способов совершенствования навыков программирования.

Исходник и сборка

На верхнем уровне этапы реверс-инжиниринга программных проектов всегда одинаковы:

  1. Найти/скачать исходный код.
  2. Установить необходимые библиотеки/компиляторы.
  3. Начать с grep’инга чего-то, наблюдаемого в выводе, или известных вам возможностей программы.
  4. Внести изменения.
  5. Выполнить вариацию ./configure && make для сборки.
  6. Запустить программу.
  7. Возвращаться к шагу 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 на уровне пользователя, на уровне разработчика эта команду, как и модуль, можно было реализовать гораздо грамотнее.

При этом также необходимы высокоуровневые инструменты, чтобы возвращать создаваемые ответы, а не вводить заголовки вручную.

Источник 📢