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

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

Две фазы выбора

Коротко

Префиксы и регулярки не соревнуются напрямую. Выбор идёт в две фазы, и это объясняет почти все «почему сюда попал этот запрос».

  • Самый длинный совпавший префикс запоминается, но не выигрывает сразу.
  • Потом проверяются регулярки, и любая совпавшая бьёт запомненный префикс - какой бы длинный тот ни был.
  • Прервать это можно двумя способами: = заканчивает поиск сразу, ^~ отменяет фазу регулярок.
  • Между регулярками решает не длина и не точность, а порядок в файле: первая совпавшая.
  • Отсюда самая частая авария раздела: регулярка по расширению перехватывает каталог с пользовательскими файлами.

Если пять шагов выбора помнишь наизусть - листай до «Теперь сам».

Сначала ответь сам

/etc/nginx/conf.d/shop.confhttp server
location /images/ { root /var/www/static; }
location ~ \.png$ { root /var/www/images; }

Запрос GET /images/logo.png. Из какого каталога уедет файл?

Инстинктивный ответ - из /var/www/static: блок написан первым и подходит по смыслу. Настоящий - /var/www/images, а точнее файл /var/www/images/images/logo.png: rootРазбирается в разделе 5, глава «root против alias»: Как URI превращается в путь приклеивает к каталогу весь путь запроса. Выиграла регулярка, хотя стоит ниже и «про картинки вообще», а не про этот каталог.

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

Это конкурс в два тура, а не общая очередь.

В первом туре отбирают лучшего из «начинающихся с» - и он не побеждает, а садится в запас. Дальше идёт второй тур, где выступают шаблоны. Вышел хоть один шаблон - приз его, запасной участник не выступает вовсе. Никто не вышел - приз забирает тот, кто сидел в запасе.

Ошибка почти всех - считать, что участники соревнуются между собой и побеждает самый подходящий. Они соревнуются внутри своих туров, а между турами действует правило старшинства.

Механизм: пять шагов целиком

1. Есть location "=" с точно таким URI?
      да → он и победил, поиск окончен
2. Найти самый длинный совпавший префикс и ЗАПОМНИТЬ его
      (обычный или ^~ - оба участвуют)
3. Запомненный префикс объявлен с ^~ ?
      да → он и победил, регулярки не проверяются
4. Проверить регулярки сверху вниз по конфигу
      первая совпавшая → победила, поиск окончен
5. Ни одна не совпала → взять префикс, запомненный на шаге 2
фаза 1: префиксы самый длинный кладётся в карман фаза 2: регулярки первая совпавшая забирает запрос никто не совпал - достаём из кармана Прервать можно только тут: = заканчивает поиск на шаге 1 ^~ отменяет вторую фазу Длина префикса решает только внутри первой фазы. Между фазами длина не значит ничего.
Префикс не проигрывает регулярке по длине - он вообще с ней не сравнивается. Он ждёт в кармане и достаётся, только если регулярок не нашлось.

Правило

Самый длинный префикс запоминается, а побеждает только если ни одна регулярка не совпала. Поэтому регулярка бьёт префикс любой длины. Между регулярками решает порядок в файле, между префиксами - длина.

Разбор: длина префикса не помогает

Возьмём два префикса, один длиннее другого, и регулярку. Проверено на nginx 1.31.5.

/etc/nginx/conf.d/shop.confhttp server
location /images/      { return 200 "префикс images\n"; }
location /images/deep/ { return 200 "префикс images/deep\n"; }
location ~ \.png$      { return 200 "регулярка png\n"; }
$  команда
for u in /images/note.txt /images/logo.png /images/deep/logo.png; do printf '%-22s -> ' $u; curl -s localhost$u; done
↳  выводfor u in /images/note.txt /images/logo.png /images/deep/logo.png; do printf '%-22s -> ' $u; curl -s localhost$u; done
/images/note.txt       -> префикс images
/images/logo.png       -> регулярка png
/images/deep/logo.png  -> регулярка png

Третья строка и есть проверка правила. Префикс /images/deep/ длиннее и точнее некуда, и всё равно проиграл: он лежал в кармане, пока проверялась регулярка, и оттуда уже не вышел.

Лечится значком ^~, и ставят его на тот префикс, который побеждает в первой фазе. Один ^~ /images/ не спасёт /images/deep/logo.png: замер показал, что длиннее совпал /images/deep/ без значка, он лёг в карман, и регулярка его побила. Значок нужен на обоих:

