Skip to main content

trillium_proxy/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(
3    clippy::dbg_macro,
4    missing_copy_implementations,
5    rustdoc::missing_crate_level_docs,
6    missing_debug_implementations,
7    missing_docs,
8    nonstandard_style,
9    unused_qualifications
10)]
11
12//! http reverse and forward proxy trillium handler
13
14#[cfg(test)]
15#[doc = include_str!("../README.md")]
16mod readme {}
17
18mod body_streamer;
19mod forward_proxy_connect;
20pub mod upstream;
21
22use body_streamer::stream_body;
23pub use forward_proxy_connect::ForwardProxyConnect;
24use full_duplex_async_copy::full_duplex_copy;
25use futures_lite::future::zip;
26use size::{Base, Size};
27use std::{borrow::Cow, fmt::Debug, future::IntoFuture};
28use trillium::{
29    Conn, Handler, KnownHeaderName,
30    Status::{NotFound, SwitchingProtocols},
31    Upgrade,
32};
33use trillium_client::ConnExt as _;
34pub use trillium_client::{Client, Connector};
35use trillium_forwarding::Forwarded;
36use trillium_http::{HeaderName, Headers, HttpContext, Status, Version};
37use upstream::{IntoUpstreamSelector, UpstreamSelector};
38pub use url::Url;
39
40/// constructs a new [`Proxy`]. alias of [`Proxy::new`]
41pub fn proxy<I>(client: impl Into<Client>, upstream: I) -> Proxy<I::UpstreamSelector>
42where
43    I: IntoUpstreamSelector,
44{
45    Proxy::new(client, upstream)
46}
47
48/// the proxy handler
49#[derive(Debug)]
50pub struct Proxy<U> {
51    upstream: U,
52    client: Client,
53    pass_through_not_found: bool,
54    halt: bool,
55    via_pseudonym: Option<Cow<'static, str>>,
56    allow_websocket_upgrade: bool,
57}
58
59impl<U: UpstreamSelector> Proxy<U> {
60    /// construct a new proxy handler that sends all requests to the upstream
61    /// provided
62    ///
63    /// ```
64    /// use trillium_proxy::Proxy;
65    /// use trillium_smol::ClientConfig;
66    ///
67    /// let proxy = Proxy::new(
68    ///     ClientConfig::default(),
69    ///     "http://docs.trillium.rs/trillium_proxy",
70    /// );
71    /// ```
72    pub fn new<I>(client: impl Into<Client>, upstream: I) -> Self
73    where
74        I: IntoUpstreamSelector<UpstreamSelector = U>,
75    {
76        let client = client
77            .into()
78            .without_default_header(KnownHeaderName::UserAgent)
79            .without_default_header(KnownHeaderName::Accept);
80
81        Self {
82            upstream: upstream.into_upstream(),
83            client,
84            pass_through_not_found: true,
85            halt: true,
86            via_pseudonym: None,
87            allow_websocket_upgrade: false,
88        }
89    }
90
91    /// chainable constructor to set the 404 Not Found handling
92    /// behavior. By default, this proxy will pass through the trillium
93    /// Conn unmodified if the proxy response is a 404 not found, allowing
94    /// it to be chained in a tuple handler. To modify this behavior, call
95    /// proxy_not_found, and the full 404 response will be forwarded. The
96    /// Conn will be halted unless [`Proxy::without_halting`] was
97    /// configured
98    ///
99    /// ```
100    /// # use trillium_smol::ClientConfig;
101    /// # use trillium_proxy::Proxy;
102    /// let proxy = Proxy::new(ClientConfig::default(), "http://trillium.rs").proxy_not_found();
103    /// ```
104    pub fn proxy_not_found(mut self) -> Self {
105        self.pass_through_not_found = false;
106        self
107    }
108
109    /// The default behavior for this handler is to halt the conn on any
110    /// response other than a 404. If [`Proxy::proxy_not_found`] has been
111    /// configured, the default behavior for all response statuses is to
112    /// halt the trillium conn. To change this behavior, call
113    /// without_halting when constructing the proxy, and it will not halt
114    /// the conn. This applies to error responses as well: a `502 Bad
115    /// Gateway` produced when the upstream is unreachable or returns no
116    /// status is also left unhalted, allowing a subsequent handler to
117    /// replace it with a user-facing body. This is useful when passing the
118    /// proxy reply through
119    /// [`trillium_html_rewriter`](https://docs.trillium.rs/trillium_html_rewriter).
120    ///
121    /// ```
122    /// # use trillium_smol::ClientConfig;
123    /// # use trillium_proxy::Proxy;
124    /// let proxy = Proxy::new(ClientConfig::default(), "http://trillium.rs").without_halting();
125    /// ```
126    pub fn without_halting(mut self) -> Self {
127        self.halt = false;
128        self
129    }
130
131    /// populate the pseudonym for a
132    /// [`Via`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Via)
133    /// header. If no pseudonym is provided, no via header will be
134    /// inserted.
135    pub fn with_via_pseudonym(mut self, via_pseudonym: impl Into<Cow<'static, str>>) -> Self {
136        self.via_pseudonym = Some(via_pseudonym.into());
137        self
138    }
139
140    /// Allow websockets to be proxied
141    ///
142    /// This is not currently the default, but that may change at some (semver-minor) point in the
143    /// future
144    pub fn with_websocket_upgrades(mut self) -> Self {
145        self.allow_websocket_upgrade = true;
146        self
147    }
148
149    fn set_via_pseudonym(&self, headers: &mut Headers, version: Version) {
150        if self.via_pseudonym.is_none() {
151            return;
152        }
153
154        use std::fmt::Write;
155        let mut via = String::new();
156        let _ = write!(&mut via, "{version}");
157
158        if let Some(pseudonym) = &self.via_pseudonym {
159            let _ = write!(&mut via, " {pseudonym}");
160        }
161
162        if let Some(old_via) = headers.get_values(KnownHeaderName::Via) {
163            for old_via in old_via {
164                let _ = write!(&mut via, ", {old_via}");
165            }
166        }
167
168        headers.insert(KnownHeaderName::Via, via);
169    }
170
171    fn halt_unless_configured_otherwise(&self, conn: Conn) -> Conn {
172        if self.halt { conn.halt() } else { conn }
173    }
174}
175
176#[derive(Debug)]
177struct UpstreamUpgrade(Upgrade);
178
179impl<U: UpstreamSelector> Handler for Proxy<U> {
180    async fn init(&mut self, info: &mut trillium::Info) {
181        // this little dance is necessary to set the swansong on the client currently.
182        // this is only necessary because we're not wiring together the client.
183        let old_context = self.client.context();
184        let new_context = HttpContext::default()
185            .with_config(*old_context.config())
186            .with_swansong(info.swansong().clone());
187        self.client.set_context(new_context);
188        log::info!("proxying to {:?}", self.upstream);
189    }
190
191    async fn run(&self, mut conn: Conn) -> Conn {
192        let Some(request_url) = self.upstream.determine_upstream(&mut conn) else {
193            return conn;
194        };
195
196        log::debug!("proxying to {}", request_url.as_str());
197
198        let mut forwarded = Forwarded::from_headers(conn.request_headers())
199            .ok()
200            .flatten()
201            .unwrap_or_default()
202            .into_owned();
203
204        if let Some(peer_ip) = conn.peer_ip() {
205            forwarded.add_for(peer_ip.to_string());
206        };
207
208        if let Some(host) = conn.host() {
209            forwarded.set_host(host);
210        }
211
212        let mut request_headers = conn
213            .request_headers()
214            .clone()
215            .without_headers([
216                KnownHeaderName::Connection,
217                KnownHeaderName::KeepAlive,
218                KnownHeaderName::ProxyAuthenticate,
219                KnownHeaderName::ProxyAuthorization,
220                KnownHeaderName::Te,
221                KnownHeaderName::Trailer,
222                KnownHeaderName::TransferEncoding,
223                KnownHeaderName::Upgrade,
224                KnownHeaderName::Host,
225                KnownHeaderName::XforwardedBy,
226                KnownHeaderName::XforwardedFor,
227                KnownHeaderName::XforwardedHost,
228                KnownHeaderName::XforwardedProto,
229                KnownHeaderName::XforwardedSsl,
230                KnownHeaderName::AltUsed,
231            ])
232            .with_inserted_header(KnownHeaderName::Forwarded, forwarded.to_string());
233
234        let mut connection_is_upgrade = false;
235        for header in conn
236            .request_headers()
237            .token_iter(KnownHeaderName::Connection)
238            .map(HeaderName::from)
239        {
240            if header == KnownHeaderName::Upgrade {
241                connection_is_upgrade = true;
242            }
243            request_headers.remove(header);
244        }
245
246        if self.allow_websocket_upgrade
247            && connection_is_upgrade
248            && conn
249                .request_headers()
250                .eq_ignore_ascii_case(KnownHeaderName::Upgrade, "websocket")
251        {
252            request_headers.extend([
253                (KnownHeaderName::Upgrade, "WebSocket"),
254                (KnownHeaderName::Connection, "Upgrade"),
255            ]);
256        }
257
258        self.set_via_pseudonym(&mut request_headers, conn.http_version());
259
260        let content_length = !matches!(
261            conn.request_headers()
262                .get_str(KnownHeaderName::ContentLength),
263            Some("0") | None
264        );
265
266        let chunked = conn
267            .request_headers()
268            .eq_ignore_ascii_case(KnownHeaderName::TransferEncoding, "chunked");
269
270        let method = conn.method();
271        let conn_result = if chunked || content_length {
272            let (body_fut, request_body) = stream_body(&mut conn);
273
274            let client_fut = self
275                .client
276                .build_conn(method, request_url)
277                .with_request_headers(request_headers)
278                .with_body(request_body)
279                .into_future();
280
281            zip(body_fut, client_fut).await.1
282        } else {
283            self.client
284                .build_conn(method, request_url)
285                .with_request_headers(request_headers)
286                .await
287        };
288
289        let mut client_conn = match conn_result {
290            Ok(client_conn) => client_conn,
291            Err(e) => {
292                let conn = conn.with_status(Status::BadGateway).with_state(e);
293                return self.halt_unless_configured_otherwise(conn);
294            }
295        };
296
297        let client_conn_version = client_conn.http_version();
298
299        let mut conn = match client_conn.status() {
300            Some(SwitchingProtocols) => {
301                conn.response_headers_mut()
302                    .extend(std::mem::take(client_conn.response_headers_mut()));
303
304                conn.with_state(UpstreamUpgrade(
305                    trillium_http::Upgrade::from(client_conn).into(),
306                ))
307                .with_status(SwitchingProtocols)
308            }
309
310            Some(NotFound) if self.pass_through_not_found => {
311                client_conn.recycle().await;
312                return conn;
313            }
314
315            Some(status) => {
316                conn.response_headers_mut().remove(KnownHeaderName::Server);
317                conn.response_headers_mut()
318                    .append_all(client_conn.response_headers().clone());
319                conn.with_body(client_conn).with_status(status)
320            }
321
322            None => {
323                return self.halt_unless_configured_otherwise(conn.with_status(Status::BadGateway));
324            }
325        };
326
327        if Some(SwitchingProtocols) != conn.status()
328            || !conn
329                .response_headers()
330                .eq_ignore_ascii_case(KnownHeaderName::Connection, "Upgrade")
331        {
332            let connection = conn
333                .response_headers_mut()
334                .remove(KnownHeaderName::Connection);
335
336            conn.response_headers_mut().remove_all(
337                connection
338                    .iter()
339                    .flatten()
340                    .filter_map(|s| s.as_str())
341                    .flat_map(|s| s.split(','))
342                    .map(|t| HeaderName::from(t.trim()).into_owned()),
343            );
344        }
345
346        conn.response_headers_mut().remove_all([
347            KnownHeaderName::KeepAlive,
348            KnownHeaderName::ProxyAuthenticate,
349            KnownHeaderName::ProxyAuthorization,
350            KnownHeaderName::Te,
351            KnownHeaderName::Trailer,
352            KnownHeaderName::TransferEncoding,
353        ]);
354
355        self.set_via_pseudonym(conn.response_headers_mut(), client_conn_version);
356
357        self.halt_unless_configured_otherwise(conn)
358    }
359
360    fn has_upgrade(&self, upgrade: &Upgrade) -> bool {
361        upgrade.state().contains::<UpstreamUpgrade>()
362    }
363
364    async fn upgrade(&self, mut upgrade: Upgrade) {
365        let Some(UpstreamUpgrade(upstream)) = upgrade.state_mut().take() else {
366            return;
367        };
368        let downstream = upgrade;
369        match full_duplex_copy(upstream, downstream).await {
370            Err(e) => log::error!("upgrade stream error: {:?}", e),
371            Ok((up, down)) => {
372                log::debug!("streamed upgrade {} up and {} down", bytes(up), bytes(down))
373            }
374        }
375    }
376}
377
378fn bytes(bytes: u64) -> String {
379    Size::from_bytes(bytes)
380        .format()
381        .with_base(Base::Base10)
382        .to_string()
383}