2

I'm trying to make a web server in Rust for a simple browser game. I want the server to be able to deliver pages through HTTPS, but also be able to communicate through WebSockets. I'm planning to put this server on Heroku, but since they only allow one port per application I have to make the WebSocket server operate on the same port as the other HTTPS code.

It seems like this is possible with crates like rust-websocket, but that crate uses an outdated version of hyper and seems to be no longer maintained. The crate tokio_tungstenite is much more up to date.

The problem is that both hyper and tungstenite have their own implementation of the HTTP protocol that WebSockets operate over with no way to convert between the two. This means that once an HTTPS request has been parsed by either hyper or tungstenite there is no way to continue the processing by the other part, so you can't really try to connect the WebSocket and match on an error in tungstenite and process it by hyper, nor can you parse the request by hyper and check if it's a WebSocket request and send it over to tungstenite. Is there any way to resolve this problem?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Loovjo
  • 534
  • 8
  • 23

2 Answers2

1

I think it should be possible to do that, the tungstenite and tokio-tungstenite allow you to specify custom headers (there are helpers functions for that, prefixed with hdr), so depending on the hyper version you use, if you can convert a request to some form, when the headers can be extracted, you can pass them to tungstenite.

You might also want to try warp crate, it's built on top of hyper and it uses tungstenite under the hood for the websocket support, so if you want to write your own version of warp, you can take a look at the source code (the source code may contain hints on how to use hyper and tungstenite together).

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Daniel
  • 635
  • 1
  • 5
  • 22
  • warp is nice and all but a router with as little as 15 endpoints take around 3 minutes to compile. Same code ported to routerify takes 3 seconds – nikoss Aug 06 '21 at 03:37
0

You can do it, but it's quite fiddly. You'll have to use tokio-tungstenite, do the handshake yourself (check header, set response headers) and spawn a new future on the runtime that will handle the websockets connection. The new future can be created by calling on_upgrade() on the request body with the latest version of hyper, and the connection can then be passed to tokio_tungstenite::WebSocketStream::from_raw_socket to turn it into a websockets connection.

Example handler (note that this doesn't fully check the request headers and assumes we want an upgrade):

fn websocket(req: Request<Body>) -> Result<Response<Body>, &'static str> {
    // TODO check other header
    let key = match req.headers().typed_get::<headers::SecWebsocketKey>() {
        Some(key) => key,
        None => return Err("failed to read ws key from headers"),
    };
    let websocket_future = req
        .into_body()
        .on_upgrade()
        .map_err(|err| eprintln!("Error on upgrade: {}", err))
        .and_then(|upgraded| {
            let ws_stream = tokio_tungstenite::WebSocketStream::from_raw_socket(
                upgraded,
                tokio_tungstenite::tungstenite::protocol::Role::Server,
                None,
            );
            let (sink, stream) = ws_stream.split();
            sink.send_all(stream)
                .map(|_| ())
                .map_err(|err| error!("{}", err))
        });
    hyper::rt::spawn(websocket_future);
    let mut upgrade_rsp = Response::builder()
        .status(StatusCode::SWITCHING_PROTOCOLS)
        .body(Body::empty())
        .unwrap();
    upgrade_rsp
        .headers_mut()
        .typed_insert(headers::Upgrade::websocket());
    upgrade_rsp
        .headers_mut()
        .typed_insert(headers::Connection::upgrade());
    upgrade_rsp
        .headers_mut()
        .typed_insert(headers::SecWebsocketAccept::from(key));
    Ok(upgrade_rsp)
}
Rubén Durá Tarí
  • 1,050
  • 1
  • 10
  • 18