Skip to main content

rama_net/
input_ext.rs

1//! Small, single-concern accessor traits for reading routing/transport
2//! properties off a service input (an http request, a connect target, …).
3//!
4//! Each concern (URI, path, authority, protocol, http version, transport) is
5//! its own small trait, so a caller reads exactly the piece it needs instead of
6//! building a combined context up front.
7//!
8//! Design (matching [`ClientIp`](crate::ClientIp)): each *resolution* trait has
9//! no [`ExtensionsRef`] bound and is never
10//! blanket-derived from another trait — every input type opts in with the
11//! resolution that fits it (the http `Request`/`Parts` impls in `rama-http-types`
12//! walk the uri → TLS SNI → `Forwarded` → `Host` fallback chain;
13//! a transport target resolves its authority directly). The only blanket impls
14//! are the trivial reference-forwarding ones and the composed
15//! [`ConnectorTargetInputExt`], whose method is purely derived.
16//!
17//! The return type *is* the fallibility contract: an `Option` may be absent (a
18//! caller that requires it does `.ok_or_else(|| …)?` with its own error);
19//! [`transport_protocol`](TransportProtocolInputExt::transport_protocol) is
20//! always known, so it returns a bare value.
21//!
22//! Each resolution trait also carries **default methods** built on its one
23//! required accessor (e.g. [`AuthorityInputExt::host_as_domain`]), so callers
24//! get ergonomic projections without re-writing the same closure chains.
25
26use crate::Protocol;
27
28#[cfg(feature = "std")]
29use crate::address::HostWithPort;
30use crate::address::{Domain, Host, HostWithOptPort};
31
32#[cfg(feature = "std")]
33use crate::client::ConnectorTarget;
34
35#[cfg(feature = "http")]
36use crate::http::Version;
37use crate::transport::TransportProtocol;
38use crate::uri::{PathRef, Uri};
39
40#[cfg(feature = "std")]
41use rama_core::extensions::ExtensionsRef;
42
43/// Read the [`Uri`] of a service input that carries one.
44///
45/// Unlike the other `*InputExt` traits (which return an `Option` because they
46/// *resolve* a property that may be absent), this is a structural **capability**:
47/// a type implements it only when it always has a URI, so `uri()` returns a
48/// `&Uri` directly. Plenty of inputs besides http requests carry a URI (a bare
49/// [`Uri`], a redirect target, a url-keyed config, …), which is why this lives
50/// next to the http request impls rather than being tied to them.
51///
52/// Note: a `UriInputExt` is **not** automatically an [`AuthorityInputExt`] /
53/// [`ProtocolInputExt`] — an http request, for instance, resolves its authority
54/// from more than just its URI (proxy target, TLS SNI, `Forwarded`, `Host`), so
55/// those impls are deliberately per-type rather than blanket-derived from here.
56pub trait UriInputExt {
57    /// The [`Uri`] this input carries.
58    fn uri(&self) -> &Uri;
59}
60
61impl<T: UriInputExt + ?Sized> UriInputExt for &T {
62    fn uri(&self) -> &Uri {
63        (**self).uri()
64    }
65}
66
67impl UriInputExt for Uri {
68    fn uri(&self) -> &Uri {
69        self
70    }
71}
72
73/// Read the URI path of a service input.
74///
75/// Implementations should return a typed [`PathRef`] and use
76/// [`Uri::path_ref_or_root`] when the path comes from a [`Uri`], so an empty URI
77/// path is observed as `/` and callers never need to fall back to raw strings.
78pub trait PathInputExt {
79    /// The path to route against.
80    fn path_ref(&self) -> PathRef<'_>;
81}
82
83impl<T: PathInputExt + ?Sized> PathInputExt for &T {
84    fn path_ref(&self) -> PathRef<'_> {
85        (**self).path_ref()
86    }
87}
88
89impl PathInputExt for Uri {
90    fn path_ref(&self) -> PathRef<'_> {
91        self.path_ref_or_root()
92    }
93}
94
95/// Read the routing **authority** (`host[:port]`) of a service input.
96///
97/// This is the HTTP routing authority — the `:authority` pseudo-header / `Host`
98/// header target — so it is a [`HostWithOptPort`], **not** the RFC-3986
99/// [`Authority`](crate::address::Authority) type (userinfo is never used for
100/// routing). Returns `None` when no authority can be resolved.
101pub trait AuthorityInputExt {
102    /// The routing authority (`host[:port]`), or `None` if none is resolvable.
103    fn authority(&self) -> Option<HostWithOptPort>;
104
105    /// The authority [`Host`], dropping any port.
106    fn host(&self) -> Option<Host> {
107        self.authority().map(|a| a.host)
108    }
109
110    /// The authority host as a [`Domain`], or `None` if absent or not a domain
111    /// (e.g. an IP literal).
112    fn host_as_domain(&self) -> Option<Domain> {
113        self.authority().and_then(|a| a.host.try_into_domain().ok())
114    }
115
116    /// The authority port, if one is set explicitly.
117    fn port(&self) -> Option<u16> {
118        self.authority().and_then(|a| a.port_u16())
119    }
120}
121
122impl<T: AuthorityInputExt + ?Sized> AuthorityInputExt for &T {
123    fn authority(&self) -> Option<HostWithOptPort> {
124        (**self).authority()
125    }
126}
127
128/// Read the application-layer [`Protocol`] (scheme) of a service input.
129pub trait ProtocolInputExt {
130    /// The application protocol, or `None` if it can't be determined.
131    fn protocol(&self) -> Option<&Protocol>;
132
133    /// The default port of the resolved [`Protocol`] (e.g. 443 for HTTPS), or
134    /// `None` if the protocol is unknown or portless.
135    fn protocol_default_port(&self) -> Option<u16> {
136        self.protocol().and_then(|p| p.default_port())
137    }
138}
139
140impl<T: ProtocolInputExt + ?Sized> ProtocolInputExt for &T {
141    fn protocol(&self) -> Option<&Protocol> {
142        (**self).protocol()
143    }
144}
145
146/// Read the negotiated HTTP [`Version`] of a service input.
147///
148/// Explicitly HTTP-named: it is `None` for non-HTTP inputs (e.g. a raw
149/// transport target).
150#[cfg(feature = "http")]
151pub trait HttpVersionInputExt {
152    /// The HTTP version, or `None` for non-HTTP inputs.
153    fn http_version(&self) -> Option<Version>;
154}
155
156#[cfg(feature = "http")]
157impl<T: HttpVersionInputExt + ?Sized> HttpVersionInputExt for &T {
158    fn http_version(&self) -> Option<Version> {
159        (**self).http_version()
160    }
161}
162
163/// Read the transport-layer [`TransportProtocol`] (TCP/UDP) of a service input.
164///
165/// Always known, so this is infallible.
166pub trait TransportProtocolInputExt {
167    /// The transport protocol (TCP or UDP).
168    fn transport_protocol(&self) -> Option<TransportProtocol>;
169}
170
171impl<T: TransportProtocolInputExt + ?Sized> TransportProtocolInputExt for &T {
172    fn transport_protocol(&self) -> Option<TransportProtocol> {
173        (**self).transport_protocol()
174    }
175}
176
177#[cfg(feature = "std")]
178mod private {
179    use super::{AuthorityInputExt, ProtocolInputExt};
180
181    /// Seals [`ConnectorTargetInputExt`](super::ConnectorTargetInputExt): it is
182    /// purely derived from [`AuthorityInputExt`] + [`ProtocolInputExt`], so it must
183    /// never be implemented by hand.
184    pub trait Sealed {}
185    impl<T: AuthorityInputExt + ProtocolInputExt + ?Sized> Sealed for T {}
186}
187
188/// Resolve the **transport address** (`host:port`) to connect to: the routing
189/// [`authority`](AuthorityInputExt::authority) with the application
190/// [`protocol`](ProtocolInputExt::protocol)'s default port as the port fallback.
191///
192/// Auto-implemented (and sealed) for every input that is both an
193/// [`AuthorityInputExt`] and a [`ProtocolInputExt`]; it yields the typed
194/// `host:port` a connector needs, and is never implemented by hand.
195#[cfg(feature = "std")]
196pub trait ConnectorTargetInputExt:
197    AuthorityInputExt + ProtocolInputExt + ExtensionsRef + private::Sealed
198{
199    /// The `host:port` to connect to: the authority's port if set, else the
200    /// protocol's default port. `None` when no host (or no port) resolves.
201    ///
202    /// NOTE that this method respects the extension [`ConnectorTarget`]
203    /// as overwrite, used for proxy connections and similar bypasses.
204    fn connector_target(&self) -> Option<HostWithPort> {
205        if let Some(ConnectorTarget(target)) = self.extensions().get_ref() {
206            return Some(target.clone());
207        }
208
209        self.authority()
210            .and_then(|a| a.into_host_with_port(self.protocol_default_port()))
211    }
212
213    /// Like [`connector_target`](Self::connector_target) but with `default_port` as
214    /// the ultimate fallback (ConnectorTarget → authority port → protocol default → `default_port`),
215    /// so it yields `Some` whenever an authority resolves at all.
216    ///
217    /// NOTE that this method respects the extension [`ConnectorTarget`]
218    /// as overwrite, used for proxy connections and similar bypasses.
219    fn connector_target_with_default_port(&self, default_port: u16) -> Option<HostWithPort> {
220        if let Some(ConnectorTarget(target)) = self.extensions().get_ref() {
221            return Some(target.clone());
222        }
223
224        self.authority()
225            .map(|a| a.into_host_with_port_or(self.protocol_default_port().unwrap_or(default_port)))
226    }
227}
228
229#[cfg(feature = "std")]
230impl<T: AuthorityInputExt + ProtocolInputExt + ExtensionsRef + ?Sized> ConnectorTargetInputExt
231    for T
232{
233}
234
235#[cfg(test)]
236mod tests {
237    use super::{PathInputExt, UriInputExt};
238    use crate::uri::{PathPattern, Uri};
239
240    #[test]
241    fn uri_input_ref_forwards_to_inner_uri() {
242        let uri: Uri = "https://example.com/a%2Fb?q=1".parse().unwrap();
243        let uri_ref = &uri;
244        let forwarded = <&Uri as UriInputExt>::uri(&uri_ref);
245
246        assert_eq!(forwarded.path_ref_or_root(), "/a%2Fb");
247        assert_ne!(forwarded.path_ref_or_root(), "/a/b");
248    }
249
250    #[test]
251    fn path_input_ref_forwards_to_inner_path() {
252        let uri: Uri = "https://example.com/a%2Fb?q=1".parse().unwrap();
253        let uri_ref = &uri;
254        let forwarded = <&Uri as PathInputExt>::path_ref(&uri_ref);
255
256        assert_eq!(uri.path_ref(), "/a%2Fb");
257        assert_eq!(forwarded, "/a%2Fb");
258        assert_ne!(forwarded, "/a/b");
259    }
260
261    #[test]
262    fn path_input_for_uri_uses_root_fallback() {
263        let uri: Uri = "https://example.com".parse().unwrap();
264
265        assert_eq!(uri.path_ref(), "/");
266    }
267
268    #[test]
269    fn uri_pattern_helpers_route_through_typed_path() {
270        let uri: Uri = "https://example.com/api/acme/widgets".parse().unwrap();
271        let pattern = PathPattern::new("/api/{tenant}/widgets");
272        let miss = PathPattern::new("/api/{tenant}/orders");
273
274        assert!(uri.is_pattern_match(&pattern));
275        assert!(!uri.is_pattern_match(&miss));
276
277        let captures = uri.pattern_captures(&pattern).unwrap();
278        assert_eq!(captures.get("tenant"), Some("acme"));
279        assert!(uri.pattern_captures(&miss).is_none());
280    }
281}