Показаны сообщения с ярлыком if. Показать все сообщения
Показаны сообщения с ярлыком if. Показать все сообщения

среда, 22 октября 2014 г.

nginx: неожиданный proxy_pass

Возьмем простую конфигурацию nginx.
worker_processes  1;
error_log  /var/log/nginx/error.log  info;

events {
    worker_connections  1024;
}

http {
    server {
        listen       80;
        server_name  proxy;

        location / {
            if ($arg_x = '') {
                proxy_pass http://127.0.0.1:8010;
                break;
            }
            proxy_pass http://$arg_x;
        }
    }

    server {
        listen       8010;
        server_name  backend;

        location / {
            echo 'I am in backend';
        }
    }
}
Что ответит сервер proxy на простой запрос без аргументов? Логично предположить, что в ответе будет строка I am in backend. Однако, вместо этого будет получена ошибка 500 Internal Server Error, а в логе появится запись invalid URL prefix in “http://”! Вы можете в этом убедиться, введя в адресной строке браузера адрес localhost или запустив curl.
curl 'http://localhost/'
<html>
<head><title>500 Internal Server Error</title></head>
<body bgcolor="white">
<center><h1>500 Internal Server Error</h1></center>
<hr><center>nginx/1.7.6</center>
</body>
</html>
Если же ввести адрес http://localhost/?x=127.0.0.1:8010, то получим ожидаемую строку I am in backend. Почему nginx не видит заданный в конфигурации дословно адрес 127.0.0.1:8010 в случае не заданного аргумента x и пытается выполнить проксирование на пустой адрес http://, в результате чего и возникает ошибка 500? Ответ кроется в реализации модуля прокси и принятого в нем механизма наследования конфигурации уровня локации (location configuration merge). Дело в том, что директива proxy_pass по-разному обрабатывает параметры, содержащие дословные адреса (такие как http://127.0.0.1:8010) и переменные (такие как http://$arg_x). Связанные с ними данные хранятся независимо в конфигурации уровня локации модуля прокси. Так, дословный адрес записывается в conf->upstream.upstream, а переменный адрес — в специальные структуры, на которые ссылаются массивы conf->proxy_lengths и conf->proxy_values: фактическое значение адреса в этом случае вычисляется во время выполнения программы путем вычисления заранее скомпилированного выражения, построенного на основании этих данных. Если директива proxy_pass была задана дословным адресом, то указатель conf->upstream.upstream будет на него ссылаться, а указатели conf->proxy_lengths и conf->proxy_values останутся нулевыми. Если же адрес в proxy_pass был задан с помощью переменных, то, наоборот, указатель conf->upstream.upstream останется нулевым, в то время как указатели conf->proxy_lengths и conf->proxy_values будут ссылаться на данные, необходимые для вычисления фактического адреса перенаправления. Проверка условия if ($arg_x = '') в простом запросе без аргумента x дает положительный результат и nginx заменяет конфигурацию уровня локации на новую. Новая конфигурация была создана еще во время чтения конфигурационного файла и ее состав полностью зависит от механизма наследования конфигурации уровня локации. Этот механизм реализован в функции ngx_http_proxy_merge_loc_conf(), нас интересует следующий фрагмент:
    if (conf->upstream.upstream == NULL) {
        conf->upstream.upstream = prev->upstream.upstream;
        conf->vars = prev->vars;
    }

    if (conf->proxy_lengths == NULL) {
        conf->proxy_lengths = prev->proxy_lengths;
        conf->proxy_values = prev->proxy_values;
    }
То есть в случае, если proxy_pass внутри условия if не содержит дословного адреса, то он будет унаследован сверху. И в случае отсутствия переменного адреса, тот тоже будет унаследован сверху. Наш proxy_pass внутри if содержит дословный адрес, при этом он также унаследует переменный адрес http://$arg_x. В итоге получаем возможность задействовать оба механизма обработки адреса одновременно, что само по себе создает неопределенность! А если взглянуть на реализацию ngx_http_proxy_handler(), то выяснится, что эта неопределенность разрешается в пользу переменного адреса (там, где вызывается ngx_http_proxy_eval()). Вот почему мы получаем ошибку 500: несмотря на то, что директива proxy_pass внутри if получила новый дословный адрес, она также унаследовала переменный адрес сверху, который модуль прокси рассматривает как основной и, поскольку аргумент x не задан, выражение http://$arg_x вычисляется как http:// и nginx не в состоянии выполнить такое перенаправление. Теперь, когда мы знаем врага в лицо, нужно попробовать избежать его воздействия. Давайте не будем наследовать настройки proxy_pass сверху, пусть они настраиваются на одном уровне в разных блоках if.
        location /1 {
            if ($arg_x = '') {
                proxy_pass http://127.0.0.1:8010;
                break;
            }
            if ($arg_x) {
                proxy_pass http://$arg_x;
                break;
            }
            return 500;
        }
Такая конфигурация, несмотря на всю ее подозрительную странность, сработает: будет выбрана одна из директив proxy_pass в зависимости от присутствия в запросе аргумента x, при этом директива return 500 никогда не сработает, поскольку два условия для if сделаны намеренно взаимоисключающими. Проверяем.
curl 'http://localhost/1'
I am in backend
curl 'http://localhost/1?x=127.0.0.1:8010'
I am in backend
Другой способ предусматривает использование директивы rewrite.
        location /2 {
            if ($arg_x) {
                rewrite ^ /3 last;
            }
            proxy_pass http://127.0.0.1:8010;
        }
        location /3 {
            internal;
            proxy_pass http://$arg_x;
        }
В разных локациях разные настройки proxy_pass: никакого наследования конфигураций между ними быть не может. Данный способ, хотя и будет работать в нашем случае, не совсем верный по сути. Проблема в том, что если на сервер proxy придет запрос вида /2?x=127.0.0.1:8010, то он будет передан на сервер backend в виде /3?x=127.0.0.1:8010, поскольку proxy_pass в location /3 будет использовать измененный URI (кстати, до версии nginx 1.1.12 в этом случае использовался оригинальный URI, затем это было исправлено как баг, хотя я бы не стал называть это багом, поскольку всегда рассматривал это как фичу, при этом весьма удобную). Итак, окончательный вариант, в котором исходный URI сохраняется.
        location /2 {
            if ($arg_x) {
                rewrite ^(.*)$ /3$1$is_args$args last;
            }
            proxy_pass http://127.0.0.1:8010;
        }
        location ~* /3(.*) {
            internal;
            proxy_pass http://$arg_x$1;
        }
Проверяем.
curl 'http://localhost/2'
I am in backend
curl 'http://localhost/2?x=127.0.0.1:8010'
I am in backend
И еще один вариант, максимально приближенный к оригинальному. Что если создать переменную, в которую записать дефолтный адрес перенаправления, и ссылаться на нее в директиве proxy_pass внутри блока if?
        location /4 {
            set $default_addr 127.0.0.1:8010;
            if ($arg_x = '') {
                proxy_pass http://$default_addr;
                break;
            }
            proxy_pass http://$arg_x;
        }
Это должно сработать, поскольку наследование конфигурации в случае выполнения условия в if будет сводиться к замене выражения для вычисления адреса перенаправления.
curl 'http://localhost/4'
I am in backend
curl 'http://localhost/4?x=127.0.0.1:8010'
I am in backend
Работает, что и требовалось доказать. Update. Описанное здесь поведение было признано багом и исправлено в основной ветке разработки nginx.

четверг, 24 апреля 2014 г.

nginx: эмуляция сложных операций в условии if с помощью регулярных выражений

Это продолжение статьи об эмуляции вложенных if в nginx с помощью регулярных выражений. На этот раз будем решать другую задачу: проверим, находится ли значение какого-нибудь объекта, например куки a, в некотором заданном списке. Пусть в этом списке, назовем его a_list, находятся значения, разделенные точкой с запятой. При равенстве куки a одному из значений в списке будем выполнять специальное действие, для простоты — выводить строку PASSED, в противном случае — выводить строку FAILED. Вот конфигурация nginx.
events {
    worker_connections  1024;
}

