Интернет-технологии, которые давно стали неотъемлемой частью жизни большей части человечества, постоянно развиваются и совершенствуются.
Не является и исключением и один из самых широко распространённых сетевых протоколов - HTTP, являющейся базой для функционирования Всемирной паутины. Появившись на свет в 1992 году, за прошедшее HTTP претерпел множество усовершенствований и измений включая и его вторую версию — HTTP/2, переход на которую в настоящее время набирает обороты. Так, по состоянию на май 2017 года HTTP/2 используется около 14% всех видимых в сети Интернет сайтов.
Если не останавливаться здесь подробно на преимуществах использования второй версии HTTP над предшествующими (с подробным описанием каковых читатель может ознакомиться самостоятельно в многочисленных статьях доступных в сети), то тезисно можно сказать, что, HTTP/2 существенно быстрее HTTP/1 и HTTP/1.1 благодаря бинарной форме передачи данных, наличию встроенного сжатия заголовков, мультиплексированию соединений, а также возможности приоретизировать передачу данных добиваясь, вследствие всего этого, существенно более быстрой отрисовки сайтов у пользователей.
Будучи клиент-серверным протоколом, все версии HTTP предполагают наличие своей поддержки как в браузерах, так и в веб-серверах. И если говоря о HTTP/2 можно смело заявить о его поддержке в той или иной степени во всех популярных браузерах, то ситуация с серверной частью пока не столь оптимистичная.
Несмотря на наличие поддержки HTTP/2 в трёх популярных веб-серверах Apache, Nginx и IIS, доля которых на март 2017 года в совокупности составляет свыше 80%, реализация данного протокола в них является неполной. Кроме того, к данным веб-серверам есть существенные вопросы по части производительности при отдаче данных по второй версии HTTP протокола.
В связи с вышеизложенным автор обратил внимание на менее распространённые и, следовательно, существенно менее известные веб-серверы с поддержкой HTTP/2, одним из которых и явился высокопроизводительный HTTP сервер H2O.
Согласно исследованиям, H2O обладает лучшей на сегодня поддержкой заложенных стандартом (см. RFC7540 черновик 16 редакции) особенностей HTTP/2 при лучшей производительности. Последнее особенно заметно при использовании при обмене данных криптографических протоколов TLS, которые, по факту, являются неотъемлемой частью в обмене данными по HTTP/2. Также H2O отличают:
Кроме того, несмотря на то, что первый выпуск веб-сервера H2O появился только в 2014 году, проект весьма активно развивается, вбирая в себя новый и улучшая существующий функционал.
Рассмотрим базовую настройку текущей актуальной версии веб-сервера H2O для типовой конфигурации в среде FreeBSD.
root@beta:~ # uname -v
FreeBSD 11.0-RELEASE-p9 #0: Tue Apr 11 08:48:40 UTC 2017 root@amd64-builder.daemonology.net:/usr/obj/usr/src/sys/GENERIC
root@beta:~ # h2o -v
h2o version 2.2.2
OpenSSL: OpenSSL 1.0.2k-freebsd 26 Jan 2017
mruby: YES
Сам сервер можно установить как из пакетов, так и из портов. В последнем случае автор не рекомендует отключать опцию встроенной поддержки скриптинга на Ruby (mruby). Из дальнейшего изложения станет ясной основания для данной рекомендации.
Основной файл конфигурации H2O соответствует стандарту YAML 1.1 и довольно легко читаем.
root@beta:/usr/local/etc/h2o # cat h2o.conf
# see https://h2o.examp1e.net/ for detailed documentation
# see h2o --help for command-line options and settings
# v.20170524 (c)2017 by Max Kostikov http://kostikov.co e-mail: max@kostikov.co
user: www
pid-file: /var/run/h2o.pid
access-log: /var/log/h2o/h2o-access.log
error-log: /var/log/h2o/h2o-error.log
expires: off
file.dirlisting: off
file.send-compressed: on
file.index: [ 'index.html', 'index.php' ]
file.custom-handler:
extension: ".php"
fastcgi.connect:
host: 127.0.0.1
port: 9000
type: tcp
listen:
port: 80
listen:
port: 443
ssl: &default_ssl
cipher-suite: ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS
cipher-preference: server
dh-file: /etc/ssl/dhparams.pem
certificate-file: /usr/local/etc/letsencrypt/live/my.server/fullchain.pem
key-file: /usr/local/etc/letsencrypt/live/my.server/privkey.pem
hosts:
# --- my.sever
"foo.my.server:80":
listen:
port: 80
paths:
"/":
redirect:
status: 301
url: https://foo.my.server/
"foo.my.server:443":
<<: !file /usr/local/etc/h2o/hdr/foo.my.server.h2o
listen:
port: 443
ssl: *default_ssl
paths:
"/":
mruby.handler: |
require "htpasswd.rb"
acl {
deny { ! path.start_with?("/amavis", "/dbadmin", "/stats") }
use Htpasswd.new("/usr/local/etc/h2o/.htpasswd", "admin")
}
file.dir: /usr/local/www
"my.server":
paths:
"/":
mruby.handler: |
Proc.new do |env|
headers = {}
File.open("/usr/local/etc/h2o/hdr/my.server.h2o") do |file|
file.each_line do |line|
data, value = line.chomp.downcase[/'(.*)'/, 1].split(': ', 2)
next if env["SERVER_PORT"] == "80" && ["strict-transport-security", "public-key-pins"].include?(data)
headers[data] = value
end
end
[399, headers, []]
end
file.dir: /usr/local/www/my.server
Для корректной настройки важно понимать, что файл конфигурации имеет четыре уровня — глобальный, хоста, пути и расширений, в зависимости от которых действуют (или не действуют) конфигурационные опции H2O.
Глобальные параметры в приведённом выше файле настроек h2o.conf по большей части очевидны и поняты без комментариев. Обращу лишь внимание на параметр file.custom-handler который определяет внешний обработчик для файлов PHP через обращение к сервису php-fpm посредством протокола FastCGI. Директивы listen сообщают на каких портах сервера будет работать H2O по умолчанию. При желании можно задать конкретные IP-адреса для привязки через параметр host. Далее в параметре ssl перечислены поддерживаемые методы шифрования, порядок их предпочтения и пути к файлам с сертификатами (подробнее о списке шифров см. статью "Усиливаем HTTPS на web-сервере").
Отдельное внимание обращаю на строку &default_ssl там же. Ею задаётся псевдоним всей следующей на более низком уровне настройки шифров и путей. В дальшейшем при конфигурировании виртуальных хостов можно будет обратиться ко всем эти значением простой отсылкой по имени псевдонима без необходимости повторного перечисления тех же значений — см. как это используется в случае с описанием хоста с именем foo.my.server:443. Если же, к примеру, для того же хоста имеется необходимость в изменении параметров скрывающихся за данным псевдонимом, то можно вспользоваться следующей формой обращения к нему.
...
"foo.my.server:443":
<<: !file /usr/local/etc/h2o/hdr/foo.my.server.h2o
listen:
port: 443
ssl:
<<: *default_ssl
certificate-file: /usr/local/etc/letsencrypt/live/foo.my.server/fullchain.pem
key-file: /usr/local/etc/letsencrypt/live/foo.my.server/privkey.pem
...
В данном случае мы воспользовались теми же параметрами шифрования, но со своим сертификатом для нашего виртуального хоста, что весьма удобно при реализации механизма TLS SNI.
Попутно заметьте, что в том же хосте foo.my.server:443 при помощи директивы <<: !file используется включение части конфигурационных настроек из внешнего файла /usr/local/etc/h2o/hdr/foo.my.server.h2o. Это также позволяет вынести типовые и / или повторяющиеся конструкции во внешние файлы включая по мере необходимости просто ссылки на них. В частности, в вышеупомянутом файле содержатся дополнительные заголовки безопасности для протокола HTTPS подробнее о которых уже писалось в этом блоге (см. статью "Заголовки HTTP и безопасность web-сервера").
root@beta:/usr/local/etc/h2o # cat hdr/foo.my.server.h2o
header.add: 'Public-Key-Pins: pin-sha256="ZAFB1O+V8DLlZid0V2ceHxA68dxuGUg+apA0k5VCsQw="; pin-sha256="FOFoPdvQnMgX485K10cTf7i0bsSWCvCW3hWgIoWuUW4="; max-age=3456000; includeSubDomains'
header.add: 'Strict-Transport-Security: max-age=15768000; includeSubDomains; preload'
Обратите внимание, что в описании того же хоста foo.my.server:80 чуть выше при обращении к нему по протоколу HTTP данные заголовки не добавляются ввиду того, что в отрыве от TLS они не имеют смысла. Там же производится переадресация на защищённое соединение с HTTP кодом 301 Moved Permanently.
Используемый для этого параметр redirect работает исключительно на уровне путей, поэтому в H2O отсутствует возможность, к примеру, задать глобальную переадресацию с HTTP на HTTPS для всех виртуальных доменов. Именно для этого и применяется раздельная запись для одного и того же хоста при обращении по разным портам (и соответствующих им протоколам). Это, как и в случае с необходимостью использовать различные заголовки, может показаться неудобным в сравнении, к примеру, с конфигурацией веб-сервера Lighttpd, который регулярно упоминается на страницах данного блога, однако тут на помощь приходит встроенная реализация mruby.
Наличие встроенного скриптинга на mruby помогает элегантно решить как вышеупомянутые ограничения, так и многие другие задачи. К примеру, на нём легко реализуется от сколь угодно сложной логики перезаписи путей вплоть до полноценного бэкэнда средствами исключительно самого H2O!
Небольшая демонстрация этой мощи приведена в описании хоста my.server. Функция следующая после оператора mruby.handler отвечает на запросы клиентов как по протоколу HTTP, так и по HTTPS без отдельного описания хоста для каждого из них. Однако, в зависимости от протокола может формировать различный набор заголовков, отдавая их полный набор, перечисленный в файле /usr/local/etc/h2o/hdr/my.server.h2o для данного хоста для HTTPS и исключая заголовки не имеющие смысла совместно с HTTP. Таким образом можно сформировать единый файл заголовков для обоих режимов передачи данных шифрованного и не шифрованного упростив запись конфигурации.
root@beta:/usr/local/etc/h2o # cat hdr/my.server.h2o
header.add: 'X-Content-Type-Options: nosniff'
header.add: 'X-XSS-Protection: 1; mode=block'
header.add: 'X-Frame-Options: DENY'
header.add: 'Public-Key-Pins: pin-sha256="ZAFB1O+V8DLlZid0V2ceHxA68dxuGUg+apA0k5VCsQw="; pin-sha256="FOFoPdvQnMgX485K10cTf7i0bsSWCvCW3hWgIoWuUW4="; max-age=3456000; includeSubDomains'
header.add: 'Strict-Transport-Security: max-age=15768000; includeSubDomains; preload'
Пронаблюдаем как это работает.
root@beta:/usr/local/etc/h2o # curl -I http://my.server
HTTP/1.1 200 OK
Date: Thu, 25 May 2017 17:11:35 GMT
Connection: keep-alive
Content-Length: 2552
Server: h2o/2.2.2
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
x-frame-options: deny
content-type: text/html
last-modified: Sun, 14 May 2017 16:04:18 GMT
etag: "59188002-9f8"
vary: accept-encoding
accept-ranges: bytes
root@beta:/usr/local/etc/h2o # curl -I https://my.server
HTTP/2 200
server: h2o/2.2.2
date: Thu, 25 May 2017 17:11:41 GMT
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
x-frame-options: deny
public-key-pins: pin-sha256="zafb1o+v8dllzid0v2cehxa68dxugug+apa0k5vcsqw="; pin-sha256="fofopdvqnmgx485k10ctf7i0bsswcvcw3hwgiowuuw4="; max-age=3456000; includesubdomains
strict-transport-security: max-age=15768000; includesubdomains; preload
content-type: text/html
last-modified: Sun, 14 May 2017 16:04:18 GMT
etag: "59188002-9f8"
vary: accept-encoding
accept-ranges: bytes
content-length: 2552
Небольшой комментарий к самому коду на Ruby. Используемая функция получает доступ к переменным среды HTTP окружения, получает список уже существующих заголовков, открывает файл с заголовками и разбирает его построчно. Из строк выделяются имена и значения добавляемых заголовков и в зависимости от режима подключения принимается решение добавлять их в ответ или нет. Далее заголовки отправляются клиенту.
Другим примером гибкости в работе со встроенным в H2O скриптингом на mruby может явится реализация механизма базовой HTTP-аутентификации и правил контроля доступа (ACL). Рассмотрим их вновь вернувшись к настройке хоста foo.my.server:443.
Данный хост используется для управления и мониторинга сервером через веб-интерфейс. Различные управляющие страницы доступны при обращении через URL вида https://foo.my.server/ и могут принимать значения перечисленные в методе deny, который блокирует доступ к адресам, заканчивающимся на отличные от них. Если путь правильный, то управление передаётся функции аутентификации из подгруженного чуть выше модуля командой require, которой на вход передаётя путь к файлу .htpasswd и имя области.
Совершенно идентичную функциональность можно получить и на "чистом" mruby не используя механизм ACL.
...
mruby.handler: |
require "htpasswd.rb"
lambda do |env|
if env["PATH_INFO"].start_with?("/amavis", "/dbadmin", "/stats")
Htpasswd.new("/usr/local/etc/h2o/.htpasswd", "admin")
return [399, {}, []]
end
return [403, {'content-type' => 'text/plain'}, ["Forbidden"]]
end
...
Как видно, запись посредством ACL существенно короче.
На этом разрешите завершить небольшое вводное описание возможностей и некоторых моментов в настройке высокопроизводительного веб-сервера H2O. Надеюсь, в дальнейшем продолжить эту тему применительно к конкретным задачам по его развёртыванию для различных практических приложений.
Статья была полезной? Тогда прошу не стесняться и поддерживать деньгами через PayPal или Яндекс.Деньги.