Skip to main content

rama_http_types/
input_ext.rs

1use crate::request::Parts;
2use crate::{HttpRequestParts, Request};
3use crate::{Uri, Version};
4#[cfg(not(feature = "tls"))]
5use rama_core::extensions::Extension;
6use rama_core::extensions::{Extensions, ExtensionsRef};
7use rama_core::telemetry::tracing;
8use rama_net::Protocol;
9use rama_net::address::{Domain, Host, HostWithOptPort};
10use rama_net::forwarded::Forwarded;
11use rama_net::transport::TransportProtocol;
12use rama_net::{
13    AuthorityInputExt, HttpVersionInputExt, PathInputExt, ProtocolInputExt,
14    TransportProtocolInputExt, UriInputExt,
15};
16
17#[cfg(feature = "tls")]
18use rama_tls::SecureTransport;
19
20#[cfg(feature = "tls")]
21fn try_get_sni_from_secure_transport(t: &SecureTransport) -> Option<Domain> {
22    use rama_tls::client::ClientHelloExtension;
23
24    t.client_hello().and_then(|h| {
25        h.extensions().iter().find_map(|e| match e {
26            ClientHelloExtension::ServerName(maybe_domain) => maybe_domain.clone(),
27            _ => None,
28        })
29    })
30}
31
32#[cfg(not(feature = "tls"))]
33#[derive(Debug, Clone, Extension)]
34#[extension(tags(tls))]
35#[non_exhaustive]
36struct SecureTransport;
37
38#[cfg(not(feature = "tls"))]
39fn try_get_sni_from_secure_transport(_: &SecureTransport) -> Option<Domain> {
40    None
41}
42
43/// Resolve the routing authority of `parts`, walking the
44/// uri → TLS SNI → `Forwarded` → `Host`-header fallback chain.
45/// `None` when none of them yields a host.
46pub(crate) fn authority_from_http_parts(parts: &impl HttpRequestParts) -> Option<HostWithOptPort> {
47    let uri = parts.uri();
48
49    let protocol = protocol_from_uri_or_extensions(parts.extensions(), uri);
50    let default_port = uri
51        .port_u16()
52        .unwrap_or_else(|| protocol.default_port().unwrap_or(80));
53
54    uri.host()
55        .map(|h| {
56            let h: Host = h.into_owned();
57            tracing::trace!(url.full = %uri, "request context: detected host {h} from (abs) uri");
58            (h, default_port).into()
59        })
60        .or_else(|| {
61            parts
62                .extensions()
63                .get_ref()
64                .and_then(try_get_sni_from_secure_transport)
65                .map(|host| {
66                    tracing::trace!(url.full = %uri, "request context: detected host {host} from SNI");
67                    (host, default_port).into()
68                })
69        })
70        .or_else(|| {
71            parts.extensions().get_ref::<Forwarded>().and_then(|f| {
72                f.client_host().map(|fauth| {
73                    let HostWithOptPort { host, port } = fauth.0.clone();
74                    let port = port.as_u16().unwrap_or(default_port);
75                    tracing::trace!(url.full = %uri, "request context: detected host {host} from forwarded info");
76                    (host, port).into()
77                })
78            })
79        })
80        .or_else(|| {
81            parts
82                .headers()
83                .get(crate::header::HOST)
84                .and_then(|host_header_value| {
85                    HostWithOptPort::try_from(host_header_value.as_bytes()).ok()
86                })
87        })
88}
89
90/// Resolve the HTTP [`Version`] from `parts`: the `Forwarded` client version
91/// when present, otherwise the request's own version.
92pub(crate) fn http_version_from_http_parts(parts: &impl HttpRequestParts) -> Version {
93    parts
94        .extensions()
95        .get_ref::<Forwarded>()
96        .and_then(|f| {
97            f.client_version().map(|v| match v {
98                rama_net::forwarded::ForwardedVersion::HTTP_09 => Version::HTTP_09,
99                rama_net::forwarded::ForwardedVersion::HTTP_10 => Version::HTTP_10,
100                rama_net::forwarded::ForwardedVersion::HTTP_11 => Version::HTTP_11,
101                rama_net::forwarded::ForwardedVersion::HTTP_2 => Version::HTTP_2,
102                rama_net::forwarded::ForwardedVersion::HTTP_3 => Version::HTTP_3,
103            })
104        })
105        .unwrap_or_else(|| parts.version())
106}
107
108/// Resolve the application [`Protocol`] (scheme) of an HTTP request from its
109/// [`Uri`] and [`Extensions`], without needing a full `Request`/`Parts`.
110///
111/// This is the same resolution [`ProtocolInputExt::protocol`] performs: URI scheme,
112/// then an inserted [`Protocol`] extension, then `Forwarded` client-proto, then a TLS
113/// `SecureTransport` marker. Exposed so layers that only hold `(&Extensions, &Uri)`
114/// (e.g. the HTTP/1 encoder) can make the same secure/insecure determination.
115pub fn protocol_from_uri_or_extensions<'a>(ext: &'a Extensions, uri: &'a Uri) -> &'a Protocol {
116    uri.scheme().or_else(|| {
117        // Can be inserted by a server stack to notify the protocol that's being served.
118        // This is especially useful for marking a HTTPS server as HTTPS,
119        // despite it not showing up anywhere due to a non-default port
120        // and it being http/1
121        ext.get_ref::<Protocol>()
122    }).or_else(|| ext.get_ref::<Forwarded>()
123        .and_then(|f| f.client_proto().map(|p| {
124            tracing::trace!(url.furi = %uri, "request context: detected protocol from forwarded client proto");
125            if p.is_secure() { &Protocol::HTTPS } else { &Protocol::HTTP }
126        })))
127        .unwrap_or_else(||
128    if ext.contains::<SecureTransport>() {
129        &Protocol::HTTPS
130    } else {
131        &Protocol::HTTP
132    })
133}
134
135impl<Body> AuthorityInputExt for Request<Body> {
136    fn authority(&self) -> Option<HostWithOptPort> {
137        authority_from_http_parts(self)
138    }
139}
140
141impl AuthorityInputExt for Parts {
142    fn authority(&self) -> Option<HostWithOptPort> {
143        authority_from_http_parts(self)
144    }
145}
146
147impl<Body> ProtocolInputExt for Request<Body> {
148    fn protocol(&self) -> Option<&Protocol> {
149        Some(protocol_from_uri_or_extensions(
150            self.extensions(),
151            self.uri(),
152        ))
153    }
154}
155
156impl ProtocolInputExt for Parts {
157    fn protocol(&self) -> Option<&Protocol> {
158        Some(protocol_from_uri_or_extensions(
159            self.extensions(),
160            HttpRequestParts::uri(self),
161        ))
162    }
163}
164
165impl<Body> HttpVersionInputExt for Request<Body> {
166    fn http_version(&self) -> Option<Version> {
167        Some(http_version_from_http_parts(self))
168    }
169}
170
171impl HttpVersionInputExt for Parts {
172    fn http_version(&self) -> Option<Version> {
173        Some(http_version_from_http_parts(self))
174    }
175}
176
177/// HTTP/3 rides on UDP; every other HTTP version on TCP.
178fn transport_protocol_for_http_version(version: Version) -> TransportProtocol {
179    match version {
180        Version::HTTP_3 => TransportProtocol::Udp,
181        _ => TransportProtocol::Tcp,
182    }
183}
184
185impl<Body> TransportProtocolInputExt for Request<Body> {
186    fn transport_protocol(&self) -> Option<TransportProtocol> {
187        Some(transport_protocol_for_http_version(self.version()))
188    }
189}
190
191impl TransportProtocolInputExt for Parts {
192    fn transport_protocol(&self) -> Option<TransportProtocol> {
193        Some(transport_protocol_for_http_version(self.version()))
194    }
195}
196
197impl<Body> UriInputExt for Request<Body> {
198    fn uri(&self) -> &Uri {
199        HttpRequestParts::uri(self)
200    }
201}
202
203impl UriInputExt for Parts {
204    fn uri(&self) -> &Uri {
205        HttpRequestParts::uri(self)
206    }
207}
208
209impl<Body> PathInputExt for Request<Body> {
210    fn path_ref(&self) -> rama_net::uri::PathRef<'_> {
211        self.uri().path_ref_or_root()
212    }
213}
214
215impl PathInputExt for Parts {
216    fn path_ref(&self) -> rama_net::uri::PathRef<'_> {
217        HttpRequestParts::uri(self).path_ref_or_root()
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use crate::{Request, header::FORWARDED};
225    use rama_core::extensions::ExtensionsRef;
226    use rama_net::forwarded::{Forwarded, ForwardedElement, NodeId};
227
228    #[test]
229    fn accessors_from_request() {
230        let req = Request::builder()
231            .uri("http://example.com:8080")
232            .version(Version::HTTP_11)
233            .body(())
234            .unwrap();
235
236        assert_eq!(req.http_version(), Some(Version::HTTP_11));
237        assert_eq!(req.protocol(), Some(&Protocol::HTTP));
238        assert_eq!(req.authority().unwrap().to_string(), "example.com:8080");
239    }
240
241    #[test]
242    fn path_accessor_from_request_and_parts() {
243        let req = Request::builder()
244            .uri("http://example.com/a%2Fb?q=1")
245            .body(())
246            .unwrap();
247
248        assert_eq!(req.path_ref(), "/a%2Fb");
249        assert_ne!(req.path_ref(), "/a/b");
250
251        let (parts, _) = req.into_parts();
252        assert_eq!(parts.path_ref(), "/a%2Fb");
253        assert_ne!(parts.path_ref(), "/a/b");
254    }
255
256    #[test]
257    fn accessors_resolve() {
258        let req = Request::builder()
259            .uri("https://example.com:8443")
260            .version(Version::HTTP_2)
261            .body(())
262            .unwrap();
263        assert_eq!(req.authority().unwrap().to_string(), "example.com:8443");
264        assert_eq!(req.protocol(), Some(&Protocol::HTTPS));
265        assert_eq!(req.http_version(), Some(Version::HTTP_2));
266
267        // origin-form with no resolvable authority -> None, but protocol and
268        // version still resolve (they don't depend on the authority).
269        let req = Request::builder().uri("/path").body(()).unwrap();
270        assert_eq!(req.authority(), None);
271        assert_eq!(req.protocol(), Some(&Protocol::HTTP));
272        assert_eq!(req.http_version(), Some(Version::HTTP_11));
273    }
274
275    #[test]
276    fn accessors_from_parts() {
277        let req = Request::builder()
278            .uri("http://example.com:8080")
279            .version(Version::HTTP_11)
280            .body(())
281            .unwrap();
282
283        let (parts, _) = req.into_parts();
284
285        assert_eq!(parts.http_version(), Some(Version::HTTP_11));
286        assert_eq!(parts.protocol(), Some(&Protocol::HTTP));
287        assert_eq!(
288            parts.authority().unwrap(),
289            HostWithOptPort::try_from("example.com:8080").unwrap()
290        );
291    }
292
293    #[test]
294    fn forwarded_parsing() {
295        for (forwarded_str_vec, expected_authority) in [
296            // base
297            (
298                vec!["host=192.0.2.60;proto=http;by=203.0.113.43"],
299                "192.0.2.60:80",
300            ),
301            // ipv6
302            (
303                vec!["host=\"[2001:db8:cafe::17]:4711\""],
304                "[2001:db8:cafe::17]:4711",
305            ),
306            // multiple values in one header
307            (vec!["host=192.0.2.60, host=127.0.0.1"], "192.0.2.60:80"),
308            // multiple header values
309            (vec!["host=192.0.2.60", "host=127.0.0.1"], "192.0.2.60:80"),
310        ] {
311            let mut req_builder = Request::builder();
312            for header in forwarded_str_vec.clone() {
313                req_builder = req_builder.header(FORWARDED, header);
314            }
315
316            let req = req_builder.body(()).unwrap();
317
318            let forwarded: Forwarded = req
319                .headers()
320                .get(FORWARDED)
321                .unwrap()
322                .as_bytes()
323                .try_into()
324                .unwrap();
325            req.extensions().insert(forwarded);
326
327            assert_eq!(
328                req.authority().map(|a| a.to_string()).as_deref(),
329                Some(expected_authority),
330                "Failed for {forwarded_str_vec:?}"
331            );
332            assert_eq!(
333                req.protocol(),
334                Some(&Protocol::HTTP),
335                "Failed for {forwarded_str_vec:?}"
336            );
337            assert_eq!(
338                req.http_version(),
339                Some(Version::HTTP_11),
340                "Failed for {forwarded_str_vec:?}"
341            );
342        }
343    }
344
345    #[test]
346    fn https_request_behind_haproxy_plain() {
347        let req = Request::builder()
348            .uri("/en/reservation/roomdetails")
349            .version(Version::HTTP_11)
350            .header("host", "echo.ramaproxy.org")
351            .header("user-agent", "curl/8.6.0")
352            .header("accept", "*/*")
353            .body(())
354            .unwrap();
355
356        req.extensions()
357            .insert(Forwarded::new(ForwardedElement::new_forwarded_for(
358                NodeId::try_from("127.0.0.1:61234").unwrap(),
359            )));
360
361        assert_eq!(req.http_version(), Some(Version::HTTP_11));
362        assert_eq!(req.protocol(), Some(&Protocol::HTTP));
363        let authority = req.authority().unwrap();
364        assert_eq!(authority.to_string(), "echo.ramaproxy.org");
365        let default_port = req
366            .protocol_default_port()
367            .unwrap_or(Protocol::HTTP_DEFAULT_PORT);
368        assert_eq!(
369            authority.into_host_with_port_or(default_port).to_string(),
370            "echo.ramaproxy.org:80"
371        );
372    }
373
374    // An origin-form request (no scheme) carrying a TLS `SecureTransport` marker
375    // — the shape of a request read off a terminated TLS connection — must
376    // resolve its protocol as HTTPS; the marker is the only secure signal here.
377    // This guards the `SecureTransport` fallback in `protocol_from_uri_or_extensions`
378    // against the real type being swapped for the tls-off dummy. See the matching
379    // cross-crate regression in rama-http-backend's `svc` tests, which catches the
380    // feature wiring (`rama-http-types/tls` must follow `rama-tls`).
381    #[cfg(feature = "tls")]
382    #[test]
383    fn secure_transport_marks_origin_form_request_https() {
384        let req = Request::builder()
385            .uri("/ping")
386            .header("host", "example.com")
387            .body(())
388            .unwrap();
389        req.extensions().insert(SecureTransport::default());
390
391        assert_eq!(req.protocol(), Some(&Protocol::HTTPS));
392    }
393}