Создание безопасного сервера 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, ознакомьтесь с другим моим руководством: