Осваиваем новую базу кода: анализируем программу 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 OK\r\n\r\n"); +    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 OK\r\n\r\n"); +    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 на уровне пользователя, на уровне разработчика эта команду, как и модуль, можно было реализовать гораздо грамотнее.

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


ссылка на оригинал статьи https://habr.com/ru/articles/575574/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *