воскресенье, 25 марта 2012 г.

nginx: переменные с дефисом (тире) в названии, часть II

О проблеме переменных с дефисом в названии я рассказывал в предыдущем сообщении. Здесь мы напишем наш собственный модуль для решения этой проблемы. Учитывая, что задача имеет весьма общий характер, мы назовем наш модуль myutil в предположении, что в дальнейшем мы сможем расширять этот модуль для решения других подобных задач. Ну а пока myutil будет решать только одну задачу, предоставляя директиву
            myutil_var_alias $dst $src;
Директива myutil_var_alias ожидает ровно два аргумента: переменную-источник ($src), и переменную-назначение ($dst), и решает очень простую задачу: создает новую переменную $dst и копирует в нее значение существующей переменной $src. Фактически наша директива решает более общую задачу, чем та, которую мы определили сначала, поскольку никаких ограничений на имя переменной-источника мы не накладываем. В частности, она решает также и нашу проблему, если имя переменной-источника содержит символ '-'.

Определим некоторые ограничения для myutil_var_alias. Пусть областью ее применения являются только блок location и блок if внутри location. Это ограничение упростит реализацию модуля, но оно не является принципиальным.

А теперь приступим к реализации модуля. Общую информацию о построении модулей nginx я приводил здесь, поэтому не буду говорить о назначении файла config и переменных типов ngx_command_t, ngx_http_module_t и ngx_module_t в исходном файле модуля.

Это содержимое файла config:
ngx_addon_name=ngx_http_myutil_module
HTTP_MODULES="$HTTP_MODULES $ngx_addon_name"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_myutil_module.c"
Это начало исходного файла ngx_http_myutil_module.c:
#include <ngx_core.h>
#include <ngx_http.h>

typedef struct
{
    ngx_array_t  varalias_data;
}  ngx_http_myutil_loc_conf_t;

typedef struct
{
    ngx_int_t    self;
    ngx_int_t    index;
}  ngx_http_myutil_var_elem_t;

static void *  ngx_http_myutil_create_loc_conf( ngx_conf_t *  cf );
static char *  ngx_http_myutil_merge_loc_conf( ngx_conf_t *  cf, void *  parent,
                                               void *  child );
static char *  ngx_http_myutil_var_alias( ngx_conf_t *  cf,
                                          ngx_command_t *  cmd, void *  conf );

static ngx_command_t  ngx_http_myutil_commands[] =
{
    {
        ngx_string( "myutil_var_alias" ),
        NGX_HTTP_LOC_CONF | NGX_HTTP_LIF_CONF | NGX_CONF_TAKE2,
        ngx_http_myutil_var_alias,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    },

    ngx_null_command
};

static ngx_http_module_t  ngx_http_myutil_module_ctx = {
    NULL,                                  /* preconfiguration */
    NULL,                                  /* postconfiguration */

    NULL,                                  /* create main configuration */
    NULL,                                  /* init main configuration */

    NULL,                                  /* create server configuration */
    NULL,                                  /* merge server configuration */

    ngx_http_myutil_create_loc_conf,       /* create location configuration */
    ngx_http_myutil_merge_loc_conf         /* merge location configuration */
};

ngx_module_t  ngx_http_myutil_module = {
    NGX_MODULE_V1,
    &ngx_http_myutil_module_ctx,           /* module context */
    ngx_http_myutil_commands,              /* module directives */
    NGX_HTTP_MODULE,                       /* module type */
    NULL,                                  /* init master */
    NULL,                                  /* init module */
    NULL,                                  /* init process */
    NULL,                                  /* init thread */
    NULL,                                  /* exit thread */
    NULL,                                  /* exit process */
    NULL,                                  /* exit master */
    NGX_MODULE_V1_PADDING
};
Наш модуль будет хранить конфигурационные данные уровня location (тип ngx_http_myutil_loc_conf_t), в то время как данные уровня http и server отсутствуют - это потому, что мы упростили себе задачу, ограничив применение директивы myutil_var_alias только двумя блоками location и if-location. Конфигурационные данные уровня location содержат массив varalias_data, элементами которого будут являться объекты типа ngx_http_myutil_var_elem_t. Этот тип предназначен для хранения двух индексов переменных - переменной $dst (поле self) и переменной $src (поле index). Массив нам нужен для того, чтобы была возможность использовать несколько директив myutil_var_alias внутри одного location.

В переменной ngx_http_myutil_module_ctx мы определили только два ненулевых хендлера конфигурации: ngx_http_myutil_create_loc_conf() и ngx_http_myutil_merge_loc_conf(). Первая функция отвечает за создание конфигурации уровня location, а также инициализирует массив varalias_data. Вторая функция определяет стратегию использования родительских данных конфигурации уровня location при переходе к дочерней конфигурации (слияние данных конфигураций происходит в контексте запроса при положительном значении условия в блоке if). Наша стратегия будет заключаться в копировании родительских данных в дочернюю конфигурацию.
static void *  ngx_http_myutil_create_loc_conf( ngx_conf_t *  cf )
{
    ngx_http_myutil_loc_conf_t *  lcf = NULL;

    lcf = ngx_pcalloc( cf->pool, sizeof( ngx_http_myutil_loc_conf_t ) );

    if ( ! lcf )
        return NULL;

    if ( ngx_array_init( &lcf->varalias_data, cf->pool, 1,
                         sizeof( ngx_http_myutil_var_elem_t ) ) != NGX_OK )
        return NULL;

    return lcf;
}


static char *  ngx_http_myutil_merge_loc_conf( ngx_conf_t *  cf, void *  parent,
                                               void *  child )
{
    ngx_http_myutil_loc_conf_t *  prev = parent;
    ngx_http_myutil_loc_conf_t *  conf = child;

    ngx_uint_t                    i;

    for ( i = 0; i < prev->varalias_data.nelts; ++)
    {
        ngx_http_myutil_var_elem_t *  elem = NULL;

        elem = ngx_array_push( &conf->varalias_data );

        if ( ! elem )
            return NGX_CONF_ERROR;

        *elem = ( ( ngx_http_myutil_var_elem_t * )
                                        prev->varalias_data.elts )[ i ];
    }

    return NGX_CONF_OK;
}
Так выглядит функция ngx_http_myutil_var_alias():
static char *  ngx_http_myutil_var_alias( ngx_conf_t *  cf,
                                          ngx_command_t *  cmd, void *  conf )
{
    ngx_str_t *                   value     = cf->args->elts;
    ngx_http_variable_t *         v         = NULL;
    ngx_http_myutil_loc_conf_t *  lcf       = conf;
    ngx_int_t                     index     = NGX_ERROR;
    ngx_int_t                     v_idx     = NGX_ERROR;
    ngx_http_myutil_var_elem_t *  res       = NULL;
    ngx_uint_t *                  v_idx_ptr = NULL;

    ngx_uint_t                    i;

    for ( i = 1; i < 3; ++)
    {
        if ( value[ i ].data[ 0 ] != '$' )
        {
            ngx_conf_log_error( NGX_LOG_EMERG, cf, 0,
                                "%V: invalid variable name '%V'",
                                &cmd->name, &value[ i ] );
            return NGX_CONF_ERROR;
        }

        value[ i ].len--;
        value[ i ].data++;
    }

    index = ngx_http_get_variable_index( cf, &value[ 2 ] );

    if ( index == NGX_ERROR )
        return NGX_CONF_ERROR;

    v = ngx_http_add_variable( cf, &value[ 1 ], NGX_HTTP_VAR_CHANGEABLE );

    if ( v == NULL )
        return NGX_CONF_ERROR;

    v_idx = ngx_http_get_variable_index( cf, &value[ 1 ] );

    if ( v_idx == NGX_ERROR )
        return NGX_CONF_ERROR;

    res = ngx_array_push( &lcf->varalias_data );

    if ( ! res )
        return NGX_CONF_ERROR;

    res->self  = v_idx;
    res->index = index;

    v_idx_ptr = ngx_pnalloc( cf->pool, sizeof( ngx_uint_t ) );

    if ( ! v_idx_ptr )
        return NGX_CONF_ERROR;

    *v_idx_ptr     = v_idx;
    v->data        = ( uintptr_t )v_idx_ptr;
    v->get_handler = ngx_http_myutil_get_var_alias_value;

    return NGX_CONF_OK;
}
Переменная value - это список аргументов директивы myutil_var_alias, начиная с индекса 1, value[ 0 ] - это имя самой директивы. Соответственно, в цикле for мы просто проверяем, что оба аргумента директивы начинаются с символа $, если это не так - то бракуем конфигурацию с сообщением об ошибке. Далее получаем индекс переменной-источника, регистрируем переменную-назначение (v), и получаем ее индекс, создаем объект res типа ngx_http_myutil_var_elem_t, присваиваем его полям полученные индексы и заталкиваем его в массив varalias_data. В самом конце определяем два важных атрибута переменной v: data, в который помещаем ее индекс (это понадобится для поиска нужного элемента в массиве varalias_data при вызове get-хендлера этой переменной), а также собственно get_handler переменной v - указатель на функцию ngx_http_myutil_get_var_alias_value(). Get-хендлер будет вызываться всякий раз, когда nginx понадобится получить значение этой переменной, собственно, эта функция предназначена для присваивания значения переменной. Вот ее код:
static ngx_int_t  ngx_http_myutil_get_var_alias_value( ngx_http_request_t *  r,
                            ngx_http_variable_value_t *  v, uintptr_t  data )
{
    ngx_http_myutil_loc_conf_t *  lcf     = NULL;
    ngx_http_variable_value_t *   var     = NULL;
    ngx_array_t *                 vaarray = NULL;
    ngx_http_myutil_var_elem_t *  vavar   = NULL;
    ngx_int_t *                   index   = NULL;

    ngx_uint_t                    i;

    if ( ! data )
        return NGX_ERROR;

    lcf = ngx_http_get_module_loc_conf( r, ngx_http_myutil_module );

    vaarray = &lcf->varalias_data;
    vavar   = vaarray->elts;
    index   = ( ngx_int_t * )data;

    for ( i = 0; i < vaarray->nelts ; ++)
    {
        ngx_http_variable_value_t *  found = NULL;

        if ( *index != vavar[ i ].self )
            continue;

        found = ngx_http_get_indexed_variable( r, vavar[ i ].index );

        if ( found && found->len > 0 )
        {
            var = found;
            break;
        }
    }

    if ( ! var )
        return NGX_ERROR;

    v->len          = var->len;
    v->data         = var->data;
    v->valid        = 1;
    v->no_cacheable = 0;
    v->not_found    = 0;

    return NGX_OK;
}
Тут вообще все просто. В цикле for ищем элемент массива varalias_data, значение поля self которого совпадает со значением, переданным в третьем аргументе функции data, затем ищем переменную, индекс которой соответствует значению поля index найденного элемента. Полям данных переменной v, переданной в функцию в качестве второго аргумента, присваиваем длину данных найденной переменной (var->len) и собственно указатель на данные (var->data), а также дежурные значения для полей valid, no_cacheable и not_found.

Вот собственно и все. Собираем модуль и проверяем его работу на следующей конфигурации:
events {
    worker_connections  1024;
}

http {
    map $arg_some-var $map_arg_some_var {
        ~(?<match>.*)   $match;
    }

    server {
        listen          8010;
        server_name     router;

        location /test_var_alias.html {
            set $some_var $arg_some-var;
            myutil_var_alias $va_some_var $arg_some-var;
            echo "value: '$arg_some-var'";
            echo "set: '$some_var', map: '$map_arg_some_var'";
            echo "alias: '$va_some_var'";
        }
    }
}
Проверяем:
$ curl 'http://localhost:8010/test_var_alias.html?some-var=a'
value: '-var'
set: '-var', map: '-var'
alias: 'a'
Работает! А прямой доступ через echo, а также set и map по-прежнему возвращают ерунду.

Надо сказать еще об одном важном моменте. Переменные конфигурации nginx имеют глобальную область видимости. Это значит, что если мы объявим переменную $va_some_var в каком-нибудь ином локейшне, или на более высоком уровне, или даже в другом виртуальном сервере в рамках одной и той же конфигурации, то это будет все равно одна и та же переменная. То, что ее значение привязано к конкретному контексту (в нашем случае - локейшну) - это полностью заслуга ее get_handler, который привязывает данные переменной к данным конкретного локейшна (в нашем случае эти данные находятся внутри конфигурации уровня location в массиве varalias_data). Что из этого следует? То, что данные, жестко связанные с переменной, не должны изменяться в процессе парсинга конфигурации. Мы используем два таких жестко связанных с переменной типа данных - это контекст, передаваемый в качестве третьего аргумента в get_handler переменной (мы передаем туда ее индекс), и собственно сам get_handler - указатель на конкретную функцию. Понятно, что индекс переменной не изменится в процессе чтения конфигурации ни разу - за этим проследит сам nginx. А вот get_handler измениться может! Представьте, что у вас есть директива (из нашего же модуля или какого-нибудь другого), которая тоже объявляет переменную и назначает ей другой хендлер, и вы применили ее с использованием того же имени переменной где-нибудь ниже по тексту конфигурации (вы то уверены, что эти две переменные, несмотря на то, что имеют одно и то же имя, никак не связаны). В этом случае произойдет неожиданная неприятность - хендлер переменной, объявленной первой директивой перезапишется! И вы будете получать в качестве ее значения совсем не тот результат! Выход из такой ситуации простой - не используйте одинаковые имена переменных в директивах, которые привязывают к этим переменным разные хендлеры. Самое логичное - это выбрать какой-нибудь специальный префикс для переменных, связанных с определенным типом директив. В нашем случае для переменных, объявленных директивой myutil_var_alias я использовал префикс va_, подразумевая расшифровку Var Alias. Если следовать этому простому правилу, можно избежать подобных неприятностей.

суббота, 24 марта 2012 г.

nginx: переменные с дефисом (тире) в названии, часть I

