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}