Apache2. Установка и настройка. Часть 2 из 2

15.04.2024

Теги: ApacheCGIFastCGISSLКонфигурацияНастройкаСерверУстановка

Настройка поддержки HTTPS

У нас тестовый веб-сервер, нужно выпустить сертификат для локального домена example.net. Это делается в несколько этапов.

Сначала создаем приватный и публичный ключ Центра Сертификации (CA, Certificate Authority). Потом публичный ключ CA подписываем приватным ключом CA. Таким образом получаем самоподписанный сертификат CA, который надо добавить в доверенные в браузере.

Далее создаем приватный и публичный ключ для домена example.net. Публичный ключ домена example.net подписываем приватным ключом CA — получаем сертификат для домена. Чтобы подписать публичный ключ домена — нужно создать запрос на подпись.

# mkdir ~/ca
# chdir ~/ca

Для создания пары ключей и подписи будем использовать утилиту openssl

$ openssl команда опции

Создание приватного ключа Центра Сертификации

# openssl genpkey \
>    -aes256 \
>    -algorithm RSA \
>    -pkeyopt rsa_keygen_bits:4096 \
>    -pass pass:qwerty \
>    -out root-ca.key

Команда genpkey (заменяет genrsa, gendh и gendsa) означает создание приватного ключа (generate private key). Алгоритм ключа RSA, дина 4096 бит, зашифрован алгоритмом aes256. Опция -pass задает пароль qwerty для обеспечения безопасности ключа. Опция -out указывает на имя файла для сохранения, без этой опции файл будет выведен в стандартный вывод.

Создание самоподписанного корневого сертификата

# openssl req \
>    -x509 \
>    -new \
>    -key root-ca.key \
>    -days 7300 \
>    -subj "/C=RU/ST=Moscow/L=Moscow/O=Demo Inc/OU=IT Dept/CN=Demo CA" \
>    -addext "basicConstraints = critical,CA:TRUE" \
>    -addext "subjectKeyIdentifier = hash" \
>    -addext "authorityKeyIdentifier = keyid:always,issuer" \
>    -addext "keyUsage = critical,keyCertSign,cRLSign" \
>    -out root-ca.crt
Enter pass phrase for root-ca.key: qwerty

Команда req означает запрос на подпись (CSR, Certificate Signing Request). Опция -new означает создание нового сертификата. Опция -key указывает на имя файла приватного ключа. Опция -days — срок действия сертификата.

Просмотр самоподписанного сертификата (не обязательно, просто для проверки, что все правильно)

# openssl x509 -text -noout -in root-ca.crt

Создание файла приватного ключа и файла запроса на подпись (сразу две операции одной командой)

# openssl req -newkey rsa:3072 \
>    -keyform PEM \
>    -outform PEM \
>    -nodes \
>    -subj "/C=RU/ST=Moscow/L=Moscow/O=Demo Inc/OU=IT Dept/CN=example.net" \
>    -addext "basicConstraints = CA:FALSE" \
>    -addext "subjectKeyIdentifier = hash" \
>    -addext "keyUsage=digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment" \
>    -addext "extendedKeyUsage=serverAuth" \
>    -addext "subjectAltName=DNS:example.net,DNS:www.example.net" \
>    -keyout example-net.key \
>    -out example-net.csr

Просмотр запроса на подпись (не обязательно, просто для проверки, что все правильно)

# openssl req -text -noout -in example-com.csr

Подпись запроса на подпись и получение сертификата на год

# openssl x509 -req \
>    -in example-net.csr \
>    -CA root-ca.crt \
>    -CAkey root-ca.key \
>    -CAcreateserial \
>    -days 365 \
>    -out example-net.crt \
>    --copy_extensions=copyall
Certificate request self-signature ok
subject=C = RU, ST = Moscow, L = Moscow, O = Demo Inc, OU = IT Dept, CN = example.net
Enter pass phrase for root-ca.key: qwerty

Просмотр подписанного сертификата (не обязательно, просто для проверки, что все правильно)

# openssl x509 -text -noout -in example-net.crt

Скопируем приватный ключ и сертификат в директорию /etc/ssl

# cp /root/ca/example-net.crt /etc/ssl/certs/
# cp /root/ca/example-net.key /etc/ssl/private/

Активируем модуль ssl_module

# a2enmod ssl
# systemctl restart apache2

Посмотрим на файл конфигурации

# nano /etc/apache2/mods-available/ssl.conf
<IfModule ssl_module>
    # Один или несколько источников для заполнения PRNG
    # (генератор псевдослучайных чисел) библиотеки SSL
    SSLRandomSeed startup builtin
    SSLRandomSeed startup file:/dev/urandom 512
    SSLRandomSeed connect builtin
    SSLRandomSeed connect file:/dev/urandom 512

    ## Глобальные настройки SSL
    ##
    ## Применяются к серверу в целом и всем виртуальным
    ## хостам с поддержкой SSL

    # MIME-типы для скачивания браузерами действительных
    # и отозванных сертификатов (CRL)
    AddType application/x-x509-ca-cert .crt
    AddType application/x-pkcs7-crl .crl

    # Когда Apache запускается, он должен прочитать файлы сертификатов (см. SSLCertificateFile)
    # и закрытых ключей (см. SSLCertificateKeyFile) виртуальных хостов с поддержкой SSL. Файлы
    # закрытого ключа обычно зашифрованы, поэтому модуль ssl должен запросить пароль.
    SSLPassPhraseDialog  exec:/usr/share/apache2/ask-for-passphrase

    # Во избежание повторяющихся SSL «рукопожатий» для параллельных HTTP-запросов (например,
    # когда веб-браузер загружает несколько картинок одновременно), должно быть разрешено
    # SSL кэширование. Если установить значение none — производительность резко снизится.
    SSLSessionCache         shmcb:${APACHE_RUN_DIR}/ssl_scache(512000)
    # Механизм dbm имеет утечки памяти и не должен использоваться
    #SSLSessionCache                 dbm:${APACHE_RUN_DIR}/ssl_scache
    # Сколько секунд должно пройти до истечения SSLSessionCache. Следует установить 300…900
    # секунд. Зависит от того, сколько времени пользователи находятся на веб-сервере. Если
    # максимальное время 15 минут, тогда значение должно быть 15 * 60 = 900.
    SSLSessionCacheTimeout  600

    # SSL Cipher Suite — наборы SSL шифров, которые могут использовать клиенты. Это алгоритмы,
    # которые защищающают соединение с помощью шифрования. Разрешаем только безопасные шифры.
    SSLCipherSuite HIGH:!aNULL

    #SSLHonorCipherOrder on
    # Значение on предписывает использовать приоритеты наборов шифров, заданные для сервера.
    # Вместо того, чтобы браузеры навязывали свои предпочтения. Значение по умолчанию — off.
    #SSLHonorCipherOrder on

    # Разрешенные SSL протоколы. Доступные значения — all, SSLv3, TLSv1, TLSv1.1, TLSv1.2.
    # Протокол SSL v2 больше не поддерживается. Разрешаем все протоколы, за исключением
    # SSL v3, у которого есть критическая уязвимость.
    SSLProtocol all -SSLv3

    # Значение on разрешает небезопасное повторное согласование с устаревшими браузерами. При
    # этом соединения SSL будут уязвимы для атаки «man middle». Значение по умолчанию — off.
    #SSLInsecureRenegotiation on

    # Индикация имени сервера (SNI или Server Name Indication) — расширение TLS протокола,
    # которое указывает позволяет серверу определить имя виртуального хоста для установки
    # защищенного соединения. Это позволяет иметь несколько доменов, привязанных к одному
    # и тому же ip-адресу и порту. И на каждый домен установить отдельный SSL сертификат.
    # Значение on запрещает браузерам, которые не поддерживают SNI, доступ к виртуальным
    # хостам на основе имен. Значение по умолчанию — off.
    #SSLStrictSNIVHostCheck On
</IfModule>

Редектируем файл конфигурации виртуального хоста

# nano /etc/apache2/sites-available/example.net.conf
<VirtualHost *:80 *:443>
    ServerName example.net
    ServerAlias www.example.net
    ServerAdmin webmaster@example.net

    <IfModule ssl_module>
        SSLEngine On
        SSLCertificateFile /etc/ssl/certs/example-net.crt
        SSLCertificateKeyFile /etc/ssl/private/example-net.key
    </IfModule>

    DocumentRoot /var/www/example.net/html
    DirectoryIndex index.php index.html
    Options -Indexes

    <FilesMatch ".+\.ph(ar|p|tml)$">
        SetHandler "proxy:unix:/run/php/php8.1-fpm-example-net.sock|fcgi://localhost"
    </FilesMatch>

    ErrorLog ${APACHE_LOG_DIR}/example-net-error.log
    CustomLog ${APACHE_LOG_DIR}/example-net-access.log combined
