Здесь мы начали разрабатывать новый модуль nginx myutil, в котором, как мы задумывали, будут реализованы разные полезные директивы. На данный момент в модуле представлена единственная директива myutil_var_alias. В этой статье будет показано, как организовать поиск по списку переменных с целью получения первого непустого значения. Пусть соответствующая директива будет называться myutil_first_not_empty. Тогда строка в конфигурации вида
myutil_first_not_empty $fne_data $arg_a $arg_b default;
указывает, что следует найти первое непустое значение среди списка переменных $arg_a и $arg_b (которые соответствуют аргументам a и b строки запроса) и записать его в новую переменную $fne_data; в случае, если переменные $arg_a и $arg_b отсутствуют, либо не содержат непустых значений, в переменную $fne_data следует записать значение default. Будем предполагать, что литералы типа default являются равноправными членами наряду с переменными типа $arg_a и $arg_b и могут записываться где-то в середине списка аргументов (хотя очевидно, что из смысла директивы myutil_first_not_empty следует, что литерал должен быть только один и он должен находиться в конце списка). Сам список аргументов может быть произвольной длины.
Какие типы данных нам понадобятся? Ну, во-первых, как и в случае директивы myutil_var_alias, это массив типа ngx_array_t, соответствующий множеству переменных $fne_... (не забываем совет, приведенный в конце прошлой статьи и добавляем узнаваемый префикс, в данном случае $fne_, ко всем переменным, объявленным нашей новой директивой). Добавляем его в ngx_http_myutil_loc_conf_t:
typedef struct
{
ngx_array_t varalias_data;
ngx_array_t first_not_empty_var_data;
} ngx_http_myutil_loc_conf_t;
В директиве myutil_var_alias в качестве элементов массива varalias_data мы использовали простую структуру
typedef struct
{
ngx_int_t self;
ngx_int_t index;
} ngx_http_myutil_var_elem_t;
При этом значение self соответствовало индексу новой переменной, созданной директивой myutil_var_alias (ее первому аргументу), а значение index - исходной переменной, т.е. той, значение которой копировалось в новую переменную (это второй аргумент myutil_var_alias).
На этот раз мы имеем дело не с двумя аргументами, а с аргументом - новой переменной, создаваемой директивой myutil_first_not_empty, и списком неопределенной длины. Очевидно, что одним из элементов новой структуры должен быть массив:
typedef struct
{
ngx_array_t data;
ngx_int_t index;
} ngx_http_myutil_fne_elem_t;
Здесь data - указанный массив, а index - индекс новой переменной. Какой тип элементов следует выбрать для массива data? Вспомните, что мы хотели записывать в него как переменные, так и литералы. Переменная легко идентифицируется индексом, а литерал - строкой, при этом будем считать, что литерал тоже имеет индекс, равный ( ngx_uint_t )NGX_ERROR: это позволит понимать, с чем мы имеем дело (переменной или литералом) в хендлере ngx_http_myutil_get_first_not_empty_value(), в котором происходит присваивание нового значения переменной $fne_.... Итак, тип элемента из списка аргументов директивы myutil_first_not_empty имеет вид
typedef struct
{
ngx_str_t key;
ngx_int_t index;
} ngx_http_myutil_var_handle_t;
Добавляем директиву myutil_first_not_empty в список ngx_http_myutil_commands[], теперь он выглядит так:
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_string( "myutil_first_not_empty" ),
NGX_HTTP_LOC_CONF | NGX_HTTP_LIF_CONF | NGX_CONF_2MORE,
ngx_http_myutil_first_not_empty,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL
},
ngx_null_command
};
Также обновляем функции создания и слияния конфигураций уровня location:
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;
if ( ngx_array_init( &lcf->first_not_empty_var_data, cf->pool, 1,
sizeof( ngx_http_myutil_fne_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;
/* all data in next sections will be imported from parent location in
* addition to already collected data in child location */
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 ];
}
for ( i = 0; i < prev->first_not_empty_var_data.nelts; ++i )
{
ngx_http_myutil_fne_elem_t * elem = NULL;
elem = ngx_array_push( &conf->first_not_empty_var_data );
if ( ! elem )
return NGX_CONF_ERROR;
*elem = ( ( ngx_http_myutil_fne_elem_t * )
prev->first_not_empty_var_data.elts )[ i ];
}
return NGX_CONF_OK;
}
Хендлером конфигурации при регистрации директивы myutil_first_not_empty в списке ngx_http_myutil_commands[] была объявлена функция ngx_http_myutil_first_not_empty(), вот ее тело (строки пронумерованы для дальнейших комментариев):
367 static char * ngx_http_myutil_first_not_empty( ngx_conf_t * cf,
368 ngx_command_t * cmd, void * conf )
369 {
370 ngx_str_t * value = cf->args->elts;
371 ngx_http_variable_t * v = NULL;
372 ngx_http_myutil_loc_conf_t * lcf = conf;
373 ngx_int_t v_idx = NGX_ERROR;
374 ngx_uint_t * v_idx_ptr = NULL;
375 ngx_http_myutil_fne_elem_t * resvar = NULL;
376
377 ngx_uint_t i;
378
379 if ( value[ 1 ].data[ 0 ] != '$' )
380 {
381 ngx_conf_log_error( NGX_LOG_EMERG, cf, 0,
382 "invalid variable name '%V'", &value[ 1 ] );
383 return NGX_CONF_ERROR;
384 }
385
386 value[ 1 ].len--;
387 value[ 1 ].data++;
388
389 /* It must never occur due to command arguments number restriction */
390 if ( cf->args->nelts < 3 )
391 return NGX_CONF_ERROR;
392
393 resvar = ngx_array_push( &lcf->first_not_empty_var_data );
394
395 if ( ! resvar )
396 return NGX_CONF_ERROR;
397
398 if ( ngx_array_init( &resvar->data, cf->pool, cf->args->nelts - 2,
399 sizeof( ngx_http_myutil_var_handle_t ) ) != NGX_OK )
400 return NGX_CONF_ERROR;
401
402 for ( i = 2; i < cf->args->nelts; ++i )
403 {
404 ngx_http_myutil_var_handle_t * res = NULL;
405 ngx_int_t index = NGX_ERROR;
406
407 ngx_uint_t isvar = value[ i ].data[ 0 ] == '$' ? 1 : 0;
408
409 res = ngx_array_push( &resvar->data );
410
411 if ( ! res )
412 return NGX_CONF_ERROR;
413
414 if ( ! isvar )
415 {
416 res->index = NGX_ERROR;
417 res->key = value[ i ];
418 continue;
419 }
420
421 value[ i ].len--;
422 value[ i ].data++;
423
424 index = ngx_http_get_variable_index( cf, &value[ i ] );
425
426 if ( index == NGX_ERROR )
427 return NGX_CONF_ERROR;
428
429 res->index = index;
430 res->key = value[ i ];
431 }
432
433 v = ngx_http_add_variable( cf, &value[ 1 ], NGX_HTTP_VAR_CHANGEABLE );
434
435 if ( v == NULL )
436 return NGX_CONF_ERROR;
437
438 v_idx = ngx_http_get_variable_index( cf, &value[ 1 ] );
439
440 if ( v_idx == NGX_ERROR )
441 return NGX_CONF_ERROR;
442
443 v_idx_ptr = ngx_pnalloc( cf->pool, sizeof( ngx_uint_t ) );
444
445 if ( ! v_idx_ptr )
446 return NGX_CONF_ERROR;
447
448 resvar->index = v_idx;
449
450 *v_idx_ptr = v_idx;
451 v->data = ( uintptr_t )v_idx_ptr;
452 v->get_handler = ngx_http_myutil_get_first_not_empty_value;
453
454 return NGX_CONF_OK;
455 }
В строках 370-377 объявлены переменные, используемые внутри функции. В строках 379-384 проверяется, что первый аргумент директивы (переменная $fne_...) начинается с символа $. В строках 386-387 первый символ $ убирается из названия переменной (мы теперь и так знаем, что это переменная, и это нас устраивает). В строках 390-391 убеждаемся, что в директиву передано более одного аргумента (хотя эта проверка излишняя: флаг NGX_CONF_2MORE, указанный при объявлении директивы, не позволит запустить nginx, если в какой-либо из директив myutil_first_not_empty в конфигурации будет менее двух аргументов). В строках 393-400 мы создаем новый элемент массива first_not_empty_var_data из структуры ngx_http_myutil_loc_conf. На этом этапе мы знаем количество элементов внутри его подмассива data (это количество элементов директивы минус два), поэтому сразу же инициализируем data указанным значением элементов.
В строках 402-431 мы проходим по оставшимся аргументам директивы и заполняем инициализированные элементы массива data index и key значениями index и value[ i ] соответственно (строки 429-430). При этом, если текущий аргумент начинается с символа $ (т.е. является переменной), значение index получаем из возвращаемого значения функции ngx_http_get_variable_index(), а если это литерал - то записываем в качестве index значение ( ngx_uint_t )NGX_ERROR. В обоих случаях значением key будет имя текущего аргумента value[ i ] (хотя для аргумента-переменной это значение использоваться не будет).
В строках 433-441 создаем новую переменную v, соответствующую нашей переменной $fne_... и получаем ее индекс v_idx. Далее выделяем память под элемент типа ngx_uint_t (переменная v_idx_ptr): здесь будет храниться значение v_idx, а сам этот участок памяти будет передаваться в функцию ngx_http_myutul_get_first_not_empty_value() в качестве третьего аргумента для поиска нужного элемента в массиве first_not_empty_var_data (благодаря присваиванию resvar->index значения v_idx, а v->data - адреса v_idx_ptr в строках 448-451). В строке 452 присваиваем v->get_handler() адрес функции ngx_http_myutul_get_first_not_empty_value(), а затем возвращаем значение NGX_CONF_OK.
Хендлер ngx_http_myutil_get_first_not_empty_value() выглядит так:
225 static ngx_int_t ngx_http_myutil_get_first_not_empty_value(
226 ngx_http_request_t * r, ngx_http_variable_value_t * v,
227 uintptr_t data )
228 {
229 ngx_http_myutil_loc_conf_t * lcf = NULL;
230 ngx_http_variable_value_t * var = NULL;
231 ngx_array_t * fne_data = NULL;
232 ngx_http_myutil_fne_elem_t * fne_elts = NULL;
233 ngx_http_myutil_fne_elem_t * fne_elem = NULL;
234 ngx_array_t * exprarray = NULL;
235 ngx_http_myutil_var_handle_t * expr = NULL;
236 ngx_int_t * index = NULL;
237 ngx_uint_t found_index = ( ngx_uint_t )NGX_ERROR;
238
239 ngx_uint_t i;
240
241 if ( ! data )
242 return NGX_ERROR;
243
244 lcf = ngx_http_get_module_loc_conf( r, ngx_http_myutil_module );
245
246 fne_data = &lcf->first_not_empty_var_data;
247 fne_elts = fne_data->elts;
248 index = ( ngx_int_t * )data;
249
250 for ( i = 0; i < fne_data->nelts; ++i )
251 {
252 if ( *index != fne_elts[ i ].index )
253 continue;
254
255 found_index = i;
256 break;
257 }
258
259 if ( found_index == ( ngx_uint_t )NGX_ERROR )
260 return NGX_ERROR;
261
262 fne_elem = &fne_elts[ found_index ];
263
264 exprarray = &fne_elem->data;
265 expr = exprarray->elts;
266
267 for ( i = 0; i < exprarray->nelts; ++i )
268 {
269 ngx_http_variable_value_t * found = NULL;
270
271 if ( expr[ i ].index == NGX_ERROR )
272 {
273 v->len = expr[ i ].key.len;
274 v->data = expr[ i ].key.data;
275 v->valid = 1;
276 v->no_cacheable = 0;
277 v->not_found = 0;
278
279 return NGX_OK;
280 }
281
282 found = ngx_http_get_indexed_variable( r, expr[ i ].index );
283
284 if ( found && found->len > 0 )
285 {
286 var = found;
287 break;
288 }
289 }
290
291 if ( ! var )
292 return NGX_ERROR;
293
294 v->len = var->len;
295 v->data = var->data;
296 v->valid = 1;
297 v->no_cacheable = 0;
298 v->not_found = 0;
299
300 return NGX_OK;
301 }
Здесь все относительно просто. В переменную index записываем значение третьего аргумента функции (значение, которое мы предусмотрительно сохранили в ngx_http_myutil_first_not_empty()) - строка 248. В строках 250-257 ищем соответствующий элемент в массиве first_not_empty_var_data. На найденный элемент будет указывать переменная fne_elem (строка 262). Получаем элементы массива data из fne_elem (строки 264-265). Далее проходим по всем элементам. Если текущий элемент переменная, то получаем ее значение по ее индексу (строка 282). В строках 284-288 проверяем, пусто ли это значение: если пусто, то продолжаем поиск, иначе выходим из цикла и присваиваем переменной v найденное значение (строки 294-298). Если же текущий элемент - литерал, то просто берем его значение key и присваиваем переменной v (строки 271-280; заметьте, что пустой литерал '' тоже сработает, так как проверка на expr[ i ].key.len не производится!).
Ну вот и все. Осталось запустить элементарную проверку. Будем проверять на следующей конфигурации:
events {
worker_connections 1024;
}
http {
server {
listen 8010;
server_name router;
location /test_first_not_empty.html {
myutil_first_not_empty $fne_1 $arg_a $arg_b default;
myutil_first_not_empty $fne_2 $arg_aa $arg_bb 'no data';
if ($arg_c) {
myutil_first_not_empty $fne_1 $arg_a $arg_c;
echo $fne_1;
echo $fne_2;
break;
}
echo $fne_1;
echo $fne_2;
}
}
}
Как видим, внутри блока if в локейшне /test_first_not_empty.html переменная $fne_1 переопределена. Нам нужно проверить, действительно ли она будет переопределена. Мы в праве этого ожидать, поскольку элементы массива first_not_empty_var_data родительской конфигурации добавляются после элементов дочерней конфигурации в функции ngx_http_myutil_merge_loc_conf(). Проверяем:
$ curl 'http://localhost:8010/test_first_not_empty.html?b=HiB'
HiB
no data
$ curl 'http://localhost:8010/test_first_not_empty.html?b=HiB&c=HiC'
HiC
no data
Действительно, присутствие аргумента c в строке запроса привело к тому, что сработала директива myutil_first_not_empty $fne_1 ... внутри if. Еще пара проверок:
$ curl 'http://localhost:8010/test_first_not_empty.html?bb=HiBB&a=HiA&b=HiB'
HiA
HiBB
$ curl 'http://localhost:8010/test_first_not_empty.html?aa=907'
default
907
Исходники модуля вместе с тестовой конфигурацией можно взять здесь.