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 contextual HTTP [`Version`] of a service input.
147///
148/// For an HTTP request this may resolve the original client version from
149/// forwarded context, so it is not necessarily the egress version a connector
150/// should establish. Connector code should use [`TargetHttpVersionInputExt`].
151/// It is `None` for non-HTTP inputs (e.g. a raw transport target).
152#[cfg(feature = "http")]
153pub trait HttpVersionInputExt {
154    /// The HTTP version, or `None` for non-HTTP inputs.
155    fn http_version(&self) -> Option<Version>;
156}
157
158#[cfg(feature = "http")]
159impl<T: HttpVersionInputExt + ?Sized> HttpVersionInputExt for &T {
160    fn http_version(&self) -> Option<Version> {
161        (**self).http_version()
162    }
163}
164
165/// Read the target HTTP [`Version`] a connector should establish.
166///
167/// This is deliberately separate from [`HttpVersionInputExt`], which may
168/// resolve the original client version from forwarded request context. A
169/// connector needs the selected egress version instead.
170#[cfg(feature = "http")]
171pub trait TargetHttpVersionInputExt {
172    /// The selected egress HTTP version, or `None` if none is available.
173    fn target_http_version(&self) -> Option<Version>;
174
175    /// Resolve the selected egress HTTP version with a post-negotiation fallback.
176    ///
177    /// Implementations that can distinguish an explicit target from an
178    /// implicit input version should override this method so `fallback` is
179    /// considered between those two sources.
180    fn target_http_version_with_fallback(&self, fallback: Option<Version>) -> Option<Version> {
181        self.target_http_version().or(fallback)
182    }
183}
184
185#[cfg(feature = "http")]
186impl<T: TargetHttpVersionInputExt + ?Sized> TargetHttpVersionInputExt for &T {
187    fn target_http_version(&self) -> Option<Version> {
188        (**self).target_http_version()
189    }
190
191    fn target_http_version_with_fallback(&self, fallback: Option<Version>) -> Option<Version> {
192        (**self).target_http_version_with_fallback(fallback)
193    }
194}
195
196/// Read the transport-layer [`TransportProtocol`] (TCP/UDP) of a service input.
197///
198/// Always known, so this is infallible.
199pub trait TransportProtocolInputExt {
200    /// The transport protocol (TCP or UDP).
201    fn transport_protocol(&self) -> Option<TransportProtocol>;
202}
203
204impl<T: TransportProtocolInputExt + ?Sized> TransportProtocolInputExt for &T {
205    fn transport_protocol(&self) -> Option<TransportProtocol> {
206        (**self).transport_protocol()
207    }
208}
209
210#[cfg(feature = "std")]
211mod private {
212    use super::{AuthorityInputExt, ProtocolInputExt};
213
214    /// Seals [`ConnectorTargetInputExt`](super::ConnectorTargetInputExt): it is
215    /// purely derived from [`AuthorityInputExt`] + [`ProtocolInputExt`], so it must
216    /// never be implemented by hand.
217    pub trait Sealed {}
218    impl<T: AuthorityInputExt + ProtocolInputExt + ?Sized> Sealed for T {}
219}
220
221/// Resolve the **transport address** (`host:port`) to connect to: the routing
222/// [`authority`](AuthorityInputExt::authority) with the application
223/// [`protocol`](ProtocolInputExt::protocol)'s default port as the port fallback.
224///
225/// Auto-implemented (and sealed) for every input that is both an
226/// [`AuthorityInputExt`] and a [`ProtocolInputExt`]; it yields the typed
227/// `host:port` a connector needs, and is never implemented by hand.
228#[cfg(feature = "std")]
229pub trait ConnectorTargetInputExt:
230    AuthorityInputExt + ProtocolInputExt + ExtensionsRef + private::Sealed
231{
232    /// The `host:port` to connect to: the authority's port if set, else the
233    /// protocol's default port. `None` when no host (or no port) resolves.
234    ///
235    /// NOTE that this method respects the extension [`ConnectorTarget`]
236    /// as overwrite, used for proxy connections and similar bypasses.
237    fn connector_target(&self) -> Option<HostWithPort> {
238        if let Some(ConnectorTarget(target)) = self.extensions().get_ref() {
239            return Some(target.clone());
240        }
241
242        self.authority()
243            .and_then(|a| a.into_host_with_port(self.protocol_default_port()))
244    }
245
246    /// Like [`connector_target`](Self::connector_target) but with `default_port` as
247    /// the ultimate fallback (ConnectorTarget → authority port → protocol default → `default_port`),
248    /// so it yields `Some` whenever an authority resolves at all.
249    ///
250    /// NOTE that this method respects the extension [`ConnectorTarget`]
251    /// as overwrite, used for proxy connections and similar bypasses.
252    fn connector_target_with_default_port(&self, default_port: u16) -> Option<HostWithPort> {
253        if let Some(ConnectorTarget(target)) = self.extensions().get_ref() {
254            return Some(target.clone());
255        }
256
257        self.authority()
258            .map(|a| a.into_host_with_port_or(self.protocol_default_port().unwrap_or(default_port)))
259    }
260}
261
262#[cfg(feature = "std")]
263impl<T: AuthorityInputExt + ProtocolInputExt + ExtensionsRef + ?Sized> ConnectorTargetInputExt
264    for T
265{
266}
267
268#[cfg(test)]
269mod tests {
270    use super::{PathInputExt, UriInputExt};
271    use crate::uri::{PathPattern, Uri};
272
273    #[test]
274    fn uri_input_ref_forwards_to_inner_uri() {
275        let uri: Uri = "https://example.com/a%2Fb?q=1".parse().unwrap();
276        let uri_ref = &uri;
277        let forwarded = <&Uri as UriInputExt>::uri(&uri_ref);
278
279        assert_eq!(forwarded.path_ref_or_root(), "/a%2Fb");
280        assert_ne!(forwarded.path_ref_or_root(), "/a/b");
281    }
282
283    #[test]
284    fn path_input_ref_forwards_to_inner_path() {
285        let uri: Uri = "https://example.com/a%2Fb?q=1".parse().unwrap();
286        let uri_ref = &uri;
287        let forwarded = <&Uri as PathInputExt>::path_ref(&uri_ref);
288
289        assert_eq!(uri.path_ref(), "/a%2Fb");
290        assert_eq!(forwarded, "/a%2Fb");
291        assert_ne!(forwarded, "/a/b");
292    }
293
294    #[test]
295    fn path_input_for_uri_uses_root_fallback() {
296        let uri: Uri = "https://example.com".parse().unwrap();
297
298        assert_eq!(uri.path_ref(), "/");
299    }
300
301    #[test]
302    fn uri_pattern_helpers_route_through_typed_path() {
303        let uri: Uri = "https://example.com/api/acme/widgets".parse().unwrap();
304        let pattern = PathPattern::new("/api/{tenant}/widgets");
305        let miss = PathPattern::new("/api/{tenant}/orders");
306
307        assert!(uri.is_pattern_match(&pattern));
308        assert!(!uri.is_pattern_match(&miss));
309
310        let captures = uri.pattern_captures(&pattern).unwrap();
311        assert_eq!(captures.get("tenant"), Some("acme"));
312        assert!(uri.pattern_captures(&miss).is_none());
313    }
314}