АЛЕКСЕЙ МИЧУРИН
Аккуратная настройка SSI
Документация и описания SSI столь же многочисленны, сколь широко применение этого механизма. И тем удивительнее то, как мало внимания уделяется оптимизации SSI и при написании документации, и при использовании SSI на практике.
С егодня мы обсудим три аспекта настройки механизма SSI. Во-первых, SSI часто используется для включения внешних файлов в состав SHTML-документа. Эти включаемые файлы, естественно, не являются полноценными, законченными HTML-документами и не предназначены для просмотра посетителями. Как запретить доступ к этим файлам? Во-вторых, включаемые файлы могут быть статическими, а могут, в свою очередь, содержать SSI-инструкции. Как регламентировать обработку этих файлов, запретив SSI-интерпретатору обрабатывать заведомо статические документы? И в-третьих, как настроить механизм кэширования SHTML-документов? Известно, что по умолчанию SHTML-документы не кэшируются, но зачастую они не так уж динамичны, чтобы полностью отказаться от кэширования.
Все поставленные задачи мы будем решать средствами сервера Apache, ориентируясь на версию 1.3, которая не торопится сдавать свои позиции. Однако все предлагаемые рецепты будут работать и на Apache 2.0. На существенных отличиях между «старым» и «новым» Apache мы будем останавливаться особо.
Сервер Apache, как известно, работает на множестве платформ. Большинство рецептов, приведённых в этой статье, столь же универсальны. Исключения будут отмечены.
Регламентируем доcтуп
Судя по названию (Server-Side Includes), основным предназначением SSI является включение файлов. Неудивительно, что наиболее часто разработчики используют SSI-директиву include.
Например, можно разместить код шапки страницы, общий для всех страниц, в отдельном файле:
<!-- файл шапки head.shtml -->
<html>
<head>
<title>Название</title>
</head>
<body>
<h1>Название</h1>
<hr>
А потом подключать содержимое этого файла ко всем документам:
<!--#include virtual="head.sthml" -->
<p>Содержимое страницы</p>
</body>
</html>
Это действительно значительно упрощает и разработку, и особенно поддержку. Ведь чтобы изменить дизайн заголовка на всех страницах, теперь достаточно отредактировать всего один файл.
Но возникает вопрос: как запретить пользователю просматривать SSI-«кирпичики», такие как head.ssi, отдельно от результирующего документа? Обычно, когда необходимо надёжно скрыть какие-то файлы от посетителя, их просто размещают за пределами части файловой системы, доступной для сервера. Это идеальное решение, но оно не применимо для нашей задачи, так как SSI позволяет включать только файлы, потенциально доступные посетителю. Иначе SSI представлял бы немалую угрозу безопасности.
Можно предложить множество решений, которые по сути будут являться лишь различными комбинациями двух «полярных» приёмов. Давайте рассмотрим эти два «полярных» решения.
Ограничение доступа средствами SSI
Проблему можно решить, используя только средства SSI. Для этого воспользуемся тем, что переменные окружения основного документа доступны и вставляемому документу.
Давайте создадим в основном документе переменную-флажок:
<!--#set var="SSI_FLAG" value="YES" -->
<!--#include virtual="head.sthml" -->
<p>Содержимое страницы</p>
</body>
</html>
А во вставляемом документе проанализируем, установлен ли флаг:
<!-- файл шапки head.shtml -->
<!--#if expr="$SSI_FLAG" -->
<html>
<head>
<title>Название</title>
</head>
<body>
<h1>Название</h1>
<hr>
<--#else -->
<html>
<head><title>ERROR 999</title></head>
<body><h1>Документ не доступен</h1></body>
</html>
<!--#endif -->
Теперь, если клиент случайно или нарочно запросит напрямую документ head.shtml, то процессор SSI не обнаружит переменную окружения SSI_FLAG, сработает ветка else и посетитель получит сообщение о загадочной ошибке 999.
Если же документ head.shtml используется для включения, то он обнаружит флаг, предварительно установленный нами.
Такой подход может быть оправдан, если у вас нет никакой возможности влиять на настройки сервера. Но, как вы видите, у него есть несколько недостатков.
Во-первых, это усложнение кода, причём и включающего документа, и включаемого. Во-вторых, теперь, даже если включаемый документ содержит только статическую информацию (как в нашем случае), мы всё равно обязаны обработать его SSI-интерпретатором. Естественно, это увеличивает нагрузку на сервер.
Ограничение доступа средствами сервера
Если у вас есть некоторые полномочия по конфигурированию сервера, то можно обойтись гораздо меньшей кровью.
Во-первых, вернёмся к прежней, простой версии шапки, но переименуем файл, скажем, в head.inc:
<!-- файл шапки head.inc -->
<html>
<head>
<title>Название</title>
</head>
<body>
<h1>Название</h1>
<hr>
Во включаемом файле тоже изменим имя файла-шапки:
<!--#include virtual="head.inc" -->
<p>Содержимое страницы</p>
</body>
</html>
А теперь настроим сервер так, чтобы он не показывал посетителю файлы с расширением .inc.
Для определённости мы будем рассматривать настройки применительно к файлу локальной конфигурации .htaccess. Естественно, все упоминаемые директивы могут быть использованы и в файлах глобальных настроек, таких как httpd.conf.
Нам понадобится всего две директивы:
SetEnvIf Request_URI "\.inc$" ssi_part
Deny from env=ssi_part
- SetEnvIf – принадлежит к группе FileInfo. Она позволяет устанавливать переменные окружения, руководствуясь некоторыми условиями. Как видите, мы тестируем запрашиваемый URI на предмет его совпадения с регулярным выражением \.inc$. То есть мы проверяем, заканчивается ли имя запрашиваемого документа символами .inc. Если это так, то сервер установит переменную ssi_part. Мы не указываем явно значения, по умолчанию сервер присвоит ей строку «1».
- Deny – принадлежит к группе Limit. Она запретит доступ к документу, если установлена переменная ssi_part. Напомню, что для правильной работы этих настроек вам может понадобиться директива Order Allow,Deny, описание которой выходит за рамки этой статьи. Если у вас нет опыта использования директив Order, Allow и Deny, обратитесь к документации на Apache.
Переменную ssi_part можно использовать в обработчике ошибки 403. Именно эта ошибка будет возникать при срабатывании директивы Deny.
Как видите, мы вернули документам прежний, предельно простой вид и решили поставленную задачу по ограничению доступа.
Можно поступить ещё проще. Создать отдельную директорию для SSI-«кирпичиков» и разместить в ней файл .htaccess следующего содержания:
SetEnv ssi_part
Deny from env=ssi_part
Теперь все файлы, находящиеся в этой директории, будут недоступны, так сказать, безусловно – независимо от расширения файла и прочего.
Такой подход оправдан, если вы оперируете с большим количеством SSI-компонентов. Тогда действительно удобно обособить их в отдельной директории. Если же таких компонентов один-два, то, возможно, и не стоит «городить огород». Выбор за вами.
Промежуточные решения
На базе двух предложенных решений можно составить множество промежуточных подходов. Скажем, если по правилам хостинга изменять настройки группы Limit запрещено (то есть директива Deny недоступна), но вы имеете право использовать директиву SetEnvIf (она, напомню, принадлежит к группе FileInfo), то устанавливать флаг можно средствами сервера, а проверять его значение – средствами SSI. Это позволит упростить код включающего документа.
Средствами SSI можно проверять и адрес документа, но здесь надо быть осторожным. Дело в том, что переменная REQUEST_URI содержит и строку запроса. То есть если вы проверяете, заканчивается ли REQUEST_URI подстрокой .inc, то злоумышленник может обмануть вашу проверку, запросив ресурс head.inc?index.sthml. Кроме того, возможны недоразумения при передаче строки запроса. Например, такие: index.sthml?var=head.inc. Пользователь запросил обычный STHML-документ, но REQUEST_URI всё-таки заканчивается на .inc, и в доступе будет отказано.
Этого недостатка лишена переменная DOCUMENT_URI; используйте её.
Однако даже использование DOCUMENT_URI не избавляет от проблем с trailing-путями, которые являются головной болью обоих подходов (и, естественно, любых комбинаций этих подходов). Давайте рассмотрим этот наиболее сложный момент на примере второго подхода – ограничение доступа средствами сервера.
Trailing-пути, <Files> и <FilesMatch>
Обработка Request_URI не справляется с trailing-путями. Например, к файлу head.inc можно обратиться, как head.inc/index.shtml. Именно эта строка окажется в переменной Request_URI и в переменной DOCUMENT_URI. При этом наша проверка не сработает, а документ head.inc будет выдан клиенту.
Своеобразное решение этой проблемы предлагает Apache версии 2.0. Дело в том, что по умолчанию Apache 2.0 теперь не обслуживает trailing-пути и переменную PATH_INFO для SSI-документов. 1.3-образное поведение можно вернуть с помощью специальной директивы AcceptPathInfo. Для надёжного решения в Apapche 1.3 так и хочется использовать секцию <Files> или <FilesMatch>:
<FilesMatch "\.(inc|ssi)$">
Deny from all
</FilesMatch>
На этот раз мы проверяем именно имя файла, и, казалось бы, теперь мы точно заблокировали просмотр файлов с расширениями .inc и .ssi. Но наша блокировка оказывается слишком жёсткой. Мы запретили доступ к этим файлам и для SSI-интерпретатора. Теперь он тоже не сможет получить их.
Чтобы застраховаться от trailing-путей, можно добавить ещё одну проверку:
SetEnvIf Request_URI "\.inc/" ssi_part
Правда, при этом мы получили неприятный побочный эффект – запретили доступ ко всем файлам, расположенным в директориях с расширениями .inc. Думаю, однако, что это не большая потеря.
Аналогичным образом можно поступить и анализируя переменную DOCUMENT_URI средствами SSI, если вы используете некий комбинированный поход.
Регламентируем обработку
Давайте рассмотрим более реалистичный пример использования SSI. Пусть ко всем страницам будет подключаться общий заголовок и общее завершение. Причём заголовок будет не совсем статичный:
<!-- файл шапки head.ssi -->
<html>
<head>
<title><!--#echo var="page_name" --></title>
</head>
<body>
<h1><!--#echo var="page_name" --></h1>
<hr>
Завершение будет статичным:
<!-- файл завершения tail.inc -->
<hr>
</body>
</html>
Подключающие документы будут иметь вид:
<!--#set var="page_name" value="название" -->
<!--#include virtual="head.ssi" -->
<p>Содержимое страницы</p>
<!--#include virtual="tail.inc" -->
Как видите, документ head.ssi должен быть обработан SSI-интерпретатором, но документ tail.inc полностью статичен и в такой обработке не нуждается. Как сэкономить ресурсы сервера и производить обработку только там, где это необходимо?
Мы не случайно дали включаемым файлам разные расширения. Теперь ответ прост: надо включить механизм SSI для файлов с расширением .ssi, и только для них. Для этого используем следующие конфигурационные директивы:
AddHandler server-parsed .ssi
AddType text/html .ssi
Первая – включает SSI-интерпретатор для .ssi-файлов. Вторая – ассоциирует их с MIME-типом text/html; это чисто косметическая мера, SSI-интерпретатор не обращает внимания на MIME-тип включаемой информации; эту директиву можно и опустить.
Конечно, мы предполагаем, что SSI-интерпретация разрешена директивой Options:
Options +Includes
Следует также добавить, что такой вариант подключения SSI справедлив для Apache 1.3. В Apache 2.0 указанные директивы будут работать точно так же, потому что разработчики позаботились об обратной совместимости. Однако на самом деле механизм SSI в Apache 2.0 реализован принципиально иначе (в виде фильтра) и «правильно» подключается иначе. В этой статье я не буду касаться специфики Apache 2.0, все приведённые примеры будут работать одинаково на обоих версиях сервера, но я бы советовал пользователям второй версии хотя бы бегло просмотреть документацию – различий между версиями гораздо больше, чем может показаться.
Теперь файлы с расширением .ssi будут обрабатываться, а файлы .inc будут вставляться «как есть». Кстати, можно было бы и для них прописать MIME-тип.
Если переименовать все файлы затруднительно, то можно воспользоваться директивой XBitHack on. Она, как вы, наверно, знаете, предписывает SSI-интерпретатору обрабатывать файлы с установленным битом исполняемости (бит x).
Это очень удобно, так как позволяет включать и выключать SSI-интерпретацию файла, не меняя его имени, а просто изменяя его атрибуты. Но есть у директивы XBitHack и большой минус – её невозможно использовать, если файловая система не допускает атрибут x. То есть под Windows эта директива абсолютно бесполезна.
Регламентируем кэширование
По понятным причинам сервер не выдаёт для SSI-документов HTTP-заголовки Last-Modified и ETag, что практически полностью исключает возможность их кэширования. Это не всегда полезно, например, если информация обновляется раз в неделю, то кэширование на час-два совсем не помешало бы.
К счастью, сервер может выдавать информацию о том, до какого времени данный документ сохранит свою актуальность. Передаётся она в HTTP-заголовке Expires. Управлять ею можно при помощи трёх директив: ExpiresActive, ExpiresByType и ExpiresDefault.
Директива ExpiresActive допускает два значения аргумента: on или off. Она разрешает или запрещает генерирование заголовка Expires. Обратите внимание: даже если заголовок Expires разрешён, он не будет генерироваться, если он не определён для документа данного MIME-типа. Обратное также верно: если заголовок определён, но его генерация не разрешена, то, естественно, тоже не будет генерироваться.
Директива ExpiresByType описывает, как именно должно вычисляться время актуальности документа данного MIME-типа. Самый простой синтаксис таков:
ExpiresByType MIME-тип <A/M>секунды
Буква A означает, что заданное количество секунд следует прибавить к текущему времени. Буква M – к дате создания/модификации файла.
То есть, чтобы файлы типа text/html сохранялись в кэше в течение часа, директива должна выглядеть так:
ExpiresByType text/html A3600
Время может быть задано и в более «читабельной» форме, например:
ExpiresByType text/html "access plus 1 hours"
Если такая форма записи представляется вам более удобной, обратитесь за подробным описанием к документации на Apache. Никаких дополнительных преимуществ эта форма записи не даёт.
Директива ExpiresDefault описывает, как рассчитывать время для документов, типы которых не были описаны явно с помощью ExpiresByType.
Предостережения
Стоит сделать два предостережения.
Во-первых, ExpiresByType – очень мощное средство. Не забывайте, что большинство CGI-сценариев выдают тип документа text/html. Используя её, следует чётко понимать, что вы описываете кэширование для любого рода информации определённого MIME-типа. Представляете недоумение пользователей чата, если его страницы будут кэшироваться на полчасика?
«Привязаться» к конкретному расширению файла можно с помощью всё тех же блоков <Files> и <FilesMatch>.
Во-вторых, не злоупотребляйте директивой ExpiresDefault. Помните, что её действие распространяется только на файлы, которые не были описаны директивой ExpiresByType. Может показаться, что достаточно разместить в .htaccess директиву:
ExpiresDefault A3600
и она обеспечит часовое кэширование и для HTML-документов, и для различных картинок, и для zip-, и для mp3-файлов, и для всего остального. Это действительно так, если ранее не применялась ни одна директива ExpiresByType. Вы уверены, что администратор сервера не написал где-нибудь в недрах httpd.conf что-то вроде этого?
ExpiresByType image/gif "access plus 1 weeks"
ExpiresByType image/jpeg "access plus 1 weeks"
ExpiresByType image/png "access plus 1 weeks"
Если это так, то ваша директива ExpiresDefault не окажет никакого влияния на файлы перечисленных типов, ведь для них правила уже оговорены.
Поэтому старайтесь явно описывать каждый MIME-тип.
Итого
Давайте подведём некоторые итоги и просуммируем сказанное. Удачной мне представляется следующая конфигурация:
1: Options +Includes
2: AddHandler server-parsed .shtml
3: AddType text/html .shtml
4: AddHandler server-parsed .lshtml
5: AddType text/html .lshtml
6: AddHandler server-parsed .ssi
7: SetEnvIf Request_URI "\.inc$" ssi_part
8: SetEnvIf Request_URI "\.ssi$" ssi_part
9: SetEnvIf Request_URI "\.inc/" ssi_part
10: SetEnvIf Request_URI "\.ssi/" ssi_part
11: Deny from env=ssi_part
12: <Files "*.lshtml">
13: ExpiresActive on
14: ExpiresByType text/html A3600
15: </Files>
Для удобства я пронумеровал строки, конечно, в .htaccess этой нумерации быть не должно.
В первой строке мы включаем SSI-интерпретатор. Очень может быть, что эта мера излишняя. В большинстве случаев эта директива присутствует в глобальных настройках сервера.
В строках 2-5 мы включаем SSI-обработку для файлов .shtml и .lshtml и назначаем им MIME-тип, соответственно. На этот раз указание MIME-типа является необходимой мерой, ведь эти файлы будут доступны посетителям сайта.
В строке 6 включаем SSI-обработку для .ssi-файлов. MIME-тип для них мы не оговариваем – эти файлы будут не доступны для клиентов.
В строках 7-11 мы осуществляем всевозможные проверки и запрещаем непосредственный доступ к .ssi- и .inc-файлам.
Последние четыре строки включают заголовок Expires для .lsthml-файлов.
Таким образом в статье мы оговорили четыре типа файлов:
- .shtml – подлежат SSI-обработке и просмотру – это обычные SHTML-файлы;
- .lshtml – отличаются от обычных только тем, что кэшируются на час;
- .ssi – подлежат обработке, но не просмотру – это динамические SSI-«кирпичики»;
- .inc – не подлежат ни обработке, ни просмотру – это статические SSI-«кирпичики».
Не забывайте, что можно отказаться от многих проверок, разместив файлы в разных директориях.
В директории с обычными SHTML-файлами помещаем .htaccess следующего содержания:
Options +Includes
AddHandler server-parsed .shtml
AddType text/html .shtml
В директории с «долгоиграющими» SHTML-документами:
Options +Includes
AddHandler server-parsed .shtml
AddType text/html .shtml
ExpiresActive on
ExpiresByType text/html A3600
Мы, как видите, отказались от экзотического расширения .lshtml в пользу классического .shtml. И создавать блок <Files> нам не пришлось.
В директории с SSI-«кирпичиками», требующими дополнительной обработки, помещаем .htaccess со следующими директивами:
Options +Includes
AddHandler server-parsed .shtml
AddType text/html .shtml
SetEnv ssi_part
Deny from env=ssi_part
Я и здесь отказался от расширения .ssi.
В этой же директории можно разместить и статические SSI-компоненты, сообщив им любое расширение, кроме .shtml (я бы предложил классическое .html или вовсе без расширения).
Опять же если вы активно используете аппарат SSI и управляете большим количеством файлов, то создание отдельных директорий может быть весьма кстати. Если же этих файлов не много, то проще написать один большой .htsaccess, положить его в корень сервера и на этом закончить с настройками.
Наконец, опытные администраторы серверов могут включить настройки, подобные приведённым здесь, в секции <Directory>, <DirectoryMatch>, <Files>, <FilesMatch>, <Location> или <LocationMatch>, разместить их в файлах глобальных настроек и сообщить тем самым необходимые настройки множеству виртуальных серверов на массовых хостингах. Или по крайней мере избавиться от нескольких файлов .htaccess, собрав настройки для разных директорий в одном месте.