Debian.pro

Про Debian


Большой мануал: часть 21. Учимся делать редиректы в nginx.

Эта статья — часть Большого Мануала по настройке lamp-сервера на debian.

Предыдущая часть цикла — Создаём конфиг для нашего сайта в nginx.

Следующая часть цикла — Ставим phpmyadmin и делаем его чуть безопаснее.

В этой статье мы поговорим о том, как в nginx нынче модно делать редиректы. Точнее, цель даже не в том, чтобы научить делать редиректы (это всё мелочи), а в том, чтобы вы после прочтения статьи точно поняли, что вы написали только что в конфиге (и зачем).

Для начала нужно узнать, как nginx выбирает нужный server{} для обработки запроса. Первым делом, разберемся с важным моментом в районе listen. Дело в том, что в nginx можно написать по-разному:

  • listen 80;
  • listen 192.168.0.1:80;

Так вот. Очень важно знать, что если вы где-то в конфиге написали listen ip:80, то для этой связки IP+порта ни один server{} с listen 80 уже работать не будет (ну только если написать 2 параметра listen =)).
Переводя для таких же тупых, как я. Если у вас на сервере много хостов и все они с listen 80, то не вздумайте писать хоть в одном конфиге listen ip:80. Nginx начнет отвечать на все запросы к этому ip на 80м порту одним вхостом. То есть на все запросы станет показывать один и тот же сайт. Я так продакшн большой ломал, ага

Так вот. Имея на руках ip и порт, nginx выбирает все server{} с максимально совпадающим listen. Дальше по server_name начинает выбирать конкретный server{} из списка. server_name может быть записан в трех видах:

  • т.н. «exact name», (server_name www.example.com;)
  • wildcard (server_name *.example.com;, отдельной группой обрабатываются имена со звездочкой на конце, а не в начале).
  • регулярки (server_name ~^(?.+)\.example\.com$;)

Regex-ы, кстати, приятны тем, что при помощи них из Host можно сразу выделить кусочек и использовать его как переменную (собственно, в примере это и показано, ну или поищите в блоге по слову evhost).
Но важно на самом деле другое. Все эти 3 группы server_name обрабатываются отдельно. То есть, если у вас имя хоста указано явным образом в одном из конфигов (exact name), то nginx не будет пытаться искать то же самое в остальных именах (в wildcard-ах или regex-ах). То бишь, www.example.com (прописанный в одном конфиге) уже исключен из *.example.com в другом конфиге. И то же самое с регэкспами — в регэкспы уже точно не попадают те хосты, которые записаны в виде wildcard или точного совпадения.

Вот. Когда nginx исходя из listen и server_name уже выберет конкретный server{} для обработки запроса — то запрос будет обрабатываться именно в нём. В лайти, например, можно было написать конфиг так, что концов не сыщешь %).

Теперь стоит узнать об основных переменных, которые могут быть полезны в простейших манипуляциях с конфигом. В первую очередь, нам тут интересны те переменные, в которые попадают различные части URL запроса:

  • $scheme — протокол запроса (http/https)
  • $host — заумно описанная переменная в документации, не используйте её, в неё может оказаться ЁНХ (кстати, она ещё и не эскейпится, что чревато багами в нашем конфиге)
  • $http_host — это значение http-заголовка Host: , её и рекомендую использовать в конфигах.
  • $uri — это часть URL после корня (после хост, то бишь) без аргументов. Именно эта часть URL-а матчится с location-ами.
  • $args — это переменная, содержащая всю строку аргументов (всё, что после ? в URL).
  • $arg_x — это переменная, содержащая значение url-аргумента x (для ?x=bla, $arg_x будет содержать bla).
  • $request_uri — переменная, содержащая и $uri, и $args (то есть весь урл, кроме хоста и прото).

Теперь о том, как лучше делать редиректы. Если есть возможность — то используйте return, а не rewrite (все примеры ниже будут именно про return). Стоит помнить, что 301й кода ответа кешируется браузером (то есть браузер запомнит, что по этому URL был такой редирект и сразу пройдет по редиректу из кеша). 302й код ответа браузерам кешировать запрещено (в семье не без урода, конечно, но вообще это достаточно надежное условие). Везде в примерах я использую 302й редирект, чтобы вы случайно копипастя не сделали «вечный» редирект. И вообще я советую все редиректы писать именно 302-е, а менять на 301-е только после того, как вы убедитесь, что всё работает верно (ну и если вы уверены, что этот редирект навсегда).

Вот. Теперь, вооружившись этими знаниями, мы понапишем всяких странных и не очень редиректов.
Первый пример. Хост www.example.com редиректим на example.com, сохраняя URL.

