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

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

Пять форм записи location

Коротко

Server-блок выбран, дальше nginx решает второй вопрос: какой location внутри него обработает путь. Форм записи пять, и они не равны между собой.

  • = - точное совпадение всего URI, ^~ - префикс, отменяющий регулярки, ~ и ~* - регулярки, без модификатора - обычный префикс.
  • Префикс сравнивается как строка, а не как путь. location /app ловит и /application.log, и /appliance.
  • Строка запроса в выборе не участвует: ?sort=price отрезается заранее.
  • URI приводится к нормальному виду ДО выбора: .. схлопывается, %2e и двойные слеши тоже. Сравнивают уже результат, а не то, что прислал клиент.
  • Порядок строк в файле решает только споры между регулярками.

Если пять форм и «префикс - это строка» уже очевидны - листай до «Теперь сам».

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

В конфиге один блок:

/etc/nginx/conf.d/shop.confhttp server
location /app { proxy_pass http://127.0.0.1:8080; }

Три запроса: /app/index.html, /application.log и /appliance. Сколько из них уйдёт на бэкенд?

Инстинктивный ответ - один: остальные два к приложению отношения не имеют. Настоящий - все три.

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

Представь сортировку заявок по шаблонам на конверте.

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

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

Nginx устроен ровно так, и главная ошибка новичка - думать, что второй сотрудник понимает, где кончается слово.

Механизм: пять форм и что каждая означает

/etc/nginx/conf.d/shop.confhttp server
location = /favicon.ico  { }   # 1. точное совпадение всего URI
location ^~ /images/     { }   # 2. префикс, отменяющий регулярки
location ~ \.php$        { }   # 3. регулярка, с учётом регистра
location ~* \.(jpg|png)$ { }   # 4. регулярка, без учёта регистра
location /api/           { }   # 5. обычный префикс

В регулярках \. - буквальная точка (без слэша точка значит «любой знак»), $ - конец строки, (jpg|png) - одно из перечисленного. Пробел между модификатором и путём ставят по традиции: location =/favicon.ico и location ~*\.jpg$ nginx тоже понимает, проверено. Пустые { } допустимы - такой блок отдаёт файлы с диска по общим правилам сервера.

что именно сравнивается с URI = /путь весь URI целиком, символ в символ ^~ /путь начало строки, и регулярки не смотрим ~ шаблон любое место URI, если не привязать ~* шаблон то же, но без учёта регистра /путь начало СТРОКИ, а не начало пути
Последняя строка и есть источник большинства неожиданностей: сравнение идёт посимвольно, границы каталога для nginx не существует.

= требует совпадения всего URI. location = / поймает / и /?a=1 - строку запроса отрезают до выбора, - но не /index.html: это уже другой путь.

^~ совпадает как обычный префикс, но если победил - регулярки не проверяются вовсе. Значок читается не «регулярка», а «префикс, и на этом хватит».

~ и ~* различаются только чувствительностью к регистру. Совпадение ищется в любом месте URI, поэтому ~ \.php без $ поймает и /upload.php.jpg.

Без модификатора - совпадение по началу строки.

Правило

Префикс сравнивается как строка, а не как путь. Имеешь в виду каталог - пиши слеш: location /app/. Без слеша блок заберёт всё, что начинается с этих букв.

Разбор: что ловит каждая форма

Собрал конфиг из всех пяти форм и прогнал по нему запросы. Проверено на nginx 1.31.5.

/etc/nginx/conf.d/shop.confhttp server
location = /favicon.ico { return 200 "точное\n"; }
location ^~ /assets/    { return 200 "^~ префикс\n"; }
location ~ \.png$       { return 200 "регулярка png\n"; }
location ~* \.JPG$      { return 200 "регулярка без регистра\n"; }
location /images/       { return 200 "префикс images\n"; }
location /app           { return 200 "префикс app\n"; }
location /              { return 200 "префикс корня\n"; }
$  команда
for u in /favicon.ico /assets/logo.png /images/logo.png /images/note.txt /photo.JPG /application.log /appliance /nothing; do printf '%-22s -> ' $u; curl -s localhost$u; done
↳  выводfor u in /favicon.ico /assets/logo.png /images/logo.png /images/note.txt /photo.JPG /application.log /appliance /nothing; do printf '%-22s -> ' $u; curl -s localhost$u; done
/favicon.ico           -> точное
/assets/logo.png       -> ^~ префикс
/images/logo.png       -> регулярка png
/images/note.txt       -> префикс images
/photo.JPG             -> регулярка без регистра
/application.log       -> префикс app
/appliance             -> префикс app
/nothing               -> префикс корня

Цикл for подставляет адреса по очереди в $u и для каждого выполняет всё между do и done; printf '%-22s -> ' печатает адрес, дополненный пробелами до ровной колонки. В терминал команда вставляется одной строкой.

Три строки стоит разобрать. /assets/logo.png ушёл в префикс, хотя регулярка \.png$ подходит: ^~ отменил её. /images/logo.png с той же регуляркой ушёл в неё, потому что у /images/ этой защиты нет - об этом весь следующий урок. А /application.log и /appliance попали в блок приложения, и это ответ на вопрос из начала урока.

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

Наивное правило: «в location сравнивается то, что клиент написал в адресе».

Сравнивается нормализованный URI - после декодирования процентов, схлопывания .. и склейки повторных слешей. %2e - закодированная точка: в адресе любой знак можно записать как % и его код. Одна тонкость опыта: curl сам схлопывает .. ещё до отправки, поэтому нужен флаг --path-as-is - он шлёт путь как написано. Тот же конфиг, ещё три запроса:

$  команда
for u in '/images/../secret/' '/images/%2e%2e/secret/' '//images//logo.txt'; do printf '%-22s -> ' $u; curl -s --path-as-is localhost$u; done
↳  выводfor u in '/images/../secret/' '/images/%2e%2e/secret/' '//images//logo.txt'; do printf '%-22s -> ' $u; curl -s --path-as-is localhost$u; done
/images/../secret/     -> префикс корня
/images/%2e%2e/secret/ -> префикс корня
//images//logo.txt     -> префикс images

А /images/logo.png?x=1 ушёл в регулярка png: строка запроса отрезана до выбора - иначе ?x=1 ломал бы привязку $ в любой регулярке. Путь /images/../secret/ превратился в /secret/ и в блок картинок не попал вовсе, а закодированный %2e%2e сначала раскодировался и дал то же самое.

Это важнее, чем выглядит. Правило доступа, написанное как location /internal/ { deny all; }, не обойти ни точками, ни процентами, ни двойными слешами: к моменту выбора все три записи уже приведены к одному виду. deny all запрещает доступ всем, и клиент получает 403 - замер дал его и на /images/../internal/x с --path-as-is. И наоборот - location не может отличить /images//logo.png от /images/logo.png, даже если тебе это зачем-то понадобилось.

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

  • В блок приложения попадают чужие адреса. Забыт слеш: location /app вместо location /app/.
  • location = / не ловит главную страницу с параметрами. Ловит: строка запроса в сравнении не участвует. Если не ловит - ищи другую причину.
  • Регулярка ~ \.php срабатывает на /photo.php.jpg. Нет привязки к концу: нужен $.
  • Правило deny не срабатывает, хотя адрес похож. Сравни его с нормализованным URI, а не с тем, что в браузере: nginx -T плюс запрос курлом отвечают за минуту.

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

Пропущенный слеш в префиксе - самая дешёвая по написанию и самая дорогая по последствиям опечатка в конфиге. Она не ломает ничего сразу: блок работает, сайт открывается, nginx -t доволен. Ломается позже, когда рядом появляется адрес с теми же первыми буквами.

Отсюда привычка, которая экономит вечера: писать префикс со слешем всегда, когда имеешь в виду каталог, и проверять новый блок не одним «правильным» адресом, а тремя - похожим, коротким и лишним:

$  команда
for u in /app/ /app /application.log; do echo -n "$u -> "; curl -s localhost$u; done

echo -n печатает адрес без перевода строки, чтобы ответ встал рядом. На удалённом сервере вместо localhost пишут его имя.

Три секунды, и видно, что блок забирает больше, чем ты думал.

Теперь сам

1. Чем location /api отличается от location /api/ для запроса /api?

Первый его поймает, второй - нет: /api не начинается с /api/. Отсюда типовой симптом «работает со слешем на конце и не работает без него». Если нужны оба, их либо перечисляют двумя блоками, либо ставят редирект с одного на другой: location = /api { return 301 /api/; } отвечает 301 с Location: http://localhost/api/, проверено.

2. Почему location ~ \.php$ не поймает /upload.PHP?

~ учитывает регистр. Поймает ~* \.php$. Регистр в путях - вечный источник расхождений между разработкой на одной системе и продом на другой.

3. Есть location ^~ /assets/ и location ~ \.png$. Куда уйдёт /assets/logo.png и куда /avatars/logo.png?

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

Главное

Форм пять: =, ^~, ~, ~* и обычный префикс. Префикс сравнивается как строка, а не как путь, поэтому /app ловит и /application.log. Сравнивается нормализованный URI - без строки запроса, с раскодированными процентами и схлопнутыми ...

Комментарии

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

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

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