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

Kate

Administrator
Команда форума
В разработке 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 на уровне пользователя, на уровне разработчика эта команду, как и модуль, можно было реализовать гораздо грамотнее.

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

 
Сверху