server {
    listen 80;
    server_name www.example.com;
    location / { return 302 http://example.com$request_uri; }
}



Второй пример. Хост www.example.com редиректим на example.com, сохраняя протокол запроса, но не сохраняя URL (ну то есть всегда на морду example.com в том же протоколе).

server {
    include listen;
    include listen_ssl;
    server_name www.example.com
    location / { return 302 $scheme://example.com/ ; }
}



Третий пример. Все хосты *.example.com редиректим на https (на всякий случай, такой конфиг будет работать только если нет другого конфига для хостов вида *.example.com (в том числе и отдельно описанных, например www.example.com), в которых было бы прописано listen 80). URL сохраняем.

server {
    listen 80;
    server_name *.example.com;
    location / { return 302 https://$http_host$request_uri; }
}



Четвертый пример. Если в URL есть аргумент lang и его значение равно en, то редиректить нужно на en.example.com, сохранив URL без аргументов.

server {
    listen 80;
    server_name example.com;
    location / {
        if ($arg_lang = en) { return 302 http://en.example.com$uri ; }
    }
}



Да, если вам нужно сделать rewrite, исходя из наличия аргумента — то без if-а не обойтись. В остальных случаях лучше пользоваться приёмом из первых примеров — отделяем запрос в отдельный location внутри конкретного server{} и внутри него делаем return.
Ах да, лучше писать location / с редиректом внутри, даже если другой конфигурации в этом server{} сейчас не будет. Если потом вы решите дописать что-то, а return прописан у вас прямо в server{} — то потом можно поседеть.

05.02.2017 byinkvizitor68sl|big-manual

Комментарии (15):

  1. Tallanvor :

    За arg_x — отдельное спасибо, был не в курсе.

  2. http_host можно же манипулировать, насколько безопасно полагаться на эту переменную?
    http://www.opennet.ru/opennews/art.shtml?num=46499

  3. Дык вы статью то прочитайте.
    Там описан случай, когда wordpress слушает default host. А в мануале как раз описано, как default host завести.

    Anyway, можно писать и статический хост вместо переменной, но тогда конфиг на каждый редирект разный будет.

  4. Кажется, что вы не поняли вопроса. Может это немного прояснит?
    https://github.com/yandex/gixy/blob/master/docs/ru/plugins/hostspoofing.md

  5. > Кажется, что вы не поняли вопроса.
    Я вопрос прекрасно понял.
    Повторяю — спуфить $http_host при правильно описанных server_name (без слишком широко трактуемых регулярок, например) — бесполезно, запрос уйдет в default host, а там на запрос ответят 403/404/whatever.

    Так что свести все ваши беспокойства можно к трем пунктам:
    1) нет регулярок в server_name?
    2) значение $http_host не передаётся напрямую в бэкэнд?
    3) мы сейчас не описываем default host для сокета?
    Тогда можно использовать $http_host.

    А вот использовать $host где-то, кроме как для передачи на бэкэнд — к лишним восклицаниям «какого хера?!» в процессе эксплуатации сервиса приводит, серьёзно.

  6. Neuro75 :

    Возможно глупый вопрос. Я верно понимаю, что если нужен безусловный редирект всех сайтов сервера на https, то исходя из примеров можно прописать в listen (который инклудится в каждый site-available):
    listen 80;
    location / { return 302 https://$http_host$request_uri; }

  7. не, тогда и https редиректиться на https будет (бесконечный редирект получится).

    Нужно явно заводить отдельную секцию server {} для http. Переносить ли в таком случае return в listen — дело вкуса, но придется следить за тем, чтобы listen и listen_ssl не пересекались в одном server{}

  8. Neuro75 :

    Т.е. если мне нужен безусловный редирект с http на https и с www на ‘без www’, то в файле sites-available/мой_домен.ru.conf, сделанном по описанному в предыдущей главе шаблону, я в Server {…..} убираю include listen; (остается include listen ssl;), затем в конце файла вставляю еще одну секцию
    Server {
    listen 443;
    server_name http://www.мой_домен.ru;
    location / { return 302 http://мой_домен.ru$request_uri; }
    }
    и еще одну
    server {
    listen 80;
    server_name мой_домен.ru;
    location / { return 302 https://мой_домен.ru$request_uri; }
    }
    И так для каждого домена. Правильно?
    И еще одно: если если мне нужен тестовый домен третьего уровня (test.мой_домен.ru), то если я заведу для него отдельную директорию в /home/username/data/www и создам такие же конфиги апача и nginx, наличие в системе домена второго уровня мешать не будет? Это же будут абсолютно независимые сайты?

  9. > наличие в системе домена второго уровня мешать не будет?
    Вебсерверу вообще наплевать на «уровни» доменов, он каждый по отдельности воспринимает/обрабатывает. Так что не будет.

    > И так для каждого домена. Правильно?
    в секции с listen 80 которая понадобится ещё server_name http://www…;
    А с «для каждого домена» вопрос открытый.
    Во-первых, можно через listen 80 default; и переменные $host/$http_host сделать редирект одним конфигом всех http-запросов.
    Во-вторых, через catch-секцию в регулярке в server_name (https://debian.pro/558) можно в один конфиг сделать редирект вида «все www на не-www»

    Что-то в духе
    listen 443 ssl;

    server_name ~^www\.(.*);
    return 302 https://$shrinked_host$request_uri;

    в простейшем приближении

  10. Ну при этом в обоих случаях выше, конечно, остаётся возможность заводить отдельные конфиги для доменов через server_name без регулярок, если для отдельного домена редирект не нужен.

    Но вообще если доменов не много — лучше не париться.

  11. Neuro75 :

    Cделал вот такой мой_домен.ru.conf

    server {
    include listen_ssl;
    include includes/letsencrypt;
    server_name мой_домен.ru;
    # дальше по тексту из мануала много строк, include listen отсутствует
    }

    server {
    listen 80;
    server_name мой_домен.ru;
    location / { return 302 https://мой_домен.ru$request_uri; }
    }

    server {
    listen 80;
    server_name http://www.мой_домен.ru;
    location / { return 302 https://мой_домен.ru$request_uri; }
    }

    Server {
    listen 443;
    server_name http://www.мой_домен.ru;
    location / { return 302 https://мой_домен.ru$request_uri; }
    }

    Иными словами в этом файле описал все четыре возможных сочетания www/не-www, http/https с редиректом трех из них на https://без_www_мой_домен.ru
    При входе в браузере http://мой_домен.ru или http://www.мой_домен.ru происходит правильное перенаправление на https://мой_домен.ru
    Однако, при входе на https://www.мой_домен.ru перенаправления не происходит, так и продолжает просматривать https://www
    Несколько раз просмотрел конфиги на наличие возможных ошибок, может пропустил где или написал не то. Вроде все правильно.
    Предполагал, что т.к. http://www.мой_домен.ru входит в мой_домен.ru, то видя в конфиге первой секцией https://мой_домен.ru nginx начинает ее обрабатывать, не доходя до описания https://www.мой_домен.ru. Поставил редиректы в начало файла — ситуация не изменилась.
    Вместо return 302 https://мой_домен.ru$request_uri; в этой секции сделал return 302 $scheme://мой_домен.ru — не работает
    Убрал секцию с «listen80 -> return https://….» и сделал точно как в примере объединенную:
    server {
    include listen;
    include listen_ssl;
    include includes/letsencrypt;
    server_name http://www.мой_домен75.ru;
    location / { return 302 $scheme://мой_домен.ru; }
    } — аналогично. http://www… перенаправляет, https://www… — нет.
    Поменял в этой объединенной секции «return 302 $scheme://мой_домен.ru;» на «return 302 https://мой_домен.ru$request_uri;» — все равно.
    Не подскажите, что я делаю не так?

  12. Neuro75 :

    В комментарии выше http:// перед www wordpress добавляет. В оригинале, конечно, все server-name с www просто начинались.

  13. Neuro75 :

    Нашел. Важное колдунство: редирект по https:// с «www» на «без-www» работает только при наличии валидного сертификата на «www.мой_домен.ru». Думаю полезно внести упоминание об этом в сответсвующую главу мануала.
    Перевыпустил сертификаты letsencrypt как с мой_домен.ru, так и с http://www.мой_домен.ру и все заработало.

  14. Ну вообще оно есть:
    «Первым делом, я напоминаю, что нельзя «включить ssl» для домена, можно включить ssl только для ip+порта. То есть, если у вас на сервере заведено 2 сайта и вы включаете ssl только для первого, то по адресу https://второй_сайт у вас тоже будет открываться первый сайт (либо другая ЁНХ, в зависимости от конфигурации). При этом вы не можете тупо повесить редирект для каждого не-https сайта на http — для каждого доменного имени вам понадобится сертификат (а ещё лучше — один сертификат на все имена). Браузер (как и любой другой https-клиент) проверяет содержимое сертификата до того, как сделать запрос к веб-серверу по https (то бишь сначала браузер делает хендшейк с сервером, устанавливает зашифрованное соединения (проверяя валидность сертификата), и только потом отправляет http-запрос с какими-либо данными внутри зашифрованной сессии). Поэтому, чтобы браузер с адреса https://второй_сайт смог средиректить на http://второй_сайт, для второго сайта тоже нужен валидный сертификат.» (с) https://debian.pro/2206

    Вот ровно про это. Во всех частях я это повторять не могу =(

  15. А про http:// вылезжий в комментарии — можно конфиги в тег

    <pre> </pre>

    прятать, они тогда мимо автоформаттера пройдут.
    Но вообще неважно, и так понятно было.

Написать комментарий