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::address::{Domain, Host, HostWithOptPort};
9use rama_net::forwarded::Forwarded;
10use rama_net::transport::TransportProtocol;
11use rama_net::{
12    AuthorityInputExt, HttpVersionInputExt, PathInputExt, ProtocolInputExt,
13    TargetHttpVersionInputExt, TransportProtocolInputExt, UriInputExt,
14};
15use rama_net::{Protocol, http::TargetHttpVersion};
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
177impl<Body> TargetHttpVersionInputExt for Request<Body> {
178    fn target_http_version(&self) -> Option<Version> {
179        self.target_http_version_with_fallback(None)
180    }
181
182    fn target_http_version_with_fallback(&self, fallback: Option<Version>) -> Option<Version> {
183        self.extensions()
184            .get_ref::<TargetHttpVersion>()
185            .map(|target| target.0)
186            .or(fallback)
187            .or(Some(self.version()))
188    }
189}
190
191impl TargetHttpVersionInputExt for Parts {
192    fn target_http_version(&self) -> Option<Version> {
193        self.target_http_version_with_fallback(None)
194    }
195
196    fn target_http_version_with_fallback(&self, fallback: Option<Version>) -> Option<Version> {
197        self.extensions()
198            .get_ref::<TargetHttpVersion>()
199            .map(|target| target.0)
200            .or(fallback)
201            .or(Some(self.version()))
202    }
203}
204
205/// HTTP/3 rides on UDP; every other HTTP version on TCP.
206fn transport_protocol_for_http_version(version: Version) -> TransportProtocol {
207    match version {
208        Version::HTTP_3 => TransportProtocol::Udp,
209        _ => TransportProtocol::Tcp,
210    }
211}
212
213impl<Body> TransportProtocolInputExt for Request<Body> {
214    fn transport_protocol(&self) -> Option<TransportProtocol> {
215        Some(transport_protocol_for_http_version(self.version()))
216    }
217}
218
219impl TransportProtocolInputExt for Parts {
220    fn transport_protocol(&self) -> Option<TransportProtocol> {
221        Some(transport_protocol_for_http_version(self.version()))
222    }
223}
224
225impl<Body> UriInputExt for Request<Body> {
226    fn uri(&self) -> &Uri {
227        HttpRequestParts::uri(self)
228    }
229}
230
231impl UriInputExt for Parts {
232    fn uri(&self) -> &Uri {
233        HttpRequestParts::uri(self)
234    }
235}
236
237impl<Body> PathInputExt for Request<Body> {
238    fn path_ref(&self) -> rama_net::uri::PathRef<'_> {
239        self.uri().path_ref_or_root()
240    }
241}
242
243impl PathInputExt for Parts {
244    fn path_ref(&self) -> rama_net::uri::PathRef<'_> {
245        HttpRequestParts::uri(self).path_ref_or_root()
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::{Request, header::FORWARDED};
253    use rama_core::extensions::ExtensionsRef;
254    use rama_net::forwarded::{Forwarded, ForwardedElement, ForwardedVersion, NodeId};
255
256    #[test]
257    fn accessors_from_request() {
258        let req = Request::builder()
259            .uri("http://example.com:8080")
260            .version(Version::HTTP_11)
261            .body(())
262            .unwrap();
263
264        assert_eq!(req.http_version(), Some(Version::HTTP_11));
265        assert_eq!(req.protocol(), Some(&Protocol::HTTP));
266        assert_eq!(req.authority().unwrap().to_string(), "example.com:8080");
267    }
268
269    #[test]
270    fn path_accessor_from_request_and_parts() {
271        let req = Request::builder()
272            .uri("http://example.com/a%2Fb?q=1")
273            .body(())
274            .unwrap();
275
276        assert_eq!(req.path_ref(), "/a%2Fb");
277        assert_ne!(req.path_ref(), "/a/b");
278
279        let (parts, _) = req.into_parts();
280        assert_eq!(parts.path_ref(), "/a%2Fb");
281        assert_ne!(parts.path_ref(), "/a/b");
282    }
283
284    #[test]
285    fn accessors_resolve() {
286        let req = Request::builder()
287            .uri("https://example.com:8443")
288            .version(Version::HTTP_2)
289            .body(())
290            .unwrap();
291        assert_eq!(req.authority().unwrap().to_string(), "example.com:8443");
292        assert_eq!(req.protocol(), Some(&Protocol::HTTPS));
293        assert_eq!(req.http_version(), Some(Version::HTTP_2));
294
295        // origin-form with no resolvable authority -> None, but protocol and
296        // version still resolve (they don't depend on the authority).
297        let req = Request::builder().uri("/path").body(()).unwrap();
298        assert_eq!(req.authority(), None);
299        assert_eq!(req.protocol(), Some(&Protocol::HTTP));
300        assert_eq!(req.http_version(), Some(Version::HTTP_11));
301    }
302
303    #[test]
304    fn accessors_from_parts() {
305        let req = Request::builder()
306            .uri("http://example.com:8080")
307            .version(Version::HTTP_11)
308            .body(())
309            .unwrap();
310
311        let (parts, _) = req.into_parts();
312
313        assert_eq!(parts.http_version(), Some(Version::HTTP_11));
314        assert_eq!(parts.protocol(), Some(&Protocol::HTTP));
315        assert_eq!(
316            parts.authority().unwrap(),
317            HostWithOptPort::try_from("example.com:8080").unwrap()
318        );
319    }
320
321    #[test]
322    fn target_version_ignores_forwarded_client_version() {
323        let req = Request::builder()
324            .version(Version::HTTP_11)
325            .body(())
326            .unwrap();
327        req.extensions()
328            .insert(Forwarded::new(ForwardedElement::new_forwarded_version(
329                ForwardedVersion::HTTP_2,
330            )));
331
332        assert_eq!(req.http_version(), Some(Version::HTTP_2));
333        assert_eq!(req.target_http_version(), Some(Version::HTTP_11));
334    }
335
336    #[test]
337    fn explicit_target_version_overrides_request_version_for_request_and_parts() {
338        let req = Request::builder()
339            .version(Version::HTTP_11)
340            .body(())
341            .unwrap();
342        req.extensions().insert(TargetHttpVersion(Version::HTTP_2));
343
344        assert_eq!(req.target_http_version(), Some(Version::HTTP_2));
345
346        let (parts, _) = req.into_parts();
347        assert_eq!(parts.target_http_version(), Some(Version::HTTP_2));
348    }
349
350    #[test]
351    fn target_version_fallback_precedes_request_version_but_not_explicit_target() {
352        let req = Request::builder()
353            .version(Version::HTTP_11)
354            .body(())
355            .unwrap();
356
357        assert_eq!(
358            req.target_http_version_with_fallback(Some(Version::HTTP_10)),
359            Some(Version::HTTP_10),
360        );
361
362        req.extensions().insert(TargetHttpVersion(Version::HTTP_2));
363        assert_eq!(
364            req.target_http_version_with_fallback(Some(Version::HTTP_10)),
365            Some(Version::HTTP_2),
366        );
367    }
368
369    #[test]
370    fn forwarded_parsing() {
371        for (forwarded_str_vec, expected_authority) in [
372            // base
373            (
374                vec!["host=192.0.2.60;proto=http;by=203.0.113.43"],
375                "192.0.2.60:80",
376            ),
377            // ipv6
378            (
379                vec!["host=\"[2001:db8:cafe::17]:4711\""],
380                "[2001:db8:cafe::17]:4711",
381            ),
382            // multiple values in one header
383            (vec!["host=192.0.2.60, host=127.0.0.1"], "192.0.2.60:80"),
384            // multiple header values
385            (vec!["host=192.0.2.60", "host=127.0.0.1"], "192.0.2.60:80"),
386        ] {
387            let mut req_builder = Request::builder();
388            for header in forwarded_str_vec.clone() {
389                req_builder = req_builder.header(FORWARDED, header);
390            }
391
392            let req = req_builder.body(()).unwrap();
393
394            let forwarded: Forwarded = req
395                .headers()
396                .get(FORWARDED)
397                .unwrap()
398                .as_bytes()
399                .try_into()
400                .unwrap();
401            req.extensions().insert(forwarded);
402
403            assert_eq!(
404                req.authority().map(|a| a.to_string()).as_deref(),
405                Some(expected_authority),
406                "Failed for {forwarded_str_vec:?}"
407            );
408            assert_eq!(
409                req.protocol(),
410                Some(&Protocol::HTTP),
411                "Failed for {forwarded_str_vec:?}"
412            );
413            assert_eq!(
414                req.http_version(),
415                Some(Version::HTTP_11),
416                "Failed for {forwarded_str_vec:?}"
417            );
418        }
419    }
420
421    #[test]
422    fn https_request_behind_haproxy_plain() {
423        let req = Request::builder()
424            .uri("/en/reservation/roomdetails")
425            .version(Version::HTTP_11)
426            .header("host", "echo.ramaproxy.org")
427            .header("user-agent", "curl/8.6.0")
428            .header("accept", "*/*")
429            .body(())
430            .unwrap();
431
432        req.extensions()
433            .insert(Forwarded::new(ForwardedElement::new_forwarded_for(
434                NodeId::try_from("127.0.0.1:61234").unwrap(),
435            )));
436
437        assert_eq!(req.http_version(), Some(Version::HTTP_11));
438        assert_eq!(req.protocol(), Some(&Protocol::HTTP));
439        let authority = req.authority().unwrap();
440        assert_eq!(authority.to_string(), "echo.ramaproxy.org");
441        let default_port = req
442            .protocol_default_port()
443            .unwrap_or(Protocol::HTTP_DEFAULT_PORT);
444        assert_eq!(
445            authority.into_host_with_port_or(default_port).to_string(),
446            "echo.ramaproxy.org:80"
447        );
448    }
449
450    // An origin-form request (no scheme) carrying a TLS `SecureTransport` marker
451    // — the shape of a request read off a terminated TLS connection — must
452    // resolve its protocol as HTTPS; the marker is the only secure signal here.
453    // This guards the `SecureTransport` fallback in `protocol_from_uri_or_extensions`
454    // against the real type being swapped for the tls-off dummy. See the matching
455    // cross-crate regression in rama-http-backend's `svc` tests, which catches the
456    // feature wiring (`rama-http-types/tls` must follow `rama-tls`).
457    #[cfg(feature = "tls")]
458    #[test]
459    fn secure_transport_marks_origin_form_request_https() {
460        let req = Request::builder()
461            .uri("/ping")
462            .header("host", "example.com")
463            .body(())
464            .unwrap();
465        req.extensions().insert(SecureTransport::default());
466
467        assert_eq!(req.protocol(), Some(&Protocol::HTTPS));
468    }
469}