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::{HeaderName, HeaderValue, CONNECTION, FORWARDED, HOST, UPGRADE};
11use hyper::{Method, Version};
12use hyper::{Request, Response, StatusCode};
13use std::net::{IpAddr, SocketAddr};
14
15/// Standard X-Forwarded headers.
16pub mod headers {
17    use hyper::header::HeaderName;
18
19    pub static X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
20    pub static X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
21    pub static X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
22    pub static X_REAL_IP: HeaderName = HeaderName::from_static("x-real-ip");
23}
24
25/// Headers that are hop-by-hop and must never reach an upstream.
26///
27/// RFC 9110 section 7.6.1. `Transfer-Encoding` belongs here even though it
28/// describes the body: the server side of hyper decodes the inbound body and
29/// the client side frames it again, so the inbound value describes a wire
30/// format the upstream never sees. Passing it through is what lets a client
31/// disagree with the proxy about where one request ends and the next begins.
32const HOP_BY_HOP_HEADERS: &[&str] = &[
33    "connection",
34    "keep-alive",
35    "proxy-authenticate",
36    "proxy-authorization",
37    "te",
38    "trailer",
39    "transfer-encoding",
40    "upgrade",
41    // Connection-specific by definition (RFC 9113 section 3.2.1 retired it,
42    // RFC 7540 required it to be named in `Connection`). Listed rather than
43    // left to the `Connection` tokens because a client need not name it, and
44    // an `h2c` upgrade the proxy has declined must not reach the backend
45    // carrying half its handshake.
46    "http2-settings",
47    // Never standard, still sent, and some servers still act on it.
48    "proxy-connection",
49];
50
51/// Headers a `Connection` token may not remove.
52///
53/// Naming these in `Connection` is not a legitimate use of the field, and
54/// honouring it would hand a client a way to strip framing or identity
55/// headers on their way through the proxy.
56const CONNECTION_TOKEN_EXEMPT: &[&str] = &["host", "content-length"];
57
58/// Message handler for request/response manipulation.
59pub struct MessageHandler;
60
61/// Drop a `:port` from an authority, leaving the host.
62///
63/// Bracket-aware: the colons inside `[::1]` are part of the address, so a naive
64/// `split(':').next()` would return `[` for every IPv6 client.
65fn strip_port(authority: &str) -> &str {
66    if authority.starts_with('[') {
67        return match authority.find(']') {
68            Some(end) => &authority[..end + 1],
69            None => authority,
70        };
71    }
72    match authority.split_once(':') {
73        Some((host, _)) => host,
74        None => authority,
75    }
76}
77
78impl MessageHandler {
79    /// The protocol a request is asking to upgrade to, if it is a valid
80    /// upgrade request.
81    ///
82    /// Requires both `Upgrade: <proto>` and an `upgrade` token in `Connection`,
83    /// per RFC 9110 section 7.8 -- `Upgrade` alone is not a request to switch.
84    /// HTTP/2 is excluded deliberately: it carries no `Upgrade` header and
85    /// negotiates extended CONNECT instead (RFC 8441), which is not implemented.
86    pub fn upgrade_protocol(request: &Request<ProxyBody>) -> Option<String> {
87        if request.version() != Version::HTTP_11 {
88            return None;
89        }
90
91        let headers = request.headers();
92        let asks_for_upgrade = headers.get_all(CONNECTION).iter().any(|value| {
93            value
94                .to_str()
95                .map(|v| {
96                    v.split(',')
97                        .any(|t| t.trim().eq_ignore_ascii_case("upgrade"))
98                })
99                .unwrap_or(false)
100        });
101        if !asks_for_upgrade {
102            return None;
103        }
104
105        headers
106            .get(UPGRADE)
107            .and_then(|v| v.to_str().ok())
108            .map(|v| v.trim().to_ascii_lowercase())
109            .filter(|v| !v.is_empty())
110    }
111
112    /// The protocol an HTTP/2 extended CONNECT is asking for (RFC 8441).
113    ///
114    /// HTTP/2 has no `Upgrade`; a WebSocket is opened with `:method = CONNECT`
115    /// plus a `:protocol` pseudo-header, which hyper surfaces as a
116    /// [`hyper::ext::Protocol`] extension. This is only ever offered when the
117    /// listener advertised SETTINGS_ENABLE_CONNECT_PROTOCOL.
118    pub fn h2_connect_protocol(request: &Request<ProxyBody>) -> Option<String> {
119        if request.version() != Version::HTTP_2 || request.method() != Method::CONNECT {
120            return None;
121        }
122        request
123            .extensions()
124            .get::<hyper::ext::Protocol>()
125            .map(|protocol| protocol.as_str().to_ascii_lowercase())
126    }
127
128    /// Re-apply the upgrade headers that [`Self::strip_hop_by_hop_headers`] took.
129    ///
130    /// An upgrade is the one case where these headers legitimately travel to the
131    /// next hop. Only a normalised `Connection: upgrade` and the requested
132    /// protocol go back -- not whatever token list the client sent -- so the
133    /// carve-out cannot be used to smuggle other headers through.
134    pub fn restore_upgrade_headers(request: &mut Request<ProxyBody>, protocol: &str) {
135        if let Ok(value) = HeaderValue::from_str(protocol) {
136            let headers = request.headers_mut();
137            headers.insert(UPGRADE, value);
138            headers.insert(CONNECTION, HeaderValue::from_static("upgrade"));
139        }
140    }
141
142    /// Whether `protocol`, as returned by [`Self::upgrade_protocol`] or
143    /// [`Self::h2_connect_protocol`], is one the proxy may tunnel.
144    ///
145    /// An exact, case-insensitive match against `allowed` -- so a request for
146    /// several protocols at once (`Upgrade: websocket, h2c`) is not a match
147    /// for any of them, and is declined.
148    pub fn upgrade_allowed(protocol: &str, allowed: &[String]) -> bool {
149        allowed
150            .iter()
151            .any(|p| p.trim().eq_ignore_ascii_case(protocol))
152    }
153
154    // There is no check here for a request carrying both `Content-Length` and
155    // `Transfer-Encoding`. There was one, and it could never fire: hyper's
156    // HTTP/1 parser refuses a `Transfer-Encoding` whose last coding is not
157    // `chunked`, and when both are present it frames the body by
158    // `Transfer-Encoding` and drops `Content-Length` before the request
159    // reaches us (RFC 9112 section 6.3). The body is re-framed on the way to
160    // the upstream, and `Transfer-Encoding` is stripped below as hop-by-hop,
161    // so the two can never disagree downstream either.
162
163    /// Why a request path cannot be routed safely, if it cannot.
164    ///
165    /// Routes match the path as sent, and a backend resolves it after
166    /// decoding and normalising -- so any path the two would read differently
167    /// is a way past a route's path rules. `/public/../admin` matched the
168    /// `/public` route and was forwarded verbatim, to a backend that served
169    /// `/admin`. Refused:
170    ///
171    /// - dot-segments (`.` and `..`), literal or percent-encoded in any case,
172    ///   and with a `;parameter` after them, which Tomcat-style servers strip
173    ///   before resolving;
174    /// - an encoded slash or backslash (`%2f`, `%5c`), which a backend may
175    ///   decode into a separator the route never saw;
176    /// - a literal backslash, which some servers treat as a separator.
177    ///
178    /// Refusing rather than normalising: a client that sends these is either
179    /// probing or broken, and normalising would mean the proxy and the
180    /// backend each have to agree with a third reading of the path.
181    pub fn unsafe_path_reason(path: &str) -> Option<&'static str> {
182        let lower = path.to_ascii_lowercase();
183        if lower.contains("%2f") || lower.contains("%5c") || path.contains('\\') {
184            return Some("Encoded or backslash path separators are not allowed");
185        }
186        for segment in lower.split('/') {
187            let segment = segment.split(';').next().unwrap_or("");
188            let decoded = segment.replace("%2e", ".");
189            if decoded == "." || decoded == ".." {
190                return Some("Dot-segments are not allowed in the request path");
191            }
192        }
193        None
194    }
195
196    /// Remove hop-by-hop headers before forwarding, per RFC 9110 section 7.6.1.
197    ///
198    /// Two sets go: the standard hop-by-hop headers, and every header named as
199    /// a token in the incoming `Connection` field. Call this *before*
200    /// [`Self::add_forwarding_headers`] -- a client that names `x-forwarded-for`
201    /// in `Connection` then strips only its own inbound value, and we set a
202    /// fresh one afterwards, rather than being able to suppress ours.
203    pub fn strip_hop_by_hop_headers(request: &mut Request<ProxyBody>) {
204        let headers = request.headers_mut();
205
206        // Read the Connection tokens up front: removing headers invalidates any
207        // borrow of the values we would otherwise still be holding.
208        let mut named: Vec<String> = Vec::new();
209        for value in headers.get_all(CONNECTION).iter() {
210            let Ok(value) = value.to_str() else { continue };
211            for token in value.split(',') {
212                let token = token.trim().to_ascii_lowercase();
213                if !token.is_empty() {
214                    named.push(token);
215                }
216            }
217        }
218
219        for name in HOP_BY_HOP_HEADERS {
220            headers.remove(*name);
221        }
222
223        for token in named {
224            if CONNECTION_TOKEN_EXEMPT.contains(&token.as_str()) {
225                continue;
226            }
227            if let Ok(name) = HeaderName::from_bytes(token.as_bytes()) {
228                headers.remove(name);
229            }
230        }
231    }
232
233    /// Add X-Forwarded-* headers to a request.
234    ///
235    /// A backend uses these to decide who the client is -- for its logs, its
236    /// own rate limits, its IP allow-lists -- so a value the client wrote is a
237    /// value the client chose. Unless `trust_client` is set
238    /// (`server.trust_forwarded_headers`), everything the client sent is
239    /// replaced with what the proxy saw: `X-Forwarded-For` and `X-Real-IP`
240    /// become the socket's peer address, and `Forwarded` is removed. It used
241    /// to be appended to, set only if absent, and passed through, which let
242    /// any client claim any address.
243    ///
244    /// With `trust_client`, the client is another proxy: its chain is kept and
245    /// this hop appended to `X-Forwarded-For` and `Forwarded`, and the
246    /// `X-Forwarded-Proto` and `X-Forwarded-Host` it set are left as they
247    /// are, since they describe the original request better than this hop
248    /// can. `X-Real-IP` is still the peer address: it names one address, and
249    /// the one this proxy saw is the one it can vouch for.
250    pub fn add_forwarding_headers(
251        request: &mut Request<ProxyBody>,
252        client_addr: SocketAddr,
253        proto: &str,
254        trust_client: bool,
255    ) {
256        let authority = request.uri().authority().map(|a| a.to_string());
257        let headers = request.headers_mut();
258        let client_ip = client_addr.ip().to_canonical();
259        let client_ip_text = client_ip.to_string();
260
261        // X-Forwarded-For. Every line the client sent is part of the chain
262        // when it is trusted; none of it is when it is not.
263        let mut chain: Vec<String> = Vec::new();
264        if trust_client {
265            for value in headers.get_all(&headers::X_FORWARDED_FOR).iter() {
266                if let Ok(value) = value.to_str() {
267                    let value = value.trim();
268                    if !value.is_empty() {
269                        chain.push(value.to_string());
270                    }
271                }
272            }
273        }
274        chain.push(client_ip_text.clone());
275        headers.remove(&headers::X_FORWARDED_FOR);
276        if let Ok(value) = HeaderValue::from_str(&chain.join(", ")) {
277            headers.insert(headers::X_FORWARDED_FOR.clone(), value);
278        }
279
280        // X-Real-IP: always the peer.
281        if let Ok(value) = HeaderValue::from_str(&client_ip_text) {
282            headers.insert(headers::X_REAL_IP.clone(), value);
283        }
284
285        // Forwarded (RFC 7239). Rewritten only to extend a trusted chain;
286        // otherwise dropped, since the backend may well read it in preference
287        // to X-Forwarded-For.
288        let mut forwarded: Vec<String> = Vec::new();
289        if trust_client {
290            for value in headers.get_all(FORWARDED).iter() {
291                if let Ok(value) = value.to_str() {
292                    let value = value.trim();
293                    if !value.is_empty() {
294                        forwarded.push(value.to_string());
295                    }
296                }
297            }
298        }
299        headers.remove(FORWARDED);
300        if trust_client {
301            forwarded.push(format!("for={};proto={}", forwarded_node(client_ip), proto));
302            if let Ok(value) = HeaderValue::from_str(&forwarded.join(", ")) {
303                headers.insert(FORWARDED, value);
304            }
305        }
306
307        // X-Forwarded-Proto
308        if !(trust_client && headers.contains_key(&headers::X_FORWARDED_PROTO)) {
309            if let Ok(value) = HeaderValue::from_str(proto) {
310                headers.insert(headers::X_FORWARDED_PROTO.clone(), value);
311            }
312        }
313
314        // X-Forwarded-Host: preserve the original authority. HTTP/2 carries it in
315        // the :authority pseudo-header rather than Host, which reaches us on the
316        // URI, so fall back to that before giving up.
317        if !(trust_client && headers.contains_key(&headers::X_FORWARDED_HOST)) {
318            let original_host = headers
319                .get(HOST)
320                .cloned()
321                .or_else(|| authority.and_then(|a| HeaderValue::from_str(&a).ok()));
322            match original_host {
323                Some(host) => {
324                    headers.insert(headers::X_FORWARDED_HOST.clone(), host);
325                }
326                None => {
327                    headers.remove(&headers::X_FORWARDED_HOST);
328                }
329            }
330        }
331    }
332
333    /// Rewrite the Host header for upstream.
334    pub fn rewrite_host_header(request: &mut Request<ProxyBody>, upstream_host: &str) {
335        if let Ok(value) = HeaderValue::from_str(upstream_host) {
336            request.headers_mut().insert(HOST, value);
337        }
338    }
339
340    /// Strip path prefix from request URI.
341    ///
342    /// Only on a segment boundary, by the same rule routes match with
343    /// ([`crate::config::path_has_prefix`]): `/api` comes off `/api/users`
344    /// but not off `/apis`.
345    pub fn strip_path_prefix(request: &mut Request<ProxyBody>, prefix: &str) {
346        let uri = request.uri();
347        let path = uri.path();
348
349        if !crate::config::path_has_prefix(path, prefix) {
350            return;
351        }
352        if let Some(new_path) = path.strip_prefix(prefix) {
353            let new_path = if new_path.is_empty() || !new_path.starts_with('/') {
354                format!("/{}", new_path.trim_start_matches('/'))
355            } else {
356                new_path.to_string()
357            };
358
359            let new_uri = if let Some(query) = uri.query() {
360                format!("{}?{}", new_path, query)
361            } else {
362                new_path
363            };
364
365            if let Ok(new_uri) = new_uri.parse() {
366                *request.uri_mut() = new_uri;
367            }
368        }
369    }
370
371    /// Apply route-specific header modifications.
372    pub fn apply_route_headers(request: &mut Request<ProxyBody>, route: &Route) {
373        let headers = request.headers_mut();
374
375        // Add headers
376        for (name, value) in &route.add_headers {
377            if let (Ok(name), Ok(value)) = (
378                HeaderName::from_bytes(name.as_bytes()),
379                HeaderValue::from_str(value),
380            ) {
381                headers.insert(name, value);
382            }
383        }
384
385        // Remove headers
386        for name in &route.remove_headers {
387            if let Ok(name) = HeaderName::from_bytes(name.as_bytes()) {
388                headers.remove(name);
389            }
390        }
391    }
392
393    /// Create a synthetic error response.
394    pub fn error_response(status: StatusCode, message: &str) -> Response<ProxyBody> {
395        Response::builder()
396            .status(status)
397            .header("content-type", "text/plain; charset=utf-8")
398            .body(string_body(message.to_string()))
399            .unwrap_or_else(|_| {
400                Response::builder()
401                    .status(StatusCode::INTERNAL_SERVER_ERROR)
402                    .body(empty_body())
403                    .unwrap()
404            })
405    }
406
407    /// Create a 400 Bad Request response.
408    pub fn bad_request(message: &str) -> Response<ProxyBody> {
409        Self::error_response(StatusCode::BAD_REQUEST, message)
410    }
411
412    /// Create a 502 Bad Gateway response.
413    pub fn bad_gateway(message: &str) -> Response<ProxyBody> {
414        Self::error_response(StatusCode::BAD_GATEWAY, message)
415    }
416
417    /// Create a 503 Service Unavailable response.
418    pub fn service_unavailable(message: &str) -> Response<ProxyBody> {
419        Self::error_response(StatusCode::SERVICE_UNAVAILABLE, message)
420    }
421
422    /// The host a request is for, without its port.
423    ///
424    /// **Not just the `Host` header.** HTTP/2 has no `Host`: RFC 9113 section
425    /// 8.3.1 replaces it with the `:authority` pseudo-header, which reaches us
426    /// on the URI. Reading only the header therefore yields an empty host for
427    /// every h2 and h2c request, and a route with a `match.host` matches
428    /// nothing -- confirmed against a running proxy, where the same
429    /// host-matched route answered 200 over HTTP/1.1 and 404 over HTTP/2.
430    ///
431    /// `add_forwarding_headers` already had this fallback for
432    /// `x-forwarded-host`; route selection did not.
433    pub fn request_host(request: &Request<impl hyper::body::Body>) -> String {
434        let from_header = request
435            .headers()
436            .get(hyper::header::HOST)
437            .and_then(|value| value.to_str().ok())
438            .map(str::to_owned);
439
440        let host = from_header
441            .or_else(|| request.uri().authority().map(|a| a.to_string()))
442            .unwrap_or_default();
443
444        strip_port(&host).to_owned()
445    }
446
447    /// Create a 504 Gateway Timeout response.
448    ///
449    /// Takes a message so the body can say how long the request waited. A bare
450    /// "Gateway Timeout" leaves the caller unable to tell a route configured
451    /// with one second from an upstream that is genuinely wedged.
452    pub fn gateway_timeout(message: &str) -> Response<ProxyBody> {
453        Self::error_response(StatusCode::GATEWAY_TIMEOUT, message)
454    }
455
456    /// Create a 429 Too Many Requests response.
457    pub fn too_many_requests() -> Response<ProxyBody> {
458        Self::error_response(StatusCode::TOO_MANY_REQUESTS, "Too Many Requests")
459    }
460
461    /// Create a 404 Not Found response.
462    pub fn not_found() -> Response<ProxyBody> {
463        Self::error_response(StatusCode::NOT_FOUND, "Not Found")
464    }
465
466    /// Create a 501 Not Implemented response.
467    pub fn not_implemented(message: &str) -> Response<ProxyBody> {
468        Self::error_response(StatusCode::NOT_IMPLEMENTED, message)
469    }
470}
471
472/// An address as an RFC 7239 node: IPv6 is bracketed and quoted, since `:` and
473/// `[` are not allowed in a bare token.
474fn forwarded_node(ip: IpAddr) -> String {
475    match ip {
476        IpAddr::V4(v4) => v4.to_string(),
477        IpAddr::V6(v6) => format!("\"[{v6}]\""),
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484
485    fn request_with(headers: &[(&str, &str)], uri: &str) -> Request<ProxyBody> {
486        let mut builder = Request::builder().uri(uri);
487        for (name, value) in headers {
488            builder = builder.header(*name, *value);
489        }
490        builder.body(empty_body()).unwrap()
491    }
492
493    #[test]
494    fn the_host_header_gives_the_host() {
495        let r = request_with(&[("host", "api.example.com")], "/x");
496        assert_eq!(MessageHandler::request_host(&r), "api.example.com");
497    }
498
499    #[test]
500    fn the_uri_authority_is_used_when_there_is_no_host_header() {
501        // This is every HTTP/2 request: RFC 9113 section 8.3.1 replaces Host
502        // with :authority, which reaches us on the URI. Reading only the header
503        // made every host-matched route 404 over h2 -- confirmed against a
504        // running proxy before this was fixed.
505        let r = request_with(&[], "http://api.example.com/x");
506        assert_eq!(MessageHandler::request_host(&r), "api.example.com");
507    }
508
509    #[test]
510    fn the_header_wins_over_the_authority() {
511        let r = request_with(&[("host", "from-header.test")], "http://from-uri.test/x");
512        assert_eq!(MessageHandler::request_host(&r), "from-header.test");
513    }
514
515    #[test]
516    fn the_port_is_not_part_of_the_host() {
517        assert_eq!(
518            MessageHandler::request_host(&request_with(&[("host", "api.example.com:8443")], "/x")),
519            "api.example.com"
520        );
521        assert_eq!(
522            MessageHandler::request_host(&request_with(&[], "http://api.example.com:8443/x")),
523            "api.example.com"
524        );
525    }
526
527    #[test]
528    fn an_ipv6_literal_keeps_its_brackets() {
529        // `split(':').next()` would return "[" here, so every IPv6 client would
530        // be matched against a host of "[".
531        assert_eq!(
532            MessageHandler::request_host(&request_with(&[("host", "[::1]:8443")], "/x")),
533            "[::1]"
534        );
535        assert_eq!(
536            MessageHandler::request_host(&request_with(&[("host", "[::1]")], "/x")),
537            "[::1]"
538        );
539    }
540
541    #[test]
542    fn a_request_with_neither_has_an_empty_host() {
543        let r = request_with(&[], "/x");
544        assert_eq!(MessageHandler::request_host(&r), "");
545    }
546
547    use crate::vendored::hyper_ext::empty_body;
548
549    #[test]
550    fn test_strip_path_prefix() {
551        let mut request = Request::builder()
552            .uri("/api/v1/users")
553            .body(empty_body())
554            .unwrap();
555
556        MessageHandler::strip_path_prefix(&mut request, "/api/v1");
557
558        assert_eq!(request.uri().path(), "/users");
559    }
560
561    #[test]
562    fn test_strip_path_prefix_root() {
563        let mut request = Request::builder().uri("/api").body(empty_body()).unwrap();
564
565        MessageHandler::strip_path_prefix(&mut request, "/api");
566
567        assert_eq!(request.uri().path(), "/");
568    }
569
570    #[test]
571    fn test_h2_connect_protocol_detected() {
572        // RFC 8441: :method CONNECT plus the :protocol pseudo-header, which
573        // hyper surfaces as an extension.
574        let mut request = Request::builder()
575            .method(Method::CONNECT)
576            .version(Version::HTTP_2)
577            .uri("/chat")
578            .body(empty_body())
579            .unwrap();
580        request
581            .extensions_mut()
582            .insert(hyper::ext::Protocol::from_static("websocket"));
583
584        assert_eq!(
585            MessageHandler::h2_connect_protocol(&request).as_deref(),
586            Some("websocket")
587        );
588        // The HTTP/1.1 detector must not claim it.
589        assert!(MessageHandler::upgrade_protocol(&request).is_none());
590    }
591
592    #[test]
593    fn test_h2_connect_without_protocol_is_a_plain_connect() {
594        let request = Request::builder()
595            .method(Method::CONNECT)
596            .version(Version::HTTP_2)
597            .uri("/chat")
598            .body(empty_body())
599            .unwrap();
600
601        assert!(MessageHandler::h2_connect_protocol(&request).is_none());
602    }
603
604    #[test]
605    fn test_h1_connect_is_not_extended_connect() {
606        // Extended CONNECT is an HTTP/2 mechanism; the h1 spelling is Upgrade.
607        let mut request = Request::builder()
608            .method(Method::CONNECT)
609            .version(Version::HTTP_11)
610            .uri("/chat")
611            .body(empty_body())
612            .unwrap();
613        request
614            .extensions_mut()
615            .insert(hyper::ext::Protocol::from_static("websocket"));
616
617        assert!(MessageHandler::h2_connect_protocol(&request).is_none());
618    }
619
620    #[test]
621    fn test_upgrade_protocol_detected() {
622        let request = Request::builder()
623            .uri("/chat")
624            .header("host", "a")
625            .header("upgrade", "websocket")
626            .header("connection", "Upgrade")
627            .body(empty_body())
628            .unwrap();
629
630        assert_eq!(
631            MessageHandler::upgrade_protocol(&request).as_deref(),
632            Some("websocket")
633        );
634    }
635
636    #[test]
637    fn test_upgrade_needs_connection_token() {
638        // Upgrade alone is not a request to switch protocols (RFC 9110 s7.8).
639        let request = Request::builder()
640            .uri("/chat")
641            .header("host", "a")
642            .header("upgrade", "websocket")
643            .body(empty_body())
644            .unwrap();
645
646        assert!(MessageHandler::upgrade_protocol(&request).is_none());
647    }
648
649    #[test]
650    fn test_upgrade_token_found_among_others() {
651        let request = Request::builder()
652            .uri("/chat")
653            .header("host", "a")
654            .header("upgrade", "WebSocket")
655            .header("connection", "keep-alive, UPGRADE")
656            .body(empty_body())
657            .unwrap();
658
659        assert_eq!(
660            MessageHandler::upgrade_protocol(&request).as_deref(),
661            Some("websocket")
662        );
663    }
664
665    #[test]
666    fn test_no_upgrade_without_upgrade_header() {
667        let request = Request::builder()
668            .uri("/chat")
669            .header("host", "a")
670            .header("connection", "upgrade")
671            .body(empty_body())
672            .unwrap();
673
674        assert!(MessageHandler::upgrade_protocol(&request).is_none());
675    }
676
677    #[test]
678    fn test_http2_never_upgrades() {
679        // HTTP/2 negotiates extended CONNECT (RFC 8441), not Upgrade.
680        let request = Request::builder()
681            .uri("/chat")
682            .version(Version::HTTP_2)
683            .header("upgrade", "websocket")
684            .header("connection", "Upgrade")
685            .body(empty_body())
686            .unwrap();
687
688        assert!(MessageHandler::upgrade_protocol(&request).is_none());
689    }
690
691    #[test]
692    fn test_restore_upgrade_headers_normalises_connection() {
693        // The carve-out must put back only `upgrade`, never the client's list.
694        let mut request = Request::builder()
695            .uri("/chat")
696            .header("host", "a")
697            .header("upgrade", "websocket")
698            .header("connection", "Upgrade, X-Sneaky")
699            .header("x-sneaky", "leaked")
700            .header("x-legit", "keep-me")
701            .body(empty_body())
702            .unwrap();
703
704        let protocol = MessageHandler::upgrade_protocol(&request).unwrap();
705        MessageHandler::strip_hop_by_hop_headers(&mut request);
706        MessageHandler::restore_upgrade_headers(&mut request, &protocol);
707
708        assert_eq!(request.headers().get("upgrade").unwrap(), "websocket");
709        assert_eq!(request.headers().get("connection").unwrap(), "upgrade");
710        assert!(!request.headers().contains_key("x-sneaky"));
711        assert_eq!(request.headers().get("x-legit").unwrap(), "keep-me");
712    }
713
714    #[test]
715    fn test_strips_each_hop_by_hop_header() {
716        // One assertion per header in the RFC 9110 section 7.6.1 set.
717        for name in HOP_BY_HOP_HEADERS {
718            let mut request = Request::builder()
719                .uri("/test")
720                .header("host", "example.com")
721                .header(*name, "some-value")
722                .body(empty_body())
723                .unwrap();
724
725            MessageHandler::strip_hop_by_hop_headers(&mut request);
726
727            assert!(
728                !request.headers().contains_key(*name),
729                "hop-by-hop header {} was forwarded",
730                name
731            );
732            // Stripping must not take unrelated headers with it.
733            assert_eq!(request.headers().get("host").unwrap(), "example.com");
734        }
735    }
736
737    #[test]
738    fn test_strips_headers_named_in_connection() {
739        // The live case found by the HTTP Garden probe.
740        let mut request = Request::builder()
741            .uri("/test")
742            .header("host", "example.com")
743            .header("connection", "keep-alive, X-Hop")
744            .header("x-hop", "SHOULD-BE-STRIPPED")
745            .body(empty_body())
746            .unwrap();
747
748        MessageHandler::strip_hop_by_hop_headers(&mut request);
749
750        assert!(!request.headers().contains_key("connection"));
751        assert!(!request.headers().contains_key("x-hop"));
752        assert_eq!(request.headers().get("host").unwrap(), "example.com");
753    }
754
755    #[test]
756    fn test_connection_tokens_are_case_insensitive_and_trimmed() {
757        let mut request = Request::builder()
758            .uri("/test")
759            .header("connection", "  X-UPPER  ,\tx-lower,,")
760            .header("x-upper", "a")
761            .header("x-lower", "b")
762            .body(empty_body())
763            .unwrap();
764
765        MessageHandler::strip_hop_by_hop_headers(&mut request);
766
767        assert!(!request.headers().contains_key("x-upper"));
768        assert!(!request.headers().contains_key("x-lower"));
769    }
770
771    #[test]
772    fn test_connection_token_cannot_strip_host_or_content_length() {
773        // Naming these in Connection is not legitimate; honouring it would let a
774        // client strip framing or identity headers on the way through.
775        let mut request = Request::builder()
776            .uri("/test")
777            .header("host", "example.com")
778            .header("content-length", "0")
779            .header("connection", "host, content-length")
780            .body(empty_body())
781            .unwrap();
782
783        MessageHandler::strip_hop_by_hop_headers(&mut request);
784
785        assert!(!request.headers().contains_key("connection"));
786        assert_eq!(request.headers().get("host").unwrap(), "example.com");
787        assert_eq!(request.headers().get("content-length").unwrap(), "0");
788    }
789
790    #[test]
791    fn test_connection_token_naming_forwarded_header_does_not_suppress_ours() {
792        // strip runs before add_forwarding_headers, so the client's inbound value
793        // goes and ours replaces it -- no suppression, no spoofing.
794        let mut request = Request::builder()
795            .uri("/test")
796            .header("host", "example.com")
797            .header("connection", "x-forwarded-for")
798            .header("x-forwarded-for", "1.2.3.4")
799            .body(empty_body())
800            .unwrap();
801
802        MessageHandler::strip_hop_by_hop_headers(&mut request);
803        let client_addr: SocketAddr = "192.168.1.100:12345".parse().unwrap();
804        MessageHandler::add_forwarding_headers(&mut request, client_addr, "http", false);
805
806        assert_eq!(
807            request.headers().get("x-forwarded-for").unwrap(),
808            "192.168.1.100"
809        );
810    }
811
812    #[test]
813    fn test_strip_is_a_no_op_without_hop_by_hop_headers() {
814        let mut request = Request::builder()
815            .uri("/test")
816            .header("host", "example.com")
817            .header("accept", "*/*")
818            .body(empty_body())
819            .unwrap();
820
821        MessageHandler::strip_hop_by_hop_headers(&mut request);
822
823        assert_eq!(request.headers().len(), 2);
824    }
825
826    #[test]
827    fn test_forwarding_headers() {
828        let mut request = Request::builder()
829            .uri("/test")
830            .header("host", "example.com")
831            .body(empty_body())
832            .unwrap();
833
834        let client_addr: SocketAddr = "192.168.1.100:12345".parse().unwrap();
835        MessageHandler::add_forwarding_headers(&mut request, client_addr, "https", false);
836
837        assert_eq!(
838            request.headers().get("x-forwarded-for").unwrap(),
839            "192.168.1.100"
840        );
841        assert_eq!(request.headers().get("x-forwarded-proto").unwrap(), "https");
842        assert_eq!(
843            request.headers().get("x-forwarded-host").unwrap(),
844            "example.com"
845        );
846    }
847
848    fn forwarded_request(headers: &[(&str, &str)]) -> Request<ProxyBody> {
849        let mut request = request_with(headers, "/test");
850        let client_addr: SocketAddr = "192.0.2.7:4000".parse().unwrap();
851        MessageHandler::add_forwarding_headers(&mut request, client_addr, "https", false);
852        request
853    }
854
855    #[test]
856    fn client_forwarding_headers_are_overwritten_by_default() {
857        // Each of these was a way for a client to choose the address the
858        // backend saw: X-Real-IP was set only if absent, X-Forwarded-For was
859        // appended to, and Forwarded passed straight through.
860        let request = forwarded_request(&[
861            ("host", "example.com"),
862            ("x-real-ip", "10.0.0.1"),
863            ("x-forwarded-for", "10.0.0.1"),
864            ("x-forwarded-for", "10.0.0.2"),
865            ("forwarded", "for=10.0.0.1;proto=https"),
866            ("x-forwarded-proto", "https-spoofed"),
867            ("x-forwarded-host", "spoofed.test"),
868        ]);
869        let h = request.headers();
870        assert_eq!(h.get("x-real-ip").unwrap(), "192.0.2.7");
871        assert_eq!(h.get_all("x-forwarded-for").iter().count(), 1);
872        assert_eq!(h.get("x-forwarded-for").unwrap(), "192.0.2.7");
873        assert!(!h.contains_key("forwarded"));
874        assert_eq!(h.get("x-forwarded-proto").unwrap(), "https");
875        assert_eq!(h.get("x-forwarded-host").unwrap(), "example.com");
876    }
877
878    #[test]
879    fn a_trusted_chain_is_kept_and_extended() {
880        let mut request = request_with(
881            &[
882                ("host", "internal.test"),
883                ("x-real-ip", "10.0.0.1"),
884                ("x-forwarded-for", "198.51.100.1, 10.0.0.1"),
885                ("forwarded", "for=198.51.100.1;proto=https"),
886                ("x-forwarded-proto", "https"),
887                ("x-forwarded-host", "public.example.com"),
888            ],
889            "/test",
890        );
891        let peer: SocketAddr = "[2001:db8::5]:4000".parse().unwrap();
892        MessageHandler::add_forwarding_headers(&mut request, peer, "http", true);
893
894        let h = request.headers();
895        assert_eq!(
896            h.get("x-forwarded-for").unwrap(),
897            "198.51.100.1, 10.0.0.1, 2001:db8::5"
898        );
899        assert_eq!(
900            h.get("forwarded").unwrap(),
901            "for=198.51.100.1;proto=https, for=\"[2001:db8::5]\";proto=http"
902        );
903        // What the first proxy saw describes the original request better than
904        // this hop can.
905        assert_eq!(h.get("x-forwarded-proto").unwrap(), "https");
906        assert_eq!(h.get("x-forwarded-host").unwrap(), "public.example.com");
907        // X-Real-IP names one address, and it is the one this proxy saw.
908        assert_eq!(h.get("x-real-ip").unwrap(), "2001:db8::5");
909    }
910
911    #[test]
912    fn a_mapped_ipv4_peer_is_reported_as_ipv4() {
913        let mut request = request_with(&[("host", "a")], "/");
914        let peer: SocketAddr = "[::ffff:192.0.2.9]:1".parse().unwrap();
915        MessageHandler::add_forwarding_headers(&mut request, peer, "http", false);
916        assert_eq!(
917            request.headers().get("x-forwarded-for").unwrap(),
918            "192.0.2.9"
919        );
920    }
921
922    #[test]
923    fn dot_segments_and_encoded_separators_are_refused() {
924        for path in [
925            "/public/../admin",
926            "/public/./admin",
927            "/public/..",
928            "/..",
929            "/public/%2e%2e/admin",
930            "/public/%2E%2E/admin",
931            "/public/.%2e/admin",
932            "/public/%2e./admin",
933            "/public/%2e/admin",
934            "/public%2f..%2fadmin",
935            "/public%2F..%2Fadmin",
936            "/public/..%2fadmin",
937            "/public%5c..%5cadmin",
938            "/public/..;/admin",
939            "/public/..;jsessionid=x/admin",
940        ] {
941            assert!(
942                MessageHandler::unsafe_path_reason(path).is_some(),
943                "{path} should be refused"
944            );
945        }
946    }
947
948    #[test]
949    fn ordinary_paths_are_allowed() {
950        for path in [
951            "/",
952            "/public",
953            "/public/",
954            "/public/a.b",
955            "/public/...",
956            "/public/.well-known/x",
957            "/public/..x",
958            "/public/x..",
959            "/public/%2e%2e%2e",
960            "/public/a%20b",
961            "/public//double",
962            "",
963        ] {
964            assert!(
965                MessageHandler::unsafe_path_reason(path).is_none(),
966                "{path} should be allowed"
967            );
968        }
969    }
970
971    #[test]
972    fn a_prefix_is_stripped_only_on_a_segment_boundary() {
973        let mut request = request_with(&[], "/apis/x?q=1");
974        MessageHandler::strip_path_prefix(&mut request, "/api");
975        assert_eq!(request.uri().path(), "/apis/x");
976
977        let mut request = request_with(&[], "/api/x?q=1");
978        MessageHandler::strip_path_prefix(&mut request, "/api");
979        assert_eq!(request.uri().path(), "/x");
980        assert_eq!(request.uri().query(), Some("q=1"));
981
982        // A trailing-slash prefix keeps a leading slash on what is left.
983        let mut request = request_with(&[], "/api/x");
984        MessageHandler::strip_path_prefix(&mut request, "/api/");
985        assert_eq!(request.uri().path(), "/x");
986    }
987
988    #[test]
989    fn only_allowed_protocols_may_be_upgraded_to() {
990        let allowed = vec!["websocket".to_string()];
991        assert!(MessageHandler::upgrade_allowed("websocket", &allowed));
992        assert!(MessageHandler::upgrade_allowed("WebSocket", &allowed));
993        // The smuggling case: an h2c tunnel lets the client send the backend
994        // arbitrary requests past every route rule.
995        assert!(!MessageHandler::upgrade_allowed("h2c", &allowed));
996        assert!(!MessageHandler::upgrade_allowed("websocket, h2c", &allowed));
997        assert!(!MessageHandler::upgrade_allowed("websocket", &[]));
998    }
999
1000    #[test]
1001    fn an_h2c_upgrade_leaves_nothing_behind_when_stripped() {
1002        // Declined upgrades are forwarded as ordinary requests, so none of the
1003        // handshake may survive -- HTTP2-Settings included, even when the
1004        // client did not name it in Connection.
1005        let mut request = request_with(
1006            &[
1007                ("host", "a"),
1008                ("connection", "Upgrade"),
1009                ("upgrade", "h2c"),
1010                ("http2-settings", "AAMAAABkAAQCAAAAAAIAAAAA"),
1011            ],
1012            "/",
1013        );
1014        MessageHandler::strip_hop_by_hop_headers(&mut request);
1015        let h = request.headers();
1016        assert!(!h.contains_key("upgrade"));
1017        assert!(!h.contains_key("connection"));
1018        assert!(!h.contains_key("http2-settings"));
1019    }
1020}