Skip to main content

postrust_proxy/vendored/
handler.rs

1//! Vendored message handler from rpxy-lib: message_handler/*.rs
2//!
3//! This module handles request/response manipulation:
4//! - X-Forwarded-* header handling
5//! - Host header rewriting
6//! - Request parsing
7
8use crate::config::Route;
9use crate::vendored::hyper_ext::{empty_body, string_body, ProxyBody};
10use hyper::header::{
11    HeaderName, HeaderValue, CONNECTION, CONTENT_LENGTH, HOST, TRANSFER_ENCODING, UPGRADE,
12};
13use hyper::{Method, Version};
14use hyper::{Request, Response, StatusCode};
15use std::net::SocketAddr;
16
17/// Standard X-Forwarded headers.
18pub mod headers {
19    use hyper::header::HeaderName;
20
21    pub static X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
22    pub static X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
23    pub static X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
24    pub static X_REAL_IP: HeaderName = HeaderName::from_static("x-real-ip");
25}
26
27/// Headers that are hop-by-hop and must never reach an upstream.
28///
29/// RFC 9110 section 7.6.1. `Transfer-Encoding` belongs here even though it
30/// describes the body: the server side of hyper decodes the inbound body and
31/// the client side frames it again, so the inbound value describes a wire
32/// format the upstream never sees. Passing it through is what lets a client
33/// disagree with the proxy about where one request ends and the next begins.
34const HOP_BY_HOP_HEADERS: &[&str] = &[
35    "connection",
36    "keep-alive",
37    "proxy-authenticate",
38    "proxy-authorization",
39    "te",
40    "trailer",
41    "transfer-encoding",
42    "upgrade",
43];
44
45/// Headers a `Connection` token may not remove.
46///
47/// Naming these in `Connection` is not a legitimate use of the field, and
48/// honouring it would hand a client a way to strip framing or identity
49/// headers on their way through the proxy.
50const CONNECTION_TOKEN_EXEMPT: &[&str] = &["host", "content-length"];
51
52/// Message handler for request/response manipulation.
53pub struct MessageHandler;
54
55impl MessageHandler {
56    /// The protocol a request is asking to upgrade to, if it is a valid
57    /// upgrade request.
58    ///
59    /// Requires both `Upgrade: <proto>` and an `upgrade` token in `Connection`,
60    /// per RFC 9110 section 7.8 -- `Upgrade` alone is not a request to switch.
61    /// HTTP/2 is excluded deliberately: it carries no `Upgrade` header and
62    /// negotiates extended CONNECT instead (RFC 8441), which is not implemented.
63    pub fn upgrade_protocol(request: &Request<ProxyBody>) -> Option<String> {
64        if request.version() != Version::HTTP_11 {
65            return None;
66        }
67
68        let headers = request.headers();
69        let asks_for_upgrade = headers.get_all(CONNECTION).iter().any(|value| {
70            value
71                .to_str()
72                .map(|v| {
73                    v.split(',')
74                        .any(|t| t.trim().eq_ignore_ascii_case("upgrade"))
75                })
76                .unwrap_or(false)
77        });
78        if !asks_for_upgrade {
79            return None;
80        }
81
82        headers
83            .get(UPGRADE)
84            .and_then(|v| v.to_str().ok())
85            .map(|v| v.trim().to_ascii_lowercase())
86            .filter(|v| !v.is_empty())
87    }
88
89    /// The protocol an HTTP/2 extended CONNECT is asking for (RFC 8441).
90    ///
91    /// HTTP/2 has no `Upgrade`; a WebSocket is opened with `:method = CONNECT`
92    /// plus a `:protocol` pseudo-header, which hyper surfaces as a
93    /// [`hyper::ext::Protocol`] extension. This is only ever offered when the
94    /// listener advertised SETTINGS_ENABLE_CONNECT_PROTOCOL.
95    pub fn h2_connect_protocol(request: &Request<ProxyBody>) -> Option<String> {
96        if request.version() != Version::HTTP_2 || request.method() != Method::CONNECT {
97            return None;
98        }
99        request
100            .extensions()
101            .get::<hyper::ext::Protocol>()
102            .map(|protocol| protocol.as_str().to_ascii_lowercase())
103    }
104
105    /// Re-apply the upgrade headers that [`Self::strip_hop_by_hop_headers`] took.
106    ///
107    /// An upgrade is the one case where these headers legitimately travel to the
108    /// next hop. Only a normalised `Connection: upgrade` and the requested
109    /// protocol go back -- not whatever token list the client sent -- so the
110    /// carve-out cannot be used to smuggle other headers through.
111    pub fn restore_upgrade_headers(request: &mut Request<ProxyBody>, protocol: &str) {
112        if let Ok(value) = HeaderValue::from_str(protocol) {
113            let headers = request.headers_mut();
114            headers.insert(UPGRADE, value);
115            headers.insert(CONNECTION, HeaderValue::from_static("upgrade"));
116        }
117    }
118
119    /// Whether the request's body framing is ambiguous.
120    ///
121    /// RFC 9112 section 6.1: a message carrying both `Content-Length` and
122    /// `Transfer-Encoding` is either a smuggling attempt or a broken client.
123    /// `Transfer-Encoding` wins per the spec, but a proxy that quietly
124    /// normalises the disagreement is how a smuggling chain starts, so reject
125    /// it -- which is also how hyper's own parser already treats a duplicate
126    /// `Content-Length`.
127    ///
128    /// Must be called *before* [`Self::strip_hop_by_hop_headers`], which
129    /// removes the `Transfer-Encoding` this inspects.
130    pub fn has_ambiguous_framing(request: &Request<ProxyBody>) -> bool {
131        let headers = request.headers();
132        headers.contains_key(CONTENT_LENGTH) && headers.contains_key(TRANSFER_ENCODING)
133    }
134
135    /// Remove hop-by-hop headers before forwarding, per RFC 9110 section 7.6.1.
136    ///
137    /// Two sets go: the standard hop-by-hop headers, and every header named as
138    /// a token in the incoming `Connection` field. Call this *before*
139    /// [`Self::add_forwarding_headers`] -- a client that names `x-forwarded-for`
140    /// in `Connection` then strips only its own inbound value, and we set a
141    /// fresh one afterwards, rather than being able to suppress ours.
142    pub fn strip_hop_by_hop_headers(request: &mut Request<ProxyBody>) {
143        let headers = request.headers_mut();
144
145        // Read the Connection tokens up front: removing headers invalidates any
146        // borrow of the values we would otherwise still be holding.
147        let mut named: Vec<String> = Vec::new();
148        for value in headers.get_all(CONNECTION).iter() {
149            let Ok(value) = value.to_str() else { continue };
150            for token in value.split(',') {
151                let token = token.trim().to_ascii_lowercase();
152                if !token.is_empty() {
153                    named.push(token);
154                }
155            }
156        }
157
158        for name in HOP_BY_HOP_HEADERS {
159            headers.remove(*name);
160        }
161
162        for token in named {
163            if CONNECTION_TOKEN_EXEMPT.contains(&token.as_str()) {
164                continue;
165            }
166            if let Ok(name) = HeaderName::from_bytes(token.as_bytes()) {
167                headers.remove(name);
168            }
169        }
170    }
171
172    /// Add X-Forwarded-* headers to a request.
173    pub fn add_forwarding_headers(
174        request: &mut Request<ProxyBody>,
175        client_addr: SocketAddr,
176        proto: &str,
177    ) {
178        let authority = request.uri().authority().map(|a| a.to_string());
179        let headers = request.headers_mut();
180        let client_ip = client_addr.ip().to_string();
181
182        // X-Forwarded-For: append client IP
183        if let Some(existing) = headers.get(&headers::X_FORWARDED_FOR) {
184            let mut new_value = existing.to_str().unwrap_or("").to_string();
185            new_value.push_str(", ");
186            new_value.push_str(&client_ip);
187            if let Ok(value) = HeaderValue::from_str(&new_value) {
188                headers.insert(headers::X_FORWARDED_FOR.clone(), value);
189            }
190        } else if let Ok(value) = HeaderValue::from_str(&client_ip) {
191            headers.insert(headers::X_FORWARDED_FOR.clone(), value);
192        }
193
194        // X-Real-IP: set if not present
195        if !headers.contains_key(&headers::X_REAL_IP) {
196            if let Ok(value) = HeaderValue::from_str(&client_ip) {
197                headers.insert(headers::X_REAL_IP.clone(), value);
198            }
199        }
200
201        // X-Forwarded-Proto
202        if let Ok(value) = HeaderValue::from_str(proto) {
203            headers.insert(headers::X_FORWARDED_PROTO.clone(), value);
204        }
205
206        // X-Forwarded-Host: preserve the original authority. HTTP/2 carries it in
207        // the :authority pseudo-header rather than Host, which reaches us on the
208        // URI, so fall back to that before giving up.
209        let original_host = headers
210            .get(HOST)
211            .cloned()
212            .or_else(|| authority.and_then(|a| HeaderValue::from_str(&a).ok()));
213        if let Some(host) = original_host {
214            headers.insert(headers::X_FORWARDED_HOST.clone(), host);
215        }
216    }
217
218    /// Rewrite the Host header for upstream.
219    pub fn rewrite_host_header(request: &mut Request<ProxyBody>, upstream_host: &str) {
220        if let Ok(value) = HeaderValue::from_str(upstream_host) {
221            request.headers_mut().insert(HOST, value);
222        }
223    }
224
225    /// Strip path prefix from request URI.
226    pub fn strip_path_prefix(request: &mut Request<ProxyBody>, prefix: &str) {
227        let uri = request.uri();
228        let path = uri.path();
229
230        if let Some(new_path) = path.strip_prefix(prefix) {
231            let new_path = if new_path.is_empty() || !new_path.starts_with('/') {
232                format!("/{}", new_path.trim_start_matches('/'))
233            } else {
234                new_path.to_string()
235            };
236
237            let new_uri = if let Some(query) = uri.query() {
238                format!("{}?{}", new_path, query)
239            } else {
240                new_path
241            };
242
243            if let Ok(new_uri) = new_uri.parse() {
244                *request.uri_mut() = new_uri;
245            }
246        }
247    }
248
249    /// Apply route-specific header modifications.
250    pub fn apply_route_headers(request: &mut Request<ProxyBody>, route: &Route) {
251        let headers = request.headers_mut();
252
253        // Add headers
254        for (name, value) in &route.add_headers {
255            if let (Ok(name), Ok(value)) = (
256                HeaderName::from_bytes(name.as_bytes()),
257                HeaderValue::from_str(value),
258            ) {
259                headers.insert(name, value);
260            }
261        }
262
263        // Remove headers
264        for name in &route.remove_headers {
265            if let Ok(name) = HeaderName::from_bytes(name.as_bytes()) {
266                headers.remove(name);
267            }
268        }
269    }
270
271    /// Create a synthetic error response.
272    pub fn error_response(status: StatusCode, message: &str) -> Response<ProxyBody> {
273        Response::builder()
274            .status(status)
275            .header("content-type", "text/plain; charset=utf-8")
276            .body(string_body(message.to_string()))
277            .unwrap_or_else(|_| {
278                Response::builder()
279                    .status(StatusCode::INTERNAL_SERVER_ERROR)
280                    .body(empty_body())
281                    .unwrap()
282            })
283    }
284
285    /// Create a 502 Bad Gateway response.
286    pub fn bad_request(message: &str) -> Response<ProxyBody> {
287        Self::error_response(StatusCode::BAD_REQUEST, message)
288    }
289
290    /// Create a 502 Bad Gateway response.
291    pub fn bad_gateway(message: &str) -> Response<ProxyBody> {
292        Self::error_response(StatusCode::BAD_GATEWAY, message)
293    }
294
295    /// Create a 503 Service Unavailable response.
296    pub fn service_unavailable(message: &str) -> Response<ProxyBody> {
297        Self::error_response(StatusCode::SERVICE_UNAVAILABLE, message)
298    }
299
300    /// Create a 504 Gateway Timeout response.
301    pub fn gateway_timeout() -> Response<ProxyBody> {
302        Self::error_response(StatusCode::GATEWAY_TIMEOUT, "Gateway Timeout")
303    }
304
305    /// Create a 429 Too Many Requests response.
306    pub fn too_many_requests() -> Response<ProxyBody> {
307        Self::error_response(StatusCode::TOO_MANY_REQUESTS, "Too Many Requests")
308    }
309
310    /// Create a 404 Not Found response.
311    pub fn not_found() -> Response<ProxyBody> {
312        Self::error_response(StatusCode::NOT_FOUND, "Not Found")
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use crate::vendored::hyper_ext::empty_body;
320
321    #[test]
322    fn test_strip_path_prefix() {
323        let mut request = Request::builder()
324            .uri("/api/v1/users")
325            .body(empty_body())
326            .unwrap();
327
328        MessageHandler::strip_path_prefix(&mut request, "/api/v1");
329
330        assert_eq!(request.uri().path(), "/users");
331    }
332
333    #[test]
334    fn test_strip_path_prefix_root() {
335        let mut request = Request::builder().uri("/api").body(empty_body()).unwrap();
336
337        MessageHandler::strip_path_prefix(&mut request, "/api");
338
339        assert_eq!(request.uri().path(), "/");
340    }
341
342    #[test]
343    fn test_h2_connect_protocol_detected() {
344        // RFC 8441: :method CONNECT plus the :protocol pseudo-header, which
345        // hyper surfaces as an extension.
346        let mut request = Request::builder()
347            .method(Method::CONNECT)
348            .version(Version::HTTP_2)
349            .uri("/chat")
350            .body(empty_body())
351            .unwrap();
352        request
353            .extensions_mut()
354            .insert(hyper::ext::Protocol::from_static("websocket"));
355
356        assert_eq!(
357            MessageHandler::h2_connect_protocol(&request).as_deref(),
358            Some("websocket")
359        );
360        // The HTTP/1.1 detector must not claim it.
361        assert!(MessageHandler::upgrade_protocol(&request).is_none());
362    }
363
364    #[test]
365    fn test_h2_connect_without_protocol_is_a_plain_connect() {
366        let request = Request::builder()
367            .method(Method::CONNECT)
368            .version(Version::HTTP_2)
369            .uri("/chat")
370            .body(empty_body())
371            .unwrap();
372
373        assert!(MessageHandler::h2_connect_protocol(&request).is_none());
374    }
375
376    #[test]
377    fn test_h1_connect_is_not_extended_connect() {
378        // Extended CONNECT is an HTTP/2 mechanism; the h1 spelling is Upgrade.
379        let mut request = Request::builder()
380            .method(Method::CONNECT)
381            .version(Version::HTTP_11)
382            .uri("/chat")
383            .body(empty_body())
384            .unwrap();
385        request
386            .extensions_mut()
387            .insert(hyper::ext::Protocol::from_static("websocket"));
388
389        assert!(MessageHandler::h2_connect_protocol(&request).is_none());
390    }
391
392    #[test]
393    fn test_upgrade_protocol_detected() {
394        let request = Request::builder()
395            .uri("/chat")
396            .header("host", "a")
397            .header("upgrade", "websocket")
398            .header("connection", "Upgrade")
399            .body(empty_body())
400            .unwrap();
401
402        assert_eq!(
403            MessageHandler::upgrade_protocol(&request).as_deref(),
404            Some("websocket")
405        );
406    }
407
408    #[test]
409    fn test_upgrade_needs_connection_token() {
410        // Upgrade alone is not a request to switch protocols (RFC 9110 s7.8).
411        let request = Request::builder()
412            .uri("/chat")
413            .header("host", "a")
414            .header("upgrade", "websocket")
415            .body(empty_body())
416            .unwrap();
417
418        assert!(MessageHandler::upgrade_protocol(&request).is_none());
419    }
420
421    #[test]
422    fn test_upgrade_token_found_among_others() {
423        let request = Request::builder()
424            .uri("/chat")
425            .header("host", "a")
426            .header("upgrade", "WebSocket")
427            .header("connection", "keep-alive, UPGRADE")
428            .body(empty_body())
429            .unwrap();
430
431        assert_eq!(
432            MessageHandler::upgrade_protocol(&request).as_deref(),
433            Some("websocket")
434        );
435    }
436
437    #[test]
438    fn test_no_upgrade_without_upgrade_header() {
439        let request = Request::builder()
440            .uri("/chat")
441            .header("host", "a")
442            .header("connection", "upgrade")
443            .body(empty_body())
444            .unwrap();
445
446        assert!(MessageHandler::upgrade_protocol(&request).is_none());
447    }
448
449    #[test]
450    fn test_http2_never_upgrades() {
451        // HTTP/2 negotiates extended CONNECT (RFC 8441), not Upgrade.
452        let request = Request::builder()
453            .uri("/chat")
454            .version(Version::HTTP_2)
455            .header("upgrade", "websocket")
456            .header("connection", "Upgrade")
457            .body(empty_body())
458            .unwrap();
459
460        assert!(MessageHandler::upgrade_protocol(&request).is_none());
461    }
462
463    #[test]
464    fn test_restore_upgrade_headers_normalises_connection() {
465        // The carve-out must put back only `upgrade`, never the client's list.
466        let mut request = Request::builder()
467            .uri("/chat")
468            .header("host", "a")
469            .header("upgrade", "websocket")
470            .header("connection", "Upgrade, X-Sneaky")
471            .header("x-sneaky", "leaked")
472            .header("x-legit", "keep-me")
473            .body(empty_body())
474            .unwrap();
475
476        let protocol = MessageHandler::upgrade_protocol(&request).unwrap();
477        MessageHandler::strip_hop_by_hop_headers(&mut request);
478        MessageHandler::restore_upgrade_headers(&mut request, &protocol);
479
480        assert_eq!(request.headers().get("upgrade").unwrap(), "websocket");
481        assert_eq!(request.headers().get("connection").unwrap(), "upgrade");
482        assert!(!request.headers().contains_key("x-sneaky"));
483        assert_eq!(request.headers().get("x-legit").unwrap(), "keep-me");
484    }
485
486    #[test]
487    fn test_ambiguous_framing_detected() {
488        // Regression: stripping Transfer-Encoding leaves a stale Content-Length
489        // describing a body hyper has already re-framed, so this must be
490        // rejected up front rather than forwarded.
491        let request = Request::builder()
492            .uri("/test")
493            .header("host", "a")
494            .header("content-length", "6")
495            .header("transfer-encoding", "chunked")
496            .body(empty_body())
497            .unwrap();
498
499        assert!(MessageHandler::has_ambiguous_framing(&request));
500    }
501
502    #[test]
503    fn test_content_length_alone_is_not_ambiguous() {
504        let request = Request::builder()
505            .uri("/test")
506            .header("host", "a")
507            .header("content-length", "6")
508            .body(empty_body())
509            .unwrap();
510
511        assert!(!MessageHandler::has_ambiguous_framing(&request));
512    }
513
514    #[test]
515    fn test_transfer_encoding_alone_is_not_ambiguous() {
516        let request = Request::builder()
517            .uri("/test")
518            .header("host", "a")
519            .header("transfer-encoding", "chunked")
520            .body(empty_body())
521            .unwrap();
522
523        assert!(!MessageHandler::has_ambiguous_framing(&request));
524    }
525
526    #[test]
527    fn test_strips_each_hop_by_hop_header() {
528        // One assertion per header in the RFC 9110 section 7.6.1 set.
529        for name in HOP_BY_HOP_HEADERS {
530            let mut request = Request::builder()
531                .uri("/test")
532                .header("host", "example.com")
533                .header(*name, "some-value")
534                .body(empty_body())
535                .unwrap();
536
537            MessageHandler::strip_hop_by_hop_headers(&mut request);
538
539            assert!(
540                !request.headers().contains_key(*name),
541                "hop-by-hop header {} was forwarded",
542                name
543            );
544            // Stripping must not take unrelated headers with it.
545            assert_eq!(request.headers().get("host").unwrap(), "example.com");
546        }
547    }
548
549    #[test]
550    fn test_strips_headers_named_in_connection() {
551        // The live case found by the HTTP Garden probe.
552        let mut request = Request::builder()
553            .uri("/test")
554            .header("host", "example.com")
555            .header("connection", "keep-alive, X-Hop")
556            .header("x-hop", "SHOULD-BE-STRIPPED")
557            .body(empty_body())
558            .unwrap();
559
560        MessageHandler::strip_hop_by_hop_headers(&mut request);
561
562        assert!(!request.headers().contains_key("connection"));
563        assert!(!request.headers().contains_key("x-hop"));
564        assert_eq!(request.headers().get("host").unwrap(), "example.com");
565    }
566
567    #[test]
568    fn test_connection_tokens_are_case_insensitive_and_trimmed() {
569        let mut request = Request::builder()
570            .uri("/test")
571            .header("connection", "  X-UPPER  ,\tx-lower,,")
572            .header("x-upper", "a")
573            .header("x-lower", "b")
574            .body(empty_body())
575            .unwrap();
576
577        MessageHandler::strip_hop_by_hop_headers(&mut request);
578
579        assert!(!request.headers().contains_key("x-upper"));
580        assert!(!request.headers().contains_key("x-lower"));
581    }
582
583    #[test]
584    fn test_connection_token_cannot_strip_host_or_content_length() {
585        // Naming these in Connection is not legitimate; honouring it would let a
586        // client strip framing or identity headers on the way through.
587        let mut request = Request::builder()
588            .uri("/test")
589            .header("host", "example.com")
590            .header("content-length", "0")
591            .header("connection", "host, content-length")
592            .body(empty_body())
593            .unwrap();
594
595        MessageHandler::strip_hop_by_hop_headers(&mut request);
596
597        assert!(!request.headers().contains_key("connection"));
598        assert_eq!(request.headers().get("host").unwrap(), "example.com");
599        assert_eq!(request.headers().get("content-length").unwrap(), "0");
600    }
601
602    #[test]
603    fn test_connection_token_naming_forwarded_header_does_not_suppress_ours() {
604        // strip runs before add_forwarding_headers, so the client's inbound value
605        // goes and ours replaces it -- no suppression, no spoofing.
606        let mut request = Request::builder()
607            .uri("/test")
608            .header("host", "example.com")
609            .header("connection", "x-forwarded-for")
610            .header("x-forwarded-for", "1.2.3.4")
611            .body(empty_body())
612            .unwrap();
613
614        MessageHandler::strip_hop_by_hop_headers(&mut request);
615        let client_addr: SocketAddr = "192.168.1.100:12345".parse().unwrap();
616        MessageHandler::add_forwarding_headers(&mut request, client_addr, "http");
617
618        assert_eq!(
619            request.headers().get("x-forwarded-for").unwrap(),
620            "192.168.1.100"
621        );
622    }
623
624    #[test]
625    fn test_strip_is_a_no_op_without_hop_by_hop_headers() {
626        let mut request = Request::builder()
627            .uri("/test")
628            .header("host", "example.com")
629            .header("accept", "*/*")
630            .body(empty_body())
631            .unwrap();
632
633        MessageHandler::strip_hop_by_hop_headers(&mut request);
634
635        assert_eq!(request.headers().len(), 2);
636    }
637
638    #[test]
639    fn test_forwarding_headers() {
640        let mut request = Request::builder()
641            .uri("/test")
642            .header("host", "example.com")
643            .body(empty_body())
644            .unwrap();
645
646        let client_addr: SocketAddr = "192.168.1.100:12345".parse().unwrap();
647        MessageHandler::add_forwarding_headers(&mut request, client_addr, "https");
648
649        assert_eq!(
650            request.headers().get("x-forwarded-for").unwrap(),
651            "192.168.1.100"
652        );
653        assert_eq!(request.headers().get("x-forwarded-proto").unwrap(), "https");
654        assert_eq!(
655            request.headers().get("x-forwarded-host").unwrap(),
656            "example.com"
657        );
658    }
659}