Skip to main content

tachyon_web/server/
multi.rs

1//! Fluent multi-transport server builder — see the [module docs](super#publishing-over-more-than-one-transport-at-once).
2//!
3//! [`MultiServer`] is the preferred way to publish one [`Router`](crate::routing::Router) over
4//! more than one transport at once. It owns exactly the boilerplate a hand-rolled
5//! `tokio::spawn` + `tokio::select!` around individual `serve_*` calls would otherwise require:
6//! one task per configured transport, all driven concurrently, with the whole group torn down
7//! as soon as any one of them finishes (success or error).
8
9use super::Server;
10use tokio::net::TcpListener;
11
12/// One transport this [`MultiServer`] will drive, alongside its configuration.
13enum Transport {
14    Http(TcpListener),
15    #[cfg(feature = "tls")]
16    Https(TcpListener, rustls::ServerConfig),
17    #[cfg(feature = "http3")]
18    H3(s2n_quic::Server),
19    #[cfg(feature = "tor")]
20    Onion(super::tor::OnionConfig),
21    #[cfg(feature = "i2p")]
22    I2p(super::i2p::I2pConfig),
23}
24
25/// Builds a group of transports to drive concurrently from one [`Server`] — see the
26/// [module docs](self).
27///
28/// Constructed via [`Server::with_http`]/[`Server::with_https`]/[`Server::with_onion`]/
29/// [`Server::with_i2p`]/[`Server::with_h3`], chained with more of the same to add further
30/// transports, and finished with [`serve`](Self::serve).
31#[must_use = "MultiServer does nothing until `.serve()` is called and awaited"]
32pub struct MultiServer<S> {
33    server: Server<S>,
34    transports: Vec<Transport>,
35}
36
37impl<S> std::fmt::Debug for MultiServer<S> {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.debug_struct("MultiServer")
40            .field("transports", &self.transports.len())
41            .finish_non_exhaustive()
42    }
43}
44
45impl<S> MultiServer<S>
46where
47    S: Clone + Send + Sync + 'static,
48{
49    pub(super) const fn new(server: Server<S>) -> Self {
50        Self {
51            server,
52            transports: Vec::new(),
53        }
54    }
55
56    /// Adds a plaintext clearnet HTTP transport bound to `listener` — see
57    /// [`Server::serve_http`].
58    pub fn with_http(mut self, listener: TcpListener) -> Self {
59        self.transports.push(Transport::Http(listener));
60        self
61    }
62
63    /// Adds a clearnet HTTPS transport bound to `listener`, terminated with `config` — see
64    /// [`Server::serve_https_config`]. Requires the `tls` feature.
65    #[cfg(feature = "tls")]
66    pub fn with_https(mut self, listener: TcpListener, config: rustls::ServerConfig) -> Self {
67        self.transports.push(Transport::Https(listener, config));
68        self
69    }
70
71    /// Adds an HTTP/3-over-QUIC transport — see [`Server::serve_h3`]. Requires the `http3`
72    /// feature.
73    #[cfg(feature = "http3")]
74    pub fn with_h3(mut self, quic_server: s2n_quic::Server) -> Self {
75        self.transports.push(Transport::H3(quic_server));
76        self
77    }
78
79    /// Adds a Tor `.onion` hidden-service transport — see [`Server::serve_onion`]. Requires
80    /// the `tor` feature.
81    #[cfg(feature = "tor")]
82    pub fn with_onion(mut self, config: super::tor::OnionConfig) -> Self {
83        self.transports.push(Transport::Onion(config));
84        self
85    }
86
87    /// Adds an I2P `.b32.i2p` eepsite transport — see [`Server::serve_i2p_config`]. Requires
88    /// the `i2p` feature ([⚠️ breaks `forbid(unsafe_code)`](super::i2p)).
89    #[cfg(feature = "i2p")]
90    pub fn with_i2p(mut self, config: super::i2p::I2pConfig) -> Self {
91        self.transports.push(Transport::I2p(config));
92        self
93    }
94
95    /// Runs every configured transport concurrently, one Tokio task each, and blocks until the
96    /// **first** one finishes — success or error — at which point every other transport task is
97    /// aborted and that outcome is returned.
98    ///
99    /// Each transport is driven from an independent clone of this `MultiServer`'s underlying
100    /// [`Server`] (cheap — [`Server`]'s settings are `Arc`/`Copy` under the hood), so
101    /// [`Server::max_body_size`]/[`Server::max_connections`]/[`Server::tls_policy`]/
102    /// [`Server::response_jitter`] apply identically across all of them.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if no transport was configured (call at least one `.with_*` method
107    /// first), if any configured transport fails to start, or if it fails at any point while
108    /// running.
109    pub async fn serve(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
110        if self.transports.is_empty() {
111            return Err(
112                "MultiServer::serve called with no transports configured — call at least one \
113                 `.with_http`/`.with_https`/`.with_h3`/`.with_onion`/`.with_i2p` first"
114                    .into(),
115            );
116        }
117
118        let mut set = tokio::task::JoinSet::new();
119        for transport in self.transports {
120            let server = self.server.clone();
121            match transport {
122                Transport::Http(listener) => {
123                    set.spawn(async move { server.serve_http(listener).await.map_err(Into::into) });
124                }
125                #[cfg(feature = "tls")]
126                Transport::Https(listener, config) => {
127                    set.spawn(async move {
128                        server
129                            .serve_https_config(listener, config)
130                            .await
131                            .map_err(Into::into)
132                    });
133                }
134                #[cfg(feature = "http3")]
135                Transport::H3(quic_server) => {
136                    set.spawn(
137                        async move { server.serve_h3(quic_server).await.map_err(Into::into) },
138                    );
139                }
140                #[cfg(feature = "tor")]
141                Transport::Onion(config) => {
142                    set.spawn(async move { server.serve_onion(config).await });
143                }
144                #[cfg(feature = "i2p")]
145                Transport::I2p(config) => {
146                    set.spawn(async move { server.serve_i2p_config(config).await });
147                }
148            }
149        }
150
151        let Some(result) = set.join_next().await else {
152            return Err("MultiServer: no transport task was actually spawned".into());
153        };
154        set.abort_all();
155
156        match result {
157            Ok(outcome) => outcome,
158            Err(join_err) => Err(Box::new(join_err)),
159        }
160    }
161}