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; ++i ) { 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; ++i ) { 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 ; ++i ) { 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. Если следовать этому простому правилу, можно избежать подобных неприятностей.