Skip to main content

trillium/
upgrade.rs

1use crate::{Headers, HttpContext, Method, Transport, TypeSet, Version};
2use futures_lite::{AsyncRead, AsyncWrite};
3use std::{
4    mem,
5    net::IpAddr,
6    sync::{
7        Arc, OnceLock,
8        atomic::{AtomicUsize, Ordering::Relaxed},
9    },
10};
11use trillium_http::Swansong;
12use trillium_macros::{AsyncRead, AsyncWrite};
13
14/// # A HTTP protocol upgrade
15#[derive(Debug, AsyncWrite, AsyncRead)]
16pub struct Upgrade {
17    #[async_write]
18    #[async_read]
19    inner: trillium_http::Upgrade<Box<dyn Transport>>,
20    path_frames: PathFrames,
21}
22
23/// A path-frame stack mutable through `&self`, because [`Handler::has_upgrade`] takes
24/// `&Upgrade`.
25///
26/// [`Upgrade::path`] lends `&str`s out of this structure for the lifetime of `&self`, so a
27/// frame may never be freed or moved while the `Upgrade` is alive — even after `pop`. Storage
28/// is therefore an append-only chain of `OnceLock` links (`OnceLock::set` takes `&self`), and
29/// each node records its parent's position, making the chain a persistent stack: `top` is the
30/// 1-based chain position of the current top frame (0 = empty, full path), push appends a node
31/// whose parent is the current top, and pop moves `top` to the parent — popped frames stay
32/// allocated until the `Upgrade` drops.
33///
34/// [`Handler::has_upgrade`]: crate::Handler::has_upgrade
35#[derive(Debug, Default)]
36struct PathFrames {
37    head: OnceLock<Box<FrameNode>>,
38    top: AtomicUsize,
39}
40
41#[derive(Debug)]
42struct FrameNode {
43    frame: String,
44    /// 1-based chain position of the frame below this one on the stack; 0 = stack bottom
45    parent: usize,
46    next: OnceLock<Box<Self>>,
47}
48
49impl PathFrames {
50    fn get(&self, position: usize) -> Option<&FrameNode> {
51        let steps = position.checked_sub(1)?;
52        let mut node = self.head.get()?;
53        for _ in 0..steps {
54            node = node.next.get()?;
55        }
56        Some(node)
57    }
58
59    fn top_frame(&self) -> Option<&str> {
60        match self.top.load(Relaxed) {
61            0 => None,
62            top => self.get(top).map(|node| &*node.frame),
63        }
64    }
65
66    fn push(&self, frame: String) {
67        let parent = self.top.load(Relaxed);
68        let mut node = Box::new(FrameNode {
69            frame,
70            parent,
71            next: OnceLock::new(),
72        });
73        let mut position = 1;
74        let mut lock = &self.head;
75        loop {
76            while let Some(occupied) = lock.get() {
77                lock = &occupied.next;
78                position += 1;
79            }
80            match lock.set(node) {
81                Ok(()) => break,
82                // a concurrent push claimed this link between the get and the set; the cell is
83                // now (or is about to finish being) initialized, so resume walking from it
84                Err(rejected) => node = rejected,
85            }
86        }
87        self.top.store(position, Relaxed);
88    }
89
90    fn pop(&self) {
91        let top = self.top.load(Relaxed);
92        if let Some(node) = self.get(top) {
93            self.top.store(node.parent, Relaxed);
94        }
95    }
96}
97
98impl<T: Transport + 'static> From<trillium_http::Upgrade<T>> for Upgrade {
99    fn from(value: trillium_http::Upgrade<T>) -> Self {
100        Self {
101            inner: value.map_transport(|t| Box::new(t) as Box<dyn Transport>),
102            path_frames: PathFrames::default(),
103        }
104    }
105}
106
107impl<T: Transport + 'static> From<trillium_http::Conn<T>> for Upgrade {
108    fn from(value: trillium_http::Conn<T>) -> Self {
109        trillium_http::Upgrade::from(value).into()
110    }
111}
112
113impl From<crate::Conn> for Upgrade {
114    fn from(value: crate::Conn) -> Self {
115        Self {
116            inner: value.inner.into(),
117            path_frames: PathFrames::default(),
118        }
119    }
120}
121
122impl AsRef<trillium_http::Upgrade<Box<dyn Transport>>> for Upgrade {
123    fn as_ref(&self) -> &trillium_http::Upgrade<Box<dyn Transport>> {
124        &self.inner
125    }
126}
127
128impl AsMut<trillium_http::Upgrade<Box<dyn Transport>>> for Upgrade {
129    fn as_mut(&mut self) -> &mut trillium_http::Upgrade<Box<dyn Transport>> {
130        &mut self.inner
131    }
132}
133
134impl Upgrade {
135    /// Borrows the HTTP request headers
136    pub fn request_headers(&self) -> &Headers {
137        self.inner.received_headers()
138    }
139
140    /// Take the HTTP request headers
141    pub fn take_request_headers(&mut self) -> Headers {
142        mem::take(self.inner.received_headers_mut())
143    }
144
145    /// Returns a copy of the HTTP request method
146    pub fn method(&self) -> Method {
147        self.inner.method()
148    }
149
150    /// Borrows the state accumulated on the Conn before negotiating the upgrade
151    pub fn state(&self) -> &TypeSet {
152        self.inner.state()
153    }
154
155    /// Takes the [`TypeSet`] accumulated on the Conn before negotiating the upgrade
156    pub fn take_state(&mut self) -> TypeSet {
157        mem::take(self.inner.state_mut())
158    }
159
160    /// Mutably borrow the [`TypeSet`] accumulated on the Conn before negotiating the upgrade
161    pub fn state_mut(&mut self) -> &mut TypeSet {
162        self.inner.state_mut()
163    }
164
165    /// Borrows the underlying transport
166    pub fn transport(&self) -> &dyn Transport {
167        self.inner.transport().as_ref()
168    }
169
170    /// Mutably borrow the underlying transport
171    ///
172    /// This returns a tuple of (buffered bytes, transport) in order to make salient the requirement
173    /// to handle any buffered bytes before using the transport directly.
174    pub fn transport_mut(&mut self) -> (&[u8], &mut dyn Transport) {
175        let (buffer, transport) = self.inner.buffer_and_transport_mut();
176        (&*buffer, &mut **transport)
177    }
178
179    /// Consumes self, returning the underlying transport
180    ///
181    /// This returns a tuple of (buffered bytes, transport) in order to make salient the requirement
182    /// to handle any buffered bytes before using the transport directly.
183    pub fn into_transport(mut self) -> (Vec<u8>, Box<dyn Transport>) {
184        let buffer = self.inner.take_buffer();
185        (buffer, self.inner.into_transport())
186    }
187
188    /// Returns a copy of the peer IP address of the connection, if available
189    pub fn peer_ip(&self) -> Option<IpAddr> {
190        self.inner.peer_ip()
191    }
192
193    /// Borrows the :authority HTTP/3 pseudo-header
194    pub fn authority(&self) -> Option<&str> {
195        self.inner.authority()
196    }
197
198    /// Borrows the :scheme HTTP/3 pseudo-header
199    pub fn scheme(&self) -> Option<&str> {
200        self.inner.scheme()
201    }
202
203    /// Borrows the :protocol HTTP/3 pseudo-header
204    pub fn protocol(&self) -> Option<&str> {
205        self.inner.protocol()
206    }
207
208    /// Borrows the HTTP version
209    pub fn http_version(&self) -> &Version {
210        self.inner.http_version()
211    }
212
213    /// Returns a copy of whether this connection was deemed secure by the handler stack
214    pub fn is_secure(&self) -> bool {
215        self.inner.is_secure()
216    }
217
218    /// Borrows the shared state [`TypeSet`] for this application
219    pub fn shared_state(&self) -> &TypeSet {
220        self.inner.shared_state()
221    }
222
223    /// Returns the HTTP request path up to but excluding any query component
224    ///
225    /// As with [`Conn::path`][crate::Conn::path], this may not represent the entire http request
226    /// path if this upgrade is being dispatched through nested routers: after
227    /// [`push_path`][Upgrade::push_path], it returns the pushed path remainder relative to the
228    /// enclosing router mount.
229    pub fn path(&self) -> &str {
230        self.path_frames
231            .top_frame()
232            .unwrap_or_else(|| self.inner.path())
233    }
234
235    /// for router implementations. pushes a route segment onto the path, the upgrade-dispatch
236    /// analog of [`Conn::push_path`][crate::Conn::push_path] — see its documentation for the
237    /// contract shared by all of a handler's hooks.
238    ///
239    /// Takes a shared reference because [`Handler::has_upgrade`][crate::Handler::has_upgrade]
240    /// does. To make that possible while [`path`][Upgrade::path] lends out plain `&str`s, frames
241    /// removed by [`pop_path`][Upgrade::pop_path] remain allocated until the `Upgrade` drops.
242    pub fn push_path(&self, path: String) {
243        self.path_frames.push(path);
244    }
245
246    /// for router implementations. removes a route segment pushed by
247    /// [`push_path`][Upgrade::push_path], the upgrade-dispatch analog of
248    /// [`Conn::pop_path`][crate::Conn::pop_path]
249    pub fn pop_path(&self) {
250        self.path_frames.pop();
251    }
252
253    /// Retrieves the query component of the path
254    pub fn querystring(&self) -> &str {
255        self.inner.querystring()
256    }
257
258    /// Retrieves a cloned [`Swansong`] graceful shutdown controller
259    pub fn swansong(&self) -> Swansong {
260        self.inner.context().swansong().clone()
261    }
262
263    /// Retrieves a clone of the [`HttpContext`] for this upgrade
264    pub fn context(&self) -> Arc<HttpContext> {
265        self.inner.context().clone()
266    }
267
268    /// Returns a clone of the H3 connection, if any
269    pub fn h3_connection(&self) -> Option<Arc<trillium_http::h3::H3Connection>> {
270        self.inner.h3_connection().cloned()
271    }
272
273    /// Inbound trailers, populated conditionally when we have read this upgrade to completion
274    pub fn request_trailers(&self) -> Option<&Headers> {
275        self.inner.received_trailers()
276    }
277
278    /// Emit trailing headers and finish the outbound stream. Consumes `self`; further
279    /// writes are statically prevented.
280    ///
281    /// Per-protocol behavior:
282    /// - HTTP/1.1 with `Transfer-Encoding: chunked`: writes the last-chunk marker (`0\r\n`), the
283    ///   trailer section, and a final CRLF, then closes the transport.
284    /// - HTTP/2: enqueues a trailing `HEADERS` frame with `END_STREAM` via the connection driver
285    ///   and returns. The driver finishes the stream after draining any pending DATA frames.
286    /// - HTTP/3: encodes a trailing `HEADERS` frame via QPACK, writes it to the stream, then closes
287    ///   the stream (QUIC `FIN`).
288    /// - HTTP/1.1 without chunked encoding (raw upgrade, CONNECT tunnel, websocket-over-h1):
289    ///   trailers can't be expressed on the wire; dropped with a `log::warn!` and `Ok(())`
290    ///   returned.
291    ///
292    /// # Errors
293    ///
294    /// Returns the underlying [`std::io::Error`] when the wire write fails, `BrokenPipe` if
295    /// the stream has already been closed, and `NotConnected` if the carried
296    /// `ProtocolSession` is missing the expected driver for h2/h3.
297    pub async fn send_trailers(self, trailers: Headers) -> std::io::Result<()> {
298        self.inner.send_trailers(trailers).await
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::PathFrames;
305
306    #[test]
307    fn path_frames_push_pop_and_divergence() {
308        let frames = PathFrames::default();
309        assert_eq!(frames.top_frame(), None);
310
311        frames.pop(); // empty pop is a no-op
312        assert_eq!(frames.top_frame(), None);
313
314        frames.push("a".into());
315        frames.push("b".into());
316        assert_eq!(frames.top_frame(), Some("b"));
317
318        let borrowed_before_pop = frames.top_frame().unwrap();
319        frames.pop();
320        assert_eq!(frames.top_frame(), Some("a"));
321        assert_eq!(borrowed_before_pop, "b"); // still valid after pop
322
323        frames.push("c".into()); // diverge from the popped "b"
324        assert_eq!(frames.top_frame(), Some("c"));
325
326        frames.pop();
327        frames.pop();
328        assert_eq!(frames.top_frame(), None);
329
330        frames.push("d".into());
331        assert_eq!(frames.top_frame(), Some("d"));
332    }
333}