Эта статья — часть Большого Мануала по настройке 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.
listen 80;
server_name www.example.com;
location / { return 302 http://example.com$request_uri; }
}
Второй пример. Хост www.example.com редиректим на example.com, сохраняя протокол запроса, но не сохраняя URL (ну то есть всегда на морду example.com в том же протоколе).
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 сохраняем.
listen 80;
server_name *.example.com;
location / { return 302 https://$http_host$request_uri; }
}
Четвертый пример. Если в URL есть аргумент lang и его значение равно en, то редиректить нужно на en.example.com, сохранив URL без аргументов.
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{} — то потом можно поседеть.
За arg_x — отдельное спасибо, был не в курсе.
http_host можно же манипулировать, насколько безопасно полагаться на эту переменную?
http://www.opennet.ru/opennews/art.shtml?num=46499
Дык вы статью то прочитайте.
Там описан случай, когда wordpress слушает default host. А в мануале как раз описано, как default host завести.
Anyway, можно писать и статический хост вместо переменной, но тогда конфиг на каждый редирект разный будет.
Кажется, что вы не поняли вопроса. Может это немного прояснит?
https://github.com/yandex/gixy/blob/master/docs/ru/plugins/hostspoofing.md
> Кажется, что вы не поняли вопроса.
Я вопрос прекрасно понял.
Повторяю — спуфить $http_host при правильно описанных server_name (без слишком широко трактуемых регулярок, например) — бесполезно, запрос уйдет в default host, а там на запрос ответят 403/404/whatever.
Так что свести все ваши беспокойства можно к трем пунктам:
1) нет регулярок в server_name?
2) значение $http_host не передаётся напрямую в бэкэнд?
3) мы сейчас не описываем default host для сокета?
Тогда можно использовать $http_host.
А вот использовать $host где-то, кроме как для передачи на бэкэнд — к лишним восклицаниям «какого хера?!» в процессе эксплуатации сервиса приводит, серьёзно.
Возможно глупый вопрос. Я верно понимаю, что если нужен безусловный редирект всех сайтов сервера на https, то исходя из примеров можно прописать в listen (который инклудится в каждый site-available):
listen 80;
location / { return 302 https://$http_host$request_uri; }
не, тогда и https редиректиться на https будет (бесконечный редирект получится).
Нужно явно заводить отдельную секцию server {} для http. Переносить ли в таком случае return в listen — дело вкуса, но придется следить за тем, чтобы listen и listen_ssl не пересекались в одном server{}
Т.е. если мне нужен безусловный редирект с 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, наличие в системе домена второго уровня мешать не будет? Это же будут абсолютно независимые сайты?
> наличие в системе домена второго уровня мешать не будет?
Вебсерверу вообще наплевать на «уровни» доменов, он каждый по отдельности воспринимает/обрабатывает. Так что не будет.
> И так для каждого домена. Правильно?
в секции с 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;
в простейшем приближении
Ну при этом в обоих случаях выше, конечно, остаётся возможность заводить отдельные конфиги для доменов через server_name без регулярок, если для отдельного домена редирект не нужен.
Но вообще если доменов не много — лучше не париться.
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;» — все равно.
Не подскажите, что я делаю не так?
В комментарии выше http:// перед www wordpress добавляет. В оригинале, конечно, все server-name с www просто начинались.
Нашел. Важное колдунство: редирект по https:// с «www» на «без-www» работает только при наличии валидного сертификата на «www.мой_домен.ru». Думаю полезно внести упоминание об этом в сответсвующую главу мануала.
Перевыпустил сертификаты letsencrypt как с мой_домен.ru, так и с http://www.мой_домен.ру и все заработало.
Ну вообще оно есть:
«Первым делом, я напоминаю, что нельзя «включить ssl» для домена, можно включить ssl только для ip+порта. То есть, если у вас на сервере заведено 2 сайта и вы включаете ssl только для первого, то по адресу https://второй_сайт у вас тоже будет открываться первый сайт (либо другая ЁНХ, в зависимости от конфигурации). При этом вы не можете тупо повесить редирект для каждого не-https сайта на http — для каждого доменного имени вам понадобится сертификат (а ещё лучше — один сертификат на все имена). Браузер (как и любой другой https-клиент) проверяет содержимое сертификата до того, как сделать запрос к веб-серверу по https (то бишь сначала браузер делает хендшейк с сервером, устанавливает зашифрованное соединения (проверяя валидность сертификата), и только потом отправляет http-запрос с какими-либо данными внутри зашифрованной сессии). Поэтому, чтобы браузер с адреса https://второй_сайт смог средиректить на http://второй_сайт, для второго сайта тоже нужен валидный сертификат.» (с) https://debian.pro/2206
Вот ровно про это. Во всех частях я это повторять не могу =(
А про http:// вылезжий в комментарии — можно конфиги в тег
прятать, они тогда мимо автоформаттера пройдут.
Но вообще неважно, и так понятно было.
Привет. Насколько я понимаю, «базовые» редиректы (как www->wo/www, http->https) правильней делать на уровне nginx. А что насчет более прикладных, вроде domain.com->domain.com/forum или domain.com/forum/admincp->domain.com/forum? Здесь нужен уровень apache2 или так же nginx?
Nginx редиректы делает с меньшими затратами по ресурсам.
Смысл в редиректах в htaccess только один — их не забываешь перенести вместе с файлами самого сайта. Если память не дырявая — лучше все редиректы в nginx-е делать.
(понятное дело, что редиректов, которые генерятся внутри php это не касается, хотя и их по возможности есть смысл переносить).