trillium_server_common/config.rs
1use crate::{
2 Acceptor, ArcHandler, BoxedAcceptor, BoxedQuicConfig, ListenerConfig, QuicConfig, RuntimeTrait,
3 Server, ServerHandle,
4 server::{PreboundListener, resolve_listener},
5};
6use async_cell::sync::AsyncCell;
7use futures_lite::StreamExt;
8use std::{
9 cell::OnceCell,
10 net::{SocketAddr, UdpSocket as StdUdpSocket},
11 pin::pin,
12 sync::Arc,
13};
14use trillium::{
15 Handler, Headers, HttpConfig, Info, KnownHeaderName, Listener, Listeners, Swansong, TypeSet,
16};
17use trillium_http::HttpContext;
18use url::Url;
19
20/// # Primary entrypoint for configuring and running a trillium server
21///
22/// The associated methods on this struct are intended to be chained.
23///
24/// ## Example
25/// ```rust,no_run
26/// trillium_smol::config() // or trillium_async_std, trillium_tokio
27/// .with_port(8080) // the default
28/// .with_host("localhost") // the default
29/// .with_nodelay()
30/// .with_max_connections(Some(10000))
31/// .without_signals()
32/// .run(|conn: trillium::Conn| async move { conn.ok("hello") });
33/// ```
34///
35/// # Socket binding
36///
37/// The socket binding logic is as follows:
38///
39/// If a LISTEN_FD environment variable is available on `cfg(unix)`
40/// systems, that will be used, overriding host and port settings
41/// Otherwise:
42/// Host will be selected from explicit configuration using
43/// [`Config::with_host`] or else the `HOST` environment variable,
44/// or else a default of "localhost".
45/// On `cfg(unix)` systems only: If the host string (as set by env var
46/// or direct config) begins with `.`, `/`, or `~`, it is
47/// interpreted to be a path, and trillium will bind to it as a unix
48/// domain socket. Port will be ignored. The socket will be deleted
49/// on clean shutdown.
50/// Port will be selected from explicit configuration using
51/// [`Config::with_port`] or else the `PORT` environment variable,
52/// or else a default of 8080.
53///
54/// ## Signals
55///
56/// On `cfg(unix)` systems, `SIGTERM`, `SIGINT`, and `SIGQUIT` are all
57/// registered to perform a graceful shutdown on the first signal and an
58/// immediate shutdown on a subsequent signal. This behavior may change as
59/// trillium matures. To disable this behavior, use
60/// [`Config::without_signals`].
61#[derive(Debug)]
62pub struct Config<ServerType: Server, AcceptorType, QuicType: QuicConfig<ServerType> = ()> {
63 pub(crate) acceptor: AcceptorType,
64 pub(crate) quic: QuicType,
65 pub(crate) binding: Option<ServerType>,
66 pub(crate) host: Option<String>,
67 pub(crate) context_cell: Arc<AsyncCell<Arc<HttpContext>>>,
68 pub(crate) max_connections: Option<usize>,
69 pub(crate) nodelay: bool,
70 pub(crate) port: Option<u16>,
71 pub(crate) register_signals: bool,
72 pub(crate) runtime: ServerType::Runtime,
73 pub(crate) context: HttpContext,
74}
75
76impl<ServerType, AcceptorType, QuicType> Config<ServerType, AcceptorType, QuicType>
77where
78 ServerType: Server,
79 AcceptorType: Acceptor<ServerType::Transport>,
80 QuicType: QuicConfig<ServerType>,
81{
82 /// Starts an async runtime and runs the provided handler with
83 /// this config in that runtime. This is the appropriate
84 /// entrypoint for applications that do not need to spawn tasks
85 /// outside of trillium's web server. For applications that embed a
86 /// trillium server inside of an already-running async runtime, use
87 /// [`Config::run_async`]
88 pub fn run(self, handler: impl Handler) {
89 self.runtime.clone().block_on(self.run_async(handler));
90 }
91
92 /// Runs the provided handler with this config, in an
93 /// already-running runtime. This is the appropriate entrypoint
94 /// for an application that needs to spawn async tasks that are
95 /// unrelated to the trillium application. If you do not need to spawn
96 /// other tasks, [`Config::run`] is the preferred entrypoint
97 pub async fn run_async(self, handler: impl Handler) {
98 let Self {
99 runtime,
100 acceptor,
101 quic,
102 max_connections,
103 nodelay,
104 binding,
105 host,
106 port,
107 register_signals,
108 context,
109 context_cell,
110 } = self;
111
112 // Materialize this single-listener configuration into a multi-listener `ListenerConfig`,
113 // which owns the one accept-loop / signal / QUIC driver. `Config` is the opinionated front
114 // door: it always resolves to exactly one binding (the 12-factor default if none was set),
115 // preserving its panic-on-bind-failure contract at this seam, then delegates.
116 let builder = ListenerConfig::<ServerType>::from_global(
117 context,
118 context_cell,
119 runtime,
120 max_connections,
121 nodelay,
122 register_signals,
123 );
124
125 let builder = match binding {
126 // A prebound server is already adopted into the runtime; adopt it as-is. QUIC is not
127 // attached here: a prebound server's address is opaque until `init`, and a prebound TCP
128 // listener paired with a QUIC/UDP endpoint is not a meaningful combination. Bind QUIC
129 // on a multi-listener `ListenerConfig` if both are genuinely needed.
130 Some(server) => {
131 if quic.is_configured() {
132 log::warn!(
133 "QUIC configuration is ignored when a prebound server is supplied; use a \
134 multi-listener ListenerConfig to bind QUIC explicitly"
135 );
136 }
137 log::debug!("taking prebound listener");
138 builder.bind_server_boxed(server, BoxedAcceptor::new(acceptor))
139 }
140
141 None => {
142 let host = host
143 .or_else(|| std::env::var("HOST").ok())
144 .unwrap_or_else(|| "localhost".into());
145 let port = port
146 .or_else(|| {
147 std::env::var("PORT")
148 .ok()
149 .map(|x| x.parse().expect("PORT must be an unsigned integer"))
150 })
151 .unwrap_or(8080);
152
153 if quic.is_configured() {
154 // QUIC binds on the single listener's resolved address, so the TCP port must be
155 // known here to claim the matching UDP socket. Any QUIC-capable server uses the
156 // default `from_host_and_port`, so resolving via the shared `resolve_listener`
157 // is behaviorally identical while exposing the port. Bind failure panics, as
158 // the `Config` contract requires.
159 match resolve_listener(&host, port)
160 .unwrap_or_else(|e| panic!("failed to bind {host}:{port}: {e}"))
161 {
162 PreboundListener::Tcp(tcp) => {
163 let addr = tcp
164 .local_addr()
165 .expect("a bound tcp listener has a local address");
166 let builder = builder.push_listener(
167 PreboundListener::Tcp(tcp),
168 BoxedAcceptor::new(acceptor),
169 );
170 let socket = StdUdpSocket::bind(addr).unwrap_or_else(|e| {
171 panic!("failed to bind QUIC UDP socket at {addr}: {e}")
172 });
173 builder.push_quic_listener(socket, BoxedQuicConfig::new(quic))
174 }
175 #[cfg(unix)]
176 PreboundListener::Unix(unix) => {
177 log::warn!("QUIC configuration is ignored on a unix-domain listener");
178 builder.push_listener(
179 PreboundListener::Unix(unix),
180 BoxedAcceptor::new(acceptor),
181 )
182 }
183 }
184 } else {
185 let server = ServerType::from_host_and_port(&host, port);
186 builder.bind_server_boxed(server, BoxedAcceptor::new(acceptor))
187 }
188 }
189 };
190
191 builder.run_async(handler).await;
192 }
193
194 /// Spawns the server onto the async runtime, returning a [`ServerHandle`].
195 ///
196 /// - `await server_handle` — waits for the server to shut down (output: `()`)
197 /// - `server_handle.info().await` — waits for the server to finish binding, then returns
198 /// [`BoundInfo`](crate::server_handle::BoundInfo)
199 /// - `server_handle.shut_down()` — initiates graceful shutdown
200 pub fn spawn(self, handler: impl Handler) -> ServerHandle {
201 let server_handle = self.handle();
202 self.runtime.clone().spawn(self.run_async(handler));
203 server_handle
204 }
205
206 /// Returns a [`ServerHandle`] for this Config. This is useful
207 /// when spawning the server onto a runtime.
208 pub fn handle(&self) -> ServerHandle {
209 ServerHandle {
210 swansong: self.context.swansong().clone(),
211 context: self.context_cell.clone(),
212 received_context: OnceCell::new(),
213 runtime: self.runtime().into(),
214 }
215 }
216
217 /// Configures the server to listen on this port. The default is
218 /// the PORT environment variable or 8080
219 pub fn with_port(mut self, port: u16) -> Self {
220 if self.has_binding() {
221 log::warn!(
222 "constructing a config with both a port and a pre-bound listener will ignore the \
223 port"
224 );
225 }
226 self.port = Some(port);
227 self
228 }
229
230 /// Configures the server to listen on this host or ip
231 /// address. The default is the HOST environment variable or
232 /// "localhost"
233 pub fn with_host(mut self, host: &str) -> Self {
234 if self.has_binding() {
235 log::warn!(
236 "constructing a config with both a host and a pre-bound listener will ignore the \
237 host"
238 );
239 }
240 self.host = Some(host.into());
241 self
242 }
243
244 /// Configures the server to NOT register for graceful-shutdown
245 /// signals with the operating system. Default behavior is for the
246 /// server to listen for SIGINT and SIGTERM and perform a graceful
247 /// shutdown.
248 pub fn without_signals(mut self) -> Self {
249 self.register_signals = false;
250 self
251 }
252
253 /// Configures the tcp listener to use TCP_NODELAY. See
254 /// <https://en.wikipedia.org/wiki/Nagle%27s_algorithm> for more
255 /// information on this setting.
256 pub fn with_nodelay(mut self) -> Self {
257 self.nodelay = true;
258 self
259 }
260
261 /// Configures the server to listen on the ip and port specified
262 /// by the provided socketaddr. This is identical to
263 /// `self.with_host(&socketaddr.ip().to_string()).with_port(socketaddr.port())`
264 pub fn with_socketaddr(self, socketaddr: SocketAddr) -> Self {
265 self.with_host(&socketaddr.ip().to_string())
266 .with_port(socketaddr.port())
267 }
268
269 /// Configures the tls acceptor for this server
270 pub fn with_acceptor<A: Acceptor<ServerType::Transport>>(
271 self,
272 acceptor: A,
273 ) -> Config<ServerType, A, QuicType> {
274 Config {
275 acceptor,
276 quic: self.quic,
277 host: self.host,
278 port: self.port,
279 nodelay: self.nodelay,
280 register_signals: self.register_signals,
281 max_connections: self.max_connections,
282 context_cell: self.context_cell,
283 context: self.context,
284 binding: self.binding,
285 runtime: self.runtime,
286 }
287 }
288
289 /// Configures QUIC/HTTP3 for this server
290 pub fn with_quic<Q: QuicConfig<ServerType>>(
291 self,
292 quic: Q,
293 ) -> Config<ServerType, AcceptorType, Q> {
294 Config {
295 acceptor: self.acceptor,
296 quic,
297 host: self.host,
298 port: self.port,
299 nodelay: self.nodelay,
300 register_signals: self.register_signals,
301 max_connections: self.max_connections,
302 context_cell: self.context_cell,
303 context: self.context,
304 binding: self.binding,
305 runtime: self.runtime,
306 }
307 }
308
309 /// use the specific [`Swansong`] provided
310 pub fn with_swansong(mut self, swansong: Swansong) -> Self {
311 self.context.set_swansong(swansong);
312 self
313 }
314
315 /// Configures the maximum number of connections to accept. The
316 /// default is 75% of the soft rlimit_nofile (`ulimit -n`) on unix
317 /// systems, and None on other systems.
318 pub fn with_max_connections(mut self, max_connections: Option<usize>) -> Self {
319 self.max_connections = max_connections;
320 self
321 }
322
323 /// configures trillium-http performance and security tuning parameters.
324 ///
325 /// See [`HttpConfig`] for documentation
326 pub fn with_http_config(mut self, config: HttpConfig) -> Self {
327 *self.context.config_mut() = config;
328 self
329 }
330
331 /// Use a pre-bound transport stream as server.
332 ///
333 /// The argument to this varies for different servers, but usually
334 /// accepts the runtime's TcpListener and, on unix platforms, the UnixListener.
335 ///
336 /// ## Note well
337 ///
338 /// Many of the other options on this config will be ignored if you provide a listener. In
339 /// particular, `host` and `port` will be ignored. All of the other options will be used.
340 ///
341 /// Additionally, cloning this config will not clone the listener.
342 pub fn with_prebound_server(mut self, server: impl Into<ServerType>) -> Self {
343 if self.host.is_some() {
344 log::warn!(
345 "constructing a config with both a host and a pre-bound listener will ignore the \
346 host"
347 );
348 }
349
350 if self.port.is_some() {
351 log::warn!(
352 "constructing a config with both a port and a pre-bound listener will ignore the \
353 port"
354 );
355 }
356
357 self.binding = Some(server.into());
358 self
359 }
360
361 fn has_binding(&self) -> bool {
362 self.binding.is_some()
363 }
364
365 /// retrieve the runtime
366 pub fn runtime(&self) -> ServerType::Runtime {
367 self.runtime.clone()
368 }
369
370 /// return the configured port
371 pub fn port(&self) -> Option<u16> {
372 self.port
373 }
374
375 /// return the configured host
376 pub fn host(&self) -> Option<&str> {
377 self.host.as_deref()
378 }
379
380 /// add arbitrary state to the [`HttpContext`]'s [`TypeSet`](trillium::TypeSet) that will be
381 /// available in the following places:
382 ///
383 /// - mutably on [`Info`](trillium::Info) as
384 /// [`Info::shared_state`](trillium::Info::shared_state) within
385 /// [`Handler::init`](trillium::Handler::init)
386 /// - immutably on every [`Conn`](trillium::Conn) as
387 /// [`Conn::shared_state`](trillium::Conn::shared_state)
388 /// - immutably on the [`BoundInfo`](crate::BoundInfo) as
389 /// [`BoundInfo::shared_state`](crate::BoundInfo::shared_state) returned by
390 /// [`ServerHandle::info`](crate::ServerHandle::info)
391 pub fn with_shared_state<T: Send + Sync + 'static>(mut self, state: T) -> Self {
392 self.context.shared_state_mut().insert(state);
393 self
394 }
395
396 /// add arbitrary state to the [`HttpContext`]'s [`TypeSet`](trillium::TypeSet) that will be
397 /// available in the following places:
398 ///
399 /// - mutably on [`Info`](trillium::Info) as
400 /// [`Info::shared_state`](trillium::Info::shared_state) within
401 /// [`Handler::init`](trillium::Handler::init)
402 /// - immutably on every [`Conn`](trillium::Conn) as
403 /// [`Conn::shared_state`](trillium::Conn::shared_state)
404 /// - immutably on the [`BoundInfo`](crate::BoundInfo) as
405 /// [`BoundInfo::shared_state`](crate::BoundInfo::shared_state) returned by
406 /// [`ServerHandle::info`](crate::ServerHandle::info)
407 pub fn set_shared_state<T: Send + Sync + 'static>(&mut self, state: T) -> &mut Self {
408 self.context.shared_state_mut().insert(state);
409 self
410 }
411}
412
413impl<ServerType: Server> Config<ServerType, ()> {
414 /// build a new config with default acceptor
415 pub fn new() -> Self {
416 Self::default()
417 }
418
419 /// Upgrade this single-listener configuration into a multi-listener [`ListenerConfig`],
420 /// carrying over the global server configuration — HTTP config, shared state, [`Swansong`],
421 /// `nodelay`, max-connections, and signal handling — but no listener binding. Bind one or
422 /// more listeners explicitly on the returned builder
423 /// ([`bind_tcp`](ListenerConfig::bind_tcp), [`bind_tls`](ListenerConfig::bind_tls),
424 /// [`bind_quic`](ListenerConfig::bind_quic), [`bind_env`](ListenerConfig::bind_env), …).
425 ///
426 /// This is available before an acceptor or QUIC configuration is set; in the multi-listener
427 /// model those are per-listener (`bind_tls`/`bind_quic`) rather than server-global. Any
428 /// host/port set with [`with_host`](Self::with_host)/[`with_port`](Self::with_port), or a
429 /// prebound server from [`with_prebound_server`](Self::with_prebound_server), is not carried
430 /// over — binding on the builder is always explicit — and a warning is logged if one was set.
431 pub fn listeners(self) -> ListenerConfig<ServerType> {
432 if self.host.is_some() || self.port.is_some() || self.binding.is_some() {
433 log::warn!(
434 "Config::listeners() does not carry over host/port/prebound-server configuration; \
435 bind listeners explicitly on the returned ListenerConfig"
436 );
437 }
438 ListenerConfig::from_global(
439 self.context,
440 self.context_cell,
441 self.runtime,
442 self.max_connections,
443 self.nodelay,
444 self.register_signals,
445 )
446 }
447}
448
449impl<ServerType: Server> Default for Config<ServerType, ()> {
450 fn default() -> Self {
451 Self {
452 acceptor: (),
453 quic: (),
454 port: None,
455 host: None,
456 nodelay: false,
457 register_signals: cfg!(unix),
458 max_connections: None,
459 context_cell: AsyncCell::shared(),
460 binding: None,
461 runtime: ServerType::runtime(),
462 context: Default::default(),
463 }
464 }
465}
466
467pub(crate) fn info_with_server_header<ServerType: Server>(
468 context: HttpContext,
469 runtime: &ServerType::Runtime,
470) -> Info {
471 let mut info = Info::from(context)
472 .with_shared_state(runtime.clone().into())
473 .with_shared_state(runtime.clone());
474
475 info.shared_state_entry::<Headers>()
476 .or_default()
477 .try_insert(KnownHeaderName::Server, trillium::headers::server_header());
478
479 info
480}
481
482/// Acceptor-independent one-time server initialization: given an `Info` whose bound address has
483/// already been populated, bind QUIC if configured, run [`Handler::init`] exactly once, and produce
484/// the `Arc`-shared [`HttpContext`] and handler that any number of per-acceptor accept loops can
485/// share.
486///
487/// `is_secure` governs URL-scheme derivation and is supplied by the caller rather than read from an
488/// acceptor, because a single shared handler may front listeners with differing security (e.g. a
489/// plaintext listener alongside a TLS one).
490#[cfg_attr(not(unix), allow(unused_mut))]
491pub(crate) async fn init_shared<ServerType, QuicType, H>(
492 mut info: Info,
493 runtime: ServerType::Runtime,
494 quic: QuicType,
495 mut max_connections: Option<usize>,
496 is_secure: bool,
497 mut handler: H,
498) -> (
499 Arc<HttpContext>,
500 ArcHandler<H>,
501 Option<QuicType::Endpoint>,
502 Option<usize>,
503)
504where
505 ServerType: Server,
506 QuicType: QuicConfig<ServerType>,
507 H: Handler,
508{
509 #[cfg(unix)]
510 if max_connections.is_none() {
511 max_connections = rlimit::getrlimit(rlimit::Resource::NOFILE)
512 .ok()
513 .and_then(|(soft, _hard)| soft.try_into().ok())
514 .map(|limit: usize| ((limit as f32) * 0.75) as usize);
515 }
516
517 log::debug!("using max connections of {max_connections:?}");
518
519 let quic_binding = if let Some(socket_addr) = info.tcp_socket_addr().copied() {
520 let quic_binding = quic
521 .bind(socket_addr, runtime, &mut info)
522 .map(|r| r.expect("failed to bind QUIC endpoint"));
523
524 if quic_binding.is_some() {
525 info.shared_state_entry::<Headers>()
526 .or_default()
527 .try_insert_with(KnownHeaderName::AltSvc, || -> &'static str {
528 format!("h3=\":{}\"", socket_addr.port()).leak()
529 });
530 }
531
532 quic_binding
533 } else {
534 None
535 };
536
537 // Populate the single-listener server's listener set, unless a multi-listener builder already
538 // installed the full set. Read the addresses out before mutating, to avoid overlapping borrows.
539 if info.shared_state::<Listeners>().is_none()
540 && let Some(primary) = primary_listener(&info, is_secure)
541 {
542 let mut listeners = vec![primary];
543 if quic_binding.is_some()
544 && let Some(addr) = info.tcp_socket_addr().copied()
545 {
546 listeners.push(Listener::quic(addr));
547 }
548 info.insert_shared_state(Listeners(listeners));
549 }
550
551 insert_url(info.as_mut(), is_secure);
552
553 handler.init(&mut info).await;
554
555 let context = Arc::new(HttpContext::from(info));
556 let handler = ArcHandler::new(handler);
557
558 (context, handler, quic_binding, max_connections)
559}
560
561/// Spawn the OS-signal graceful-shutdown handler onto `runtime` if `register` is set. Standalone so
562/// multi-listener flows can register signals once regardless of which (or whether any) TCP or QUIC
563/// listener is involved.
564pub(crate) fn spawn_signals_loop<R: RuntimeTrait>(
565 context: Arc<HttpContext>,
566 register: bool,
567 runtime: R,
568) {
569 if !register {
570 return;
571 }
572 let swansong = context.swansong().clone();
573 runtime.clone().spawn(async move {
574 let mut signals = pin!(runtime.hook_signals([2, 3, 15]));
575 while signals.next().await.is_some() {
576 let guard_count = swansong.guard_count();
577 if swansong.state().is_shutting_down() {
578 eprintln!(
579 "\nSecond interrupt, shutting down harshly (dropping {guard_count} guards)"
580 );
581 std::process::exit(1);
582 } else {
583 println!(
584 "\nShutting down gracefully. Waiting for {guard_count} shutdown guards to \
585 drop.\nControl-c again to force."
586 );
587 swansong.shut_down();
588 }
589 }
590 });
591}
592
593/// The single-listener server's primary [`Listener`] — its TCP listener, or on unix its
594/// Unix-domain listener — derived from the addresses `Server::init` populated. `None` if neither is
595/// present (e.g. a not-yet-bound config).
596pub(crate) fn primary_listener(info: &Info, is_secure: bool) -> Option<Listener> {
597 if let Some(addr) = info.tcp_socket_addr().copied() {
598 return Some(Listener::tcp(addr, is_secure));
599 }
600 #[cfg(unix)]
601 if let Some(path) = info
602 .unix_socket_addr()
603 .and_then(|addr| addr.as_pathname().map(std::path::Path::to_path_buf))
604 {
605 return Some(Listener::unix(Some(path), is_secure));
606 }
607 None
608}
609
610fn insert_url(state: &mut TypeSet, secure: bool) -> Option<()> {
611 let socket_addr = state.get::<SocketAddr>().copied()?;
612 let vacant_entry = state.entry::<Url>().into_vacant()?;
613
614 let host = if socket_addr.ip().is_loopback() {
615 "localhost".to_string()
616 } else {
617 socket_addr.ip().to_string()
618 };
619
620 let url = match (secure, socket_addr.port()) {
621 (true, 443) => format!("https://{host}"),
622 (false, 80) => format!("http://{host}"),
623 (true, port) => format!("https://{host}:{port}/"),
624 (false, port) => format!("http://{host}:{port}/"),
625 };
626
627 let url = Url::parse(&url).ok()?;
628
629 vacant_entry.insert(url);
630 Some(())
631}