Недавно столкнулся с занимательной проблемой в nginx: как получить значение переменной с символом '-' в названии в файле конфигурации. Такими переменными могут легко оказаться какие-нибудь аргументы строки запроса (например, $arg_some-var) или имя куки (например, $cookie_some-cookie). Скажем, мы ожидаем, что в аргументе some-var находится имя апстрима, на который нужно переправить запрос (немного надуманно, но почему бы нет), и наша простейшая тестовая конфигурация выглядит так:
events {
    worker_connections  1024;
}

http {
    upstream ubackend {
        server localhost:8020;
    }

    server {
        listen          8010;
        server_name     router;

        location /test_dash_variable.html {
            proxy_pass http://$arg_some-var;
            echo_after_body "Hi! Value is '$arg_some-var'";
        }
    }

    server {
        listen          8020;
        server_name     backend;

        location /test_dash_variable.html {
            echo "In backend!";
        }
    }
}
Видите, даже подсветке кода не нравится имя $arg_some-var. Что же произойдет, если мы направим запрос с аргументом some-var, равным ubackend? Мы ожидаем попасть в бэкенд, а вот что произойдет на самом деле:
$ curl 'http://localhost:8010/test_dash_variable.html?some-var=ubackend'
<html>
<head><title>502 Bad Gateway</title></head>
<body bgcolor="white">
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.0.8</center>
</body>
</html>
Hi! Value is '-var'
Получили 502 и какое-то неожиданное значение переменной $arg_some-var, равное -var. Загадка имеет простое объяснение: выражение справа от директивы proxy_pass обрабатывается внутренним парсером выражений nginx, для которого символ '-' является своего рода лексемой. Этот внутренний парсер разбирает выражение $arg_some-var как значение-переменной-$arg_some -- символ '-' -- строка 'var'. Переменная $arg_some не определена, соответственно значением данного выражения будет -var. Эта проблема поднималась раньше и был предложен простой патч (см. здесь), в котором символу '-' было разрешено быть частью имен переменных. Патч не был принят из опасения сломать обратную совместимость (я понял так).

Как можно обойти эту проблему? Правильный путь - воспользоваться директивой, которая не передает аргумент с переменной парсеру выражений, а берет имя переменной как есть. Директива if из модуля rewrite - отличный кандидат. Она рассматривает первый аргумент после открывающей круглой скобки как имя переменной. Мы проверим, как это работает, добавив директиву if в location /test_dash_variable.html сервера router:
        location /test_dash_variable.html {
            proxy_pass http://$arg_some-var;
            echo_after_body "Hi! Value is '$arg_some-var'";
            if ($arg_some-var = 'a') {
                echo "Bye! Value is '$arg_some-var'";
                break;
            }
        }
Теперь, если мы передадим значение a в аргументе some-var, мы должны будем попасть в обработчик внутри if, если if действительно работает так, как мы ожидаем:
$ curl 'http://localhost:8010/test_dash_variable.html?some-var=a'
Bye! Value is '-var'
Работает! Хотя echo по-прежнему настаивает, что значение $arg_some-var равно -var. Ничего удивительного, echo использует парсер выражений для получения значения своего аргумента. Ладно, if c одной стороны хорошо, но с другой стороны if - это зло! К тому же if не может отобразить бесконечное множество разных вариантов обработки переменных в конфигурации nginx. Нужно искать другие решения. Первое что приходит на ум - воспользоваться директивой set из того же модуля rewrite. После простого эксперимента выясняется, что set тоже пользуется парсером выражений (да и следовало ли ожидать другого?) и нам не подходит:
        location /test_dash_variable.html {
            set $some_var $arg_some-var;
            echo "Hi! Variable 'some_var' is '$some_var'";
        }
Проверяем:
$ curl 'http://localhost:8010/test_dash_variable.html?some-var=a'
Hi! Variable 'some_var' is '-var'
Все то же самое. Можно еще воспользоваться директивой map, казалось бы это то, что нам нужно: берет одну переменную и мапит ее на другую. Имя второй переменной мы выбираем сами, а содержание маппинга мы, с помощью регулярных выражений, настроим таким образом, чтобы мапились любые значения первой переменной в значения второй переменной один к одному. Вот соответствующая конфигурация:
events {
    worker_connections  1024;
}

http {
    map $arg_some-var $map_arg_some_var {
        ~(?<match>.*)   $match;
    }

    server {
        listen          8010;
        server_name     router;

        location /test_dash_variable.html {
            set $some_var $arg_some-var;
            echo "Hi! Variable 'map_arg_some_var' is '$map_arg_some_var'";
        }
    }
}
Проверяем:
$ curl 'http://localhost:8010/test_dash_variable.html?some-var=a'
Hi! Variable 'map_arg_some_var' is '-var'
Увы, ничего не получается. А все потому, что директива map мапит не переменную-в-переменную, а выражение-в-переменную. В роли выражения выступает наша переменная $arg_some-var, а что из этого следует, мы уже прекрасно знаем.

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