Создание безопасного сервера WebSocket с использованием Rust & Warp с Docker
Начало работы с TLS для серверов WebSocket для развертывания безопасных приложений в облаке с помощью Rust.
Итак, у вас есть API на основе Rust / серверная часть с малой задержкой, которую вы теперь хотите подключить к своим внешним клиентам? В этом руководстве рассказывается, как настроить сервер HTTP/WebSocket с TLS и запустить его в контейнере для ваших собственных облачных приложений — от начала до конца.
Примечание. Прежде чем продолжить, убедитесь, что у вас установлены последние версии Rust и Docker.
Настройка проекта Rust
Давайте начнем с настройки проекта Rust, используя Cargo. В этом случае для создания нашего нового проекта hello_rs_wss:
$ cargo new hello_rs_wss
Затем мы должны увидеть такую структуру каталогов:
|-Cargo.toml |-.gitignore |-src | |-main.rs
Прежде чем продолжить, нам нужно будет настроить зависимости для использования Warp. Для этого измените файл Cargo.toml
, чтобы он выглядел следующим образом:
[package] name = "hello_rs_wss" version = "0.1.0" edition = "2022" [dependencies] tokio = {version = "1.4.0", features = ["rt", "rt-multi-thread", "macros"]} warp = {version="*", features = ["tls"]} futures = "*"
Затем вы можете собрать проект для установки зависимостей:
$ cargo build
Начиная с простого HTTP-сервера
Для начала давайте сначала настроим HTTP-сервер с Warp.
Это вернет простую страницу index.html
, когда пользователь запрашивает ее. Для начала создайте index.html
в том же корневом каталоге, что и проект Rust, и измените его так, чтобы он выглядел так:
<!DOCTYPE html> <html> <body> <script> window.onload = () => { const BACKEND_URL = "wss://" + window.location.hostname + ":9231/echo" const socket = new WebSocket(BACKEND_URL) socket.onopen = () => { console.log("Socket Opened") setInterval(_ => socket.send("Hello rust!"), 3000) } socket.onmessage = (msg) => alert(msg.data) socket.onerror = (err) => console.error(err) socket.onclose = () => console.log("Socket Closed") } </script> </body> </html>
Это настраивает базовый клиент WebSocket, который отправит «Hello rust!» на наш сервер WebSocket каждые 3 секунды из браузера клиента.
Затем нам нужно сгенерировать пару открытого и закрытого ключей, которую Warp будет использовать для безопасного обслуживания контента. В этом случае мы можем настроить это, используя (в корневом каталоге проекта):
$ openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.rsa -out cert.pem
Теперь в файле main.rs
мы можем настроить HTTP-сервер, который будет возвращать HTML-код, который мы только что создали, используя простой маршрут Warp:
use warp::Filter; #[tokio::main] async fn main() { let current_dir = std::env::current_dir().expect("failed to read current directory"); let routes = warp::get().and(warp::fs::dir(current_dir)); warp::serve(routes) .tls() .cert_path("cert.pem") .key_path("key.rsa") .run(([0, 0, 0, 0], 9231)).await; }
Теперь вы можете снова запустить это с помощью:
$ cargo run
Это откроет пустую веб-страницу по адресу https://0.0.0.0:9231/index.html и потребует от вас принять предупреждение о сертификате при переходе на сайт в браузере.
Прямо сейчас страница попытается подключиться к нашему серверу WebSocket, которого еще не существует.
Реализация сервера WebSocket
Затем мы можем настроить сервер WebSocket в качестве дополнительного маршрута на сервере Warp. Вы можете изменить файл main.rs
, чтобы он выглядел следующим образом:
use futures::StreamExt; use futures::FutureExt; use warp::Filter; #[tokio::main] async fn main() { let echo = warp::path("echo") .and(warp::ws()) .map(|ws: warp::ws::Ws| { ws.on_upgrade(|websocket| { let (tx, rx) = websocket.split(); rx.forward(tx).map(|result| { if let Err(e) = result { eprintln!("websocket error: {:?}", e); } }) }) }); let current_dir = std::env::current_dir().expect("failed to read current directory"); let routes = warp::get().and(echo.or(warp::fs::dir(current_dir))); warp::serve(routes) .tls() .cert_path("cert.pem") .key_path("key.rsa") .run(([0, 0, 0, 0], 9231)).await; }
Теперь это создает дополнительный маршрут с именем echo
, который предоставляет конечную точку WebSocket. Приведенная выше реализация просто отправляет все, что получено через WebSocket, соответствующему клиенту.
Теперь, если вы запустите это с cargo run
и перейдете на тот же сайт, что и раньше (https://0.0.0.0:9231/index.html), он загрузит JavaScript (из index.html
созданного ранее), который попытается подключиться к конечной точке echo
WebSocket и отправлять сообщение каждые 3 секунды.
Вы сможете увидеть, как клиент получает ответ от сервера через механизм alert(...)
, встроенный в браузер.
Примечание. Этот пример создан с использованием Warp docs и комбинации их примеров — взгляните на базовый TLS и базовый WebSocket.
Создание и запуск с помощью Docker
Теперь, когда у нас есть работающий безопасный сервер HTTP и WebSocket, давайте создадим контейнер, в котором мы сможем собрать и запустить наше приложение для последующего развертывания.
Для начала создайте файл в корневом каталоге проекта Rust с именем Dockerfile
и добавьте в него следующее содержимое:
# Tells docker to use the latest Rust official image FROM rust:latest # Copy our current working directory into the container COPY ./ ./ # Create the release build RUN cargo build --release # Generate our self signed certs (change these parameters accordingly!) RUN openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.rsa -out cert.pem \ -subj "/C=GB/ST=London/L=London/O=Global Security/OU=IT Department/CN=example.com" # Expose the port our app is running on EXPOSE 9231 # Run the application! CMD ["./target/release/hello_rs_wss"]
Это очень простой пример того, как мы можем собрать наше приложение Rust в контейнере с генерацией самоподписанного сертификата в процессе сборки — вам нужно будет соответствующим образом изменить параметры для информации о вашем сертификате.
Затем мы можем собрать и запустить контейнер с помощью следующих команд:
$ docker build -t hello_rs_wss . $ docker run -p 9231:9231 -t hello_rs_wss
В приведенном выше примере будет создан контейнер с именем hello_rs_wss, а затем запущен этот контейнер с открытым портом 9231 для связи с нашим приложением.
Как и раньше, во время работы контейнера вы можете снова перейти по тому же URL-адресу, что и раньше, локально, чтобы получить доступ к приложению и проверить, все ли работает — https://0.0.0.0:9231/index.html (не забывая принять риск самозаверяющих сертификатов).
Вот и все! Теперь у вас есть:
- Настройте приложение Rust с зависимостями, управляемыми Cargo.
- Созданы собственные сертификаты для TLS-шифрования связи HTTP и WebSocket.
- Созданы и открыты конечные точки HTTP и WebSocket.
- Продемонстрировано безопасное подключение клиентского интерфейса к внутреннему API.
- Создано и запущено все приложение в Docker, готовое к быстрому развертыванию в облаке.
Что дальше?
Теперь, когда у вас есть работающее приложение, вот несколько отличных мест для дальнейшего улучшения вашего проекта:
- Настройте распознаваемые браузером сертификаты с ЦС (например, Let’s Encrypt)
- Увеличьте размер сборки Docker с помощью нескольких этапов
- Разверните свое приложение в облачном провайдере (например, AWS)
Если вы также заинтересованы в создании HTTP-сервера с actix-web в Rust и Docker, ознакомьтесь с другим моим руководством: