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) 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.
18//! - the `h2ts-proxy` binary — a standalone WS→upstream-h2c proxy.
19//!
20//! Part of [h2ts](https://github.com/debdattabasu/h2ts): this server pairs with
21//! the TypeScript [`@debdattabasu/h2ts`](https://www.npmjs.com/package/@debdattabasu/h2ts)
22//! and Rust/WASM [`h2ts-client`](https://crates.io/crates/h2ts-client) frontend
23//! clients — both behavior-mirrored against it by the shared conformance suite —
24//! or any client that forwards a byte stream over a WebSocket.
25
26use std::io;
27use std::pin::Pin;
28use std::task::{Context, Poll};
29use std::time::Duration;
30
31use hyper::body::{Body, Incoming};
32use hyper::server::conn::http2;
33use hyper::service::Service;
34use hyper::{Request, Response};
35use hyper_util::rt::{TokioExecutor, TokioIo};
36use tokio::io::{AsyncRead, AsyncWrite, DuplexStream, ReadBuf};
37
38mod handshake;
39mod idle;
40mod wslay;
41
42pub use handshake::{
43    accept, accept_with, accept_with_options, is_upgrade_request, offered_protocols, AcceptOptions,
44    UpgradedIo, WebSocketError, DEFAULT_SUBPROTOCOL,
45};
46pub use wslay::{
47    bridge, bridge_with, control_channel, BridgeConfig, CloseFrame, CloseHook, ControlHook,
48    ControlReceiver, KeepAlive, WsControl,
49};
50
51/// Size of the in-memory duplex between the WebSocket pump and the app side.
52const DUPLEX_BUF: usize = 64 * 1024;
53
54/// A WebSocket presented as a raw byte duplex (`AsyncRead + AsyncWrite`) — item
55/// 1 in its "looks like a TCP stream" form.
56///
57/// A [`bridge`] runs on a spawned task pumping the WebSocket to one end of an
58/// in-memory [`tokio::io::duplex`]; this handle is the other end. That makes it
59/// usable anywhere a TCP stream is — most importantly, handed straight to an
60/// h2c server via `serve_connection` to run an in-process HTTP/2 service over
61/// the tunnel (item 2).
62pub struct WsByteStream {
63    inner: DuplexStream,
64}
65
66impl WsByteStream {
67    /// Wrap an upgraded WebSocket byte stream (from [`accept`]), spawning the
68    /// bridge pump onto the current tokio runtime.
69    pub fn new<S>(ws_io: S) -> Self
70    where
71        S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
72    {
73        Self::with_config(ws_io, BridgeConfig::default())
74    }
75
76    /// Like [`WsByteStream::new`], but with control-frame configuration and hooks
77    /// ([`BridgeConfig`]) — e.g. a [`control_channel`] to send control frames, or
78    /// `on_close` to observe the peer's close reason.
79    pub fn with_config<S>(ws_io: S, config: BridgeConfig) -> Self
80    where
81        S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
82    {
83        let (app_side, ws_side) = tokio::io::duplex(DUPLEX_BUF);
84        tokio::spawn(async move {
85            let _ = bridge_with(ws_io, ws_side, config).await;
86        });
87        Self { inner: app_side }
88    }
89}
90
91impl AsyncRead for WsByteStream {
92    fn poll_read(
93        self: Pin<&mut Self>,
94        cx: &mut Context<'_>,
95        buf: &mut ReadBuf<'_>,
96    ) -> Poll<io::Result<()>> {
97        Pin::new(&mut self.get_mut().inner).poll_read(cx, buf)
98    }
99}
100
101impl AsyncWrite for WsByteStream {
102    fn poll_write(
103        self: Pin<&mut Self>,
104        cx: &mut Context<'_>,
105        buf: &[u8],
106    ) -> Poll<io::Result<usize>> {
107        Pin::new(&mut self.get_mut().inner).poll_write(cx, buf)
108    }
109
110    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
111        Pin::new(&mut self.get_mut().inner).poll_flush(cx)
112    }
113
114    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
115        Pin::new(&mut self.get_mut().inner).poll_shutdown(cx)
116    }
117}
118
119/// Serve **any** hyper HTTP/2 service over a WebSocket tunnel (item 2).
120///
121/// `service` is any `hyper::service::Service<Request<Incoming>>` — a
122/// `service_fn`, an `axum::Router`, a `tower` service via hyper's compat, etc.
123/// The WebSocket is presented as a byte stream via [`WsByteStream`] and the
124/// request/response traffic is real HTTP/2 (h2c, prior-knowledge) on top.
125///
126/// The tunnel runs server-initiated WebSocket keepalive **on by default**
127/// ([`BridgeConfig::default`]) so a silently-dead client can't leak the
128/// connection; use [`serve_h2_with`] with a custom [`BridgeConfig`] to tune or
129/// disable it (`keepalive: None`).
130///
131/// Typical use in a route handler:
132/// ```ignore
133/// let (response, ws_fut) = h2ts_server::accept(&mut req)?;
134/// tokio::spawn(async move {
135///     if let Ok(ws) = ws_fut.await {
136///         let _ = h2ts_server::serve_h2(ws, my_service).await;
137///     }
138/// });
139/// Ok(response) // send the 101 back
140/// ```
141///
142/// For custom HTTP/2 settings, build the connection yourself over
143/// [`WsByteStream::new`] instead.
144pub async fn serve_h2<S, Svc, B>(ws_io: S, service: Svc) -> hyper::Result<()>
145where
146    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
147    Svc: Service<Request<Incoming>, Response = Response<B>> + Send + 'static,
148    Svc::Future: Send + 'static,
149    Svc::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
150    B: Body + Send + 'static,
151    B::Data: Send,
152    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
153{
154    serve_h2_with(ws_io, service, BridgeConfig::default()).await
155}
156
157/// Like [`serve_h2`], but with control-frame configuration and hooks
158/// ([`BridgeConfig`]) applied to the underlying WebSocket bridge — send control
159/// frames via a [`control_channel`], observe the peer's close reason, etc.
160///
161/// For an HTTP/2 idle timeout too, use [`serve_h2_with_config`].
162pub async fn serve_h2_with<S, Svc, B>(
163    ws_io: S,
164    service: Svc,
165    config: BridgeConfig,
166) -> hyper::Result<()>
167where
168    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
169    Svc: Service<Request<Incoming>, Response = Response<B>> + Send + 'static,
170    Svc::Future: Send + 'static,
171    Svc::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
172    B: Body + Send + 'static,
173    B::Data: Send,
174    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
175{
176    serve_h2_with_config(
177        ws_io,
178        service,
179        ServeConfig {
180            bridge: config,
181            idle_timeout: None,
182        },
183    )
184    .await
185}
186
187/// Configuration for [`serve_h2_with_config`]: the WebSocket [`BridgeConfig`]
188/// plus an optional HTTP/2 idle timeout.
189#[derive(Default)]
190pub struct ServeConfig {
191    /// WebSocket bridge configuration (keepalive, control frames, close hooks).
192    /// [`ServeConfig::default`] uses [`BridgeConfig::default`] — keepalive on.
193    pub bridge: BridgeConfig,
194    /// If `Some`, gracefully close the connection — an HTTP/2 `GOAWAY`, then the
195    /// WebSocket close — once it has had **no open HTTP/2 streams** for this long,
196    /// reaping a tunnel that is healthy but idle.
197    ///
198    /// This is distinct from keepalive ([`BridgeConfig::keepalive`]), which
199    /// detects a *dead* peer: a client that dutifully answers keepalive pings but
200    /// opens no streams for `idle_timeout` is still reaped. WebSocket and HTTP/2
201    /// pings are **not** activity — only streams reset the timer. `None` (the
202    /// default) never reaps on idle, matching hyper.
203    pub idle_timeout: Option<Duration>,
204}
205
206/// Like [`serve_h2_with`], but also accepts an HTTP/2 [`idle_timeout`] via
207/// [`ServeConfig`]: after that long with no open HTTP/2 streams, the connection
208/// is closed with a graceful `GOAWAY` (then the WebSocket close), so a healthy
209/// but idle client reconnects fresh. hyper has no built-in idle timeout, so this
210/// tracks open streams and drives `graceful_shutdown` itself; a live stream
211/// (even a quiet one) is never reaped, and pings never reset the timer.
212///
213/// [`idle_timeout`]: ServeConfig::idle_timeout
214pub async fn serve_h2_with_config<S, Svc, B>(
215    ws_io: S,
216    service: Svc,
217    config: ServeConfig,
218) -> hyper::Result<()>
219where
220    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
221    Svc: Service<Request<Incoming>, Response = Response<B>> + Send + 'static,
222    Svc::Future: Send + 'static,
223    Svc::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
224    B: Body + Send + 'static,
225    B::Data: Send,
226    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
227{
228    let ServeConfig {
229        bridge,
230        idle_timeout,
231    } = config;
232    let io = TokioIo::new(WsByteStream::with_config(ws_io, bridge));
233
234    // Wrap the service so we can count open streams and reap on idle.
235    let counter = idle::StreamCounter::new();
236    let service = idle::TrackedService::new(service, counter.clone());
237    let conn = http2::Builder::new(TokioExecutor::new()).serve_connection(io, service);
238
239    let Some(idle) = idle_timeout else {
240        return conn.await;
241    };
242
243    let watch = counter.watch();
244    tokio::pin!(conn);
245    tokio::select! {
246        res = conn.as_mut() => res,
247        // No open streams for `idle`: GOAWAY, admit nothing new, drive to close.
248        () = idle::wait_idle(watch, idle) => {
249            conn.as_mut().graceful_shutdown();
250            conn.as_mut().await
251        }
252    }
253}