</VirtualHost>

Перезагружаем сервер, чтобы применить новые настройки

# systemctl restart apache2.service

Теперь нужно скопировать корневой сертификат /root/ca/root-ca.crt на свой компьютер и добавить его в доверенные в браузере. Для примера, в браузере Chrome нужно зайти в настройки, перейти в «Безопасность и конфиденциальность», потом «Безопасность», дальше «Настроить сертификаты». И добавить сертификат в «Доверенные корневые центры сертификации».

Модуль преобразования URL

1. Активация модуля

Модуль rewrite_module используется для преобразования URL адресов. С его помощью можно настраивать редиректы, изменять URL адреса, блокировать доступ и т.д.

# a2enmod rewrite

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

# nano /etc/apache2/sites-available/example.net.conf
<VirtualHost *:80 *:443>
    ServerName example.net
    ServerAlias www.example.net
    ServerAdmin webmaster@example.net

    <IfModule rewrite_module>
        # временно, для отладки правил — уровень trace3 для модуля rewrite;
        # доступны уровни детализации лога от trace1 (min) до trace8 (max)
        LogLevel warn rewrite:trace3
        # формат лога ошибок: только дата-время, название модуля и сообщение
        ErrorLogFormat "[%t] [%-m] %M"
        # Разрешить работу модуля rewrite для этого виртуального хоста
        RewriteEngine On
        # перенаправление с http на https с кодом 301 Moved Permanently
        RewriteCond %{HTTPS} =off
        # RewriteCond %{SERVER_PORT} =80
        # RewriteCond %{REQUEST_SCHEME} =http
        RewriteRule .* https://example.net%{REQUEST_URI} [R=301]
    </IfModule>

    <IfModule ssl_module>
        SSLEngine On
        SSLCertificateFile /etc/ssl/certs/example-net.crt
        SSLCertificateKeyFile /etc/ssl/private/example-net.key
    </IfModule>

    DocumentRoot /var/www/example.net/html
    DirectoryIndex index.php index.html
    Options -Indexes

    <FilesMatch ".+\.ph(ar|p|tml)$">
        SetHandler "proxy:unix:/run/php/php8.1-fpm-example-net.sock|fcgi://localhost"
    </FilesMatch>

    ErrorLog ${APACHE_LOG_DIR}/example-net-error.log
    CustomLog ${APACHE_LOG_DIR}/example-net-access.log combined
</VirtualHost>

Перезагружаем сервер, чтобы применить новые настройки

# systemctl restart apache2.service

Набираем в адресной строке браузера http://www.example.net/index.php и смотрим файл лога

# cat /var/log/apache2/example-net-error.log | grep rewrite
[…] [rewrite] … […][…] init rewrite engine with requested uri /index.php
[…] [rewrite] … […][…] applying pattern '.*' to uri '/index.php'
[…] [rewrite] … […][…] pass through /index.php

2. Директивы модуля

Директива RewriteEngine включает или выключает механизм перезаписи во время выполнения. Позволяет отключить правила в определенном контексте, а не комментировать все RewriteRule директивы.

Директива RewriteCond задает условия срабатывания правила перезаписи RewriteRule. Таких условий перед правилом RewriteRule может быть несколько. Директива RewriteCond не является обязательной и может отсутствовать.

Директива RewriteRule — здесь указывается само правило перезаписи, которое для конкретного преобразования должно быть единственным.

3. Директива RewriteRule

Синтаксис директивы

RewriteRule Шаблон Подстановка [Флаги]
  • Шаблон — условие, выполнение которого запускает исполнение правила
  • Подстановка — правило изменения (преобразования) URL
  • [Флаги] — дополняют преобразование URL

Примеры директивы

# Преобразуем URL www.example.net/page/123/ в URL
# www.example.net/index.php?show=page&id=123
# (внутренний редирект)
RewriteRule ^/page/(\d+)/$ /index.php?show=page&id=$1 [L]
# Запрет посещений веб-сайта для робота поисковой системы
# Google (при вызове возвращает ошибку 403 Forbidden)
RewriteCond %{USER_AGENT} Googlebot
# Дефис означает, что преобразование URL не требуется
RewriteRule .* - [F]
# Исправление пропущенной буквы при наборе адреса пользователем
# /index.htm => /index.html, /path/index.htm => /path/index.html
RewriteRule ^(.*)/([a-z]+)\.htm$ $1/$2.html [R=301]

Некоторые флаги директивы

  • [NC] — делает Шаблон нечувствительным к регистру, когда Шаблон применяется к текущему URL
  • [QSA] — добавить строку запроса из исходного URL к строке запроса, созданной правилами перезаписи
  • [L] — остановить процесс преобразования на этом месте и не применять больше никаких правил преобразований
  • [N] — перезапустить процесс преобразований (начав с первого правила). В этом случае URL снова сопоставляется неким условиям, но не оригинальный URL, а URL вышедший из последнего правила преобразования.
  • [F] — сервер возвращает браузеру ошибку с кодом 403.
  • [R] — редирект с кодом ответа браузеру 302 (временно перемещен).
  • [R=code] — редирект с кодом ответа браузеру code.

4. Директива RewriteCond

Синтаксис директивы

RewriteCond Строка Условие [Флаги]
  • Строка — строка, которая будет проверятся на соответствие выражению, указанному в параметре Условие.
  • Условие — это логическое выражение, по которому проверяется параметр Строка. Часто в Условие применяют регулярные выражения.
  • [Флаги] — позволяют задать дополнительные опции, например, можно установить логику объединения правил RewriteCond через логическое И (по умолчанию) или через логическое ИЛИ. Или, будет ли сравнение в условии RewriteCond выполнятся с учетом регистра или без учета регистра.

Примеры нескольких директив RewriteCond

# Одна точка входа, все запросы (кроме файлов и директорий)
# на /index.php (внутренний редирект)
RewriteCond $1 !=favicon.ico [AND]
RewriteCond %{REQUEST_FILENAME} !-f [AND]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule (.*) /index.php
# Редирект с URL www.example.net/index.php?show=page&id=123
# на новый URL www.example.net/page/123/ с кодом 301
RewriteCond %{QUERY_STRING} show=page [NC]
RewriteCond %{QUERY_STRING} id=(\d+) [NC]
RewriteRule .* /page/%1/ [R=301]

Несмотря на то, что директива RewriteCond стоит выше, чем правило RewriteRule, модуль rewrite сначала проверяет строку на соответствие с шаблоном в RewriteRule, и если строка совпадает с шаблоном, он смотрит на указанные выше условия в RewriteCond. Если условия тоже совпадают, происходит преобразование согласно правилу RewriteRule.

Строка может, к примеру, содержать часть или весь URL. Подставить в параметр Строка часть URL можно при помощи переменных подстановки ($1, $2, $3), которые были созданы в соответствующем RewriteRule. Также параметр Строка может содержать различные переменные окружения сервера — %{REQUEST_URI}, %{HTTP_HOST}, %{QUERY_STRING} и т.д.

Условие может иметь вид

  • -d — проверка, что директория существует;
  • -f — проверка, что файл существует.

Дополнительно, перед условием, допускается использование логических символов

  • !Условие — инвертирование значения, т.е. сравниваемая строка должна не соответствовать шаблону условия
  • =УсловиеУсловие считается простой строкой и лексически сравнивается с Строка. Истинно, если эти две строки полностью одинаковы (символ в символ). Если Условие имеет вид "" — это сравнивает Строка с пустой строкой.

Некоторые флаги директивы

  • [NC] (от No Case) — регистр не имеет значения, как в Строка так и в Условие. Этот флаг эффективен только для сравнений между Строка и Условие, он не работает при проверках в файловой системе.
  • [OR] — логическое ИЛИ, достаточно совпадения любого RewriteCond.​
  • [AND] — логическое И, требуется совпадение всех RewriteCond.

5. Переменные подстановки

Если в директивах RewriteCond и/или RewriteRule часть символов заключить в круглые скобки, то можно обращаться к содержимому в этих скобках через переменные $1, $2, $3 и/или %1, %2, %3:

  • $n — позволяет использовать группу символов из шаблона директивы RewriteRule
  • %n — позволяет использовать группу символов из шаблона директивы RewriteCond
