Skip to main content

trillium_server_common/
client.rs

1use crate::{Runtime, RuntimeTrait, Transport, UdpTransport, Url};
2use smallvec::SmallVec;
3use std::{
4    any::Any,
5    borrow::Cow,
6    fmt::{self, Debug, Formatter},
7    future::Future,
8    io,
9    net::SocketAddr,
10    pin::Pin,
11    sync::Arc,
12};
13
14/// Everything a [`Connector`] needs to open one connection: where to dial, whether to wrap it in
15/// TLS, and any per-connection ALPN.
16///
17/// Construct with [`new_with_host`](Destination::new_with_host) — resolve a domain, or dial
18/// pre-resolved addresses while validating against the domain — or
19/// [`new_with_socket_addrs`](Destination::new_with_socket_addrs) — dial pre-resolved addresses as a
20/// bare IP, with no SNI. There is deliberately no `Default`: a destination with neither a host nor
21/// any address is unconnectable, so each constructor fills at least one dial source.
22#[derive(Debug, Clone)]
23pub struct Destination {
24    secure: bool,
25    host: Option<String>,
26    port: u16,
27    addrs: SmallVec<[SocketAddr; 4]>,
28    /// `None` leaves the connector's configured default ALPN in place; `Some` overrides it for
29    /// this connection — including `Some([])`, which advertises no ALPN at all.
30    alpn: Option<SmallVec<[Cow<'static, [u8]>; 4]>>,
31}
32
33impl Destination {
34    /// A destination identified by host name and port.
35    ///
36    /// With no [`addrs`](Destination::with_addrs) added, the connector resolves `host` itself.
37    /// Adding pre-resolved addresses dials those instead while still validating the certificate
38    /// against `host`.
39    pub fn new_with_host(secure: bool, host: impl Into<String>, port: u16) -> Self {
40        Self {
41            secure,
42            host: Some(host.into()),
43            port,
44            addrs: SmallVec::new(),
45            alpn: None,
46        }
47    }
48
49    /// A destination identified only by pre-resolved socket addresses: a bare-IP connection with no
50    /// SNI, where TLS validates against the address actually dialed. The port is taken from the
51    /// first address.
52    pub fn new_with_socket_addrs(
53        secure: bool,
54        addrs: impl IntoIterator<Item = SocketAddr>,
55    ) -> Self {
56        let addrs = addrs.into_iter().collect::<SmallVec<[SocketAddr; 4]>>();
57        let port = addrs.first().map_or(0, SocketAddr::port);
58        Self {
59            secure,
60            host: None,
61            port,
62            addrs,
63            alpn: None,
64        }
65    }
66
67    /// Build a destination from a URL: maps `http`/`https` to plaintext/TLS and extracts the host
68    /// and port. An IP-literal host becomes a
69    /// [`new_with_socket_addrs`](Destination::new_with_socket_addrs) destination, so it is never
70    /// sent to a resolver; a domain becomes a [`new_with_host`](Destination::new_with_host)
71    /// destination.
72    ///
73    /// # Errors
74    ///
75    /// Returns an error if the scheme is neither `http` nor `https`, or the URL has no host or
76    /// port.
77    pub fn from_url(url: &Url) -> io::Result<Self> {
78        let secure = match url.scheme() {
79            "http" => false,
80            "https" => true,
81            other => {
82                return Err(io::Error::new(
83                    io::ErrorKind::InvalidInput,
84                    format!("unknown scheme {other}"),
85                ));
86            }
87        };
88        let port = url.port_or_known_default().ok_or_else(|| {
89            io::Error::new(io::ErrorKind::InvalidInput, format!("{url} missing port"))
90        })?;
91        match url.host() {
92            Some(url::Host::Domain(domain)) => Ok(Self::new_with_host(secure, domain, port)),
93            Some(url::Host::Ipv4(ip)) => Ok(Self::new_with_socket_addrs(
94                secure,
95                [SocketAddr::from((ip, port))],
96            )),
97            Some(url::Host::Ipv6(ip)) => Ok(Self::new_with_socket_addrs(
98                secure,
99                [SocketAddr::from((ip, port))],
100            )),
101            None => Err(io::Error::new(
102                io::ErrorKind::InvalidInput,
103                format!("{url} missing host"),
104            )),
105        }
106    }
107
108    /// Reconstruct a URL for this destination's origin, used by the default
109    /// [`connect_to`](Connector::connect_to) implementation to fall back to
110    /// [`connect`](Connector::connect). Pre-resolved addresses and ALPN are not represented.
111    ///
112    /// # Errors
113    ///
114    /// Returns an error if the destination has neither a host nor any address, or the result does
115    /// not parse as a URL.
116    pub fn to_url(&self) -> io::Result<Url> {
117        let scheme = if self.secure { "https" } else { "http" };
118        let authority = match &self.host {
119            Some(host) => format!("{host}:{}", self.port),
120            None => self
121                .addrs
122                .first()
123                .ok_or_else(|| {
124                    io::Error::new(
125                        io::ErrorKind::InvalidInput,
126                        "destination has neither host nor addresses",
127                    )
128                })?
129                .to_string(),
130        };
131        Url::parse(&format!("{scheme}://{authority}"))
132            .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))
133    }
134
135    /// Whether this destination should be reached over TLS.
136    #[must_use]
137    pub fn secure(&self) -> bool {
138        self.secure
139    }
140
141    /// The host name used for resolution and certificate validation, or `None` for a bare-IP
142    /// destination.
143    #[must_use]
144    pub fn host(&self) -> Option<&str> {
145        self.host.as_deref()
146    }
147
148    /// The origin port. Pre-resolved [`addrs`](Destination::addrs) carry their own ports; this is
149    /// the port used when resolving the host.
150    #[must_use]
151    pub fn port(&self) -> u16 {
152        self.port
153    }
154
155    /// The pre-resolved addresses to dial, or an empty slice to resolve the host.
156    #[must_use]
157    pub fn addrs(&self) -> &[SocketAddr] {
158        &self.addrs
159    }
160
161    /// The per-connection ALPN override: `Some` advertises exactly these protocols for this
162    /// connection (`Some([])` advertises none), `None` leaves the connector's configured default in
163    /// place.
164    #[must_use]
165    pub fn alpn(&self) -> Option<&[Cow<'static, [u8]>]> {
166        self.alpn.as_deref()
167    }
168
169    /// Set the pre-resolved addresses to dial, replacing any already present.
170    #[must_use]
171    pub fn with_addrs(mut self, addrs: impl IntoIterator<Item = SocketAddr>) -> Self {
172        self.set_addrs(addrs);
173        self
174    }
175
176    /// Set the pre-resolved addresses to dial, replacing any already present.
177    pub fn set_addrs(&mut self, addrs: impl IntoIterator<Item = SocketAddr>) -> &mut Self {
178        self.addrs = addrs.into_iter().collect();
179        self
180    }
181
182    /// Override the ALPN protocols to advertise for this connection, replacing any already present.
183    /// An empty iterator advertises no ALPN at all; to instead defer to the connector's configured
184    /// default, use [`without_alpn`](Destination::without_alpn).
185    #[must_use]
186    pub fn with_alpn(mut self, alpn: impl IntoIterator<Item = Cow<'static, [u8]>>) -> Self {
187        self.set_alpn(alpn);
188        self
189    }
190
191    /// Override the ALPN protocols to advertise for this connection, replacing any already present.
192    /// An empty iterator advertises no ALPN at all; to instead defer to the connector's configured
193    /// default, use [`clear_alpn`](Destination::clear_alpn).
194    pub fn set_alpn(&mut self, alpn: impl IntoIterator<Item = Cow<'static, [u8]>>) -> &mut Self {
195        self.alpn = Some(alpn.into_iter().collect());
196        self
197    }
198
199    /// Clear any per-connection ALPN override so this connection uses the connector's configured
200    /// default.
201    #[must_use]
202    pub fn without_alpn(mut self) -> Self {
203        self.clear_alpn();
204        self
205    }
206
207    /// Clear any per-connection ALPN override so this connection uses the connector's configured
208    /// default.
209    pub fn clear_alpn(&mut self) -> &mut Self {
210        self.alpn = None;
211        self
212    }
213
214    /// Return a copy of this destination with the `secure` flag overridden.
215    #[must_use]
216    pub fn with_secure(mut self, secure: bool) -> Self {
217        self.secure = secure;
218        self
219    }
220}
221
222/// Interface for runtime and tls adapters for the trillium client
223///
224/// See
225/// [`trillium_client`](https://docs.trillium.rs/trillium_client) for more
226/// information on usage.
227pub trait Connector: Send + Sync + 'static {
228    /// the [`Transport`] that [`connect`](Connector::connect) returns
229    type Transport: Transport;
230
231    /// The [`RuntimeTrait`] for this Connector
232    type Runtime: RuntimeTrait;
233
234    /// The async UDP socket type for this connector. Used by QUIC adapters
235    /// for HTTP/3 support. Connectors that do not support UDP should set
236    /// this to `()`.
237    type Udp: UdpTransport;
238
239    /// Initiate a connection to the provided url
240    fn connect(&self, url: &Url) -> impl Future<Output = io::Result<Self::Transport>> + Send;
241
242    /// Open a connection to `destination`: dialing its pre-resolved addresses if present, otherwise
243    /// resolving its host, and advertising any per-connection ALPN it carries.
244    ///
245    /// A domain destination keeps its host as the certificate identity (SNI) regardless of the
246    /// addresses dialed, so pre-resolved addresses may come from any resolver (e.g. a DNS cache)
247    /// without affecting certificate validation.
248    ///
249    /// The default implementation reconstructs a URL via [`Destination::to_url`] and calls
250    /// [`connect`](Connector::connect), which ignores pre-resolved addresses and per-connection
251    /// ALPN; connectors that honor those override this method.
252    ///
253    /// `destination` is taken by value so connectors can adjust it (e.g. clearing `secure` before
254    /// delegating the TCP dial to an inner connector) without copying. A caller that needs to
255    /// retain it should clone before calling.
256    fn connect_to(
257        &self,
258        destination: Destination,
259    ) -> impl Future<Output = io::Result<Self::Transport>> + Send {
260        async move { self.connect(&destination.to_url()?).await }
261    }
262
263    /// Returns an object-safe [`ArcedConnector`]. Do not implement this.
264    fn arced(self) -> ArcedConnector
265    where
266        Self: Sized,
267    {
268        ArcedConnector(Arc::new(self))
269    }
270
271    /// Perform a DNS lookup for a given host-and-port
272    fn resolve(
273        &self,
274        host: &str,
275        port: u16,
276    ) -> impl Future<Output = io::Result<Vec<SocketAddr>>> + Send;
277
278    /// Returns the runtime
279    fn runtime(&self) -> Self::Runtime;
280}
281
282/// An Arced and type-erased [`Connector`]
283#[derive(Clone)]
284pub struct ArcedConnector(Arc<dyn ObjectSafeConnector>);
285
286impl Debug for ArcedConnector {
287    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
288        f.debug_tuple("ArcedConnector").finish()
289    }
290}
291
292impl ArcedConnector {
293    /// Constructs a new `ArcedConnector`
294    #[must_use]
295    pub fn new(connector: impl Connector) -> Self {
296        connector.arced()
297    }
298
299    /// Determine if this `ArcedConnector` is the specified type
300    pub fn is<T: Any + 'static>(&self) -> bool {
301        self.as_any().is::<T>()
302    }
303
304    /// Attempt to borrow this `ArcedConnector` as the provided type, returning None if it does not
305    /// contain the type
306    pub fn downcast_ref<T: Any + 'static>(&self) -> Option<&T> {
307        self.0.as_any().downcast_ref()
308    }
309
310    /// Attempt to mutably borrow this `ArcedConnector` as the provided type, returning None if it
311    /// does not contain the type or if there are multiple outstanding clones of this arc
312    pub fn downcast_mut<T: Any + 'static>(&mut self) -> Option<&mut T> {
313        Arc::get_mut(&mut self.0)?.as_mut_any().downcast_mut()
314    }
315
316    /// Returns an object-safe [`Runtime`]
317    pub fn runtime(&self) -> Runtime {
318        self.0.runtime()
319    }
320}
321
322// clippy thinks this is better ¯\(ツ)/¯
323type ConnectResult<'fut> =
324    Pin<Box<dyn Future<Output = io::Result<Box<dyn Transport>>> + Send + 'fut>>;
325
326trait ObjectSafeConnector: Send + Sync + 'static {
327    #[must_use]
328    fn connect<'connector, 'url, 'fut>(&'connector self, url: &'url Url) -> ConnectResult<'fut>
329    where
330        'connector: 'fut,
331        'url: 'fut,
332        Self: 'fut;
333    fn as_any(&self) -> &dyn Any;
334    fn as_mut_any(&mut self) -> &mut dyn Any;
335    fn runtime(&self) -> Runtime;
336
337    fn resolve<'connector, 'host, 'fut>(
338        &'connector self,
339        host: &'host str,
340        port: u16,
341    ) -> Pin<Box<dyn Future<Output = io::Result<Vec<SocketAddr>>> + Send + 'fut>>
342    where
343        'connector: 'fut,
344        'host: 'fut,
345        Self: 'fut;
346
347    #[must_use]
348    fn connect_to<'connector, 'fut>(
349        &'connector self,
350        destination: Destination,
351    ) -> ConnectResult<'fut>
352    where
353        'connector: 'fut,
354        Self: 'fut;
355}
356
357impl<T: Connector> ObjectSafeConnector for T {
358    fn connect<'connector, 'url, 'fut>(
359        &'connector self,
360        url: &'url Url,
361    ) -> Pin<Box<dyn Future<Output = io::Result<Box<dyn Transport>>> + Send + 'fut>>
362    where
363        'connector: 'fut,
364        'url: 'fut,
365        Self: 'fut,
366    {
367        Box::pin(async move {
368            Connector::connect(self, url)
369                .await
370                .map(|t| Box::new(t) as Box<dyn Transport>)
371        })
372    }
373
374    fn as_any(&self) -> &dyn Any {
375        self
376    }
377
378    fn as_mut_any(&mut self) -> &mut dyn Any {
379        self
380    }
381
382    fn runtime(&self) -> Runtime {
383        Connector::runtime(self).into()
384    }
385
386    fn resolve<'connector, 'host, 'fut>(
387        &'connector self,
388        host: &'host str,
389        port: u16,
390    ) -> Pin<Box<dyn Future<Output = io::Result<Vec<SocketAddr>>> + Send + 'fut>>
391    where
392        'connector: 'fut,
393        'host: 'fut,
394        Self: 'fut,
395    {
396        Box::pin(async move { Connector::resolve(self, host, port).await })
397    }
398
399    fn connect_to<'connector, 'fut>(
400        &'connector self,
401        destination: Destination,
402    ) -> ConnectResult<'fut>
403    where
404        'connector: 'fut,
405        Self: 'fut,
406    {
407        Box::pin(async move {
408            Connector::connect_to(self, destination)
409                .await
410                .map(|t| Box::new(t) as Box<dyn Transport>)
411        })
412    }
413}
414
415impl Connector for ArcedConnector {
416    type Runtime = Runtime;
417    type Transport = Box<dyn Transport>;
418    type Udp = ();
419
420    async fn connect(&self, url: &Url) -> io::Result<Box<dyn Transport>> {
421        self.0.connect(url).await
422    }
423
424    fn arced(self) -> ArcedConnector {
425        self
426    }
427
428    fn runtime(&self) -> Self::Runtime {
429        self.0.runtime()
430    }
431
432    async fn resolve(&self, host: &str, port: u16) -> io::Result<Vec<SocketAddr>> {
433        self.0.resolve(host, port).await
434    }
435
436    async fn connect_to(&self, destination: Destination) -> io::Result<Box<dyn Transport>> {
437        self.0.connect_to(destination).await
438    }
439}
440
441/// Factory for creating client-side QUIC endpoints.
442///
443/// Parameterised over `C: Connector` so that the concrete runtime and UDP socket types
444/// are available to the implementation without coupling the QUIC library to any specific
445/// runtime adapter.
446///
447/// Implementations should produce a [`QuicEndpoint`](crate::QuicEndpoint) bound to the
448/// given local address. TLS configuration is embedded in the implementation.
449pub trait QuicClientConfig<C: Connector>: Send + Sync + 'static {
450    /// The endpoint type produced by [`bind`](QuicClientConfig::bind).
451    type Endpoint: crate::QuicEndpoint;
452
453    /// Bind a QUIC endpoint to the given local address.
454    ///
455    /// `runtime` is the runtime from the paired [`Connector`]; use it for spawning,
456    /// timers, and UDP I/O.
457    fn bind(&self, addr: SocketAddr, runtime: &C::Runtime) -> io::Result<Self::Endpoint>;
458}
459
460// -- Type-erased QuicClientConfig --
461
462trait ObjectSafeQuicClientConfig: Send + Sync + 'static {
463    fn bind(&self, addr: SocketAddr) -> io::Result<crate::ArcedQuicEndpoint>;
464}
465
466/// Binds a [`QuicClientConfig<C>`] together with its runtime before type erasure.
467struct BoundQuicClientConfig<Q, C: Connector> {
468    config: Q,
469    runtime: C::Runtime,
470}
471
472impl<C: Connector, Q: QuicClientConfig<C>> ObjectSafeQuicClientConfig
473    for BoundQuicClientConfig<Q, C>
474{
475    fn bind(&self, addr: SocketAddr) -> io::Result<crate::ArcedQuicEndpoint> {
476        let endpoint = self.config.bind(addr, &self.runtime)?;
477        Ok(crate::ArcedQuicEndpoint::from(endpoint))
478    }
479}
480
481/// An arc-wrapped, type-erased QUIC client config (endpoint factory).
482///
483/// Created via [`Client::new_with_quic`](https://docs.rs/trillium-client/latest/trillium_client/struct.Client.html#method.new_with_quic), which
484/// binds the connector's runtime into the wrapper before erasure.
485#[derive(Clone)]
486pub struct ArcedQuicClientConfig(Arc<dyn ObjectSafeQuicClientConfig>);
487
488impl Debug for ArcedQuicClientConfig {
489    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
490        f.debug_tuple("ArcedQuicClientConfig").finish()
491    }
492}
493
494impl ArcedQuicClientConfig {
495    /// Binds `config` to the runtime from `connector` and wraps the result for type erasure.
496    #[must_use]
497    pub fn new<C: Connector, Q: QuicClientConfig<C>>(connector: &C, config: Q) -> Self {
498        Self(Arc::new(BoundQuicClientConfig {
499            runtime: connector.runtime(),
500            config,
501        }))
502    }
503
504    /// Create a type-erased QUIC endpoint bound to the given local address.
505    pub fn bind(&self, addr: SocketAddr) -> io::Result<crate::ArcedQuicEndpoint> {
506        self.0.bind(addr)
507    }
508}