Алгоритм выбора location в Nginx

    Алгоритм выбора location обязателен к знанию при настройке nginx. Тем не менее, на официальном сайте nginx (на 2018 год) не сказано ни слова про алгоритм выбора в случаях, когда какие-то location'ы вложены друг в друга, а в статьях в интернете приводятся в корне неверные алгоритмы. В статье также будет дан пример уязвимого конфига.

    Частный случай с одним уровнем вложенности


    Если Вы новичок в nginx, то следует рассмотреть вначале частный случай без использования вложненных location, т. к. алгоритм для частного случая значительно проще:

    1. Вначале будет искаться равенство (=). Оно имеет высший приоритет.
    2. Потом будет искаться максимальный по длине префиксный location ((   ) или (^~)), после чего будет проверено, есть ли на найденном location модификатор приоритета (^~), и если он есть, то будет возвращён этот location.
    3. Потом будут проверяться регулярные выражения ((~) и (~*)) сверху вниз. При совпадении будет возвращён первый location из них.
    4. Потом вернётся тот префиксный location, который мы нашли до этого.

    Обратите внимание, что этот алгоритм не применим при наличии вложенных location.

    Общий случай с вложенными location


    1. Стартуем с верхнего уровня.
    2. Если на текущем уровне выполняется равенство (=), поиск прекращается — это и будет результат, т. к. такой location не может иметь никаких других вложенных location.
    3. В противном случае ищем на текущем уровне самый большой префиксный location ((   ) или (^~)).
      • Если такой префиксный location существует, то делаем его текущим уровнем и переходим к п. 2.
      • В противном случае выходим из цикла.
    4. Мы вышли из цикла. На данный момент мы нашли «самый большой» префиксный location, но не думайте, что это самый большой из всех. Пример:

      location /abc {
      	location /abcdefghi {
      		…
      	}
      }
      
      location /abcdef {
      	…
      }
      

      В данном примере мы перейдём в /abcdef, т. к. на его уровне он переборол более короткий /abc. Но по факту существуют location и больше него.
    5. Теперь в найденном location мы ищем первый верный regexp. При нахождении поиск полностью прекращается. Обратите внимание: в этом пункте мы по факту ищем regexp на самом нижнем уровне, а не на верхнем, как многие могли бы подумать. Т. е. поиск regexp идёт снизу, а не сверху (но внутри одного уровня идёт сверху, а не снизу).
      • Далее, если ничего не найдено, поднимаемся на один уровень вверх и аналогично ищем первый regexp, но в этот раз уже только при условии, что location, в котором мы были до этого, не имел метки (^~). Повторяем этот пункт до тех пор, пока подниматься будет некуда.
      • При этом нужно иметь ввиду:
        • Даже если какой-то из уровней имеет метку (^~), это не значит, что мы не осуществляем подъём. Подъём осуществляется всегда, но если более нижний уровень имел метку (^~), то на текущем уровне поиск regexp'ов не проводится.
        • Возможности запретить проверку regexp в самом нижнем уровне нет — для этого нужно создать ещё один вложенный уровень. А вот запретить проверку regexp на нулевом уровне можно — для этого location первого уровня (который находится на нулевом уровне) должен иметь метку (^~).
    6. Мы сделали подъём по дереву, но так и не нашли ни одного regexp. Раз regexp не найден, возвращаем «почти самый большой» префиксным location, который был найден ранее. Готово.

    Также при этом:

    • В версиях 0.7.1–0.8.41 префиксный location (   ) при точном совпадении действует как =

    Пример уязвимого конфига


    location ~ \.php$ {
    	deny all; # Здесь должно быть проксирование на php-fpm
    }
    
    location /posts/ {
    	location ~ (.*)_2x(\.[a-z]+)$ {
    		try_files $uri $1$2 =404;
    	}
    }
    

    В данном конфиге мы настроили игнорирование "_2x", если файл не найден. Например, nginx попробует найти файл /posts/img/a_2x.png как по указанному пути, так и по пути /posts/img/a.png. Но в реальности, если мы запросим /posts/authData_2x.php, то мы получим исходный текст скрипта authData.php в голом виде. Чтобы избежать таких ошибок, нужно знать, как обрабатывается location в nginx.

    Также дополнительной защитой может являться хранение скриптов в отдельной директории, недоступной из под обычных location. В этом случае, если наш location на php по каким-то причинам не сработает, пользователь получит ошибку 404, а не исходный текст скрипта.

    Перенаправление location


    1. Если try_files не содержит кода ошибки последним параметром, то будет сделано перенаправление в другой location, т. к. последний параметр всегда делает перенаправление. Обратите внимание: код ошибки в try_files должен писаться через равно (=).
    2. index и error_page при срабатывании всегда делают перенаправление в другой location. Также перенаправление делает rewrite, если добавить в него флаг last.

    Другое


    1. При выборе location не учитывается строка запроса, которая начинается со знака "?".

    Отказ от ответственности


    Алгоритм в статье составлен мной на основе моих личных наблюдений, и не факт, что он является правильным. К сожалению, официальной документации нет, а исходники я не читал. Если кто-то найдёт ошибки в алгоритме, просьба написать личным сообщением или в комментариях.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 21

      0
      почему же тогда пришлось переместить этот кусок
      location ~ \.html$ {
      try_files $uri =404;
      }

      над этим
      location ~ / {
      rewrite . /index.php last;
      }

      иначе всегда отрабатывал location ~ / при запросе к *.html файлам
        0
        Перечитайте статью. У Вас один уровень вложенности. В пределах одного уровня вложенности поиск регулярных выражений происходит сверху вниз.

        Цитата из статьи:
        (но внутри одного уровня идёт сверху, а не снизу).

        Но в пределах нескольких уровней мы вначале находим префиксный location, а потом делаем обратный подъём по дереву.
          0
          у меня как-то не сошлось понимание написанного:
          1. Стартуем с верхнего уровня.
          2.…
          3. В противном случае ищем на текущем уровне самый большой префиксный location ((   ) или (^~)).

          «текущим» для этого простого конфига является самый верхний уровень. Тогда почему игнорируется пункт №3?
            0
            Всё верно, на первой итерации текущий уровень — это верхний. Но ведь в третьем пункте сказано:
            Если такой префиксный location существует, то делаем его текущим уровнем и переходим к п. 2.

            В общем, я не понимаю Ваш вопрос. Возможно, Вам стоит перечитать алгоритм. Либо мне попытаться объяснить его более просто, но способов упрощения я пока не знаю.

            Тогда почему игнорируется пункт №3?
            Где он игнорируется?
              0
              Если уровней всего один, то какой location будет выбран первым? Вот это, если можно, хочется услышать дополнительно в алгоритме.
                0
                Если уровней один, то всё слишком просто.

                1. Вначале будет искаться равенство
                2. Потом будет искаться максимальный префиксный location, после чего будет проверено, есть ли на нём модификатор приоритета
                3. Потом regexp сверху вниз
                4. Потом вернётся тот префиксный location, который мы нашли до этого

                Я сейчас добавлю это в статью, т. к. не все пишут конфиги с вложенными location, а без них алгоритм можно описать значительно проще. Просто я думал, что это уже итак все знают, и интересен был более общий случай.
                  0
                  Добавил частный случай с одним уровнем вложенности.
                    0
                    Это привлечёт внимание тех, кто ищет с одним уровнем вложенности. Заодно разберутся и с тем, когда несколько уровней.
                      0
                      Спасибо, думаю, Вы правы. Вначале поставил описание частного случая, а потом уже описание общего случая с вложенными location. Многим вложенные уровни вообще могут быть не нужны, и они сюда пришли за простым алгоритмом, а в итоге получили супер-навороченный алгоритм, в котором нереально разобраться.
            0
            Ну и на всякий случай скопирую пример из статьи, чтобы было понятнее.

            location ~ \.php$ {
            	…
            }
            
            location /posts/ {
            	location ~ (.*)_2x(\.[a-z]+)$ {
            		try_files $uri $1$2 =404;
            	}
            }
            

            Здесь при запросе /posts/*** в случаях, когда выражение подходит и под первый regexp-location, и под второй regexp-location, приоритет будет отдан второму regexp-location'у, даже несмотря на то, что он идёт после первого.
              0
              Потому что второй локейшен не regexp-location, а префиксный.
              Документация гласит:

              location можно задать префиксной строкой или регулярным выражением. Регулярные выражения задаются либо с модификатором “~*” (для поиска совпадения без учёта регистра символов), либо с модификатором “~” (с учётом регистра). Чтобы найти location, соответствующий запросу, вначале проверяются location’ы, заданные префиксными строками (префиксные location’ы).
                0
                Я имел ввиду второй regexp (среди location'ом он третий по счёту).

                А насчёт документации — там не говорится ничего чётко. Вот цитата, которую Вы привели:
                Чтобы найти location, соответствующий запросу, вначале проверяются location’ы, заданные префиксными строками

                Ну вот нашли мы префиксный location, а что дальше? И имелся ли ввиду поиск префиксного location на текущем уровне или сразу на всех? На эти два вопроса и даёт ответ данная статья. А вопросы то очень важные.
                  0
                  Ну вот нашли мы префиксный location, а что дальше?

                  А дальше забываем про все location, что "выше". Нет их.
                  Теперь есть только то, что есть здесь и ниже.
                  На мой взгляд, такой подход все объясняет (и упрощает).


                  И имелся ли ввиду поиск префиксного location на текущем уровне или сразу на всех?

                  На текущем.
                  Боюсь, здесь люди попадают в ловушку того, что сами себя перемудрили.
                  Надо просто считать так: верхние уровни ничего не знают про нижние, а нижние — ничего не знают про верхние: "вот есть один уровень, посмотрим, куда упадет запрос".

            0
            К сожалению, официальной документации нельзя доверять

            Почему? Что там не так?

              0
              Ну как минимум там даже не потрудились привести алгоритм выбора location при наличии вложенных location. А то, что приведено, не сказано, что это для частного случая, когда вложенных location нет. Нет, там и не сказано, что это для общего случая, но блин… :) Это немного ввод в заблуждение.
                +1

                Черт его знает, мне все понятно, но я пользовался вложенными location-ами еще когда они были задокументированы только на языке С, так что мне сложно судить, насколько оно может быть непонятно. Если знаете, как написать лучше — напишите, патчи на документацию они принимают.

              0
              В пункте 2 лучше объяснить подробнее что такое префиксный location, чтобы избежать путаниц.
              Фактически возвращается самый длинный подходящий не регулярный location, если такого нет — идёт поиск по регулярным сверху вниз.
                –2
                Предполагается, что читающий уже знает, что такое префиксный location. Это location (   ) или (^~).
                –2
                Хотели как лучше, а получилось как всегда.
                Раз уже замутили схему, где у тебя половина конфига рассматривается как попало, а вторая половина по расположению в конфиге, то могли бы уж тестирование локейшенов сделать. А то сейчас единственный способ — это запуск в debug режиме, прогон запроса ручками и чтение портянки логов.
                  +3
                    0

                    Кучу лет пользуюсь nginx, и только сейчас узнал, что location могут быть вложенными! С одной стороны ни разу не было необходимости в таком, с другой предупрежден — значит вооружен. Спасибо!

                    Only users with full accounts can post comments. Log in, please.