Возьмем простую конфигурацию nginx.
worker_processes 1;
error_log /var/log/nginx/error.log info;
events {
worker_connections 1024;
}
http {
server {
listen 80;
server_name proxy;
location / {
if ($arg_x = '') {
proxy_pass http://127.0.0.1:8010;
break;
}
proxy_pass http://$arg_x;
}
}
server {
listen 8010;
server_name backend;
location / {
echo 'I am in backend';
}
}
}
Что ответит сервер proxy на простой запрос без аргументов? Логично предположить, что в ответе будет строка I am in backend. Однако, вместо этого будет получена ошибка 500 Internal Server Error, а в логе появится запись invalid URL prefix in “http://”! Вы можете в этом убедиться, введя в адресной строке браузера адрес localhost или запустив curl.
curl 'http://localhost/'
<html>
<head><title>500 Internal Server Error</title></head>
<body bgcolor="white">
<center><h1>500 Internal Server Error</h1></center>
<hr><center>nginx/1.7.6</center>
</body>
</html>
Если же ввести адрес http://localhost/?x=127.0.0.1:8010, то получим ожидаемую строку I am in backend.
Почему nginx не видит заданный в конфигурации дословно адрес 127.0.0.1:8010 в случае не заданного аргумента x и пытается выполнить проксирование на пустой адрес http://, в результате чего и возникает ошибка 500? Ответ кроется в реализации модуля прокси и принятого в нем механизма наследования конфигурации уровня локации (location configuration merge). Дело в том, что директива proxy_pass по-разному обрабатывает параметры, содержащие дословные адреса (такие как http://127.0.0.1:8010
) и переменные (такие как http://$arg_x
). Связанные с ними данные хранятся независимо в конфигурации уровня локации модуля прокси. Так, дословный адрес записывается в conf->upstream.upstream
, а переменный адрес — в специальные структуры, на которые ссылаются массивы conf->proxy_lengths
и conf->proxy_values
: фактическое значение адреса в этом случае вычисляется во время выполнения программы путем вычисления заранее скомпилированного выражения, построенного на основании этих данных. Если директива proxy_pass была задана дословным адресом, то указатель conf->upstream.upstream
будет на него ссылаться, а указатели conf->proxy_lengths
и conf->proxy_values
останутся нулевыми. Если же адрес в proxy_pass был задан с помощью переменных, то, наоборот, указатель conf->upstream.upstream
останется нулевым, в то время как указатели conf->proxy_lengths
и conf->proxy_values
будут ссылаться на данные, необходимые для вычисления фактического адреса перенаправления.
Проверка условия if ($arg_x = '')
в простом запросе без аргумента x дает положительный результат и nginx заменяет конфигурацию уровня локации на новую. Новая конфигурация была создана еще во время чтения конфигурационного файла и ее состав полностью зависит от механизма наследования конфигурации уровня локации. Этот механизм реализован в функции ngx_http_proxy_merge_loc_conf(), нас интересует следующий фрагмент:
if (conf->upstream.upstream == NULL) {
conf->upstream.upstream = prev->upstream.upstream;
conf->vars = prev->vars;
}
if (conf->proxy_lengths == NULL) {
conf->proxy_lengths = prev->proxy_lengths;
conf->proxy_values = prev->proxy_values;
}
То есть в случае, если proxy_pass внутри условия if не содержит дословного адреса, то он будет унаследован сверху. И в случае отсутствия переменного адреса, тот тоже будет унаследован сверху. Наш proxy_pass внутри if содержит дословный адрес, при этом он также унаследует переменный адрес http://$arg_x
. В итоге получаем возможность задействовать оба механизма обработки адреса одновременно, что само по себе создает неопределенность! А если взглянуть на реализацию ngx_http_proxy_handler(), то выяснится, что эта неопределенность разрешается в пользу переменного адреса (там, где вызывается ngx_http_proxy_eval()). Вот почему мы получаем ошибку 500: несмотря на то, что директива proxy_pass внутри if получила новый дословный адрес, она также унаследовала переменный адрес сверху, который модуль прокси рассматривает как основной и, поскольку аргумент x не задан, выражение http://$arg_x
вычисляется как http://
и nginx не в состоянии выполнить такое перенаправление.
Теперь, когда мы знаем врага в лицо, нужно попробовать избежать его воздействия. Давайте не будем наследовать настройки proxy_pass сверху, пусть они настраиваются на одном уровне в разных блоках if.
location /1 {
if ($arg_x = '') {
proxy_pass http://127.0.0.1:8010;
break;
}
if ($arg_x) {
proxy_pass http://$arg_x;
break;
}
return 500;
}
Такая конфигурация, несмотря на всю ее подозрительную странность, сработает: будет выбрана одна из директив proxy_pass в зависимости от присутствия в запросе аргумента x, при этом директива return 500
никогда не сработает, поскольку два условия для if сделаны намеренно взаимоисключающими.
Проверяем.
curl 'http://localhost/1'
I am in backend
curl 'http://localhost/1?x=127.0.0.1:8010'
I am in backend
Другой способ предусматривает использование директивы rewrite.
location /2 {
if ($arg_x) {
rewrite ^ /3 last;
}
proxy_pass http://127.0.0.1:8010;
}
location /3 {
internal;
proxy_pass http://$arg_x;
}
В разных локациях разные настройки proxy_pass: никакого наследования конфигураций между ними быть не может. Данный способ, хотя и будет работать в нашем случае, не совсем верный по сути. Проблема в том, что если на сервер proxy придет запрос вида /2?x=127.0.0.1:8010
, то он будет передан на сервер backend в виде /3?x=127.0.0.1:8010
, поскольку proxy_pass в location /3
будет использовать измененный URI (кстати, до версии nginx 1.1.12 в этом случае использовался оригинальный URI, затем это было исправлено как баг, хотя я бы не стал называть это багом, поскольку всегда рассматривал это как фичу, при этом весьма удобную). Итак, окончательный вариант, в котором исходный URI сохраняется.
location /2 {
if ($arg_x) {
rewrite ^(.*)$ /3$1$is_args$args last;
}
proxy_pass http://127.0.0.1:8010;
}
location ~* /3(.*) {
internal;
proxy_pass http://$arg_x$1;
}
Проверяем.
curl 'http://localhost/2'
I am in backend
curl 'http://localhost/2?x=127.0.0.1:8010'
I am in backend
И еще один вариант, максимально приближенный к оригинальному. Что если создать переменную, в которую записать дефолтный адрес перенаправления, и ссылаться на нее в директиве proxy_pass внутри блока if?
location /4 {
set $default_addr 127.0.0.1:8010;
if ($arg_x = '') {
proxy_pass http://$default_addr;
break;
}
proxy_pass http://$arg_x;
}
Это должно сработать, поскольку наследование конфигурации в случае выполнения условия в if будет сводиться к замене выражения для вычисления адреса перенаправления.
curl 'http://localhost/4'
I am in backend
curl 'http://localhost/4?x=127.0.0.1:8010'
I am in backend
Работает, что и требовалось доказать.
Update. Описанное здесь поведение было признано багом и исправлено в основной ветке разработки nginx.
IF is Evil! :)
ОтветитьУдалитьmap $arg_x $backend {
"" "127.0.0.1:8010";
default $arg_x;
}
server {
listen 80;
server_name proxy;
location / {
proxy_pass http://$backend;
}
}
server {
listen 8010;
server_name backend;
location / {
return 200 'I am in backend';
add_header Content-Type text/html;
}
}