http {
    server {
        listen       80;
        server_name  localhost;

        set $a_list " foo; bar ;  1qwerty;a";

        location / {
            set $check_a $a_list::$cookie_a;
            if ($check_a ~* "(?:[^;]+\s*;\s*)*(?<=;|^)\s*([^;]+)\s*(?=;|::).*::\1$") {
                echo "PASSED";
                break;
            }
            echo "FAILED";
        }
    }
}
Я намеренно расставил разное количество пробелов вокруг точек с запятыми в списке a_list, чтобы показать, что это не будет являться проблемой при правильно составленном регулярном выражении в условии if. Как и в предыдущей статье, здесь создается проверочная переменная check_a, состоящая из двух частей — списка a_list и значения куки a, разделенных двумя двоеточиями. Внутри регулярного выражения в условии if два двоеточия соответствуют этому разделению двух переменных. В правой части выражения, которая соответствует значению куки a, находится обратная ссылка \1 на значение, выделенное круглыми скобками в левой части — в них мы ожидаем одно из значений из списка a_list. Таким образом, если значение куки a совпадет с одним из значений в списке a_list, то переменная check_a будет соответствовать этому регулярному выражению и условие if окажется верным. Самое сложное здесь — это составить выражение для левой части регулярного выражения в условии if. Его центральная часть — атом ([^;]+), который будет соответствовать обратной ссылке \1 из правой части. Этот атом может заканчиваться некоторым количеством пробельных символов (\s*) и точкой с запятой, либо двумя двоеточиями ((?=;|::)), если значение справа окажется равным последнему элементу из списка a_list. Перед центральным атомом могут находиться другие элементы, разделенные точкой с запятой ((?:[^;]+\s*;\s*)*), которые нас не интересуют, а также некоторое количество пробельных символов (\s*). Важно проверить, что перед центральным атомом в левой части, включая возможные пробельные символы вначале, стоит точка с запятой, либо это начало строки ((?<=;|^)), иначе хвосты элементов из списка a_list, такие как oo для foo или даже y для 1qwerty приведут к срабатыванию всего регулярного выражения. Давайте проверим конфигурацию.
curl -b 'a=foo' 'http://localhost:80/'
PASSED
curl -b 'a=oo' 'http://localhost:80/'
FAILED
curl -b 'a=foo1' 'http://localhost:80/'
FAILED
curl -b 'a=a' 'http://localhost:80/'
PASSED
curl -b 'a=' 'http://localhost:80/'
FAILED
curl -b 'a=qwert' 'http://localhost:80/'
FAILED
curl -b 'a=1qwerty' 'http://localhost:80/'
PASSED
Работает! А теперь усложним задачу. Будем сравнивать не на точное соответствие значения переменной справа, а на присутствие в ней слова из списка a_list. Это еще не всё. Пусть при этом значение некоторой другой переменной (например, cookie_b) будет равно SUCCESS — опять эмуляция вложенных if! Где это можно применить? Подставьте вместо cookie_b имя ssl_client_verify, а вместо cookie_a — имя ssl_client_i_dn. Получаем проверку клиентского SSL сертификата с дополнительной проверкой того, что SSL issuer входит в заготовленный нами список a_list (теперь мы можем назвать этот список более осмысленно, например valid_ssl_issuers). Итак, добавим в нашу конфигурацию новый локейшн.
        location /2 {
            set $check_a $cookie_b::$a_list::$cookie_a;
            if ($check_a ~* "^SUCCESS::(?:[^;]+\s*;\s*)*(?<=;|::)\s*([^;]+)\s*(?=;|::).*::.*\b\1(?:\b|$)") {
                echo "PASSED";
                break;
            }
            echo "FAILED";
        }
