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