Skip to main content

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}