Отлаживаем правила RewriteRule, или немного об интимной жизни mod_rewrite
У меня RewriteEngine всегда был довольно стрессовой темой. Только вот недавно я вдруг обнаружил, что все как-то улеглось и стало более или менее понятно. Поскольку я совершенно обычный человек, я уверен, что ситуация ошибки конфигурации веб-сервера «доставала» не одного лишь меня, и я спешу поделиться своим опытом.
Получилось нечто среднее между руководством по использованию модуля mod_rewrite и своеобразным справочником по конфигурированию веб-сервера с помощью файла .htaccess. Попутно хотелось бы заострить внимание на особо сложных или неочевидных моментах.
Предполагается, что читатель использует урл-рерайтинг в своей работе, знает, в общих чертах, что такое RewriteEngine и уже провел несколько часов за его настройкой. Эта статья не совсем для начинающих, но и не для супер-профи, конечно.
Исходные данные для опытов
- Все опыты производятся на локальном хосте.
- Установлен сервер lampp
- Версия Apache: 2.4.9 (сборка для Unix)
- В папке /opt/lampp/htdocs/bbb/_engine лежит опытный сайт на домене engine.bbb.ru. Указанная папка является корневой (DocumentRoot).
- В корневой папке сайта лежит всего одна страница ind.php.
- На сайте есть одна папка /opt/lampp/htdocs/bbb/_engine/local.
- В ней лежит один скриптовый файл ind1.php
Настройка виртуальных хостов
Для того, чтобы было проще работать, отлаживать и не трепать себе нервы там, где можно этого не делать, хорошо бы настроить виртуальные хосты с точки зрения удобства. Рассмотрим простейшие настройки, которые очень сильно облегчат нам жизнь.Настраиваем логи
Мало у кого на локальном сервере всего один домен. Доменов обычно много. Хорошо бы разделить логи по доменам и по дням, чтобы они не слишком разрастались. Делается это через раздел
Для лога ошибок на нашем домене добавляем следующие две строчки.
ErrorLog "|/opt/lampp/bin/rotatelogs /opt/lampp/logs/engine-bbb-error.%Y.%m.%d.log 86400"
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" combined
Первая задает имя лога ошибок для виртуального сервера и заставляет начинать новый лог каждые 86400 секунд. Rotatelogs является программой, которая в общем случае входит в комплект веб-сервера Apache и я надеюсь, что у вас она тоже установлена.
Вторая строчка задает формат каждой строчки лога ошибок. Подробности можно прочитать в документации к серверу Apache. Там все довольно понятно. В рамках этой статьи важно просто иметь ввиду, что формат настраивается.
Для лога доступа я у себя включаю только одну строчку. Формат строки «по умолчанию» меня обычно устраивает.
CustomLog "|/opt/lampp/bin/rotatelogs /opt/lampp/logs/engine-bbb-access.%Y.%m.%d.log 86400" combined
В обоих случаях обратите внимание на пути к программам веб-сервера и к логам. Вы должны установить у себя те пути, которые существуют на вашем компьютере.
Самые общие сведения о том, как все работает
Предположим, мы хотим включить в некоторой папке нашего домена сервис переписывания адресов. Для этого мы располагаем строчку
RewriteEngine on
в файле .htaccess, который будет управлять этой папкой. Кроме этого мы располагаем ниже этой строчки другие директивы и несколько правил для рерайтинга.
Предположим, сервер получает на вход некий урл. RewriteEngine начинает этот урл проверять, используя правила. Делает он это сверху вниз по порядку. Если входной урл НЕ УДОВЛЕТВОРЯЕТ ни одному правилу, то он, что называется «проходит». Так, например, предположим, что у нас есть в корневой папке файл index.php. Если входной ури »/index.php» не удовлетворяет ни одному правилу, то мы увидим в браузере результат работы этого скрипта.
Если мы имеем следующее правило
RewriteRule ^index\.php$ / [L]
то, очевидно, это правило для ури «index.php» сработает. В этом случае ури будет переписан на » и новый ури »/» будет послан на вход серверу. И весь процесс применения правил пойдет заново. Только в том случае, если ури »/» не удовлетворит ни одному нашему правилу, мы увидим то, что хотим. А если удовлетворит, то он будет опять переписан и все повторится заново.
Как работает флаг [L]
Наверное, этот флаг вносит особенно много недопониманий. Наличие флага предотвращает проверку входного ури на следующие за ним правила, если это правило сработало. Только и всего. То есть, если наш ури «index.php» прошел проверку (правило для него сработало), то ввиду наличия флага [L] мы прерываем все последующие проверки, и веб сервер сразу производит рерайтинг «index.php» → » и получает на вход ури »/» ([INTERNAL REDIRECT]), и все повторяется с начала, с первого правила. А если этого флага нет, то рерайтинг все равно происходит и проверка продолжается со следующего правила. Но ури будет уже измененный, а именно »/».
Понимание этого процесса сразу предотвращает много циклических редиректов.
Но позвольте, не значит ли написанное выше, что если не использовать флаг [L], мы сэкономим время и страница откроется быстрее? Мы натыкаемся на флаг [L] и должны пройти заново по всем правилам без исключения, а если не ставить флаг [L], то мы сделаем рерайтинг на сработавшем правиле, пройдем до конца всех правил и на этом закончим?
Я проверил. Это не срабатывает. В случае отсутствия флагов [L], модуль, как и предполагается, заменяет ури на сработавшем правиле, идет по всем оставшимся правилам до конца, потом производит [INTERNAL REDIRECT] и все равно проходит с этим ури все правила еще раз. То есть подтверждается то, о чем мы писали выше. Это правило, похоже, не имеет исключений.
Вывод: всегда, когда срабатывает правило RewriteRule, происходит [INTERNAL REDIRECT] и повторное применение всех правил. Этот второй проход начинается либо сразу после применения правила с флагом [L], или после того, как кончатся все правила, если работаем без флагов [L]. Ситуация «прохода» урла, a она называется «pass through» может произойти только в том случае, если ни одно правило не было применено. Флаг [L] действительно может уменьшить время обработки ури и его следует использовать везде, где возможно.
Что есть RewriteBase?
Эта инструкция, на мой взгляд, просто рекордсмен по непонятности! Я бы дал ей за это приз! Ввиду этого у меня есть две истории про этого зверя — короткая и длинная. Короткая история для тех, кто не хочет на эту инструкцию заморачиваться. Длинная для интересующихся.Короткая история
Если вы занимаетесь относительно простым урл-рерайтингом с помощью файлов .htaccess, то рекомендую всегда поступать следующим образом.
- Не использовать описываемую директиву вообще.
- Во всех правилах рерайтинга целевой урл всегда начинать со слэша (указание на то, что ури указывается относительно корня сайта)
Длинная история
При рерайтинге будут происходить следующие процессы:
Имеем:
- Document Root: /opt/lampp/htdocs/bbb/_engine
- Файл .htaccess лежит там же, в Document Root
Мы запрашиваем урл engine.bbb.ru/ind.php
- Сервис рерайтинга приведет путь запрашиваемого файла к его пути в файловой системе, а именно opt/lampp/htdocs/bbb/_engine/ind.php
- Удалит из него префикс opt/lampp/htdocs/bbb/_engine/ (совпадает с путем к папке, в которой лежит .htaccess)
- Будет применять правила рерайтинга, используя строку «ind.php»
Если в папке /opt/lampp/htdocs/bbb/_engine/local нет файла .htaccess или есть, но в нем не включен RewriteEngine
- Мы запрашиваем урл engine.bbb.ru/local/ind1.php
- Сервис рерайтинга приведет путь запрашиваемого файла к его пути в файловой системе, а именно opt/lampp/htdocs/bbb/_engine/local/ind1.php
- Удалит из него префикс opt/lampp/htdocs/bbb/_engine/
- Будет применять правила рерайтинга, используя строку «local/ind.php»
Если в папке /opt/lampp/htdocs/bbb/_engine/local есть файл .htaccess и в нем включен RewriteEngine
- Мы запрашиваем урл engine.bbb.ru/local/ind1.php
- Сервис рерайтинга приведет путь запрашиваемого файла к его пути в файловой системе, а именно opt/lampp/htdocs/bbb/_engine/local/ind1.php
- Удалит из него префикс opt/lampp/htdocs/bbb/_engine/local/ (это путь к папке, где лежит файл .htaccess директории /local)
- Будет применять правила рерайтинга, используя строку «ind1.php»
Внимание! Такой алгоритм будет выполняться всегда. Этот алгоритм выражает специфику термина »per-dir», то есть »по-директорного» подхода, заложенного в сервере Apache. Значение директивы RewriteBase никак на него (на алгоритм) не влияет.На что же влияет директива RewriteBase?
Нужно очень хорошо помнить, что в директиве RewriteBase указывается URL! Нельзя указать там »local/» Будет ошибка! Можно только »/local».
Пусть в нашем /opt/lampp/htdocs/bbb/_engine/local/.htaccess мы указали
RewriteBase /local
Мы запрашиваем урл engine.bbb.ru/local/
Тогда правило
RewriteRule ^$ ind1.php
Сработает! И будет осуществлен переход на ури /local/ind1.php
А правило
RewriteRule ^$ /ind1.php
тоже сработает, но переход будет осуществлен на ури /ind1.php. Файл не найден! Такого ури (относительно корня сайта) у нас нет!
Вывод 1: URL, который мы указываем в RewriteBase добавляется в качестве префикса к целевому ури в том случае, если он является относительным, то есть в начале нет слэша.
Вывод 2: Если мы никогда не используем относительные целевые ури в правилах, то и директива RewriteBase нам не нужна!
Вывод 3: Если мы используем «RewriteBase /», то при срабатывании правила
RewriteRule ^$ ind1.php
Будет попытка прейти на ури /ind1.php. Мы просто используем »/» в качестве префикса.
RewriteEngine on
RewriteRule ^$ ind.php
Запрашиваем при этом engine.bbb.ru
Если RewriteBase является урлом, то давайте установим
RewriteBase http://bbb.ru
Нет. Не проходит. Ошибка »RewriteBase: argument is not a valid URL». Странно, правда? Но мы не сдаемся! Меняем RewriteBase!
RewriteBase //bbb.ru
В этом случае ошибки нет! Что же у нас происходит с путями? Очень много интересного!
Сервер честно получает путь /opt/lampp/htdocs/bbb/_engine/, удаляет из него префикс /opt/lampp/htdocs/bbb/_engine/ и работает с пустой строкой ('').
Натыкаемся на правило и меняем пустую строку на 'ind.php'
Честно добавляем префикс »//bbb.ru» и отправляемся на следующий проход. Этот второй проход эквивалентен вызову engine.bbb.ru//bbb.ru/ind.php, что, по большому счету является совсем не тем, что мы хотели (было первоначальное желание скакнуть на другой сайт). Короче, идея себя не оправдала. В итоге у нас возникает ошибка 404, что логично. Кстати,»//» были в процессе рерайтинга заменены сервером на »/». Трассировка этого примера дана значительно ниже.
Как я получил все эти захватывающие дух сведенья об интимной жизни сервера Apache? Или наконец-то об отладке
Действительно! Как я увидел ошибки, который выдает сервис переименования урлов? Ведь именно это и есть отладка! Есть очень полезная директива, которую я вставил в виртуальные хосты для домена engine.bbb.ru. А именно
LogLevel warn rewrite:trace4
После вставки я перезагрузил Apache. И с этого момента в лог ошибок домена, а именно в файл /opt/lampp/logs/engine-bbb-error.2015.08.08.log стали вставляться строки трассировки, относящиеся к модулю rewrite. Строк много. Почему trace4? Может быть можно вставить trace3? Можно. Но тогда нельзя будет отладить RewriteCond, не будет детальной информации что и с каким паттерном мы сравниваем и пропадет информация о некоторых других событиях (не столько важных, сколько интересных).
А что такое »warn»? Буквально наша запись LogLevel означает, что для всех модулей уровень ошибок warn и только для модуля rewrite — trace4
Что мы получаем в результате включения отладки?
Получаем трассировку, или очень-очень подробный лог. Трассировочных строк действительно много. Если я попадаю в сложный затык с правилами, и после какого-то времени мучений у меня ничего не получается, и я принимаю решение включить трассировку, то я в своем .htaccess отключаю все правила, которые не относятся к испытуемому урлу. Ставлю перед ними знак комментария »#». После этого перезагружаю страницу, которая не работает и стараюсь найти нужные строки в логе.
Представляю трассировку рерайтинга со следующими условиями:
Запрашиваем:
http://engine.bbb.ru/
Правила:
RewriteEngine on
RewriteRule ^$ /ind.php [L]
[Sat Aug 08 15:41:38.664920 2015] [rewrite:trace3] [pid 21776] mod_rewrite.c(475): [client 127.0.0.1:45382] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#dd7890/initial] [perdir /opt/lampp/htdocs/bbb/_engine/] strip per-dir prefix: /opt/lampp/htdocs/bbb/_engine/ ->
[Sat Aug 08 15:41:38.664955 2015] [rewrite:trace3] [pid 21776] mod_rewrite.c(475): [client 127.0.0.1:45382] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#dd7890/initial] [perdir /opt/lampp/htdocs/bbb/_engine/] applying pattern '^$' to uri ''
[Sat Aug 08 15:41:38.664960 2015] [rewrite:trace2] [pid 21776] mod_rewrite.c(475): [client 127.0.0.1:45382] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#dd7890/initial] [perdir /opt/lampp/htdocs/bbb/_engine/] rewrite '' -> '/ind.php'
[Sat Aug 08 15:41:38.664966 2015] [rewrite:trace1] [pid 21776] mod_rewrite.c(475): [client 127.0.0.1:45382] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#dd7890/initial] [perdir /opt/lampp/htdocs/bbb/_engine/] internal redirect with /ind.php [INTERNAL REDIRECT]
[Sat Aug 08 15:41:38.665040 2015] [rewrite:trace3] [pid 21776] mod_rewrite.c(475): [client 127.0.0.1:45382] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#dde8b8/initial/redir#1] [perdir /opt/lampp/htdocs/bbb/_engine/] strip per-dir prefix: /opt/lampp/htdocs/bbb/_engine/ind.php -> ind.php
[Sat Aug 08 15:41:38.665044 2015] [rewrite:trace3] [pid 21776] mod_rewrite.c(475): [client 127.0.0.1:45382] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#dde8b8/initial/redir#1] [perdir /opt/lampp/htdocs/bbb/_engine/] applying pattern '^$' to uri 'ind.php'
[Sat Aug 08 15:41:38.665046 2015] [rewrite:trace1] [pid 21776] mod_rewrite.c(475): [client 127.0.0.1:45382] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#dde8b8/initial/redir#1] [perdir /opt/lampp/htdocs/bbb/_engine/] pass through /opt/lampp/htdocs/bbb/_engine/ind.php
[Sat Aug 08 15:09:37.475389 2015] [rewrite:trace3] [pid 21775] mod_rewrite.c(475): [client 127.0.0.1:45327] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#de2740/initial] [perdir /opt/lampp/htdocs/bbb/_engine/] strip per-dir prefix: /opt/lampp/htdocs/bbb/_engine/ ->
[Sat Aug 08 15:09:37.475406 2015] [rewrite:trace3] [pid 21775] mod_rewrite.c(475): [client 127.0.0.1:45327] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#de2740/initial] [perdir /opt/lampp/htdocs/bbb/_engine/] applying pattern '^$' to uri ''
[Sat Aug 08 15:09:37.475411 2015] [rewrite:trace2] [pid 21775] mod_rewrite.c(475): [client 127.0.0.1:45327] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#de2740/initial] [perdir /opt/lampp/htdocs/bbb/_engine/] rewrite '' -> 'ind.php'
[Sat Aug 08 15:09:37.475414 2015] [rewrite:trace3] [pid 21775] mod_rewrite.c(475): [client 127.0.0.1:45327] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#de2740/initial] [perdir /opt/lampp/htdocs/bbb/_engine/] add per-dir prefix: ind.php -> /opt/lampp/htdocs/bbb/_engine/ind.php
[Sat Aug 08 15:09:37.475418 2015] [rewrite:trace2] [pid 21775] mod_rewrite.c(475): [client 127.0.0.1:45327] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#de2740/initial] [perdir /opt/lampp/htdocs/bbb/_engine/] trying to replace prefix /opt/lampp/htdocs/bbb/_engine/ with //bbb.ru
[Sat Aug 08 15:09:37.475420 2015] [rewrite:trace4] [pid 21775] mod_rewrite.c(475): [client 127.0.0.1:45327] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#de2740/initial] add subst prefix: ind.php -> //bbb.ru/ind.php
[Sat Aug 08 15:09:37.475422 2015] [rewrite:trace1] [pid 21775] mod_rewrite.c(475): [client 127.0.0.1:45327] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#de2740/initial] [perdir /opt/lampp/htdocs/bbb/_engine/] internal redirect with //bbb.ru/ind.php [INTERNAL REDIRECT]
[Sat Aug 08 15:09:37.475469 2015] [rewrite:trace3] [pid 21775] mod_rewrite.c(475): [client 127.0.0.1:45327] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#dd8dc8/initial/redir#1] [perdir /opt/lampp/htdocs/bbb/_engine/] add path info postfix: /opt/lampp/htdocs/bbb/_engine/bbb.ru -> /opt/lampp/htdocs/bbb/_engine/bbb.ru/ind.php
[Sat Aug 08 15:09:37.475473 2015] [rewrite:trace3] [pid 21775] mod_rewrite.c(475): [client 127.0.0.1:45327] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#dd8dc8/initial/redir#1] [perdir /opt/lampp/htdocs/bbb/_engine/] strip per-dir prefix: /opt/lampp/htdocs/bbb/_engine/bbb.ru/ind.php -> bbb.ru/ind.php
[Sat Aug 08 15:09:37.475476 2015] [rewrite:trace3] [pid 21775] mod_rewrite.c(475): [client 127.0.0.1:45327] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#dd8dc8/initial/redir#1] [perdir /opt/lampp/htdocs/bbb/_engine/] applying pattern '^$' to uri 'bbb.ru/ind.php'
[Sat Aug 08 15:09:37.475478 2015] [rewrite:trace1] [pid 21775] mod_rewrite.c(475): [client 127.0.0.1:45327] 127.0.0.1 - - [engine.bbb.ru/sid#85a910][rid#dd8dc8/initial/redir#1] [perdir /opt/lampp/htdocs/bbb/_engine/] pass through /opt/lampp/htdocs/bbb/_engine/bbb.ru
Обратите внимание на то, что каждая строка помечена указанием уровня (rewrite: trace_). Видимо, если вы нашли в логе какую-то одну, особо нужную вам строку, и хотите посмотреть только однотипные, то меняете уровень трассировки, перезагружаете Apache и повторяете операцию. Мне лично кажется такой путь не совсем облегчающим задачу. Куда легче, на мой взгляд, сначала скопировать строки в отдельный файл, ориентируясь только по времени операции (по минутам). Потом отделить от них другие нужные строки путем удаления ненужной информации (search-replace). Я сначала даже думал сделать на PHP инструмент для просмотра логов такого рода. Но потом необходимость отпала сама собой (остановлюсь на этом ниже).
Отладка действует именно для того виртуального хоста, для которого указана
Если домен engine.bbb.ru использует внешние стили css, которые берутся с домена bbb.ru, и проблема именно в этом, то не надо включать отладку в пределах виртуального сервера engine.bbb.ru, а надо включать в виртуальном сервере bbb.ru. Тогда все вызовы к домену bbb.ru надо смотреть в логах ошибок (не доступа!) домена bbb.ru. При этом вызовов к трассируемым объектам не будет в логах доступа вообще!
А можно не пользоваться столь стрессовым RewriteEngine вообще?
Можно перейти на использование всего одного скрипта на весь сайт и делать весь рерайтинг в нем. На PHP это сделать легче, да и отлаживать куда проще. Кроме явных преимуществ в вопросах безопасности сайта, мы получаем удобство рерайтинга без нервотрепки. Для того, чтобы перейти на такую схему работы, наш .htaccess должен быть примерно такой:
RewriteEngine on
# правило перенаправления "с www" на "без www"
RewriteCond %{HTTP_HOST} ^www\.our-site\.ru$
RewriteRule ^(.*)$ http://our-site.ru/$1 [R=301,L]
# всего 4 конкретных файла, которые проходят мимо правила.
RewriteCond %{REQUEST_URI} !favicon\.ico$
RewriteCond %{REQUEST_URI} !robots\.txt$
RewriteCond %{REQUEST_URI} !sitemap\.xml$
RewriteCond %{REQUEST_URI} !^/dispatch\.php$
RewriteRule ^.*$ /dispatch.php [L]
И в скрипте dispatch.php очень советую не забыть запретить прямой вызов самого dispatch.php.
Если вам этот подход вдруг захочется перенять, то рекомендую скрипт dispatch.php назвать как-нибудь иначе. Я использовал это название только в целях наглядности.
К слову, этот подход внедряется довольно активно. Этому мы должны быть благодарны внедрению ЧПУ (урлов, понятных для человека, хотя лично для меня они очень непонятны). Практически во всех современных движках он уже действует.
ServerAdmin webmaster@serv1.ru
DocumentRoot "/opt/lampp/htdocs/bbb/_engine"
ServerName "engine.bbb.ru"
ServerAlias "www.engine.bbb.ru"
ScriptAlias /cgi/ "/opt/lampp/cgi-bin/"
ScriptAlias /cgi-bin/ "/opt/lampp/cgi-bin/"
ErrorLog "|/opt/lampp/bin/rotatelogs /opt/lampp/logs/engine-bbb-error.%Y.%m.%d.log 86400"
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" combined
CustomLog "|/opt/lampp/bin/rotatelogs /opt/lampp/logs/engine-bbb-access.%Y.%m.%d.log 86400" combined
# Следующую строку раскомментарить для включения отладки.
# LogLevel warn rewrite:trace4