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