Skip to main content

proxima_primitives/pipe/
upgrade.rs

1//! Connection upgrade / hijack.
2//!
3//! After the H1 listener writes a `200 Connection established` (for
4//! CONNECT) or `101 Switching Protocols` (for Upgrade) response
5//! head, the H1 framing is done with this socket — subsequent bytes
6//! belong to the new protocol. The listener cedes the raw socket to
7//! a Pipe-provided `UpgradeHandler` which drives whatever comes
8//! next: a TCP tunnel, a WebSocket loop, h2c, etc.
9//!
10//! The listener itself stays protocol-agnostic. It only knows: "this
11//! Response carries an upgrade handler — write the head, then call
12//! the handler with the unsplit socket and any bytes I happened to
13//! buffer past the request head."
14//!
15//! For CONNECT, the request body is empty by definition (no
16//! Content-Length, no Transfer-Encoding), so `leftover` is normally
17//! empty too. An aggressive client that pipelined tunnel data ahead
18//! of the server's 200 will have those bytes surfaced via
19//! `leftover` — the handler must drain that before reading the
20//! socket.
21
22#[cfg(feature = "std")]
23use core::future::Future;
24#[cfg(feature = "std")]
25use core::pin::Pin;
26
27#[cfg(feature = "std")]
28use alloc::boxed::Box;
29#[cfg(feature = "std")]
30use bytes::Bytes;
31#[cfg(feature = "std")]
32use futures::io::{AsyncRead, AsyncWrite};
33#[cfg(feature = "std")]
34use proxima_core::ProximaError;
35
36/// Trait alias for the socket types the listener can hand off, keyed on
37/// `futures::io::{AsyncRead, AsyncWrite} + Send + Unpin`. Runtime-agnostic
38/// at the trait surface — tokio-shaped streams cross the boundary via
39/// `tokio_util::compat::TokioAsyncReadCompatExt` inside the listener glue.
40#[cfg(feature = "std")]
41pub trait HijackStream: AsyncRead + AsyncWrite + Send + Unpin {}
42#[cfg(feature = "std")]
43impl<T: AsyncRead + AsyncWrite + Send + Unpin + ?Sized> HijackStream for T {}
44
45/// Socket handed to an upgrade handler. Owns the underlying stream
46/// (the listener has dropped its references) and surfaces any bytes
47/// the listener buffered past the request head.
48#[cfg(feature = "std")]
49pub struct HijackedSocket {
50    pub stream: Box<dyn HijackStream>,
51    pub leftover: Bytes,
52}
53
54#[cfg(feature = "std")]
55impl HijackedSocket {
56    #[must_use]
57    pub fn new(stream: Box<dyn HijackStream>, leftover: Bytes) -> Self {
58        Self { stream, leftover }
59    }
60}
61
62#[cfg(feature = "std")]
63pub type UpgradeFuture = Pin<Box<dyn Future<Output = Result<(), ProximaError>> + Send>>;
64
65/// Pipe-provided hook that runs the post-upgrade protocol. The
66/// listener invokes it exactly once after writing the upgrade
67/// response head. The handler owns the socket for its lifetime;
68/// dropping the future closes the socket.
69#[cfg(feature = "std")]
70pub struct UpgradeHandler {
71    inner: Box<dyn FnOnce(HijackedSocket) -> UpgradeFuture + Send>,
72}
73
74#[cfg(feature = "std")]
75impl UpgradeHandler {
76    #[must_use]
77    pub fn new<F, Fut>(handler: F) -> Self
78    where
79        F: FnOnce(HijackedSocket) -> Fut + Send + 'static,
80        Fut: Future<Output = Result<(), ProximaError>> + Send + 'static,
81    {
82        Self {
83            inner: Box::new(move |socket| Box::pin(handler(socket))),
84        }
85    }
86
87    pub fn invoke(self, socket: HijackedSocket) -> UpgradeFuture {
88        (self.inner)(socket)
89    }
90}
91
92#[cfg(feature = "std")]
93impl core::fmt::Debug for UpgradeHandler {
94    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
95        formatter
96            .debug_struct("UpgradeHandler")
97            .finish_non_exhaustive()
98    }
99}
100
101/// Erased `() -> UpgradeHandler` accept hook: the connection-layer sibling
102/// of [`crate::pipe::handler::PipeHandle`]. An unconditional-upgrade
103/// protocol pipe (redis/pgwire/mqtt/amqp) upgrades every accepted socket
104/// with zero dependence on the inbound bytes, so its `SendPipe::In` is
105/// `()` rather than a synthetic `Request<Bytes>`.
106#[cfg(feature = "std")]
107pub type AcceptHandle = crate::pipe::alloc_tier::PipeHandle<(), UpgradeHandler>;
108
109// Local (?Send) upgrade variant for the io_uring listener path.
110//
111// io_uring's `tokio_uring::net::TcpStream` is `!Send` (held in `Rc`),
112// so the post-upgrade handler must be `!Send` too. The Send-bound
113// `UpgradeHandler` above can't carry an `Rc<TcpStream>`-backed
114// stream — hence the parallel `Local*` types here.
115//
116// Pipe authors install a `LocalUpgradeHandler` via the
117// thread-local registry (see `local_slots` below) using the ticket
118// the io_uring listener placed in `request.context.local_upgrade_ticket`.
119// The listener picks the handler back up after `Pipe::call` returns
120// and hijacks the socket. The Send-bounded `Response.upgrade` field
121// is irrelevant on the io_uring path and is ignored when a local
122// handler is installed.
123
124#[cfg(feature = "std")]
125pub trait LocalHijackStream: AsyncRead + AsyncWrite + Unpin {}
126#[cfg(feature = "std")]
127impl<T: AsyncRead + AsyncWrite + Unpin + ?Sized> LocalHijackStream for T {}
128
129#[cfg(feature = "std")]
130pub struct LocalHijackedSocket {
131    pub stream: Box<dyn LocalHijackStream>,
132    pub leftover: Bytes,
133}
134
135#[cfg(feature = "std")]
136impl LocalHijackedSocket {
137    #[must_use]
138    pub fn new(stream: Box<dyn LocalHijackStream>, leftover: Bytes) -> Self {
139        Self { stream, leftover }
140    }
141}
142
143#[cfg(feature = "std")]
144pub type LocalUpgradeFuture = Pin<Box<dyn Future<Output = Result<(), ProximaError>>>>;
145
146#[cfg(feature = "std")]
147pub struct LocalUpgradeHandler {
148    inner: Box<dyn FnOnce(LocalHijackedSocket) -> LocalUpgradeFuture>,
149}
150
151#[cfg(feature = "std")]
152impl LocalUpgradeHandler {
153    #[must_use]
154    pub fn new<F, Fut>(handler: F) -> Self
155    where
156        F: FnOnce(LocalHijackedSocket) -> Fut + 'static,
157        Fut: Future<Output = Result<(), ProximaError>> + 'static,
158    {
159        Self {
160            inner: Box::new(move |socket| Box::pin(handler(socket))),
161        }
162    }
163
164    pub fn invoke(self, socket: LocalHijackedSocket) -> LocalUpgradeFuture {
165        (self.inner)(socket)
166    }
167}
168
169#[cfg(feature = "std")]
170impl core::fmt::Debug for LocalUpgradeHandler {
171    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
172        formatter
173            .debug_struct("LocalUpgradeHandler")
174            .finish_non_exhaustive()
175    }
176}
177
178/// Thread-local installer registry for io_uring upgrade handlers.
179///
180/// The io_uring listener mints a ticket per request and places it in
181/// `request.context.local_upgrade_ticket` before dispatching. Pipe
182/// authors that want to upgrade call [`local_slots::install`] with that ticket
183/// and a `LocalUpgradeHandler`. The listener calls [`local_slots::take`] after dispatch
184/// completes to retrieve the handler and hijack the socket.
185///
186/// The registry is `thread_local!` because the io_uring listener
187/// pins each accepted connection to one OS thread (via `spawn_local`
188/// on `tokio_uring`'s current-thread runtime). The Pipe future is
189/// polled on the same thread for its lifetime, so install + take see
190/// the same map.
191///
192/// Gotcha: don't call [`local_slots::install`] from a `tokio::spawn`'d task nested
193/// inside `Pipe::call` — that task may run on a different OS
194/// thread and miss the listener's `take`. Install directly from the
195/// `Pipe::call` future.
196#[cfg(feature = "std")]
197pub mod local_slots {
198    use super::LocalUpgradeHandler;
199    use std::cell::RefCell;
200    use std::collections::BTreeMap;
201    use std::sync::atomic::{AtomicU64, Ordering};
202
203    thread_local! {
204        static SLOTS: RefCell<BTreeMap<u64, LocalUpgradeHandler>> = const { RefCell::new(BTreeMap::new()) };
205    }
206
207    static TICKET_GEN: AtomicU64 = AtomicU64::new(1);
208
209    #[must_use]
210    pub fn next_ticket() -> u64 {
211        TICKET_GEN.fetch_add(1, Ordering::Relaxed)
212    }
213
214    pub fn install(ticket: u64, handler: LocalUpgradeHandler) {
215        SLOTS.with(|cell| {
216            cell.borrow_mut().insert(ticket, handler);
217        });
218    }
219
220    pub fn take(ticket: u64) -> Option<LocalUpgradeHandler> {
221        SLOTS.with(|cell| cell.borrow_mut().remove(&ticket))
222    }
223
224    pub fn discard(ticket: u64) {
225        let _ = take(ticket);
226    }
227}