HTTPS
Волга поддерживает протоколы HTTPS/TLS, реализованные поверх библиотеки rustls.
Если вы не используете набор функций full, убедитесь, что вы включили функцию tls в Cargo.toml:
[dependencies]
volga = { version = "...", features = ["tls"] }
Простой HTTPS сервер
Использование сертификатов разработки
Для локальной разработки и тестирования вы можете использовать автоматическую генерацию самоподписанных сертификатов. Для этого, сначала необходимо подключить функцию dev-cert в Cargo.toml:
[dependencies]
volga = { version = "...", features = ["tls", "dev-cert"] }
Затем, в файле main.rs вы можете включить сертификаты разработки с помощью метода with_dev_cert(). Он принимает перечисление DevCertMode, в котором, доступны два режима:
DevCertMode::Ask— проверяет наличие существующих сертификатов и спрашивает, нужно ли их генерировать, если они отсутствуют.DevCertMode::Auto— автоматически генерирует сертификаты без запроса.
Инфо
В релизных сборках этот метод не выполняется.
use volga::{App, tls::DevCertMode};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new()
.with_tls(|tls| tls
.with_dev_cert(DevCertMode::Ask));
app.map_get("/hello", || async {
"Hello, World!"
});
app.run().await
}
При запуске веб-сервера он будет искать папку cert, в корне проекта, с файлами сертификатов: dev-cert.pem и dev-key.pem. Если они отсутствуют и вы используете DevCertMode::Ask, сервер предложит вам сгенерировать их. Если вы согласитесь, сертификаты будут созданы и использованы автоматически.
Если вы хотите избежать запроса на создание и всегда генерировать отсутствующие сертификаты, используйте DevCertMode::Auto:
use volga::{App, tls::DevCertMode};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new()
.with_tls(|tls| tls
.with_dev_cert(DevCertMode::Auto));
app.map_get("/hello", || async {
"Hello, World!"
});
app.run().await
}
Генерация Self-Signed сертификатов в ручную
Прежде всего, вам необходимо сгенерировать сертификат и закрытый ключ. Для тестирования вы можете использовать следующую команду:
openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost'`
Код для использования сертификата и закрытого ключа
Если вы сгенерировали сертификат и закрытый ключ в папке, где находится ваш Cargo.toml, вы можете просто сделать следующее:
use volga::{App, tls::TlsConfig};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new()
.set_tls(TlsConfig::new());
app.map_get("/hello", || async {
"Hello, World!"
});
app.run().await
}
По умолчанию TlsConfig считывает эти файлы из этой папки и ожидает имена: cert.pem и key.pem. Если вы создали эти файлы в другой папке, вы можете настроить TLS следующим образом:
let config = TlsConfig::from_pem("path/to/certs");
В случае, если у вас другие имена файлов, вы можете указать их отдельно:
let config = TlsConfig::new()
.with_cert_path("path/to/certs/server.pem")
.with_key_path("path/to/certs/server.key");
Вы можете протестировать приведенный выше код, используя команду curl:
> curl -v "https://localhost:7878/hello"
Проверка подлинности клиента
Аутентификация клиента отключена по-умолчанию. Вы можете включить ее как необязательную или обязательную. Основное отличие в том, что в первом случае сервер все равно разрешает анонимные запросы.
Генерация CA сертификата и закрытого ключе
Сначала давайте выполним следующие команды для генерации сертификата CA (Client Authority) и закрытого ключа:
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.pem -subj "/C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=CA"
Необязательная Аутентификация
Таким образом, можно настроить необязательную (опциональную) аутентификацию:
let config = TlsConfig::new()
.with_cert_path("path/to/certs/server.pem")
.with_key_path("path/to/certs/server.key")
.with_optional_client_auth("path/to/certs/ca.pem");
Обязательная Аутентификация
Вот так, можно настроить обязательную аутентификацию:
let config = TlsConfig::new()
.with_cert_path("tests/tls/server.pem")
.with_key_path("tests/tls/server.key")
.with_required_client_auth("path/to/certs/ca.pem");
Затем вам, так же, необходимо сгенерировать клиентский сертификат и закрытый ключ:
openssl req -x509 -newkey rsa:4096 -nodes -keyout client.key -out client.pem -days 365 -subj '/CN=localhost'`
И, наконец, можно протестировать при помощи curl:
> curl --cert client.pem --key client.key --cacert ca.pem https://localhost:7878/hello
Перенаправление HTTPS
Волга также позволяет вам настроить перенаправление с HTTP-запроса на HTTPS. Вы можете настроить его, используя метод with_https_redirection():
let config = TlsConfig::new()
.with_cert_path("path/to/certs/server.pem")
.with_key_path("path/to/certs/server.key")
.with_https_redirection();
HTTP-порт по умолчанию — 7879, но вы можете изменить его на любой другой:
let config = TlsConfig::new()
.with_cert_path("path/to/certs/server.pem")
.with_key_path("path/to/certs/server.key")
.with_https_redirection()
.with_http_port(7979);
Теперь, если вы запустите следующую команду curl, ваш запрос будет перенаправлен на https://localhost:7878/hello:
> curl -v "http://localhost:7979/hello"
Внутри, когда вы запускаете этот код в режиме отладки, он использует Temporary Redirect (307), поскольку кэширование ссылок может привести к нестабильному поведению в среде для разработки. Однако в режиме релиза он отвечает 308 - Permanent Redirect.
На какой хост ведёт редирект
Location строится по хосту, которому был адресован запрос: порт подставляется HTTPS-ный, путь и query остаются как были. Сам хост читается сначала из цели запроса и только потом из заголовка Host — в том порядке, который задаёт RFC 9112 §3.2.2, и это единственный порядок, который вообще что-то находит в HTTP/2, где хост приходит в псевдозаголовке :authority и заголовком Host не становится (RFC 9113 §8.3.1).
Запрос, по которому хост определить нельзя — его нет вовсе, Host пришёл дважды или это не валидный authority, — получает 400, как того требует RFC 9112 §3.2. Перенаправлять его некуда.
Сам слушатель обслуживает и HTTP/1.1, и HTTP/2, поэтому браузер — который на открытый порт идёт по HTTP/1.1 — получает перенаправление независимо от того, включена ли фича http2.
HTTP Strict Transport Security Protocol (HSTS)
HTTP Strict Transport Security (HSTS) — это дополнительное улучшение безопасности, которое указывается веб-сервером с помощью специального заголовка ответа. Когда браузер, поддерживающий HSTS, получает этот заголовок:
- Браузер сохраняет конфигурацию для домена, которая запрещает отправку любых сообщений по HTTP.
- Браузер принудительно переводит все сообщения по HTTPS.
- Браузер не позволяет пользователю использовать ненадежные или недействительные сертификаты.
- Браузер отключает запросы, которые позволяют пользователю временно доверять такому сертификату.
Поскольку HSTS принудительно применяется клиентом, у него есть некоторые ограничения:
- Клиент должен поддерживать HSTS.
- HSTS требует как минимум одного успешного запроса HTTPS для установки политики HSTS.
- Приложение должно проверять каждый запрос HTTP и перенаправлять или отклонять запрос HTTP.
HSTS всегда включен по-умолчанию, однако вы можете настроить его используя метод with_hsts():
use volga::{App, tls::TlsConfig};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new()
.with_tls(|tls| tls
.with_https_redirection()
.with_hsts(|hsts| hsts.with_preload())
);
// Включает HSTS middleware
app.use_hsts();
app.map_get("/hello", || async {
"Hello, World!"
});
app.run().await
}
Затем, если вы запустите этот код, вы получите HTTP-заголовок Strict-Transport-Security вместе с успешным ответом.
Инфо
with_preload() и with_sub_domains() больше не принимают аргументов — они включают соответствующие флаги. Для отключения используйте without_preload() / without_sub_domains(). Все настройки HSTS задаются через замыкание with_hsts(|h| ...) на TlsConfig.
Исключение хостов
Некоторым хостам не стоит говорить «навсегда только HTTPS» — например, localhost, к которому разработчик ходит по обычному HTTP, или внутреннему имени, у которого нет публичного сертификата. Такие хосты перечисляет with_exclude_hosts():
use volga::{App, tls::TlsConfig};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new()
.with_tls(|tls| tls
.with_https_redirection()
.with_hsts(|hsts| hsts
.with_exclude_hosts(["localhost", "internal.example.com"])));
app.map_get("/hello", || async { "Hello, World!" });
app.run().await
}
Хост сопоставляется только по имени, на любом порту: браузер хранит политику HSTS по имени хоста и применяет её, на каком бы порту он к этому хосту ни обратился, — различать порты этому списку просто незачем. Порт в записи игнорируется, как и регистр, окружающие пробелы, префикс user@ и завершающая точка: localhost покрывает и LOCALHOST:8443, и localhost..
Тот же список можно задать в файле конфигурации — как exclude_hosts в секции [tls.hsts_config], — и читается он точно так же. Хост самого запроса берётся из его цели раньше, чем из заголовка Host, поэтому список работает и в HTTP/2.
Больше примеров вы можете найти здесь.