Skip to main content

static_web_server/
server.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 intended to construct a multi-threaded HTTP or HTTP/2 web server.
7//!
8
9use hyper::server::Server as HyperServer;
10use listenfd::ListenFd;
11use std::net::{IpAddr, SocketAddr, TcpListener};
12use std::sync::Arc;
13use tokio::sync::{Mutex, watch::Receiver};
14
15use crate::handler::{RequestHandler, RequestHandlerOpts};
16
17#[cfg(feature = "metrics")]
18use crate::metrics;
19#[cfg(any(unix, windows))]
20use crate::signals;
21
22#[cfg(feature = "http2")]
23use {
24    crate::tls::{TlsAcceptor, TlsConfigBuilder},
25    crate::{error, error_page, https_redirect},
26    hyper::server::conn::{AddrIncoming, AddrStream},
27    hyper::service::{make_service_fn, service_fn},
28};
29
30#[cfg(feature = "directory-listing")]
31use crate::directory_listing;
32
33#[cfg(feature = "directory-listing-download")]
34use crate::directory_listing_download;
35
36#[cfg(feature = "fallback-page")]
37use crate::fallback_page;
38
39#[cfg(any(
40    feature = "compression",
41    feature = "compression-deflate",
42    feature = "compression-gzip",
43    feature = "compression-brotli",
44    feature = "compression-zstd",
45))]
46use crate::compression;
47
48use crate::compression_static;
49
50#[cfg(feature = "basic-auth")]
51use crate::basic_auth;
52
53#[cfg(feature = "experimental")]
54use crate::mem_cache;
55
56use crate::{Context, Result, service::RouterService};
57use crate::{
58    Settings, control_headers, cors, health, helpers, log_addr, maintenance_mode, security_headers,
59};
60
61/// Define a multi-threaded HTTP or HTTP/2 web server.
62pub struct Server {
63    opts: Settings,
64    worker_threads: usize,
65    max_blocking_threads: usize,
66}
67
68impl Server {
69    /// Create a new multi-threaded server instance.
70    pub fn new(opts: Settings) -> Result<Server> {
71        // Configure number of worker threads
72        let cpus = std::thread::available_parallelism()
73            .with_context(|| {
74                "unable to get current platform cpus or lack of permissions to query available parallelism"
75            })?
76            .get();
77        let worker_threads = match opts.general.threads_multiplier {
78            0 | 1 => cpus,
79            n => cpus * n,
80        };
81        let max_blocking_threads = opts.general.max_blocking_threads;
82
83        Ok(Server {
84            opts,
85            worker_threads,
86            max_blocking_threads,
87        })
88    }
89
90    /// Run the multi-threaded `Server` as standalone.
91    /// This is a top-level function of [run_server_on_rt](#method.run_server_on_rt).
92    ///
93    /// It accepts an optional [`cancel`] parameter to shut down the server
94    /// gracefully on demand as a complement to the termination signals handling.
95    ///
96    /// [`cancel`]: <https://docs.rs/tokio/latest/tokio/sync/watch/struct.Receiver.html>
97    pub fn run_standalone(self, cancel: Option<Receiver<()>>) -> Result {
98        self.run_server_on_rt(cancel, || {}, true)
99    }
100
101    /// Run the multi-threaded `Server` which will be used by a Windows service.
102    /// This is a top-level function of [run_server_on_rt](#method.run_server_on_rt).
103    ///
104    /// It accepts an optional [`cancel`] parameter to shut down the server
105    /// gracefully on demand and an optional `cancel_fn` that will be executed
106    /// right after the server is shut down.
107    ///
108    /// [`cancel`]: <https://docs.rs/tokio/latest/tokio/sync/watch/struct.Receiver.html>
109    #[cfg(windows)]
110    pub fn run_as_service<F>(self, cancel: Option<Receiver<()>>, cancel_fn: F) -> Result
111    where
112        F: FnOnce(),
113    {
114        self.run_server_on_rt(cancel, cancel_fn, true)
115    }
116
117    /// Build and run the multi-threaded `Server` on the Tokio runtime.
118    ///
119    /// Setting `exit_on_error` to `true` will exit the entire process if
120    /// the server fails to start (previous behaviour).
121    pub fn run_server_on_rt<F>(
122        self,
123        cancel_recv: Option<Receiver<()>>,
124        cancel_fn: F,
125        exit_on_error: bool,
126    ) -> Result
127    where
128        F: FnOnce(),
129    {
130        tracing::debug!(%self.worker_threads, "initializing tokio runtime with multi-threaded scheduler");
131
132        let rt = tokio::runtime::Builder::new_multi_thread()
133            .worker_threads(self.worker_threads)
134            .max_blocking_threads(self.max_blocking_threads)
135            .thread_name("static-web-server")
136            .enable_all()
137            .build()?;
138
139        let res = rt.block_on(async {
140            tracing::trace!("tokio runtime initialized");
141            self.start_server(cancel_recv, cancel_fn).await
142        });
143
144        if let Err(err) = &res {
145            tracing::error!("server failed to start up: {:?}", err);
146            if exit_on_error {
147                std::process::exit(1)
148            }
149        }
150        res
151    }
152
153    /// Run the inner Hyper `HyperServer` (HTTP1/HTTP2) forever on the current thread
154    // using the given configuration.
155    async fn start_server<F>(self, _cancel_recv: Option<Receiver<()>>, _cancel_fn: F) -> Result
156    where
157        F: FnOnce(),
158    {
159        tracing::trace!("starting web server");
160        tracing::info!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
161
162        // Config "general" options
163        let general = self.opts.general;
164        // Config-file "advanced" options
165        let advanced_opts = self.opts.advanced;
166
167        tracing::info!("log level: {}", general.log_level);
168
169        // Config file option
170        let config_file = general.config_file;
171        if config_file.is_file() {
172            tracing::info!("config file used: {}", config_file.display());
173        } else {
174            tracing::debug!(
175                "config file path not found or not a regular file: {}",
176                config_file.display()
177            );
178        }
179
180        // Determine TCP listener either file descriptor or TCP socket
181        let (tcp_listener, addr_str);
182        match general.fd {
183            Some(fd) => {
184                addr_str = format!("@FD({fd})");
185                tcp_listener = ListenFd::from_env()
186                    .take_tcp_listener(fd)?
187                    .with_context(|| "failed to convert inherited 'fd' into a 'tcp' listener")?;
188                tracing::info!(
189                    "converted inherited file descriptor {} to a 'tcp' listener",
190                    fd
191                );
192            }
193            None => {
194                let ip = general
195                    .host
196                    .parse::<IpAddr>()
197                    .with_context(|| format!("failed to parse {} address", general.host))?;
198                let addr = SocketAddr::from((ip, general.port));
199                tcp_listener = TcpListener::bind(addr)
200                    .with_context(|| format!("failed to bind to {addr} address"))?;
201                addr_str = addr.to_string();
202                tracing::info!("server bound to tcp socket {}", addr_str);
203            }
204        }
205
206        // Number of worker threads option
207        let threads = self.worker_threads;
208        tracing::info!("runtime worker threads: {}", threads);
209
210        // Maximum number of blocking threads
211        tracing::info!(
212            "runtime max blocking threads: {}",
213            general.max_blocking_threads
214        );
215
216        // Check for a valid root directory
217        let root_dir = helpers::get_valid_dirpath(&general.root)
218            .with_context(|| "root directory was not found or inaccessible")?;
219
220        // Canonicalize the root directory once at startup
221        // so further checks can compare against a precomputed canonical base
222        // without paying a `canonicalize` syscall on every request.
223        // Falls back to the validated path if canonicalization fails.
224        let root_dir = root_dir.canonicalize().unwrap_or(root_dir);
225
226        // Custom HTML error page files
227        // NOTE: in the case of relative paths, they're joined to the root directory
228        let mut page404 = general.page404;
229        if page404.is_relative() && !page404.starts_with(&root_dir) {
230            page404 = root_dir.join(page404);
231        }
232        if !page404.is_file() {
233            tracing::debug!(
234                "404 file path not found or not a regular file: {}",
235                page404.display()
236            );
237        }
238        let mut page50x = general.page50x;
239        if page50x.is_relative() && !page50x.starts_with(&root_dir) {
240            page50x = root_dir.join(page50x);
241        }
242        if !page50x.is_file() {
243            tracing::debug!(
244                "50x file path not found or not a regular file: {}",
245                page50x.display()
246            );
247        }
248
249        // Log remote address option
250        let log_remote_address = general.log_remote_address;
251
252        // Log the X-Real-IP header.
253        let log_x_real_ip = general.log_x_real_ip;
254
255        // Log the X-Forwarded-For header.
256        let log_forwarded_for = general.log_forwarded_for;
257
258        // Trusted IPs for remote addresses.
259        let trusted_proxies = general.trusted_proxies;
260
261        // Log redirect trailing slash option
262        let redirect_trailing_slash = general.redirect_trailing_slash;
263        tracing::info!(
264            "redirect trailing slash: enabled={}",
265            redirect_trailing_slash
266        );
267
268        // Ignore hidden files option
269        let ignore_hidden_files = general.ignore_hidden_files;
270        tracing::info!("ignore hidden files: enabled={}", ignore_hidden_files);
271
272        // Disable symlinks option
273        let disable_symlinks = general.disable_symlinks;
274        tracing::info!("disable symlinks: enabled={}", disable_symlinks);
275
276        // Default charset for text/* responses
277        let default_text_charset = general.text_charset;
278        tracing::info!("text charset: enabled={default_text_charset}");
279
280        // Grace period option
281        let grace_period = general.grace_period;
282        tracing::info!("grace period before graceful shutdown: {}s", grace_period);
283
284        // Index files option
285        let index_files = general
286            .index_files
287            .split(',')
288            .map(|s| s.trim().to_owned())
289            .collect::<Vec<_>>();
290        if index_files.is_empty() {
291            bail!("index files list is empty, provide at least one index file")
292        }
293        tracing::info!("index files: {}", general.index_files);
294
295        // Request handler options, some settings will be filled in by modules
296        let mut handler_opts = RequestHandlerOpts {
297            root_dir,
298            page404: page404.clone(),
299            page50x: page50x.clone(),
300            log_remote_address,
301            log_x_real_ip,
302            log_forwarded_for,
303            trusted_proxies,
304            redirect_trailing_slash,
305            ignore_hidden_files,
306            disable_symlinks,
307            accept_markdown: general.accept_markdown,
308            text_charset: general.text_charset,
309            index_files,
310            advanced_opts,
311            ..Default::default()
312        };
313
314        // Directory listing options
315        #[cfg(feature = "directory-listing")]
316        directory_listing::init(
317            general.directory_listing,
318            general.directory_listing_order,
319            general.directory_listing_format,
320            &mut handler_opts,
321        );
322
323        // Directory listing download options
324        #[cfg(feature = "directory-listing-download")]
325        directory_listing_download::init(&general.directory_listing_download, &mut handler_opts);
326
327        // Fallback page option
328        #[cfg(feature = "fallback-page")]
329        fallback_page::init(&general.page_fallback, &mut handler_opts);
330
331        // Health endpoint option
332        health::init(general.health, &mut handler_opts);
333
334        // Log remote address option
335        log_addr::init(general.log_remote_address, &mut handler_opts);
336
337        // Metrics endpoint option
338        #[cfg(feature = "metrics")]
339        metrics::init(general.metrics, &mut handler_opts);
340
341        // CORS option
342        cors::init(
343            &general.cors_allow_origins,
344            &general.cors_allow_headers,
345            &general.cors_expose_headers,
346            &mut handler_opts,
347        );
348
349        // `Basic` HTTP Authentication Schema option
350        #[cfg(feature = "basic-auth")]
351        basic_auth::init(&general.basic_auth, &mut handler_opts);
352
353        // Maintenance mode option
354        maintenance_mode::init(
355            general.maintenance_mode,
356            general.maintenance_mode_status,
357            general.maintenance_mode_file,
358            &mut handler_opts,
359        );
360
361        // Check pre-compressed files based on the `Accept-Encoding` header
362        compression_static::init(general.compression_static, &mut handler_opts);
363
364        // Auto compression based on the `Accept-Encoding` header
365        #[cfg(any(
366            feature = "compression",
367            feature = "compression-deflate",
368            feature = "compression-gzip",
369            feature = "compression-brotli",
370            feature = "compression-zstd",
371        ))]
372        compression::init(
373            general.compression,
374            general.compression_level,
375            &mut handler_opts,
376        );
377
378        // Cache control headers option
379        control_headers::init(general.cache_control_headers, &mut handler_opts);
380
381        // Security Headers option
382        security_headers::init(general.security_headers, &mut handler_opts);
383
384        // In-Memory cache option
385        #[cfg(feature = "experimental")]
386        mem_cache::cache::init(&mut handler_opts)?;
387
388        // Create a service router for Hyper
389        let router_service = RouterService::new(RequestHandler {
390            opts: Arc::from(handler_opts),
391        });
392
393        #[cfg(windows)]
394        let (sender, receiver) = tokio::sync::watch::channel(());
395
396        // Windows ctrl+c listening
397        #[cfg(windows)]
398        let ctrlc_task = tokio::spawn(async move {
399            if !general.windows_service {
400                tracing::info!("installing graceful shutdown ctrl+c signal handler");
401                if let Err(err) = tokio::signal::ctrl_c().await {
402                    tracing::error!("failed to install ctrl+c signal handler: {err:?}");
403                    return;
404                }
405                tracing::info!("installing graceful shutdown ctrl+c signal handler");
406                let _ = sender.send(());
407            }
408        });
409
410        // Run the corresponding HTTP Server asynchronously with its given options
411        #[cfg(feature = "http2")]
412        if general.http2 {
413            // HTTP to HTTPS redirect option
414            let https_redirect = general.https_redirect;
415            tracing::info!("http to https redirect: enabled={}", https_redirect);
416            tracing::info!(
417                "http to https redirect host: {}",
418                general.https_redirect_host
419            );
420            tracing::info!(
421                "http to https redirect from port: {}",
422                general.https_redirect_from_port
423            );
424            tracing::info!(
425                "http to https redirect from hosts: {}",
426                general.https_redirect_from_hosts
427            );
428
429            // HTTP/2 + TLS
430            tcp_listener
431                .set_nonblocking(true)
432                .with_context(|| "failed to set TCP non-blocking mode")?;
433            let listener = tokio::net::TcpListener::from_std(tcp_listener)
434                .with_context(|| "failed to create tokio::net::TcpListener")?;
435            let mut incoming = AddrIncoming::from_listener(listener).with_context(
436                || "failed to create an AddrIncoming from the current tokio::net::TcpListener",
437            )?;
438            incoming.set_nodelay(true);
439
440            let http2_tls_cert = match general.http2_tls_cert {
441                Some(v) => v,
442                _ => bail!("failed to initialize TLS because cert file missing"),
443            };
444            let http2_tls_key = match general.http2_tls_key {
445                Some(v) => v,
446                _ => bail!("failed to initialize TLS because key file missing"),
447            };
448
449            let tls = TlsConfigBuilder::new()
450                .cert_path(&http2_tls_cert)
451                .key_path(&http2_tls_key)
452                .build()
453                .with_context(
454                    || "failed to initialize TLS probably because invalid cert or key file",
455                )?;
456
457            #[cfg(unix)]
458            let signals = signals::create_signals()
459                .with_context(|| "failed to register termination signals")?;
460            #[cfg(unix)]
461            let handle = signals.handle();
462
463            let http2_server =
464                HyperServer::builder(TlsAcceptor::new(tls, incoming)).serve(router_service);
465
466            #[cfg(unix)]
467            let http2_cancel_recv = Arc::new(Mutex::new(_cancel_recv));
468            #[cfg(unix)]
469            let redirect_cancel_recv = http2_cancel_recv.clone();
470
471            #[cfg(unix)]
472            let http2_server = http2_server.with_graceful_shutdown(signals::wait_for_signals(
473                signals,
474                grace_period,
475                http2_cancel_recv,
476            ));
477
478            #[cfg(windows)]
479            let http2_cancel_recv = Arc::new(Mutex::new(_cancel_recv));
480            #[cfg(windows)]
481            let redirect_cancel_recv = http2_cancel_recv.clone();
482
483            #[cfg(windows)]
484            let http2_ctrlc_recv = Arc::new(Mutex::new(Some(receiver)));
485            #[cfg(windows)]
486            let redirect_ctrlc_recv = http2_ctrlc_recv.clone();
487
488            #[cfg(windows)]
489            let http2_server = http2_server.with_graceful_shutdown(async move {
490                if general.windows_service {
491                    signals::wait_for_ctrl_c(http2_cancel_recv, grace_period).await;
492                } else {
493                    signals::wait_for_ctrl_c(http2_ctrlc_recv, grace_period).await;
494                }
495            });
496
497            tracing::info!(
498                parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads),
499                "http2 server is listening on https://{}",
500                addr_str
501            );
502
503            // HTTP to HTTPS redirect server
504            if general.https_redirect {
505                let ip = general
506                    .host
507                    .parse::<IpAddr>()
508                    .with_context(|| format!("failed to parse {} address", general.host))?;
509                let addr = SocketAddr::from((ip, general.https_redirect_from_port));
510                let tcp_listener = TcpListener::bind(addr)
511                    .with_context(|| format!("failed to bind to {addr} address"))?;
512                tracing::info!(
513                    parent: tracing::info_span!("Server::start_server", ?addr, ?threads),
514                    "http1 redirect server is listening on http://{}",
515                    addr
516                );
517                tcp_listener
518                    .set_nonblocking(true)
519                    .with_context(|| "failed to set TCP non-blocking mode")?;
520
521                #[cfg(unix)]
522                let redirect_signals = signals::create_signals()
523                    .with_context(|| "failed to register termination signals")?;
524                #[cfg(unix)]
525                let redirect_handle = redirect_signals.handle();
526
527                // Allowed redirect hosts
528                let redirect_allowed_hosts = general
529                    .https_redirect_from_hosts
530                    .split(',')
531                    .map(|s| s.trim().to_owned())
532                    .collect::<Vec<_>>();
533                if redirect_allowed_hosts.is_empty() {
534                    bail!("https redirect allowed hosts is empty, provide at least one host or IP")
535                }
536
537                // Redirect options
538                let redirect_opts = Arc::new(https_redirect::RedirectOpts {
539                    https_hostname: general.https_redirect_host,
540                    https_port: general.port,
541                    allowed_hosts: redirect_allowed_hosts,
542                });
543
544                let server_redirect = HyperServer::from_tcp(tcp_listener)
545                    .unwrap()
546                    .tcp_nodelay(true)
547                    .serve(make_service_fn(move |_: &AddrStream| {
548                        let redirect_opts = redirect_opts.clone();
549                        let page404 = page404.clone();
550                        let page50x = page50x.clone();
551                        async move {
552                            Ok::<_, error::Error>(service_fn(move |req| {
553                                let redirect_opts = redirect_opts.clone();
554                                let page404 = page404.clone();
555                                let page50x = page50x.clone();
556                                async move {
557                                    let uri = req.uri();
558                                    let method = req.method();
559                                    match https_redirect::redirect_to_https(&req, redirect_opts) {
560                                        Ok(resp) => Ok(resp),
561                                        Err(status) => error_page::error_response(
562                                            uri, method, &status, &page404, &page50x,
563                                        ),
564                                    }
565                                }
566                            }))
567                        }
568                    }));
569
570                #[cfg(unix)]
571                let server_redirect = server_redirect.with_graceful_shutdown(
572                    signals::wait_for_signals(redirect_signals, grace_period, redirect_cancel_recv),
573                );
574                #[cfg(windows)]
575                let server_redirect = server_redirect.with_graceful_shutdown(async move {
576                    if general.windows_service {
577                        signals::wait_for_ctrl_c(redirect_cancel_recv, grace_period).await;
578                    } else {
579                        signals::wait_for_ctrl_c(redirect_ctrlc_recv, grace_period).await;
580                    }
581                });
582
583                // HTTP/2 server task
584                let server_task = tokio::spawn(async move {
585                    if let Err(err) = http2_server.await {
586                        tracing::error!("http2 server failed to start up: {:?}", err);
587                        std::process::exit(1)
588                    }
589                });
590
591                // HTTP/1 redirect server task
592                let redirect_server_task = tokio::spawn(async move {
593                    if let Err(err) = server_redirect.await {
594                        tracing::error!("http1 redirect server failed to start up: {:?}", err);
595                        std::process::exit(1)
596                    }
597                });
598
599                tracing::info!("press ctrl+c to shut down the servers");
600
601                #[cfg(windows)]
602                tokio::try_join!(ctrlc_task, server_task, redirect_server_task)?;
603                #[cfg(unix)]
604                tokio::try_join!(server_task, redirect_server_task)?;
605
606                #[cfg(unix)]
607                redirect_handle.close();
608            } else {
609                tracing::info!("press ctrl+c to shut down the server");
610                http2_server.await?;
611            }
612
613            #[cfg(unix)]
614            handle.close();
615
616            #[cfg(windows)]
617            _cancel_fn();
618
619            tracing::warn!("termination signal caught, shutting down the server execution");
620            return Ok(());
621        }
622
623        // HTTP/1
624
625        #[cfg(unix)]
626        let signals =
627            signals::create_signals().with_context(|| "failed to register termination signals")?;
628        #[cfg(unix)]
629        let handle = signals.handle();
630
631        tcp_listener
632            .set_nonblocking(true)
633            .with_context(|| "failed to set TCP non-blocking mode")?;
634
635        let http1_server = HyperServer::from_tcp(tcp_listener)
636            .unwrap()
637            .tcp_nodelay(true)
638            .serve(router_service);
639
640        #[cfg(unix)]
641        let http1_cancel_recv = Arc::new(Mutex::new(_cancel_recv));
642
643        #[cfg(unix)]
644        let http1_server = http1_server.with_graceful_shutdown(signals::wait_for_signals(
645            signals,
646            grace_period,
647            http1_cancel_recv,
648        ));
649
650        #[cfg(windows)]
651        let http1_server = http1_server.with_graceful_shutdown(async move {
652            let http1_cancel_recv = if general.windows_service {
653                // http1_cancel_recv
654                Arc::new(Mutex::new(_cancel_recv))
655            } else {
656                // http1_ctrlc_recv
657                Arc::new(Mutex::new(Some(receiver)))
658            };
659            signals::wait_for_ctrl_c(http1_cancel_recv, grace_period).await;
660        });
661
662        tracing::info!(
663            parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads),
664            "http1 server is listening on http://{}",
665            addr_str
666        );
667
668        tracing::info!("press ctrl+c to shut down the server");
669
670        #[cfg(unix)]
671        http1_server.await?;
672
673        #[cfg(windows)]
674        let http1_server_task = tokio::spawn(async move {
675            if let Err(err) = http1_server.await {
676                tracing::error!("http1 server failed to start up: {:?}", err);
677                std::process::exit(1)
678            }
679        });
680        #[cfg(windows)]
681        tokio::try_join!(ctrlc_task, http1_server_task)?;
682
683        #[cfg(windows)]
684        _cancel_fn();
685
686        #[cfg(unix)]
687        handle.close();
688
689        tracing::warn!("termination signal caught, shutting down the server execution");
690        Ok(())
691    }
692}