1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 1 из 5

Чем опасен if

Коротко

if внутри location заводит неявный вложенный блок: когда условие срабатывает, запрос обслуживает он, а не блок вокруг. Отсюда исчезающие заголовки и proxy_pass, уходящий на другой путь в зависимости от параметра запроса. Безопасны внутри if ровно четыре директивы: return, rewrite, set, break. Всё остальное либо не грузится, либо ведёт себя не так, как написано.

Правило «в if только return и rewrite» знаешь - листай до «Теперь сам». Разборы про исчезнувший заголовок и раздвоившийся прокси.

Блок ставит заголовок безопасности всем ответам, а внутри условия добавляет второй. Что получит клиент по запросу /hdr?x=1?

location /hdr {
    add_header X-Снаружи да always;
    if ($arg_x) { add_header X-Внутри да always; }
    return 200 "ok\n";
}

Ожидание - два заголовка. На стенде приходит один: X-Внутри. Внешний исчез.

На что это похоже

Это как отдать заказ на субподряд. Пока условие ложно, работает твой цех со всеми твоими правилами. Как только условие истинно, работу забирает другой цех - формально твой, стоящий в том же здании, но со своим набором правил. Он унаследует то, чего у него нет своего, и не унаследует ничего из того, где у него есть хоть что-то своё. Заголовки как раз из второй категории: правило наследования у них «всё или ничего», и оно уже встречалось в разделе про язык конфига.

Механизм: неявный вложенный блок

location /hdr add_header X-Снаружи if ($arg_x) add_header X-Внутри return 200 условие ложно: отвечает внешний блок условие истинно: отвечает вложенный
Сработавший if - это второй блок, а не строка внутри первого

Правило

Внутри if можно писать только то, что не относится к обслуживанию запроса: return, rewrite, set и break. Всё остальное - либо ошибка загрузки, либо тихая подмена обработчика.

Разбор: заголовок, который исчез

Три замера одного и того же блока:

запрос заголовки в ответе
/hdr?x=1 (условие истинно) только X-Внутри
/hdr (условие ложно) только X-Снаружи
/hdr-bez (блока if нет вовсе) только X-Снаружи

Первая строка и есть дефект. Пока условие ложно, всё выглядит правильно - поэтому такую конфигурацию выкатывают, посмотрев ответ на обычном запросе. Ломается она только на тех запросах, ради которых её и писали.

Если заголовок нужен всем, а второй - по условию, условие уносят из блока в карту:

/etc/nginx/conf.d/shop.local.confhttp
map $arg_x $вложенный {
    default "";
    ~.      "да";
}

server {
    location /hdr {
        add_header X-Снаружи да always;
        add_header X-Внутри  $вложенный always;   # пустой не отдаётся вовсе
        return 200 "ok\n";
    }
}

Заголовок с пустым значением nginx не отправляет - значит одна строка заменяет и условие, и оба варианта ответа.

Разбор: один блок, два разных бэкенда

location /api/ {
    if ($arg_x) { proxy_pass http://back:8000; }
    proxy_pass http://back:8000/внешний/;
}

Конфиг грузится: nginx -t доволен. Замер того, что видит бэкенд:

Бэкенд печатает пришедший путь (return 200 "path = $request_uri"):

$  команда
curl -s http://localhost/внешний/cart
curl -s 'http://localhost/внешний/cart?x=1'
↳  выводдва curl выше: без параметра и с ?x=1
path = /внешний/cart
path = /api/cart?x=1

Один location отправляет запросы на два разных пути, и решает это параметр строки запроса. В конфиге об этом не написано ни слова: обе строки выглядят как один и тот же адрес. Разница в том, что у внешней есть путь /внешний/ (значит, подстановка URI), а у внутренней его нет (значит, URI уходит как есть).

Для сравнения - try_files внутри if nginx не принимает вовсе:

↳  выводnginx -t
nginx: [emerg] "try_files" directive is not allowed here in /etc/nginx/nginx.conf:7

То есть часть запрещений проверяется при старте, а часть - нет, и надеяться на nginx -t как на защиту нельзя.

Что ломается без этого

  • Два if подряд выполняются оба. set $r "${r}A" и set $r "${r}B" в двух условиях дали r=AB. Это не «первый выигравший», а последовательное выполнение на фазе rewrite - и потому длинные цепочки условий незаметно дорожают.
  • Проверка -f смотрит на $request_filename, то есть уже на собранный путь с учётом root и alias. В блоке с alias она проверит не тот файл, о котором идёт речь.
  • if в блоке server безопаснее, чем в location, но и там он остаётся фазой rewrite: return внутри него сработает раньше выбора location и раньше любых ограничений доступа.

Зачем это в работе

Правило «if is evil» появилось не из вкусовщины: у директивы нет своего контекста выполнения, она переиспользует механизм вложенного location. Поэтому на каждую задачу, ради которой тянутся к if, есть замена:

Задача Чем заменить
условие по имени домена отдельный блок server со своим server_name
условие по началу пути отдельный location
условие по методу запроса limit_except GET { deny all; }
«если файла нет» try_files $uri $uri/ /index.php;
условие по адресу клиента geo
всё остальное map

Оставшиеся законные применения if - редирект и перезапись по признаку, который иначе не выразить: if ($http_user_agent ~* бот) { return 403; }. Здесь внутри только return, значит неявный блок ничего не ломает.

Теперь сам

Найди, что не так, и скажи, при каком запросе это выстрелит:

location /files/ {
    add_header Cache-Control "public, max-age=3600" always;
    if ($arg_download) {
        add_header Content-Disposition attachment always;
    }
    alias /srv/files/;
}

Ответ: при ?download=1 из ответа пропадёт Cache-Control - останется только Content-Disposition. Сработавший if обслуживает запрос сам, а заголовки наследуются целиком или никак. Лечится картой: map $arg_download $раздача, и обе строки add_header стоят рядом на одном уровне.

Главное

Внутри if в location учитывай правила конфигурационных блоков и фазы обработки. Проверяй фактические заголовки и обработчик после выполнения условия. Для выбора значений используй map, для разделения маршрутов - location: так область действия настройки видна без условного вложенного блока.

Комментарии

Пока нет комментариев. Будь первым!

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

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.