/etc/nginx/conf.d/shop.confhttp server
location ^~ /images/      { return 200 "префикс images\n"; }
location ^~ /images/deep/ { return 200 "префикс images/deep\n"; }
location ~ \.png$         { return 200 "регулярка png\n"; }
↳  выводfor u in /images/logo.png /images/deep/logo.png; do printf '%-22s -> ' $u; curl -s localhost$u; done
/images/logo.png       -> префикс images
/images/deep/logo.png  -> префикс images/deep

А /avatars/user.png по-прежнему уходит в регулярку: под /images/ он не подошёл, защищать нечего.

Регулярка, съедающая пользовательские файлы

Типовая авария из двух безобидных правок, сделанных с разницей в полгода. Сначала в конфиг добавляют location ~ \.php$ { proxy_pass ... } для приложения. Потом - location /uploads/ для файлов, которые загружают пользователи. Кто-то загружает avatar.php, запрос за «картинкой» совпадает с регуляркой и уходит в интерпретатор PHP - программу, которая выполняет файл как код. Префикс тут не спасает - его бьёт регулярка. Спасает ^~ /uploads/.

Разбор: где наивное правило подводит

Наивное правило: «из двух регулярок сработает более точная».

Шаг 4 звучит «первая совпавшая», и это буквально порядок строк в файле.

/etc/nginx/conf.d/shop.confhttp server
location ~ ^/api/    { return 200 "регулярка api\n"; }
location ~ ^/api/v2/ { return 200 "регулярка api v2\n"; }

^ в шаблоне - начало строки. К модификатору ^~ он отношения не имеет, совпадение знаков случайное.

↳  выводcurl -s localhost/api/v2/cart
регулярка api

Второй блок не получит ни одного запроса за всё время жизни конфига. Ни ошибки, ни предупреждения: с точки зрения nginx обе строки корректны. Более специфичные регулярки всегда пишут выше общих - это единственная защита.

Есть и вторая сторона того же правила. Порядок в файле - это порядок в собранном конфиге, а собирают его директивы include по алфавиту имён файлов. Разложил регулярки по разным файлам - и порядок между ними задаётся теперь именами файлов, а не твоим намерением. Поэтому регулярки одного сайта держат в одном файле и рядом.

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

  • Файл уезжает не из того каталога. Регулярка по расширению перехватила каталог. Ищи ~ \.(png|jpg)$ и подобные; лечится ^~ на каталоге.
  • Блок с регуляркой не получает запросов. Выше есть более общая регулярка. Порядок решает всё.
  • Пользовательский файл выполнился как код. Тот же перехват, только последствия дороже: ~ \.php$ забрал файл из каталога загрузок.
  • После переименования файла конфига маршруты поехали. Порядок регулярок задавался алфавитом имён файлов.

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

Пять шагов проходят вслух за пятнадцать секунд: точное - длинный префикс - ^~? - регулярки сверху вниз - запомненный префикс. Это быстрее, чем ставить опыт, и надёжнее, чем догадка.

Когда конфиг чужой и блоков три десятка, работает второй приём - пометить блоки и спросить сервер:

/etc/nginx/conf.d/shop.confhttp server
location /uploads/ { add_header X-Loc "uploads" always; }

Блок, где кроме add_header ничего нет, отдаёт файлы по общим правилам сервера, а заголовок показывает, кто ответил: curl -sI localhost/uploads/x | grep X-Loc даёт X-Loc: uploads даже на 404, потому что стоит always.

В задаче следующего шага то же самое делает кнопка «почему?»: она показывает эти пять шагов для твоего конфига и твоего URI. Разница с живым сервером лишь в том, что тут не нужно ничего перезагружать.

Теперь сам

1. Есть location = / и location /. Куда попадёт /index.html?

В location /. Точное совпадение требует, чтобы URI был ровно /, а /index.html - другая строка. Пара из этих двух блоков как раз и нужна, чтобы главная страница обрабатывалась отдельно от всего остального.

2. Как отдать каталог /static/ с диска, если в конфиге есть location ~* \.(jpg|png|css|js)$ с настройками кеша?

location ^~ /static/. Обычный префикс регулярка перехватит, а ^~ отменит её проверку для всего каталога. Второй вариант - завести регулярку, которая исключает этот путь, но читаться такой конфиг будет вдвое хуже.

3. Почему ^~ не помогает, если запрос пришёл на /avatars/logo.png, а защищён ^~ /images/?

Потому что ^~ работает, только когда этот префикс победил в первой фазе. /avatars/... под него не подошёл, префикс в карман не попал, и отменять вторую фазу некому.

Главное

Выбор идёт в две фазы: самый длинный префикс запоминается, но побеждает только если ни одна регулярка не совпала. Прервать это можно двумя способами - = заканчивает поиск сразу, ^~ отменяет фазу регулярок. Между регулярками выигрывает первая по порядку в собранном конфиге.

Комментарии

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

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

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