Что изменилось? Переменная check_a теперь состоит из трех частей — первой частью является значение куки b, которое мы будем проверять на соответствие значению SUCCESS. Оставшиеся две части, как и раньше — элементы списка a_list и значение куки a. Соответственно изменилось регулярное выражение в условии if. Теперь оно начинается с SUCCESS::. Вторая часть, за исключением look-behind атома (?<=;|::), который теперь проверяет, что центральный атом центральной части выражения начинается с точки с запятой либо с двух двоеточий, возможно дополненных пробельными символами, не изменилась. Наконец, последняя часть, которая соответствует значению куки a, претерпела небольшие изменения — в ней проверяется, что где-либо внутри значения куки a находится какое-либо слово из списка a_list (.*\b\1(?:\b|$)). Проверяем.
curl -b 'b=SUCCESS; a=foo' 'http://localhost:80/2'
PASSED
curl -b 'b=SUCCES; a=foo' 'http://localhost:80/2'
FAILED
curl -b 'b=SUCCESS; a=rty' 'http://localhost:80/2'
FAILED
curl -b 'b=SUCCESS; a=1qwerty' 'http://localhost:80/2'
PASSED
curl -b 'b=SUCCESS; a=--1qwerty' 'http://localhost:80/2'
PASSED
curl -b 'b=SUCCESS; a=7-1qwerty:7' 'http://localhost:80/2'
PASSED
Все правильно. Напомню, что символы - и : являются границами слова, поэтому два последних случая удовлетворяют нашему регулярному выражению.

воскресенье, 2 февраля 2014 г.

nginx: почему if уровня server почти бесполезен при разработке модуля

Давайте посмотрим, как работает следующая конфигурация nginx.
worker_processes  1;
error_log  logs/error.log  notice; 

events {
    worker_connections  1024;
}

http {
    server {
        listen       80;
        server_name  localhost;

        if ($arg_a) {
            rewrite_log on;
            rewrite ^(.*)$ /catch$1;
        }

        location /catch {
            internal;
            rewrite ^/catch(.*)$ $1 last;
        }

        location /test {
            return 403;
        }
    }
}
Логика здесь простая: если в URI присутствует аргумент a, то мы хотим увидеть в логе трейс всех реврайтов (rewrite_log on). Сделаем запрос без аргумента a.
$ curl 'http://localhost/test'
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.5.9</center>
</body>
</html>
Ожидаемый ответ 403, ожидаемое отсутствие записей в логе. А теперь добавим в URI аргумент a.
$ curl 'http://localhost/test?a=1'
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.5.9</center>
</body>
</html>
Всё то же самое, в том числе и отсутствие записей в логе. Но последний факт - это уже неожиданность, ведь мы установили флаг rewrite_log внутри серверного if ($arg_a).

В чем же проблема? Давайте взглянем на определение директивы rewrite_log в исходном коде модуля rewrite в файле src/http/modules/ngx_http_rewrite_module.c.
    { ngx_string("rewrite_log"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_SIF_CONF|NGX_HTTP_LOC_CONF
                        |NGX_HTTP_LIF_CONF|NGX_CONF_FLAG,
      ngx_conf_set_flag_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_rewrite_loc_conf_t, log),
      NULL },
Из определения видно, что директива устанавливает флаг log, который определен в конфигурации уровня location. А теперь посмотрим, что делает директива if с конфигурацией уровня location (в том же файле в функции ngx_http_rewrite_if()).
    if (pclcf->name.len == 0) {
        if_code->loc_conf = NULL;
        cf->cmd_type = NGX_HTTP_SIF_CONF;

    } else {
        if_code->loc_conf = ctx->loc_conf;
        cf->cmd_type = NGX_HTTP_LIF_CONF;
    }
В случае, если директива if находится на уровне server, контекст уровня location отсутствует! Это объясняет, почему директивы, настраивающие данные конфигурации уровня location, такие как rewrite_log в модуле rewrite, не работают в серверном if. С другой стороны, директивы типа set и rewrite из того же модуля будут прекрасно работать внутри серверного if, поскольку они не связаны с изменением данных внутри конфигурации уровня location, а создают глобальные объекты для дальнейшей интерпретации во время исполнения программы.

