суббота, 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, а что из этого следует, мы уже прекрасно знаем.

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

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

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