Skip to main content

h2ts_server/
lib.rs

1//! Make a WebSocket carry a raw byte stream, and serve/proxy HTTP/2 over it.
2//!
3//! [`accept`] performs the server-side WebSocket handshake on a hyper request
4//! (pluggable into any hyper/axum route — item 4) and yields the upgraded
5//! connection as a byte stream. [`bridge`] then pumps bytes full-duplex between
6//! that stream and any `AsyncRead + AsyncWrite` peer (a TCP upstream, an
7//! in-process h2c server, …). WebSocket message *payloads* become a continuous
8//! byte stream, so h2c framing rides straight through.
9//!
10//! Framing is done by [`wslay`](https://github.com/tatsuhiro-t/wslay) (vendored
11//! C, via the `wslay-sys` crate). Driven through its event API with buffering
12//! off, wslay streams each frame's payload **incrementally** — it never holds a
13//! whole frame in memory, no matter how large — and auto-handles ping/close.
14//!
15//! Three entry points sit on top of [`bridge`]:
16//! - [`WsByteStream`] — the WebSocket as an `AsyncRead + AsyncWrite` handle.
17//! - [`serve_h2`] — run any hyper `Service` as HTTP/2 over the tunnel (item 2).
18//! - the `h2ts-proxy` binary — a standalone WS→upstream-h2c proxy (item 3).
19
20use std::io;
21use std::pin::Pin;
22use std::task::{Context, Poll};
23
24use hyper::body::{Body, Incoming};
25use hyper::server::conn::http2;
26use hyper::service::Service;
27use hyper::{Request, Response};
28use hyper_util::rt::{TokioExecutor, TokioIo};
29use tokio::io::{AsyncRead, AsyncWrite, DuplexStream, ReadBuf};
30
31mod handshake;
32mod wslay;
33
34pub use handshake::{
35    accept, accept_with, accept_with_options, is_upgrade_request, offered_protocols, AcceptOptions,
36    UpgradedIo, WebSocketError, DEFAULT_SUBPROTOCOL,
37};
38pub use wslay::{
39    bridge, bridge_with, control_channel, BridgeConfig, CloseFrame, CloseHook, ControlHook,
40    ControlReceiver, KeepAlive, WsControl,
41};
42
43/// Size of the in-memory duplex between the WebSocket pump and the app side.
44const DUPLEX_BUF: usize = 64 * 1024;
45
46/// A WebSocket presented as a raw byte duplex (`AsyncRead + AsyncWrite`) — item
47/// 1 in its "looks like a TCP stream" form.
48///
49/// A [`bridge`] runs on a spawned task pumping the WebSocket to one end of an
50/// in-memory [`tokio::io::duplex`]; this handle is the other end. That makes it
51/// usable anywhere a TCP stream is — most importantly, handed straight to an
52/// h2c server via `serve_connection` to run an in-process HTTP/2 service over
53/// the tunnel (item 2).
54pub struct WsByteStream {
55    inner: DuplexStream,
56}
57
58impl WsByteStream {
59    /// Wrap an upgraded WebSocket byte stream (from [`accept`]), spawning the
60    /// bridge pump onto the current tokio runtime.
61    pub fn new<S>(ws_io: S) -> Self
62    where
63        S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
64    {
65        Self::with_config(ws_io, BridgeConfig::default())
66    }
67
68    /// Like [`WsByteStream::new`], but with control-frame configuration and hooks
69    /// ([`BridgeConfig`]) — e.g. a [`control_channel`] to send control frames, or
70    /// `on_close` to observe the peer's close reason.
71    pub fn with_config<S>(ws_io: S, config: BridgeConfig) -> Self
72    where
73        S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
74    {
75        let (app_side, ws_side) = tokio::io::duplex(DUPLEX_BUF);
76        tokio::spawn(async move {
77            let _ = bridge_with(ws_io, ws_side, config).await;
78        });
79        Self { inner: app_side }
80    }
81}
82
83impl AsyncRead for WsByteStream {
84    fn poll_read(
85        self: Pin<&mut Self>,
86        cx: &mut Context<'_>,
87        buf: &mut ReadBuf<'_>,
88    ) -> Poll<io::Result<()>> {
89        Pin::new(&mut self.get_mut().inner).poll_read(cx, buf)
90    }
91}
92
93impl AsyncWrite for WsByteStream {
94    fn poll_write(
95        self: Pin<&mut Self>,
96        cx: &mut Context<'_>,
97        buf: &[u8],
98    ) -> Poll<io::Result<usize>> {
99        Pin::new(&mut self.get_mut().inner).poll_write(cx, buf)
100    }
101
102    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
103        Pin::new(&mut self.get_mut().inner).poll_flush(cx)
104    }
105
106    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
107        Pin::new(&mut self.get_mut().inner).poll_shutdown(cx)
108    }
109}
110
111/// Serve **any** hyper HTTP/2 service over a WebSocket tunnel (item 2).
112///
113/// `service` is any `hyper::service::Service<Request<Incoming>>` — a
114/// `service_fn`, an `axum::Router`, a `tower` service via hyper's compat, etc.
115/// The WebSocket is presented as a byte stream via [`WsByteStream`] and the
116/// request/response traffic is real HTTP/2 (h2c, prior-knowledge) on top.
117///
118/// Typical use in a route handler:
119/// ```ignore
120/// let (response, ws_fut) = h2ts_server::accept(&mut req)?;
121/// tokio::spawn(async move {
122///     if let Ok(ws) = ws_fut.await {
123///         let _ = h2ts_server::serve_h2(ws, my_service).await;
124///     }
125/// });
126/// Ok(response) // send the 101 back
127/// ```
128///
129/// For custom HTTP/2 settings, build the connection yourself over
130/// [`WsByteStream::new`] instead.
131pub async fn serve_h2<S, Svc, B>(ws_io: S, service: Svc) -> hyper::Result<()>
132where
133    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
134    Svc: Service<Request<Incoming>, Response = Response<B>> + Send + 'static,
135    Svc::Future: Send + 'static,
136    Svc::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
137    B: Body + Send + 'static,
138    B::Data: Send,
139    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
140{
141    serve_h2_with(ws_io, service, BridgeConfig::default()).await
142}
143
144/// Like [`serve_h2`], but with control-frame configuration and hooks
145/// ([`BridgeConfig`]) applied to the underlying WebSocket bridge — send control
146/// frames via a [`control_channel`], observe the peer's close reason, etc.
147pub async fn serve_h2_with<S, Svc, B>(
148    ws_io: S,
149    service: Svc,
150    config: BridgeConfig,
151) -> hyper::Result<()>
152where
153    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
154    Svc: Service<Request<Incoming>, Response = Response<B>> + Send + 'static,
155    Svc::Future: Send + 'static,
156    Svc::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
157    B: Body + Send + 'static,
158    B::Data: Send,
159    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
160{
161    let io = TokioIo::new(WsByteStream::with_config(ws_io, config));
162    http2::Builder::new(TokioExecutor::new())
163        .serve_connection(io, service)
164        .await
165}