Давайте посмотрим, что было бы, если бы флаг log был объявлен внутри конфигурации уровня server. В модуле rewrite нет конфигурации уровня server, мы ее создадим... В общем, я просто приведу патч относительно версии nginx 1.5.9.
--- src/http/modules/ngx_http_rewrite_module.c 2014-02-02 19:52:13.954829708 +0400
+++ src/http/modules/ngx_http_rewrite_module.c.new 2014-02-02 19:15:00.060722697 +0400
@@ -11,15 +11,22 @@
 
 
 typedef struct {
+    ngx_flag_t    log;
+} ngx_http_rewrite_srv_conf_t;
+
+
+typedef struct {
     ngx_array_t  *codes;        /* uintptr_t */
 
     ngx_uint_t    stack_size;
 
-    ngx_flag_t    log;
     ngx_flag_t    uninitialized_variable_warn;
 } ngx_http_rewrite_loc_conf_t;
 
 
+static void *ngx_http_rewrite_create_srv_conf(ngx_conf_t *cf);
+static char *ngx_http_rewrite_merge_srv_conf(ngx_conf_t *cf,
+    void *parent, void *child);
 static void *ngx_http_rewrite_create_loc_conf(ngx_conf_t *cf);
 static char *ngx_http_rewrite_merge_loc_conf(ngx_conf_t *cf,
     void *parent, void *child);
@@ -86,8 +93,8 @@
       NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_SIF_CONF|NGX_HTTP_LOC_CONF
                         |NGX_HTTP_LIF_CONF|NGX_CONF_FLAG,
       ngx_conf_set_flag_slot,
-      NGX_HTTP_LOC_CONF_OFFSET,
-      offsetof(ngx_http_rewrite_loc_conf_t, log),
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_rewrite_srv_conf_t, log),
       NULL },
 
     { ngx_string("uninitialized_variable_warn"),
@@ -109,8 +116,8 @@
     NULL,                                  /* create main configuration */
     NULL,                                  /* init main configuration */
 
-    NULL,                                  /* create server configuration */
-    NULL,                                  /* merge server configuration */
+    ngx_http_rewrite_create_srv_conf,      /* create server configuration */
+    ngx_http_rewrite_merge_srv_conf,       /* merge server configuration */
 
     ngx_http_rewrite_create_loc_conf,      /* create location configuration */
     ngx_http_rewrite_merge_loc_conf        /* merge location configuration */
@@ -142,6 +149,7 @@
     ngx_http_core_srv_conf_t     *cscf;
     ngx_http_core_main_conf_t    *cmcf;
     ngx_http_rewrite_loc_conf_t  *rlcf;
+    ngx_http_rewrite_srv_conf_t  *rscf;
 
     cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
     cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
@@ -169,10 +177,12 @@
         return NGX_HTTP_INTERNAL_SERVER_ERROR;
     }
 
+    rscf = ngx_http_get_module_srv_conf(r, ngx_http_rewrite_module);
+
     e->ip = rlcf->codes->elts;
     e->request = r;
     e->quote = 1;
-    e->log = rlcf->log;
+    e->log = rscf->log;
     e->status = NGX_DECLINED;
 
     while (*(uintptr_t *) e->ip) {
@@ -227,6 +237,34 @@
 
 
 static void *
+ngx_http_rewrite_create_srv_conf(ngx_conf_t *cf)
+{
+    ngx_http_rewrite_srv_conf_t  *conf;
+
+    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_rewrite_srv_conf_t));
+    if (conf == NULL) {
+        return NULL;
+    }
+
+    conf->log = NGX_CONF_UNSET;
+
+    return conf;
+}
+
+
+static char *
+ngx_http_rewrite_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
+{
+    ngx_http_rewrite_srv_conf_t *prev = parent;
+    ngx_http_rewrite_srv_conf_t *conf = child;
+
+    ngx_conf_merge_value(conf->log, prev->log, 0);
+
+    return NGX_CONF_OK;
+}
+
+
+static void *
 ngx_http_rewrite_create_loc_conf(ngx_conf_t *cf)
 {
     ngx_http_rewrite_loc_conf_t  *conf;
@@ -237,7 +275,6 @@
     }
 
     conf->stack_size = NGX_CONF_UNSET_UINT;