# Редирект с адреса с www на адрес без www
RewriteCond %{HTTP_HOST} ^www\.example\.net$ [NC]
RewriteRule ^(.*)$ http://example.net/$1 [R=301]
# Редирект с адреса с www на адрес без www
RewriteCond %{HTTP_HOST} ^www\.(.*) [NC]
RewriteRule ^(.*)$ http://%1/$1 [R=301]
# Редирект с адреса без www на адрес с www
RewriteCond %{HTTP_HOST} ^example\.net$ [NC]
RewriteRule ^(.*)$ http://www.example.net/$1 [R=301]
# Редирект с адреса без www на адрес с www
RewriteCond %{HTTP_HOST} !^www\.(.*) [NC]
RewriteRule ^(.*)$ http://www.%1/$1 [R=301]

6. Внешние и внутрение редиректы

Редиректы могут быть внешние и внутрение. Внешний редирект — отправка клиенту HTTP-кода 301 или 302, которые предписывают запросить новый URL. Внутренний редирект — перезапись URI и поиск подходящего файла, который нужно отдать клиенту.

Допустим, в корневой директории виртуального хоста есть поддиректория info, в которой размещается файл about.html.

# nano /etc/apache2/sites-available/example.net.conf
<VirtualHost *:80>
    # .......... прочие директивы конфигурации ..........
    <IfModule rewrite_module>
        # временно, для отладки правил — уровень trace3 для модуля rewrite;
        # доступны уровни детализации лога от trace1 (min) до trace8 (max)
        LogLevel error rewrite:trace3
        # формат лога ошибок: только дата-время, название модуля и сообщение
        ErrorLogFormat "[%t] [%-m] %M"

        # Разрешить работу модуля rewrite для этого виртуального хоста
        RewriteEngine On

        # Внутренний редирект /info/about.html => /data/index.php?show=info?name=about
        RewriteRule ^/([a-z]+)/([a-z]+)\.html$ /data/index.php?show=$1&name=$2 [L]
    </IfModule>
    # .......... прочие директивы конфигурации ..........
</VirtualHost>

При запросе URI /info/about.html, пользователю кажется, будто оно просматривает файл about.html в директории info, но на самом деле отрабатывает скрипт index.php в директории data, который формирует страницу «на лету».

# nano /var/www/example.net/html/info/about.html
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Чем мы занимаемся</title>
  </head>
  <body>
    <h1>Чем мы занимаемся</h1>
    <p>Файл /var/www/example.net/html/info/about.html</p>
  </body>
</html>
# nano /var/www/example.net/html/data/index.php
<?php
$show = $_GET['show'] ?? '';
$name = $_GET['name'] ?? '';
if (!preg_match('~[a-z]+~', $show)) not_found();
if (!preg_match('~[a-z]+~', $name)) not_found();

$file = $_SERVER['DOCUMENT_ROOT'] . '/' . $show . '/' . $name . '.html';
if (!is_file($file)) not_found();
readfile($file);
echo 'Сформировано скриптом ', __FILE__;

function not_found() {
    header('HTTP/1.1 404 Not Found');
    echo '<h1>404 Page Not Found</h1>';
    echo 'Сформировано скриптом ', __FILE__;
    die();
}

Посмотрим файл лога — какие правила перезаписи сработали

# cat /var/log/apache2/example-net-error.log | grep rewrite
[…] [rewrite] … […][…] init rewrite engine with requested uri /info/about.html
[…] [rewrite] … […][…] applying pattern '^/([a-z]+)/([a-z]+)\\.html$' to uri '/info/about.html'
[…] [rewrite] … […][…] rewrite '/info/about.html' -> '/data/index.php?show=info&name=about'
[…] [rewrite] … […][…] split uri=/data/index.php?show=info&name=about -> uri=/data/index.php, args=show=info&name=about
[…] [rewrite] … […][…] local path result: /data/index.php
[…] [rewrite] … […][…] prefixed with document_root to /var/www/example.net/html/data/index.php
[…] [rewrite] … […][…] go-ahead with /var/www/example.net/html/data/index.php [OK]

Усложним задачу. Мы хотим сообщить клиентам, что страница /info/about.html теперь расположена по другому адресу — это /page/info/about/. Но это виртуальный путь, директория page и ее поддиректории физически не существуют. А показывать страницу /info/about.html будет скрипт /data/index.php.

# nano /etc/apache2/sites-available/example.net.conf
<VirtualHost *:80>
    # .......... прочие директивы конфигурации ..........
    <IfModule rewrite_module>
        # временно, для отладки правил — уровень trace3 для модуля rewrite;
        # доступны уровни детализации лога от trace1 (min) до trace8 (max)
        LogLevel error rewrite:trace3
        # формат лога ошибок: только дата-время, название модуля и сообщение
        ErrorLogFormat "[%t] [%-m] %M"

        # Разрешить работу модуля rewrite для этого виртуального хоста
        RewriteEngine On

        # Внешний редирект /info/about.html => /page/info/about/ с кодом 301
        RewriteRule ^/([a-z]+)/([a-z]+)\.html$ /page/$1/$2/ [R=301]
        # Внутренний редирект /page/info/about/ => /data/index.php?show=info?name=about
        RewriteRule ^/page/([a-z]+)/([a-z]+)/$ /data/index.php?show=$1&name=$2 [L]
    </IfModule>
    # .......... прочие директивы конфигурации ..........
</VirtualHost>

Выполним запрос страницы /info/about.html с помощью утилиты curl

$ curl -i -L http://example.net/info/about.html
HTTP/1.1 301 Moved Permanently
Date: Sat, 06 Apr 2024 09:12:40 GMT
Server: Apache/2.4.52 (Ubuntu)
Location: http://example.net/page/info/about/
Content-Length: 320
Content-Type: text/html; charset=iso-8859-1

HTTP/1.1 200 OK
Date: Sat, 06 Apr 2024 09:12:40 GMT
Server: Apache/2.4.52 (Ubuntu)
Vary: Accept-Encoding
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Чем мы занимаемся</title>
  </head>
  <body>
    <h1>Чем мы занимаемся</h1>
    <p>Файл /var/www/example.net/html/info/about.html</p>
  </body>
</html>
Сформировано скриптом /var/www/example.net/html/data/index.php

Посмотрим файл лога — какие правила перезаписи сработали

# cat /var/log/apache2/example-net-error.log | grep rewrite
[…] [rewrite] … […][…] init rewrite engine with requested uri /info/about.html
[…] [rewrite] … […][…] applying pattern '^/([a-z]+)/([a-z]+)\\.html$' to uri '/info/about.html'
[…] [rewrite] … […][…] rewrite '/info/about.html' -> '/page/info/about/'
[…] [rewrite] … […][…] explicitly forcing redirect with http://example.net/page/info/about/
[…] [rewrite] … […][…] escaping http://example.net/page/info/about/ for redirect
[…] [rewrite] … […][…] redirect to http://example.net/page/info/about/ [REDIRECT/301]

[…] [rewrite] … […][…] init rewrite engine with requested uri /page/info/about/
[…] [rewrite] … […][…] applying pattern '^/([a-z]+)/([a-z]+)\\.html$' to uri '/page/info/about/'
[…] [rewrite] … […][…] applying pattern '^/page/([a-z]+)/([a-z]+)/$' to uri '/page/info/about/'
[…] [rewrite] … […][…] rewrite '/page/info/about/' -> '/data/index.php?show=info&name=about'
[…] [rewrite] … […][…] split uri=/data/index.php?show=info&name=about -> uri=/data/index.php, args=show=info&name=about
[…] [rewrite] … […][…] local path result: /data/index.php
[…] [rewrite] … […][…] prefixed with document_root to /var/www/example.net/html/data/index.php
[…] [rewrite] … […][…] go-ahead with /var/www/example.net/html/data/index.php [OK]

7. Полезные внешние редиректы

Для сайта нужно настроить 301 редирект с домена www.example.net на домен example.net. Или наоборот, с домена example.com на домен www.example.com. Кроме того, нужно настроить редирект с http на https. И желательно — редирект с /some/path/index.php на /some/path/.

# nano /etc/apache2/sites-available/example.net.conf
<VirtualHost *:80 *:443>
    ServerName example.net
    ServerAlias www.example.net
    ServerAdmin webmaster@example.net
    # .......... прочие директивы конфигурации ..........
    <IfModule rewrite_module>
        # Разрешить работу модуля rewrite для этого виртуального хоста
        RewriteEngine On

        # Внешний редирект с http на https с кодом 301 Moved Permanently
        RewriteCond %{HTTPS} =off
        RewriteRule .* https://example.net%{REQUEST_URI} [R=301]

        # Внешний редирект с www.example.net на example.net с кодом 301
        RewriteCond %{HTTP_HOST} ^www [NC]
        RewriteRule .* https://example.net%{REQUEST_URI} [R=301]

        # Внешний 301-й редирект с /some/path/index.php на /some/path/
        RewriteCond %{REQUEST_URI} index\.php$
        # возможны варианты /index.php => / и /path/index.php => /path/
        RewriteRule ^(/|/.+/)index\.php$ https://example.net$1 [R=301]
    </IfModule>
    # .......... прочие директивы конфигурации ..........
</VirtualHost>

Базовая HTTP-авторизация

Базовая авторизация HTTP — один из самых простых методов закрытия доступа к сайту. Давайте создадим файл .htpasswd для доступа двух пользователей с помощью утилиты htpasswd, которая входит в пакет apache2-utils. Опция -c создает новый файл, так что использовать ее нужно только один раз. Обратите внимание, что повтороное использование опции удалит существующий файл паролей.

# htpasswd -c /var/www/example.net/.htpasswd evgeniy
New password: 123456
Re-type new password: 123456
Adding password for user evgeniy
# htpasswd /var/www/example.net/.htpasswd sergey
New password: qwerty
Re-type new password: qwerty
Adding password for user sergey
# cat /var/www/example.net/.htpasswd
evgeniy:$apr1$fLWbkbFP$p9XstREKS16XdvYOhZWoV/
sergey:$apr1$GYo5yHM3$HmiviAK47iP8SD5AkA6Ht/

Защищаем директорию admin виртуального хоста паролем

# nano /etc/apache2/sites-available/example.net.conf
<VirtualHost *:80 *:443>
    ServerName example.net
    ServerAlias www.example.net
    ServerAdmin webmaster@example.net

    <IfModule rewrite_module>
        # Разрешить работу модуля rewrite для этого виртуального хоста
        RewriteEngine On

        # Внешний редирект с http на https с кодом 301 Moved Permanently
        RewriteCond %{HTTPS} =off [OR]
        # Внешний редирект с www.example.net на example.net с кодом 301
        RewriteCond %{HTTP_HOST} ^www [NC]
        RewriteRule .* https://example.net%{REQUEST_URI} [R=301]
    </IfModule>

    <IfModule ssl_module>
        SSLEngine On
        SSLCertificateFile /etc/ssl/certs/example-net.crt
        SSLCertificateKeyFile /etc/ssl/private/example-net.key
    </IfModule>

    DocumentRoot /var/www/example.net/html
    DirectoryIndex index.php index.html
    Options -Indexes

    # Доступ к этой директории только по логину и паролю
    <Directory "/var/www/example.net/html/admin">
        AuthType Basic
        AuthName "Restricted Content"
        AuthUserFile /var/www/example.net/.htpasswd
        Require valid-user
    </Directory>

    <FilesMatch ".+\.ph(ar|p|tml)$">
        SetHandler "proxy:unix:/run/php/php8.1-fpm-example-net.sock|fcgi://localhost"
    </FilesMatch>

    ErrorLog ${APACHE_LOG_DIR}/example-net-error.log
    CustomLog ${APACHE_LOG_DIR}/example-net-access.log combined
</VirtualHost>

Файл конфигурации .htaccess

Файл конфигурации .htaccess (сокращение от «hypertext access») переопределяет для директории настройки, заданные для сервера в целом или виртуального хоста. Нет особого смысла в использовании .htaccess, если есть доступ к главному файлу конфигурации. Чаще всего .htaccess нужен при использовании виртуального хостинга.

Чтобы разрешить переопределение настроек через .htaccess для какой-то директории и ее поддиректорий — нужно задать подходящее значение директивы AllowOverride для этой директории. Для поддиректории можно создать свой .htaccess — настройки в нем будет иметь более высокий приоритет.

# nano /etc/apache2/sites-available/example.net.conf
<VirtualHost *:80>
    ServerName example.net
    ServerAlias www.example.net
    ServerAdmin webmaster@example.net

    DocumentRoot /var/www/example.net/html
    DirectoryIndex index.php index.html
    Options -Indexes

    # Разрешаем изменять настройки через файл .htaccess
    <Directory "/var/www/example.net/html">
        AllowOverride All
    </Directory>
    # .......... прочие директивы конфигурации ..........
</VirtualHost>

Пример файла .htaccess в корневой директории виртуального хоста

AddDefaultCharset utf-8
Options -Indexes 
ErrorDocument 404 /error/404.php
ErrorDocument 403 /error/403.php

Если PHP установлен как модуль Apache — в файле .htaccess можно переопределять настройки, заданные в файле php.ini.

# поиск файлов header.php и footer.php выполняется в директориях,
# которые перечислены в директиве include_path файла php.ini
php_value auto_prepend_file header.php
php_value auto_append_file footer.php

Переменные окружения сервера

В файлах конфигурации Apache можно использовать переменные из операционной системы, которые были созданы до запуска веб-сервера (имя начинается с $). Такие переменные можно создать самостоятельно в файле конфигурации /etc/apache2/envvars. Еще можно использовать переменные среды веб-сервера — их создает сам Apache (имя начинается с %).

%{HTTP_ACCEPT} — заголовок сообщает типы контента, которые клиент способен понимать. Сервер затем выбирает одно из предложений, использует его и информирует клиента о своём выборе заголовком ответа Content-Type.

%{HTTP_COOKIE} — содержит сохраненные HTTP-файлы cookie, ранее отправленные сервером с заголовком Set-Cookie.

%{HTTP_FORWARDED} — содержит информацию с клиентской стороны, которая изменена или потеряна когда в путь запроса вовлечён прокси.

%{HTTP_HOST} — указывает доменное имя сервера или виртуального хоста, куда клиент отправляет HTTP-запрос. Другими словами, это содержимое заголовка Host клиента.

%{HTTP_PROXY_CONNECTION} — определяет, остается ли сетевое соединение открытым после завершения текущей транзакции. Если отправляется keep-alive, соединение является постоянным и не закрытым, что позволяет выполнять последующие запросы в рамках этого соединения.

%{HTTP_REFERER} — содержит адрес предыдущей веб-страницы, с которой была сделана ссылка на запрашиваемую страницу.

%{HTTP_USER_AGENT} — содержит строку-характеристику, которая позволяет идентифицировать тип приложения, операционную систему, поставщика программного обеспечения запрашивающего программного агента.

%{DOCUMENT_ROOT} — содержит значение директивы DocumentRoot текущего виртуального хоста.

%{SERVER_NAME} — содержит значение директивы ServerName или ServerAlias текущего виртуального хоста.

%{SERVER_ADDR} — содержит ip-адрес виртуального хоста, обслуживающего запрос.

%{SERVER_PORT} — содержит номер порта виртуального хоста, обслуживающего запрос.

%{SERVER_PROTOCOL} — содержит протокол, используемый клиентом для запроса.

%{REQUEST_METHOD} — HTTP метод входящего запроса (например, GET, POST, HEAD).

%{QUERY_STRING} — строка запроса (после символа ?, например /some/path/index.php?foo=bar).

%{REMOTE_ADDR} — ip-адрес удалённого хоста (клиента, от которого пришел запрос).

%{REMOTE_HOST} — доменное имя удалённого хоста (клиента, от которого пришел запрос).

%{HTTPS} — содержит строку on для SSL-соединения, в противном случае — строку off.

%{REMOTE_ADDR} — ip-адрес удалённого хоста (см.модуль mod_remoteip).

%{REQUEST_SCHEME} — содержит схему запроса, обычно http или https.

%{REQUEST_URI} — компонент пути запрошенного URI, например /some/path/index.php.

Пользовательские переменные

1. Директивы SetEnv и UnsetEnv

Эти директивы предоставляются модулем env_module и позволяют создать или удалить переменную среды. Контекст переменной — сервер, виртуальный хост, директория, файл .htaccess. Переменная создается после выполнения большинства ранних директив обработки запроса, таких как управление доступом и сопоставление URI с именем файла. Если переменная предназначена для использования на этой ранней стадии (например, в директивах модуля перезаписи) — нужно создать ее с помощью SetEnvIf.

# Есть два виртуальных хоста, которые обслуживают директории develop.example.net и testing.example.net
# для разработки и для тестирования. Время от времени изменения переносятся из директории для разработки
# в директорию для тестирования. Уже протестированные изменения выкладываются на production сервер. Для
# разработки и тестирования нужны разные настройки PHP — например, показ ошибок. При этом виртуальные
# хосты работают с одним пулом процессов PHP-FPM службы, но с двумя разными базами данных (хотя сервер
# БД один). Возможно, для разработки и тестирования есть смысл создать отдельные пулы процессов PHP-FPM.

<VirtualHost *:80>
    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html
    DirectoryIndex index.html
    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

<VirtualHost *:80>
    SetEnv PHPAPP_CONFIG /var/www/develop.example.net/app.config.php
    SetEnv DATABASE_HOST 192.168.110.250
    SetEnv DATABASE_NAME develop_db_name
    SetEnv DATABASE_USER develop_db_user
    SetEnv DATABASE_PASS qwerty123456
    SetEnv DATABASE_PORT 3306

    ServerName develop.example.net
    ServerAdmin webmaster@example.net

    DocumentRoot /var/www/develop.example.net/html
    DirectoryIndex index.php index.html
    Options -Indexes

    <FilesMatch ".+\.ph(ar|p|tml)$">
        SetHandler "proxy:unix:/run/php/php8.1-fpm-example-net.sock|fcgi://localhost"
    </FilesMatch>

    ErrorLog ${APACHE_LOG_DIR}/develop-example-net-error.log
    CustomLog ${APACHE_LOG_DIR}/develop-example-net-access.log combined
</VirtualHost>

<VirtualHost *:80>
    SetEnv PHPAPP_CONFIG /var/www/testing.example.net/app.config.php
    SetEnv DATABASE_HOST 192.168.110.250
    SetEnv DATABASE_NAME testing_db_name
    SetEnv DATABASE_USER testing_db_user
    SetEnv DATABASE_PASS 123456qwerty
    SetEnv DATABASE_PORT 3306

    ServerName testing.example.net
    ServerAdmin webmaster@example.net

    DocumentRoot /var/www/testing.example.net/html
    DirectoryIndex index.php index.html
    Options -Indexes

    <FilesMatch ".+\.ph(ar|p|tml)$">
        SetHandler "proxy:unix:/run/php/php8.1-fpm-example-net.sock|fcgi://localhost"
    </FilesMatch>

    ErrorLog ${APACHE_LOG_DIR}/testing-example-net-error.log
    CustomLog ${APACHE_LOG_DIR}/testing-example-net-access.log combined
</VirtualHost>

Получить в php-скрипте доступ к переменным окружения, заданным в конфигурации виртуального хоста, можно с помощью функции getenv() или через суперглобальный массив $_SERVER.

<h3>Суперглобальный массив $_SERVER</h3>
<pre>
<?php
print_r($_SERVER);
?>
</pre>

<h3>Подключаем файл настроек PHP<h3>
<?php
require_once getenv('PHPAPP_CONFIG');
?>

<h3>Данные для подключения к БД<h3>
<pre>
<?php
echo 'DATABASE_HOST=', $_SERVER['DATABASE_HOST'], PHP_EOL;
echo 'DATABASE_NAME=', $_SERVER['DATABASE_NAME'], PHP_EOL;
echo 'DATABASE_USER=', $_SERVER['DATABASE_USER'], PHP_EOL;
echo 'DATABASE_PASS=', $_SERVER['DATABASE_PASS'], PHP_EOL;
echo 'DATABASE_PORT=', $_SERVER['DATABASE_PORT'], PHP_EOL;
?>
</pre>
<?php
# файл /var/www/develop.example.net/app.config.php
error_reporting(E_ALL);
ini_set('display_errors', true);

2. Директивы SetEnvIf и SetEnvIfNoCase

Модуль setenvif_module позволяет устанавливать переменные среды в зависимости от того, соответствуют ли заголовки http-запроса указанным регулярным выражениям. Эти переменные могут использоваться позже для принятия решений о действиях, которые необходимо предпринять, а также становятся доступными для сценариев CGI и страниц SSI.

SetEnvIf[NoCase] attribute regexp [!]env-variable[=value] [[!]env-variable[=value]]

Первый аргумент attribute может быть

  1. Заголовок HTTP-запроса, например — Host, User-Agent, Referer, Accept-Language. Допускается использование регулярного выражения для указания набора заголовков запроса.
  2. Предопределенное значение из списка — Remote_Host, Remote_Addr, Request_Method, Request_Protocol, Request_URI.
  3. Имя переменной среды веб-сервера, в том числе — установленной выше директивой SetEnvIf[NoCase].

Второй аргумент regexp представляет собой регулярное выражение. Если регулярное выражение соответствует атрибуту, то оцениваются остальные аргументы.

Остальные аргументы задают имена и значения переменных, которые нужно установить. Если значение переменной не задано — устанавливается значение 1. Если перед именем переменной есть ! — эта переменная удаляется.

# Создаем переменную, если заголовок Host равен www.example.net
SetEnvIf Host ^www\.example\.net$ IS_WWW_HOST=yes
# Выполняем редирект на example.net, если существует IS_WWW_HOST
RewriteCond %{ENV:IS_WWW_HOST} =yes
RewriteRule .* https://example.net%{REQUEST_URI} [R=301]
# Создаем переменную, если клиент запрашивает изображение
SetEnvIf Request_URI \.(png|gif|jpg)$ IS_IMG_REQUEST=yes
# Не записываем в лог, если клиент запрашивает изображение
CustomLog ${APACHE_LOG_DIR}/access.log combined env=!IS_IMG_REQUEST

3. Директивы Define и IfDefine

Директива Define позволяет задать переменную, к которой можно обращаться в файлах конфигурации с использованием синтексиса ${VAR}. Переменная всегда определена глобально и не ограничивается областью окружающего раздела конфигурации. Используется вместе с директивой IfDefine, которая позволяет запускать сервер с одним файлом конфигурации — но при этом с разными настройками.

# Есть два виртуальных хоста, каждый из которых может обслуживать две директории — для разработки и
# для тестирования. Время от времени изменения переносятся из директории для разработки в директорию
# для тестирования. Веб-сервер перезапускается, чтобы обслуживать директорию для тестирования вместо
# директории для разработки. Уже протестированные изменения выкладываются на production сервер. А
# веб-сервер опять перезапускается для обслуживания директории для разработки.

Define RUN_TESTING_EXAMPLE_NET
# Define RUN_TESTING_EXAMPLE_ORG

<IfDefine RUN_TESTING_EXAMPLE_NET>
    Define SERVER_EXAMPLE_NET testing.example.net
</IfDefine>
<IfDefine !RUN_TESTING_EXAMPLE_NET>
    Define SERVER_EXAMPLE_NET develop.example.net
</IfDefine>

<IfDefine RUN_TESTING_EXAMPLE_ORG>
    Define SERVER_EXAMPLE_ORG testing.example.org
</IfDefine>
<IfDefine !RUN_TESTING_EXAMPLE_ORG>
    Define SERVER_EXAMPLE_ORG develop.example.org
</IfDefine>

<VirtualHost *:80>
    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html
    DirectoryIndex index.html
    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

<VirtualHost *:80>
    ServerName ${SERVER_EXAMPLE_NET}
    ServerAdmin webmaster@example.net

    DocumentRoot /var/www/${SERVER_EXAMPLE_NET}/html
    DirectoryIndex index.html
    Options -Indexes

    ErrorLog ${APACHE_LOG_DIR}/${SERVER_EXAMPLE_NET}-error.log
    CustomLog ${APACHE_LOG_DIR}/${SERVER_EXAMPLE_NET}-access.log combined
</VirtualHost>

<VirtualHost *:80>
    ServerName ${SERVER_EXAMPLE_ORG}
    ServerAdmin webmaster@example.org

    DocumentRoot /var/www/${SERVER_EXAMPLE_ORG}/html
    DirectoryIndex index.html
    Options -Indexes

    ErrorLog ${APACHE_LOG_DIR}/${SERVER_EXAMPLE_ORG}-error.log
    CustomLog ${APACHE_LOG_DIR}/${SERVER_EXAMPLE_ORG}-access.log combined
</VirtualHost>

Созданную ранее переменную можно удалить, используя директиву UnDefine.

Страницы ошибок 404, 403, 50x

В случае проблемы или ошибки Apache можно настроить для выполнения одного из четырех действий

  1. вывести простое жестко закодированное сообщение об ошибке
  2. вывести настроенное администратором сообщение об ошибке
  3. внутренний редирект на локальный URI для обработки ошибки
  4. внешний редирект на любой URL-адрес для обработки ошибки

Первый вариант используется по умолчанию, варианты 2-4 настраиваются с помощью директивы ErrorDocument, за которой следует код ответа HTTP + URI|URL для редиректа или сообщение. Для варианта по умолчанию можно использовать специальное значение default.

# жестко закодированное сообщение об ошибке Apache
ErrorDocument 500 default
# внешнее перенаправление на какой-то URL-адрес
ErrorDocument 500 http://example.com/cgi-bin/error.cgi
# внутреннее перенаправление на локальный URI
ErrorDocument 500 /error/500.html
ErrorDocument 404 /error/404.html
# настроенное администратором сообщение об ошибке
ErrorDocument 403 "Access denied"

При внутреннем редиректе на локальный URI устанавливаются дополнительные переменные среды. Переменные REDIRECT_URL, REDIRECT_STATUS и REDIRECT_QUERY_STRING будут гарантированно установлены, остальные переменные будут установлены только в том случае, если они существовали до возникновения ошибки. То есть, если до редиректа существовала переменная SERVER_NAME, то после редиректа будет установлена переменная REDIRECT_SERVER_NAME.

Давайте создадим директорию error и разместим в ней страницы ошибок

# mkdir /var/www/example.net/html/error
# nano /var/www/example.net/html/error/404.html
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Страница не найдена</title>
  </head>
  <body>
    <h3>Страница не найдена</h3>
    <p>Файл /var/www/example.net/html/error/404.html</p>
  </body>
</html>
# nano /var/www/example.net/html/error/500.html
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Внутренняя ошибка сервера</title>
  </head>
  <body>
    <h3>Внутренняя ошибка сервера</h3>
    <p>Файл /var/www/example.net/html/error/500.html</p>
  </body>
</html>

Теперь настроим для виртуального хоста example.net показ этих страниц

# nano /etc/apache2/sites-available/example.net.conf
<VirtualHost *:80 *:443>
    ServerName example.net
    ServerAlias www.example.net
    ServerAdmin webmaster@example.net

    <IfModule rewrite_module>
        # разрешить работу модуля rewrite для этого виртуального хоста
        RewriteEngine On

        # Внешний редирект с http на https с кодом 301 Moved Permanently
        RewriteCond %{HTTPS} =off [OR]
        # Внешний редирект с www.example.net на example.net с кодом 301
        RewriteCond %{HTTP_HOST} ^www [NC]
        RewriteRule (.*) https://example.net$1 [R=301]

        # Внешний 301 редирект с /some/path/index.php на /some/path/
        RewriteCond %{REQUEST_URI} index\.php$
        RewriteRule ^(/|/.+/)index\.php$ https://example.net$1 [R=301]
    </IfModule>

    <IfModule ssl_module>
        SSLEngine on
        SSLCertificateFile /etc/ssl/certs/example-net.crt
        SSLCertificateKeyFile /etc/ssl/private/example-net.key
    </IfModule>

    DocumentRoot /var/www/example.net/html
    DirectoryIndex index.php index.html
    Options -Indexes

    # Доступ к этой директории только по логину и паролю
    <Directory "/var/www/example.net/html/admin">
        AuthType Basic
        AuthName "Restricted Content"
        AuthUserFile /var/www/example.net/.htpasswd
        Require valid-user
    </Directory>

    <FilesMatch ".+\.ph(ar|p|tml)$">
        SetHandler "proxy:unix:/run/php/php8.1-fpm-example-net.sock|fcgi://localhost"
    </FilesMatch>

    ErrorDocument 404 /error/404.html
    ErrorDocument 403 /error/403.html
    ErrorDocument 500 /error/500.html
    ErrorDocument 503 /error/503.html

    ErrorLog ${APACHE_LOG_DIR}/example-net-error.log
    CustomLog ${APACHE_LOG_DIR}/example-net-access.log combined
</VirtualHost>

Есть смысл добавить ещё один блок в конфигурацию виртуального хоста, чтобы клиенты не могли запрашивать страницы ошибок напрямую. Тут возможны разные варианты, расмотрим два из них. Мы можем просто запрещать доступ к этим страницам. Или сообщать клиентам, будто этих страниц вовсе нет.

<VirtualHost *:80 *:443>
    # .......... прочие директивы конфигурации ..........
    ErrorDocument 404 /error/404.html
    ErrorDocument 403 /error/403.html
    ErrorDocument 500 /error/500.html
    ErrorDocument 503 /error/503.html
    # Доступ по URI /error/(40x|50x).html возможен только после внутреннего перенаправления,
    # который задается директивой ErrorDocument. При попытке прямого доступа — 403 Forbidden.
    <LocationMatch "^/error/">
        Require env REDIRECT_STATUS
    </LocationMatch>
    # .......... прочие директивы конфигурации ..........
</VirtualHost>
<VirtualHost *:80 *:443>
    # .......... прочие директивы конфигурации ..........
    ErrorDocument 404 /error/404.html
    ErrorDocument 403 /error/403.html
    ErrorDocument 500 /error/500.html
    ErrorDocument 503 /error/503.html
    # При попытке прямого доступа к /error/(40x|50x).html — показываем страницу 404 Not Found
    # (первая директива RewriteRule) или 403 Forbidden (вторая директива RewriteRule).
    <Directory "/var/www/example.net/html/error">
        RewriteCond %{ENV:REDIRECT_STATUS} ^$
        RewriteRule .* - [R=404]
        # RewriteRule .* - [F]
    </Directory>
    # .......... прочие директивы конфигурации ..........
</VirtualHost>

Хорошо бы еще оперативно узнавать о срабатывании директивы ErrorDocument — давайте напишем скрипт, который будет сообщать об ошибках в телегам-чат. И укажем этот скрипт в качестве второго аргумента директивы ErrorDocument.

<VirtualHost *:80 *:443>
    # .......... прочие директивы конфигурации ..........
    ErrorDocument 404 /error/handler.php
    ErrorDocument 403 /error/handler.php
    ErrorDocument 500 /error/handler.php
    ErrorDocument 503 /error/handler.php
    # .......... прочие директивы конфигурации ..........
</VirtualHost>
<?php
$status = isset($_SERVER['REDIRECT_STATUS']) ? $_SERVER['REDIRECT_STATUS'] : 'empty';

$name = $status . '.html';
$file = is_file($name) ? $name : '500.html';
readfile($file);

$url = isset($_SERVER['REDIRECT_URL']) ? $_SERVER['REDIRECT_URL'] : 'empty';
$message = "Произошла ошибка! STATUS=${status}, URL=${url}";
telegram($message);

function telegram($message) {
    $token = '..........';
    $chatId = '.........';
    $data = http_build_query([
        'chat_id' => $chatId,
        'text' => $message
    ]);
    $opts = [
        'http' => [
            'method'  => 'POST',
            'header'  => 'Content-type: application/x-www-form-urlencoded',
            'content' => $data
        ]
    ];
    $context  = stream_context_create($opts);
    $response = file_get_contents('https://api.telegram.org/bot' . $token . '/sendMessage', false, $context);
}

Использование шаблонов

Модуль macro_module предоставляет возможность использования шаблонов в файлах конфигурации. Шаблон — это фрагмент конфигурации, который можно использовать многократно. Внутри блока Macro можно использовать параметры типа ${param} — которые заменяются настоящими значениями.

# a2enmod macro
# systemctl restart apache2.service
<Macro VirtHost ${name} ${host} ${port}>
    <VirtualHost *:${port}>
        ServerName ${host}
        ServerAlias www.${host}
        ServerAdmin webmaster@${host}

        DocumentRoot "/var/www/${host}/html"
        DirectoryIndex index.php index.html
        Options -Indexes

        <FilesMatch ".+\.ph(ar|p|tml)$">
            SetHandler "proxy:unix:/run/php/php8.1-fpm-${name}.sock|fcgi://localhost"
        </FilesMatch>

        ErrorLog ${APACHE_LOG_DIR}/${name}-error.log
        CustomLog ${APACHE_LOG_DIR}/${name}-access.log combined
    </VirtualHost>
</Macro>
Use VirtHost example-org example.org 80
Use VirtHost example-com example.com 443
# рекомендуется отменить определение макроса после его использования
UndefMacro VirtHost

Настройка реверс-прокси

У меня есть виртуальная машина apache, на которую установлен тестовый веб-сервер. Этот веб-сервер обслуживает домены example.net и example.org. Давайте создим еще две виртуальные машины в той же локальной сети — и установим на каждой веб-сервер Apache. А виртуальную машину apache настроим как прокси-сервер, который будет перенаправлять запросы на backend-серверы www-one (ip-адрес 192.168.110.10) и www-two (ip-адрес 192.168.110.20).

Виртуальная машина apache

Нам потребуется основной модуль proxy_module и несколько дополнительных, которые расширяют функционал основного

  • proxy_module — главный модуль Apache для перенаправления соединений
  • proxy_http_module — позволяет использовать прокси для протокола HTTP
  • proxy_balancer_module и lbmethod_byrequests_module — обеспечивают балансировку нагрузки
# a2enmod proxy
# a2enmod proxy_http
# a2enmod proxy_balancer
# a2enmod lbmethod_byrequests
# systemctl restart apache2

Редактируем файл конфигурации виртуального хоста example.net

<VirtualHost *:80 *:443>
    ServerName example.net
    ServerAlias www.example.net
    ServerAdmin webmaster@example.net

    <IfModule rewrite_module>
        RewriteEngine On

        RewriteCond %{HTTPS} =off [OR]
        RewriteCond %{HTTP_HOST} ^www [NC]
        RewriteRule (.*) https://example.net$1 [R=301]

        RewriteCond %{REQUEST_URI} index\.php$
        RewriteRule ^(/|/.+/)index\.php$ https://example.net$1 [R=301]
    </IfModule>

    <IfModule ssl_module>
        SSLEngine on
        SSLCertificateFile /etc/ssl/certs/example-net.crt
        SSLCertificateKeyFile /etc/ssl/private/example-net.key
    </IfModule>

    # Основная директива настройки реверс-прокси: все, что идет после
    # URI / — будет отправлено на бэкенд-сервер. Например, если пришел
    # запрос на /foo — он будет отправлен на http://192.168.110.10/foo
    ProxyPass / http://192.168.110.10/
    # Директива должна иметь такие же значения, как и ProxyPass. Она
    # сообщает Apache, как изменить заголовки в ответе от backend. И
    # браузер клиента будет правильно перенаправлен на прокси-адрес.
    ProxyPassReverse / http://192.168.110.10/
    # Отправлять backend-серверу заголовок Host — так backend будет
    # считать, что он обслуживает домен, а не просто ip-адрес.
    ProxyPreserveHost on
    # ProxyRequests следует отключать при использовании ProxyPass.
    # Это предотвращает использование сервера как forward-прокси.
    ProxyRequests off
    
    ErrorLog ${APACHE_LOG_DIR}/example-net-error.log
    CustomLog ${APACHE_LOG_DIR}/example-net-access.log combined
</VirtualHost>

Редактируем файл конфигурации виртуального хоста example.org

# nano /etc/apache2/sites-available/example.net.conf
<VirtualHost *:80>
    ServerName example.org
    ServerAlias www.example.org
    ServerAdmin webmaster@example.org

    <IfModule rewrite_module>
        RewriteEngine On

        RewriteCond %{HTTP_HOST} ^www [NC]
        RewriteRule (.*) https://example.org$1 [R=301]

        RewriteCond %{REQUEST_URI} index\.php$
        RewriteRule ^(/|/.+/)index\.php$ https://example.org$1 [R=301]
    </IfModule>

    # Основная директива настройки реверс-прокси: все, что идет после
    # URI / — будет отправлено на бэкенд-сервер. Например, если пришел
    # запрос на /foo — он будет отправлен на http://192.168.110.20/foo
    ProxyPass / http://192.168.110.20/
    # Директива должна иметь такие же значения, как и ProxyPass. Она
    # сообщает Apache, как изменить заголовки в ответе от backend. И
    # браузер клиента будет правильно перенаправлен на прокси-адрес.
    ProxyPassReverse / http://192.168.110.20/
    # Отправлять backend-серверу заголовок Host — так backend будет
    # считать, что он обслуживает домен, а не просто ip-адрес.
    ProxyPreserveHost on
    # ProxyRequests следует отключать при использовании ProxyPass.
    # Это предотвращает использование сервера как forward-прокси.
    ProxyRequests off
    
    ErrorLog ${APACHE_LOG_DIR}/example-org-error.log
    CustomLog ${APACHE_LOG_DIR}/example-org-access.log combined
</VirtualHost>

Включаем виртуальный хост example.org

# a2ensite example.net.conf
# systemctl resart apache2.service

Виртуальная машина www-one

Создаем корневую директорию виртуального хоста example.net

# mkdir -p /var/www/example.net/html

Создаем файл конфигурации виртуального хоста example.net

# nano /etc/apache2/sites-available/example.net.conf
<VirtualHost *:80>
    ServerName example.net
    ServerAlias www.example.net
    ServerAdmin webmaster@example.net

    DocumentRoot /var/www/example.net/html
    DirectoryIndex index.php index.html
    Options -Indexes

    <FilesMatch ".+\.ph(ar|p|tml)$">
        SetHandler "proxy:unix:/run/php/php8.1-fpm.sock|fcgi://localhost"
    </FilesMatch>

    ErrorDocument 404 /error/handler.php
    ErrorDocument 403 /error/handler.php
    ErrorDocument 500 /error/handler.php
    ErrorDocument 503 /error/handler.php

    <LocationMatch "^/error/">
        Require env REDIRECT_STATUS
    </LocationMatch>

    ErrorLog ${APACHE_LOG_DIR}/example-net-error.log
    CustomLog ${APACHE_LOG_DIR}/example-net-access.log combined
</VirtualHost>

Устанавливаем службу PHP-FPM для обработки php-скриптов

# apt install php-fpm
Для простоты не будем создавать отдельный пул процессов, а будем использовать тот, что запущен изначально после установки службы PHP-FPM.

Разрешаем взаимодействие сервера Apache и службы PHP-FPM

# a2enmod proxy_fcgi setenvif
# a2enconf php8.1-fpm
# systemctl restart apache2.service

Создаем файлы в корневой директории виртуального хоста

# nano /var/www/example.net/html/index.html
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Виртуальная машина www-one</title>
  </head>
  <body>
    <h3>Виртуальная машина www-one</h3>
    <p>Файл /var/www/example.net/html/index.html</p>
  </body>
</html>
# nano /var/www/example.net/html/phpinfo.php
<h3>Виртуальная машина www-one</h3>
<?php phpinfo(); ?>
# nano /var/www/example.net/html/server.php
<h3>Виртуальная машина www-one</h3>
<pre>
<?php
print_r($_SERVER);
?>
</pre>

Виртуальная машина www-two

Здесь все будет аналогично, так что только файл конфигурации виртуального хоста example.org

# nano /etc/apache2/sites-available/example.org.conf
<VirtualHost *:80>
    ServerName example.org
    ServerAlias www.example.org
    ServerAdmin webmaster@example.org

    DocumentRoot "/var/www/example.org/html"
    DirectoryIndex index.php index.html
    Options -Indexes

    <FilesMatch ".+\.ph(ar|p|tml)$">
        SetHandler "proxy:unix:/run/php/php8.1-fpm.sock|fcgi://localhost"
    </FilesMatch>

    ErrorLog ${APACHE_LOG_DIR}/example-org-error.log
    CustomLog ${APACHE_LOG_DIR}/example-org-access.log combined
</VirtualHost>

Реальный ip-адрес клиента

При использовании прокси сервера теряется правильное значение переменной сервера REMOTE_ADDR — вместо ip-адреса клиента она содержит ip-адрес прокси-сервера. Но при этом прокси сервер добавляет HTTP-заголовок HTTP_X_FORWARDED_FOR и записывает в него ip-адрес клиента. На веб-сервере www-one мы можем восстановить настоящий ip-адрес клиента с помощью модуля remoteip_module.

