Skip to main content

static_web_server/server/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// This file is part of Static Web Server.
3// See https://static-web-server.net/ for more information
4// Copyright (C) 2019-present Jose Quintana <joseluisq.net>
5
6//! Server module to construct a multi-threaded HTTP or HTTP/2 web server.
7
8use std::net::TcpListener;
9use std::sync::Arc;
10use tokio::sync::watch::Receiver;
11
12use crate::handler::RequestHandler;
13use crate::service::RouterService;
14use crate::{Context, Result, Settings};
15
16mod http1;
17mod listener;
18mod opts;
19
20#[cfg(feature = "tls")]
21mod http1_tls;
22#[cfg(feature = "http2")]
23mod http2;
24#[cfg(feature = "tls")]
25mod redirect;
26#[cfg(unix)]
27mod uds;
28
29/// TLS configuration shared by the HTTP/1+TLS and HTTP/2+TLS server modes.
30#[cfg(feature = "tls")]
31pub(crate) struct TlsConfig {
32    /// Path to the TLS certificate file.
33    pub tls_cert: std::path::PathBuf,
34    /// Path to the TLS private key file.
35    pub tls_key: std::path::PathBuf,
36    /// Enable HTTP to HTTPS redirect server.
37    pub https_redirect: bool,
38    /// Target hostname used in HTTPS redirect responses.
39    pub https_redirect_host: String,
40    /// Port the HTTP redirect server binds on.
41    pub https_redirect_from_port: u16,
42    /// Comma-separated list of hosts allowed to be redirected.
43    pub https_redirect_from_hosts: String,
44    /// Server host address (needed to bind the redirect listener).
45    pub host: String,
46    /// HTTPS port (used in redirect target URLs).
47    pub port: u16,
48    /// Resolved 404 error page (used by the redirect server).
49    pub page404: std::path::PathBuf,
50    /// Resolved 50x error page (used by the redirect server).
51    pub page50x: std::path::PathBuf,
52}
53
54/// Shutdown context passed to each server sub-module so they can respond to
55/// both OS signals and optional programmatic cancellation.
56pub(crate) struct ShutdownCtx {
57    /// Grace period in seconds before the server is forcefully terminated.
58    pub grace_period: u8,
59    /// Optional programmatic cancel receiver.
60    pub cancel_recv: Option<Receiver<()>>,
61    #[cfg(windows)]
62    /// Whether the server is running as a Windows service.
63    pub windows_service: bool,
64    #[cfg(windows)]
65    /// Ctrl+C watch receiver used when not running as a Windows service.
66    pub ctrl_c_recv: Receiver<()>,
67    #[cfg(windows)]
68    /// Background task that listens for Ctrl+C and signals the watch channel.
69    pub ctrlc_task: tokio::task::JoinHandle<crate::Result<()>>,
70}
71
72/// A multi-threaded HTTP or HTTP/2 web server.
73pub struct Server {
74    opts: Settings,
75    worker_threads: usize,
76    max_blocking_threads: usize,
77    /// Optional pre-bound TCP listener injected by the caller.
78    /// When set, the server uses this listener instead of creating one
79    /// from the `--host` / `--port` settings.
80    pre_bound_listener: Option<(TcpListener, String)>,
81}
82
83impl Server {
84    /// Create a new multi-threaded server instance.
85    pub fn new(opts: Settings) -> Result<Server> {
86        let cpus = std::thread::available_parallelism()
87            .with_context(|| {
88                "unable to get current platform cpus or lack of permissions to query available parallelism"
89            })?
90            .get();
91        let worker_threads = match opts.general.threads_multiplier {
92            0 | 1 => cpus,
93            n => cpus * n,
94        };
95        let max_blocking_threads = opts.general.max_blocking_threads;
96        Ok(Server {
97            opts,
98            worker_threads,
99            max_blocking_threads,
100            pre_bound_listener: None,
101        })
102    }
103
104    /// Attach a pre-bound TCP listener. The server will use this listener
105    /// instead of creating one from the `--host` / `--port` settings.
106    /// The listener must already be in a listening state (created via
107    /// [`std::net::TcpListener::bind`]).
108    pub fn with_pre_bound_listener(mut self, listener: std::net::TcpListener) -> Self {
109        let addr = listener
110            .local_addr()
111            .map(|a| a.to_string())
112            .unwrap_or_else(|_| "pre-bound".into());
113        self.pre_bound_listener = Some((listener, addr));
114        self
115    }
116
117    /// Run the multi-threaded `Server` as standalone.
118    ///
119    /// It accepts an optional [`cancel`] parameter to shut down the server
120    /// gracefully on demand as a complement to the termination signals handling.
121    ///
122    /// [`cancel`]: <https://docs.rs/tokio/latest/tokio/sync/watch/struct.Receiver.html>
123    pub fn run_standalone(self, cancel: Option<Receiver<()>>) -> Result {
124        self.run_server_on_rt(cancel, || {}, true)
125    }
126
127    /// Run the multi-threaded `Server` which will be used by a Windows service.
128    ///
129    /// It accepts an optional [`cancel`] parameter to shut down the server
130    /// gracefully on demand and a `cancel_fn` that will be executed right after
131    /// the server shuts down.
132    ///
133    /// [`cancel`]: <https://docs.rs/tokio/latest/tokio/sync/watch/struct.Receiver.html>
134    #[cfg(windows)]
135    pub fn run_as_service<F>(self, cancel: Option<Receiver<()>>, cancel_fn: F) -> Result
136    where
137        F: FnOnce(),
138    {
139        self.run_server_on_rt(cancel, cancel_fn, true)
140    }
141
142    /// Build and run the multi-threaded `Server` on the Tokio runtime.
143    ///
144    /// Setting `exit_on_error` to `true` will exit the entire process if
145    /// the server fails to start (previous behaviour).
146    pub fn run_server_on_rt<F>(
147        self,
148        cancel_recv: Option<Receiver<()>>,
149        cancel_fn: F,
150        exit_on_error: bool,
151    ) -> Result
152    where
153        F: FnOnce(),
154    {
155        tracing::debug!(
156            %self.worker_threads,
157            "initializing tokio runtime with multi-threaded scheduler"
158        );
159
160        let rt = tokio::runtime::Builder::new_multi_thread()
161            .worker_threads(self.worker_threads)
162            .max_blocking_threads(self.max_blocking_threads)
163            .thread_name("static-web-server")
164            .enable_all()
165            .build()?;
166
167        let res = rt.block_on(async {
168            tracing::trace!("tokio runtime initialized");
169            self.start_server(cancel_recv, cancel_fn).await
170        });
171
172        if let Err(err) = &res {
173            tracing::error!("server failed to start up: {:?}", err);
174            if exit_on_error {
175                std::process::exit(1)
176            }
177        }
178        res
179    }
180
181    /// Start the Hyper server (HTTP/1 or HTTP/2 + TLS) and block until shutdown.
182    ///
183    /// This method orchestrates listener creation, options initialization, and
184    /// delegates to the appropriate server sub-module.
185    async fn start_server<F>(self, cancel_recv: Option<Receiver<()>>, cancel_fn: F) -> Result
186    where
187        F: FnOnce(),
188    {
189        tracing::trace!("starting web server");
190        tracing::info!(
191            name = env!("CARGO_PKG_NAME"),
192            version = env!("CARGO_PKG_VERSION"),
193            "starting Static Web Server"
194        );
195
196        let general = self.opts.general;
197        let advanced = self.opts.advanced;
198        let pre_bound = self.pre_bound_listener;
199
200        tracing::info!(log_level = %general.log_level, "log level");
201        if general.config_file.is_file() {
202            tracing::info!(path = %general.config_file.display(), "config file used");
203        } else {
204            tracing::debug!(
205                "config file path not found or not a regular file: {}",
206                general.config_file.display()
207            );
208        }
209
210        // Choose listener kind: Unix Domain Socket (when --unix-socket is set,
211        // Unix only) or a TCP socket otherwise. Clap already enforces mutual
212        // exclusion with host/port/fd/tls, but we still resolve the listener
213        // here so the dispatch below can branch on it.
214        #[cfg(unix)]
215        let unix_listener_info = if let Some(path) = general.unix_socket.as_ref() {
216            use crate::server::listener::create_unix_listener;
217
218            Some(create_unix_listener(
219                path,
220                general.unix_socket_mode,
221                general.unix_socket_force,
222            )?)
223        } else {
224            None
225        };
226
227        // The TCP listener is only bound when no UDS path was provided. Binding
228        // both would either waste a port or fail with a host parse error on
229        // platforms where `host` is required.
230        // When a pre-bound listener was injected (e.g. by tests), use it
231        // instead of creating a new one — this avoids TOCTOU port races.
232        #[cfg(unix)]
233        let tcp_listener_info = if unix_listener_info.is_none() {
234            Some(match pre_bound {
235                Some(pre) => pre,
236                None => crate::server::listener::create_tcp_listener(&general)?,
237            })
238        } else {
239            None
240        };
241        #[cfg(not(unix))]
242        let tcp_listener_info = Some(match pre_bound {
243            Some(pre) => pre,
244            None => crate::server::listener::create_tcp_listener(&general)?,
245        });
246
247        tracing::info!(
248            worker_threads = self.worker_threads,
249            "runtime worker threads"
250        );
251        tracing::info!(
252            max_blocking_threads = general.max_blocking_threads,
253            "runtime max blocking threads"
254        );
255        tracing::info!(
256            grace_period_seconds = general.grace_period,
257            "grace period before graceful shutdown"
258        );
259
260        // Initialize request handler options from configuration
261        let opts_result = opts::init(&general, advanced)?;
262        let router_service = RouterService::new(RequestHandler {
263            opts: Arc::from(opts_result.handler_opts),
264        });
265
266        // Windows: spawn a background task that bridges Ctrl+C into a watch channel
267        #[cfg(windows)]
268        let (sender, ctrl_c_recv) = tokio::sync::watch::channel(());
269        #[cfg(windows)]
270        let windows_service = general.windows_service;
271        #[cfg(windows)]
272        let ctrlc_task = tokio::spawn(async move {
273            if !windows_service {
274                tracing::info!("installing graceful shutdown ctrl+c signal handler");
275                if let Err(err) = tokio::signal::ctrl_c().await {
276                    return Err(
277                        crate::Error::new(err).context("failed to install ctrl+c signal handler")
278                    );
279                }
280                tracing::info!("graceful shutdown ctrl+c signal received");
281                let _ = sender.send(());
282            }
283            Ok::<_, crate::Error>(())
284        });
285
286        let ctx = ShutdownCtx {
287            grace_period: general.grace_period,
288            cancel_recv,
289            #[cfg(windows)]
290            windows_service,
291            #[cfg(windows)]
292            ctrl_c_recv,
293            #[cfg(windows)]
294            ctrlc_task,
295        };
296
297        // Unix Domain Socket dispatch (Unix only, no TLS). Clap already forbids
298        // combining `--unix-socket` with TLS so we never reach the TLS branch
299        // below when a UDS listener is present.
300        #[cfg(unix)]
301        if let Some((unix_listener, socket_path, addr_str)) = unix_listener_info {
302            return uds::run(
303                unix_listener,
304                socket_path,
305                router_service,
306                &addr_str,
307                self.worker_threads,
308                ctx,
309                cancel_fn,
310            )
311            .await;
312        }
313
314        // Safe to unwrap: when no UDS listener was created, `tcp_listener_info`
315        // is `Some` by construction above.
316        let (tcp_listener, addr_str) = tcp_listener_info.unwrap();
317
318        // Dispatch to a TLS-enabled server (HTTP/1+TLS or HTTP/2+TLS) when --tls is set
319        #[cfg(feature = "tls")]
320        if general.tls {
321            let tls_cert = general
322                .tls_cert
323                .ok_or_else(|| anyhow!("TLS cert file path is required when --tls is enabled"))?;
324            let tls_key = general
325                .tls_key
326                .ok_or_else(|| anyhow!("TLS key file path is required when --tls is enabled"))?;
327
328            let tls_cfg = TlsConfig {
329                tls_cert,
330                tls_key,
331                https_redirect: general.https_redirect,
332                https_redirect_host: general.https_redirect_host,
333                https_redirect_from_port: general.https_redirect_from_port,
334                https_redirect_from_hosts: general.https_redirect_from_hosts,
335                host: general.host,
336                port: general.port,
337                page404: opts_result.page404,
338                page50x: opts_result.page50x,
339            };
340
341            // If HTTP/2 is also enabled, use the HTTP/2+TLS accept loop
342            #[cfg(feature = "http2")]
343            if general.http2 {
344                return http2::run(
345                    tcp_listener,
346                    router_service,
347                    &addr_str,
348                    self.worker_threads,
349                    tls_cfg,
350                    ctx,
351                    cancel_fn,
352                )
353                .await;
354            }
355
356            // Otherwise serve HTTP/1 over TLS
357            return http1_tls::run(
358                tcp_listener,
359                router_service,
360                &addr_str,
361                self.worker_threads,
362                tls_cfg,
363                ctx,
364                cancel_fn,
365            )
366            .await;
367        }
368
369        // Plain HTTP/1 (no TLS by default)
370        http1::run(
371            tcp_listener,
372            router_service,
373            &addr_str,
374            self.worker_threads,
375            ctx,
376            cancel_fn,
377        )
378        .await
379    }
380}