-    conf->log = NGX_CONF_UNSET;
     conf->uninitialized_variable_warn = NGX_CONF_UNSET;
 
     return conf;
@@ -252,7 +289,6 @@
 
     uintptr_t  *code;
 
-    ngx_conf_merge_value(conf->log, prev->log, 0);
     ngx_conf_merge_value(conf->uninitialized_variable_warn,
                          prev->uninitialized_variable_warn, 1);
     ngx_conf_merge_uint_value(conf->stack_size, prev->stack_size, 10);
В новой экспериментальной версии модуля rewrite мы перенесли флаг log из конфигурации уровня location в специально созданную конфигурацию уровня server, а также объявили новые хендлеры для создания и мерджа конфигураций уровня server, в которые перенесли все манипуляции с флагом log из соответствующих хендлеров конфигураций уровня location.

Компилируем nginx, перезапускаем сервер и посылаем те же тестовые запросы. Оба запроса возвращают ожидаемый 403 Forbidden, но теперь в случае, когда в URI запроса присутствует аргумент а, мы получаем следующую запись в логе:
2014/02/02 20:46:05 [notice] 7345#0: *2 "^(.*)$" matches "/test", client: 127.0.0.1, server: localhost, request: "GET /test?a=1 HTTP/1.1", host: "localhost"
2014/02/02 20:46:05 [notice] 7345#0: *2 rewritten data: "/catch/test", args: "a=1", client: 127.0.0.1, server: localhost, request: "GET /test?a=1 HTTP/1.1", host: "localhost"
2014/02/02 20:46:05 [notice] 7345#0: *2 "^/catch(.*)$" matches "/catch/test", client: 127.0.0.1, server: localhost, request: "GET /test?a=1 HTTP/1.1", host: "localhost"
2014/02/02 20:46:05 [notice] 7345#0: *2 rewritten data: "/test", args: "a=1", client: 127.0.0.1, server: localhost, request: "GET /test?a=1 HTTP/1.1", host: "localhost"
Здорово, мы добились того, к чему стремились! Однако потеряли несравненно большее. Теперь мы не сможем гибко настраивать флаг rewrite_log для каждого отдельного локейшна. Сервер просто не запустится, если этот флаг будет упомянут в одном блоке server более одного раза.

Давайте подытожим. Использование директивы, настраивающей данные в контексте location внутри директивы if уровня server бесполезно. Если бы это работало, то было бы несомненно полезно тем, что позволило бы записывать конфигурационный файл nginx в более выразительном и лаконичном стиле. С другой стороны, выразительный стиль все же доступен, если директива настраивает данные уровня server, однако в этом случае совершенно теряется возможность гибко настраивать конфигурацию для отдельных локейшнов сервера.

Я столкнулся с подобной проблемой при разработке кастомного модуля nginx. И мне пришлось пожертвовать гибкостью настройки внутри отдельных локейшнов ради возможности вызывать директиву из серверного if: в моем случае это казалось более обещающим, чем гибкость настройки. Естественно, перед тем как принять решение, я попытался изучить, как работают с серверным if модули, поставляемые вместе с nginx. Результат сначала показался удивительным: кроме модуля rewrite ни один стандартный модуль не работал с серверным if. Чтобы убедиться в этом, достаточно ввести команду
grep -r NGX_HTTP_SIF_CONF src/
внутри директории с исходниками nginx. Однако, если учесть только что описанный недостаток серверного if, то это уже не выглядит столь странно.