# a2enmod remoteip
# systemctl restart apache2
# nano /etc/apache2/sites-available/example.net.conf
<VirtualHost *:80>
    ServerName example.net
    ServerAlias www.example.net
    ServerAdmin webmaster@example.net

    DocumentRoot /var/www/example.net/html
    DirectoryIndex index.php index.html
    Options -Indexes

    <IfModule remoteip_module>
        # восстановить настроящий ip-адрес клиента из этого заголовка
        RemoteIPHeader X-Forwarded-For
        # доверять ip-адресу 192.168.110.50, это наш прокси-сервер
        RemoteIPInternalProxy 192.168.110.50
    </IfModule>

    <FilesMatch ".+\.ph(ar|p|tml)$">
        SetHandler "proxy:unix:/run/php/php8.1-fpm.sock|fcgi://localhost"
    </FilesMatch>

    ErrorDocument 404 /error/handler.php
    ErrorDocument 403 /error/handler.php
    ErrorDocument 500 /error/handler.php
    ErrorDocument 503 /error/handler.php

    <LocationMatch "^/error/">
        Require env REDIRECT_STATUS
    </LocationMatch>

    ErrorLog ${APACHE_LOG_DIR}/example-net-error.log
    CustomLog ${APACHE_LOG_DIR}/example-net-access.log combined
</VirtualHost>
# systemctl restart apache2

Балансировка нагрузки

Давайте распределим запросы к example.net между backend-серверами www-one и www-two. Для этого на вартуальной машине www-two добавим виртуальный хост example.net. При этом нам нужно, чтобы содержимое директории /var/www/example.net/html было полностью идентичным на www-one и www-two. Напримерм, с некоторой периодичностью синхронизировать эти директории с помощью rsync. Или вынести эту директорию на отдельный сервер, а www-one и www-two будут ее монтировать по NFS.

Создаем файл конфигурации виртуального хоста example.net на виртуальной машине www-two

# nano /etc/apache2/sites-available/example.net.conf
<VirtualHost *:80>
    ServerName example.net
    ServerAlias www.example.net
    ServerAdmin webmaster@example.net

    DocumentRoot /var/www/example.net/html
    DirectoryIndex index.php index.html
    Options -Indexes

    <IfModule remoteip_module>
        RemoteIPHeader X-Forwarded-For
        RemoteIPInternalProxy 192.168.110.50
    </IfModule>

    <FilesMatch ".+\.ph(ar|p|tml)$">
        SetHandler "proxy:unix:/run/php/php8.1-fpm.sock|fcgi://localhost"
    </FilesMatch>

    ErrorDocument 404 /error/handler.php
    ErrorDocument 403 /error/handler.php
    ErrorDocument 500 /error/handler.php
    ErrorDocument 503 /error/handler.php

    <LocationMatch "^/error/">
        Require env REDIRECT_STATUS
    </LocationMatch>

    ErrorLog ${APACHE_LOG_DIR}/example-net-error.log
    CustomLog ${APACHE_LOG_DIR}/example-net-access.log combined
</VirtualHost>

Редактируем файл конфигурации виртуального хоста example.net на виртуальной машине apache

<VirtualHost *:80 *:443>
    ServerName example.net
    ServerAlias www.example.net
    ServerAdmin webmaster@example.net

    <IfModule rewrite_module>
        RewriteEngine On

        RewriteCond %{HTTPS} =off [OR]
        RewriteCond %{HTTP_HOST} ^www [NC]
        RewriteRule (.*) https://example.net$1 [R=301]

        RewriteCond %{REQUEST_URI} index\.php$
        RewriteRule ^(/|/.+/)index\.php$ https://example.net$1 [R=301]
    </IfModule>

    <IfModule ssl_module>
        SSLEngine on
        SSLCertificateFile /etc/ssl/certs/example-net.crt
        SSLCertificateKeyFile /etc/ssl/private/example-net.key
    </IfModule>

    # Этот URI не надо проксировать на backend-сервер
    ProxyPass /balancer-manager !
    # По URI /balancer-manager будет доступен менеджер
    <Location "/balancer-manager">
        SetHandler balancer-manager
        # Доступ разрешен только с ip-адреса 192.168.110.2
        Require ip 192.168.110.2
    </Location>

    <Proxy balancer://wwwcluster>
        # На backend-сервер www-one балансировщик будет отправлять в
        # два раза больше запросов, чем на backend-сервер www-two
        BalancerMember http://192.168.110.10 loadfactor=2
        BalancerMember http://192.168.110.20 loadfactor=1
        ProxySet lbmethod=byrequests
    </Proxy>

    ProxyPass / balancer://wwwcluster/
    ProxyPassReverse / balancer://wwwcluster/
    ProxyPreserveHost on
    ProxyRequests off

    ErrorLog ${APACHE_LOG_DIR}/example-net-error.log
    CustomLog ${APACHE_LOG_DIR}/example-net-access.log combined
</VirtualHost>

Модуль proxy_balancer использует алгоритм балансировки, который предоставляет модуль lbmethod_byrequests. Можно использовать еще три модуля — lbmethod_bytraffic, lbmethod_bybusyness, lbmethod_heartbeat.

Кроме балансировки нагрузки мы добавили возможность изменения настроек балансировщика на странице https://example.net/balancer-manager. Но изменения, внесенные на этой странице, не сохраняются при перезагрузке веб-сервера. Чтобы это исправить — нужно добавить директиву BalancerPersist и установить для нее значение On.

Обеспечение «липкости»

Балансировщик нагрузки должен обеспечивать «липкость». Когда запрос пересылается на backend-сервер, все последующие запросы от того же пользователя должны быть перенаправлены тот же backend-сервер. Модуль proxy_balancer реализует липкость «липкость» с помощью cookie или параметра запроса. Модуль получает значение route из cookie или параметра — и проксирует запрос на соответствующий backend-сервер. Параметр к каждой ссылке обычно добавляется на backend-е, на прокси-сервере это сделать трудно. А вот добавить cookie легко как на backend-серверах, так и на прокси-сервере. Разумеется, добавлять нужно либо на прокси-сервере, либо на backend-серверах, дублировать не нужно.

Добавляем cookie на прокси-сервере, виртуальная машина apache

<VirtualHost *:80 *:443>
    # .......... прочие директивы конфигурации ..........
    ProxyPass /balancer-manager !
    <Location "/balancer-manager">
        SetHandler balancer-manager
        Require ip 192.168.110.2
    </Location>

    # Добавляем cookie BACKEND_SERVER для обеспечения «липкости»,
    # только если cookie не добавляется на backend-серверах
    Header set Set-Cookie "BACKEND_SERVER=.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED

    <Proxy balancer://wwwcluster>
        BalancerMember http://192.168.110.10 loadfactor=2 route=www-one
        BalancerMember http://192.168.110.20 loadfactor=1 route=www-two
        ProxySet lbmethod=byrequests
        # Имя cookie BACKEND_SERVER, в нее записываем .www-one или .www-two
        ProxySet stickysession=BACKEND_SERVER
    </Proxy>

    ProxyPass / balancer://wwwcluster/
    ProxyPassReverse / balancer://wwwcluster/
    ProxyPreserveHost on
    ProxyRequests off
    # .......... прочие директивы конфигурации ..........
</VirtualHost>

Добавляем cookie на backend-сервере, виртуальная машина www-one

<VirtualHost *:80>
    ServerName example.net
    # .......... прочие директивы конфигурации ..........
    Header set Set-Cookie "BACKEND_SERVER=.www-one; path=/"
    # .......... прочие директивы конфигурации ..........
</VirtualHost>

Добавляем cookie на backend-сервере, виртуальная машина www-two

<VirtualHost *:80>
    ServerName example.net
    # .......... прочие директивы конфигурации ..........
    Header set Set-Cookie "BACKEND_SERVER=.www-two; path=/"
    # .......... прочие директивы конфигурации ..........
</VirtualHost>

Cookie устанавливается при каждом ответе backend-сервера, но можно сделать чуть лучше — устанавливать cookie только в том случае, если не была установлена раньше.

<VirtualHost *:80>
    ServerName example.net
    # .......... прочие директивы конфигурации ..........
    SetEnvIf Cookie BACKEND_SERVER BACKEND_SERVER=yes
    Header set Set-Cookie "BACKEND_SERVER=.www-one; path=/" env=!BACKEND_SERVER
    # .......... прочие директивы конфигурации ..........
</VirtualHost>
<VirtualHost *:80>
    ServerName example.net
    # .......... прочие директивы конфигурации ..........
    SetEnvIf Cookie BACKEND_SERVER BACKEND_SERVER=yes
    Header set Set-Cookie "BACKEND_SERVER=.www-two; path=/" env=!BACKEND_SERVER
    # .......... прочие директивы конфигурации ..........
</VirtualHost>

Поиск: Apache • CGI • FastCGI • SSL • Конфигурация • Настройка • Сервер • Установка

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.