В разработке nginx участия я никогда не принимал, так как мой навык работы в Си находится где-то на уровне 1/10. Однако меня не страшит идея скачать исходный код, разобрать его, скомпилировать и запустить. Цель этой статьи помочь и вам преодолеть собственный страх проделать то же самое.
И дело не в том, что вам стоит выполнять в продакшене собственные ответвления программы, а в том, что я вижу многих разработчиков, которых даже не посещала идея познакомиться с исходным кодом привычного им серьезного инструмента или зависимости.
Самое же главное, что изучение зрелых проектов является одним из лучших способов совершенствования навыков программирования.
На верхнем уровне этапы хакинга программных проектов всегда одинаковы:
Давайте проделаем все эти шаги для 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 на уровне пользователя, на уровне разработчика эта команду, как и модуль, можно было реализовать гораздо грамотнее.
При этом также необходимы высокоуровневые инструменты, чтобы возвращать создаваемые ответы, а не вводить заголовки вручную.
И дело не в том, что вам стоит выполнять в продакшене собственные ответвления программы, а в том, что я вижу многих разработчиков, которых даже не посещала идея познакомиться с исходным кодом привычного им серьезного инструмента или зависимости.
Самое же главное, что изучение зрелых проектов является одним из лучших способов совершенствования навыков программирования.
Исходник и сборка
На верхнем уровне этапы хакинга программных проектов всегда одинаковы:
- Найти/скачать исходный код.
- Установить необходимые библиотеки/компиляторы.
- Начать с 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 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 на уровне пользователя, на уровне разработчика эта команду, как и модуль, можно было реализовать гораздо грамотнее.
При этом также необходимы высокоуровневые инструменты, чтобы возвращать создаваемые ответы, а не вводить заголовки вручную.
Осваиваем новую базу кода: анализируем программу nginx
В разработке nginx участия я никогда не принимал, так как мой навык работы в Си находится где-то на уровне 1/10. Однако меня не страшит идея скачать исходный код, разобрать его, скомпилировать и...
habr.com