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