Skip to main content

postrust_proxy/vendored/
forwarder.rs

1//! Vendored forwarder client from rpxy-lib: forwarder/client.rs
2//!
3//! This module handles forwarding requests to upstream backends.
4
5use crate::config::{Backend, UpstreamHttpVersion};
6use crate::vendored::hyper_ext::{empty_body, IncomingBodyExt, ProxyBody};
7use crate::vendored::types::ProxyError;
8use base64::Engine;
9use hyper::body::Incoming;
10use hyper::header::{CONNECTION, HOST, UPGRADE};
11use hyper::{Method, Request, Response, StatusCode, Version};
12use hyper_util::client::legacy::Client;
13use hyper_util::rt::TokioExecutor as HyperTokioExecutor;
14use hyper_util::rt::TokioIo;
15use rand::RngCore;
16use std::time::Duration;
17use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
18use tokio::net::TcpStream;
19use tracing::debug;
20
21/// HTTP connector type alias.
22type HttpConnector = hyper_util::client::legacy::connect::HttpConnector;
23
24/// Forwarder client for sending requests to upstream backends.
25pub struct ForwarderClient {
26    /// HTTP/1.1 client, used for every backend that has not opted into h2c.
27    http_client: Client<HttpConnector, ProxyBody>,
28    /// Prior-knowledge h2c client, for backends declared as `http_version = "h2c"`.
29    ///
30    /// A separate client because the choice is per-connection and h2c has no
31    /// ALPN to negotiate it -- the pool has to be told up front which protocol
32    /// its connections speak.
33    http2_client: Client<HttpConnector, ProxyBody>,
34    /// Request timeout
35    timeout: Duration,
36}
37
38impl ForwarderClient {
39    /// Create a new forwarder client.
40    pub fn new(timeout: Duration) -> Self {
41        let mut http_connector = HttpConnector::new();
42        // Without this, Nagle batches small writes and, paired with delayed ACK
43        // on the far side, adds latency to every small forwarded request.
44        http_connector.set_nodelay(true);
45        let http_client = Client::builder(HyperTokioExecutor::new())
46            .pool_idle_timeout(Duration::from_secs(30))
47            .pool_max_idle_per_host(32)
48            .build(http_connector);
49
50        let mut http2_connector = HttpConnector::new();
51        http2_connector.set_nodelay(true);
52        let http2_client = Client::builder(HyperTokioExecutor::new())
53            .pool_idle_timeout(Duration::from_secs(30))
54            .http2_only(true)
55            .build(http2_connector);
56
57        Self {
58            http_client,
59            http2_client,
60            timeout,
61        }
62    }
63
64    /// Forward a request to a backend.
65    pub async fn forward(
66        &self,
67        backend: &Backend,
68        mut request: Request<ProxyBody>,
69    ) -> Result<Response<Incoming>, ProxyError> {
70        // Build the upstream URL
71        let uri = request.uri();
72        let path_and_query = uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/");
73
74        let upstream_uri = format!("{}://{}{}", backend.scheme, backend.address, path_and_query);
75
76        // Update request URI
77        *request.uri_mut() = upstream_uri
78            .parse()
79            .map_err(|e| ProxyError::Request(format!("Invalid upstream URI: {}", e)))?;
80
81        // HTTP/2 is a per-hop protocol, so the inbound version never carries onto
82        // the upstream connection: it is whatever this backend was declared to
83        // speak. Getting this wrong is not subtle -- the client rejects the
84        // request as UserUnsupportedVersion before a byte goes out.
85        let (client, version) = match backend.http_version {
86            UpstreamHttpVersion::H2c => (&self.http2_client, Version::HTTP_2),
87            UpstreamHttpVersion::Http11 => (&self.http_client, Version::HTTP_11),
88        };
89        *request.version_mut() = version;
90
91        // Send request with timeout
92        let response = tokio::time::timeout(self.timeout, client.request(request))
93            .await
94            .map_err(|_| ProxyError::Timeout)?
95            .map_err(|e| ProxyError::Connection(e.to_string()))?;
96
97        Ok(response)
98    }
99
100    /// Forward a protocol-upgrade request (WebSocket) and tunnel the result.
101    ///
102    /// This cannot use the pooled client: once the upstream answers 101 the
103    /// connection stops being request/response and becomes an opaque byte
104    /// stream, which a connection pool has no way to hand back. So dial the
105    /// backend directly, keep the lower-level connection, and splice the two
106    /// upgraded halves together.
107    ///
108    /// HTTP/1.1 only. WebSocket over HTTP/2 is RFC 8441 extended CONNECT, a
109    /// different mechanism that this does not implement.
110    pub async fn forward_upgrade(
111        &self,
112        backend: &Backend,
113        mut request: Request<ProxyBody>,
114    ) -> Result<Response<ProxyBody>, ProxyError> {
115        // Claim the client half of the upgrade before the request is consumed.
116        // This also takes the OnUpgrade extension out, so it is not forwarded.
117        let client_upgrade = hyper::upgrade::on(&mut request);
118
119        // The low-level h1 sender wants origin-form; Host carries the authority.
120        let path_and_query = request
121            .uri()
122            .path_and_query()
123            .map(|pq| pq.as_str().to_string())
124            .unwrap_or_else(|| "/".to_string());
125        *request.uri_mut() = path_and_query
126            .parse()
127            .map_err(|e| ProxyError::Request(format!("Invalid upstream URI: {}", e)))?;
128        *request.version_mut() = Version::HTTP_11;
129
130        let stream = tokio::time::timeout(self.timeout, TcpStream::connect(&backend.address))
131            .await
132            .map_err(|_| ProxyError::Timeout)?
133            .map_err(|e| ProxyError::Connection(e.to_string()))?;
134
135        // A tunnel relays whatever chunking the client used. With Nagle on, small
136        // writes coalesce and the upstream sees frames batched together that the
137        // client sent separately -- which changes what the upstream does, not just
138        // when it does it. Autobahn 3.4 is exactly that: the client sends a text
139        // frame and then a bad frame in one-byte chops, and a batched upstream
140        // fails the connection without ever echoing the first.
141        if let Err(e) = stream.set_nodelay(true) {
142            debug!("Could not set TCP_NODELAY on the upstream socket: {}", e);
143        }
144
145        let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(stream))
146            .await
147            .map_err(|e| ProxyError::Connection(e.to_string()))?;
148
149        // with_upgrades keeps the connection task alive past the 101 so the
150        // upgraded stream is actually reachable.
151        tokio::spawn(async move {
152            if let Err(e) = conn.with_upgrades().await {
153                debug!("Upstream upgrade connection error: {}", e);
154            }
155        });
156
157        let mut response = tokio::time::timeout(self.timeout, sender.send_request(request))
158            .await
159            .map_err(|_| ProxyError::Timeout)?
160            .map_err(|e| ProxyError::Connection(e.to_string()))?;
161
162        // The upstream declined to upgrade: pass its answer straight through.
163        if response.status() != StatusCode::SWITCHING_PROTOCOLS {
164            let (parts, body) = response.into_parts();
165            return Ok(Response::from_parts(parts, body.boxed_body()));
166        }
167
168        let upstream_upgrade = hyper::upgrade::on(&mut response);
169
170        // Both halves only resolve after their 101 has gone out, so the splice
171        // has to outlive this call.
172        tokio::spawn(async move {
173            match tokio::try_join!(client_upgrade, upstream_upgrade) {
174                Ok((client_io, upstream_io)) => {
175                    Self::splice(TokioIo::new(client_io), TokioIo::new(upstream_io)).await
176                }
177                Err(e) => debug!("Upgrade handshake failed: {}", e),
178            }
179        });
180
181        // Hand the 101 and its headers back; the body is the tunnel, not content.
182        let (parts, _) = response.into_parts();
183        Ok(Response::from_parts(parts, empty_body()))
184    }
185
186    /// Forward an HTTP/2 extended CONNECT (RFC 8441) to an HTTP/1.1 WebSocket
187    /// origin, translating between the two handshakes.
188    ///
189    /// The two are not the same conversation, so this is a rewrite rather than
190    /// a relay:
191    ///
192    /// - HTTP/2 carries the intent in `:method = CONNECT` and `:protocol`;
193    ///   HTTP/1.1 carries it in `Upgrade` plus a `Connection` token.
194    /// - HTTP/2 has no `Sec-WebSocket-Key`. The nonce exists to stop an HTTP/1.1
195    ///   cache from mistaking the handshake for an ordinary response, which does
196    ///   not apply to a multiplexed stream. The h1 origin still requires one, so
197    ///   the proxy generates it here.
198    /// - Success is `101` on the wire to the origin but `200` back to the
199    ///   client; a 101 has no meaning on an HTTP/2 stream.
200    ///
201    /// The origin's `Sec-WebSocket-Accept` is not verified against the generated
202    /// key. Status 101 plus a websocket `Upgrade` is taken as sufficient, which
203    /// avoids a SHA-1 dependency for a check that guards against a
204    /// misconfigured origin rather than an attacker.
205    pub async fn forward_h2_websocket(
206        &self,
207        backend: &Backend,
208        mut request: Request<ProxyBody>,
209        protocol: &str,
210    ) -> Result<Response<ProxyBody>, ProxyError> {
211        let client_upgrade = hyper::upgrade::on(&mut request);
212
213        let path_and_query = request
214            .uri()
215            .path_and_query()
216            .map(|pq| pq.as_str().to_string())
217            .unwrap_or_else(|| "/".to_string());
218
219        // A fresh nonce for the HTTP/1.1 leg, which the HTTP/2 client never sent.
220        let mut nonce = [0u8; 16];
221        rand::rng().fill_bytes(&mut nonce);
222        let websocket_key = base64::engine::general_purpose::STANDARD.encode(nonce);
223
224        let mut builder = Request::builder()
225            .method(Method::GET)
226            .uri(&path_and_query)
227            .version(Version::HTTP_11)
228            .header(HOST, &backend.address)
229            .header(UPGRADE, protocol)
230            .header(CONNECTION, "upgrade")
231            .header("sec-websocket-version", "13")
232            .header("sec-websocket-key", &websocket_key);
233
234        // Carry the client's own headers over. The handshake fields above are
235        // ours to set, so skip any the client happened to send.
236        const REWRITTEN: [&str; 5] = [
237            "host",
238            "upgrade",
239            "connection",
240            "sec-websocket-key",
241            "sec-websocket-version",
242        ];
243        for (name, value) in request.headers() {
244            if !REWRITTEN.contains(&name.as_str()) {
245                builder = builder.header(name, value);
246            }
247        }
248
249        let upstream_request = builder
250            .body(empty_body())
251            .map_err(|e| ProxyError::Request(format!("could not build upgrade request: {}", e)))?;
252
253        let stream = tokio::time::timeout(self.timeout, TcpStream::connect(&backend.address))
254            .await
255            .map_err(|_| ProxyError::Timeout)?
256            .map_err(|e| ProxyError::Connection(e.to_string()))?;
257        if let Err(e) = stream.set_nodelay(true) {
258            debug!("Could not set TCP_NODELAY on the upstream socket: {}", e);
259        }
260
261        let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(stream))
262            .await
263            .map_err(|e| ProxyError::Connection(e.to_string()))?;
264        tokio::spawn(async move {
265            if let Err(e) = conn.with_upgrades().await {
266                debug!("Upstream upgrade connection error: {}", e);
267            }
268        });
269
270        let mut response =
271            tokio::time::timeout(self.timeout, sender.send_request(upstream_request))
272                .await
273                .map_err(|_| ProxyError::Timeout)?
274                .map_err(|e| ProxyError::Connection(e.to_string()))?;
275
276        if response.status() != StatusCode::SWITCHING_PROTOCOLS {
277            // The origin declined. Pass its answer back on the HTTP/2 stream,
278            // minus the 101 semantics that do not translate.
279            let status = response.status();
280            let mut out = Response::builder().status(status);
281            for (name, value) in response.headers() {
282                if name != UPGRADE && name != CONNECTION {
283                    out = out.header(name, value);
284                }
285            }
286            return out
287                .body(empty_body())
288                .map_err(|e| ProxyError::Response(e.to_string()));
289        }
290
291        let upstream_upgrade = hyper::upgrade::on(&mut response);
292
293        tokio::spawn(async move {
294            match tokio::try_join!(client_upgrade, upstream_upgrade) {
295                Ok((client_io, upstream_io)) => {
296                    Self::splice(TokioIo::new(client_io), TokioIo::new(upstream_io)).await
297                }
298                Err(e) => debug!("RFC 8441 upgrade handshake failed: {}", e),
299            }
300        });
301
302        // 200, not 101: on an HTTP/2 stream a 2xx is what completes an extended
303        // CONNECT. Carry across only what the client still needs to know.
304        let mut out = Response::builder().status(StatusCode::OK);
305        for name in ["sec-websocket-protocol", "sec-websocket-extensions"] {
306            if let Some(value) = response.headers().get(name) {
307                out = out.header(name, value);
308            }
309        }
310        out.body(empty_body())
311            .map_err(|e| ProxyError::Response(e.to_string()))
312    }
313
314    /// Splice two upgraded streams, draining both directions independently.
315    ///
316    /// Deliberately not `copy_bidirectional`, which returns on the first error
317    /// in either direction and drops the other mid-flight. That loses data in a
318    /// real and reachable case: the upstream fails the connection and closes
319    /// while the client is still writing, the client-to-upstream copy takes an
320    /// EPIPE, and whatever the upstream had already sent -- its last frames and
321    /// its close -- is discarded instead of reaching the client. Autobahn case
322    /// 3.4 caught exactly that, intermittently, because it is a race.
323    ///
324    /// Each direction here runs to its own end and half-closes the peer's write
325    /// side so the other side sees a clean EOF. `join!` rather than `try_join!`
326    /// so that one direction failing cannot cancel the other.
327    async fn splice<A, B>(client_io: A, upstream_io: B)
328    where
329        A: AsyncRead + AsyncWrite + Unpin,
330        B: AsyncRead + AsyncWrite + Unpin,
331    {
332        let (mut client_read, mut client_write) = tokio::io::split(client_io);
333        let (mut upstream_read, mut upstream_write) = tokio::io::split(upstream_io);
334
335        let to_upstream = async {
336            let result = tokio::io::copy(&mut client_read, &mut upstream_write).await;
337            // Half-close: tell the upstream the client is done, without tearing
338            // down the half still carrying its reply.
339            let _ = upstream_write.shutdown().await;
340            result
341        };
342
343        let to_client = async {
344            let result = tokio::io::copy(&mut upstream_read, &mut client_write).await;
345            let _ = client_write.shutdown().await;
346            result
347        };
348
349        let (up, down) = tokio::join!(to_upstream, to_client);
350
351        match (up, down) {
352            (Ok(up), Ok(down)) => {
353                debug!("Tunnel closed: {} bytes up, {} bytes down", up, down)
354            }
355            (up, down) => {
356                // One side erroring is normal for an abrupt close; log both
357                // outcomes rather than only the first failure.
358                debug!("Tunnel closed with errors: up={:?} down={:?}", up, down)
359            }
360        }
361    }
362}
363
364impl Default for ForwarderClient {
365    fn default() -> Self {
366        Self::new(Duration::from_secs(30))
367    }
368}