# теория · шаг 2 из 5
Две фазы выбора
Коротко
Префиксы и регулярки не соревнуются напрямую. Выбор идёт в две фазы, и это объясняет почти все «почему сюда попал этот запрос».
- Самый длинный совпавший префикс запоминается, но не выигрывает сразу.
- Потом проверяются регулярки, и любая совпавшая бьёт запомненный префикс - какой бы длинный тот ни был.
- Прервать это можно двумя способами:
=заканчивает поиск сразу,^~отменяет фазу регулярок. - Между регулярками решает не длина и не точность, а порядок в файле: первая совпавшая.
- Отсюда самая частая авария раздела: регулярка по расширению перехватывает каталог с пользовательскими файлами.
Если пять шагов выбора помнишь наизусть - листай до «Теперь сам».
Сначала ответь сам
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
Правило
Самый длинный префикс запоминается, а побеждает только если ни одна регулярка не совпала. Поэтому регулярка бьёт префикс любой длины. Между регулярками решает порядок в файле, между префиксами - длина.
Разбор: длина префикса не помогает
Возьмём два префикса, один длиннее другого, и регулярку. Проверено на nginx 1.31.5.
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
/images/note.txt -> префикс images
/images/logo.png -> регулярка png
/images/deep/logo.png -> регулярка png
Третья строка и есть проверка правила. Префикс /images/deep/ длиннее и точнее
некуда, и всё равно проиграл: он лежал в кармане, пока проверялась регулярка,
и оттуда уже не вышел.
Лечится значком ^~, и ставят его на тот префикс, который побеждает в первой
фазе. Один ^~ /images/ не спасёт /images/deep/logo.png: замер показал, что
длиннее совпал /images/deep/ без значка, он лёг в карман, и регулярка его
побила. Значок нужен на обоих:
location ^~ /images/ { return 200 "префикс images\n"; }
location ^~ /images/deep/ { return 200 "префикс images/deep\n"; }
location ~ \.png$ { return 200 "регулярка png\n"; }
/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 звучит «первая совпавшая», и это буквально порядок строк в файле.
location ~ ^/api/ { return 200 "регулярка api\n"; }
location ~ ^/api/v2/ { return 200 "регулярка api v2\n"; }
^ в шаблоне - начало строки. К модификатору ^~ он отношения не имеет,
совпадение знаков случайное.
регулярка api
Второй блок не получит ни одного запроса за всё время жизни конфига. Ни ошибки, ни предупреждения: с точки зрения nginx обе строки корректны. Более специфичные регулярки всегда пишут выше общих - это единственная защита.
Есть и вторая сторона того же правила. Порядок в файле - это порядок в
собранном конфиге, а собирают его директивы include по алфавиту имён
файлов. Разложил регулярки по разным файлам - и порядок между ними задаётся
теперь именами файлов, а не твоим намерением. Поэтому регулярки одного сайта
держат в одном файле и рядом.
Что ломается без этого
- Файл уезжает не из того каталога. Регулярка по расширению перехватила
каталог. Ищи
~ \.(png|jpg)$и подобные; лечится^~на каталоге. - Блок с регуляркой не получает запросов. Выше есть более общая регулярка. Порядок решает всё.
- Пользовательский файл выполнился как код. Тот же перехват, только
последствия дороже:
~ \.php$забрал файл из каталога загрузок. - После переименования файла конфига маршруты поехали. Порядок регулярок задавался алфавитом имён файлов.
Зачем это в работе
Пять шагов проходят вслух за пятнадцать секунд: точное - длинный префикс -
^~? - регулярки сверху вниз - запомненный префикс. Это быстрее, чем ставить
опыт, и надёжнее, чем догадка.
Когда конфиг чужой и блоков три десятка, работает второй приём - пометить блоки и спросить сервер:
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/... под него не подошёл, префикс в карман не попал, и отменять
вторую фазу некому.
Главное
Выбор идёт в две фазы: самый длинный префикс запоминается, но побеждает
только если ни одна регулярка не совпала. Прервать это можно двумя способами -
= заканчивает поиск сразу, ^~ отменяет фазу регулярок. Между регулярками
выигрывает первая по порядку в собранном конфиге.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий