понедельник, 13 августа 2012 г.

nginx: расширяем модуль myutil

Здесь мы начали разрабатывать новый модуль 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; ++)
    {
        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; ++)
    {
        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; ++)
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; ++)
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; ++)
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
Исходники модуля вместе с тестовой конфигурацией можно взять здесь.

Комментариев нет:

Отправить комментарий