1use 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
59pub struct Server {
61 opts: Settings,
62 worker_threads: usize,
63 max_blocking_threads: usize,
64}
65
66impl Server {
67 pub fn new(opts: Settings) -> Result<Server> {
69 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 pub fn run_standalone(self, cancel: Option<Receiver<()>>) -> Result {
96 self.run_server_on_rt(cancel, || {}, true)
97 }
98
99 #[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 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 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 let general = self.opts.general;
162 let advanced_opts = self.opts.advanced;
164
165 tracing::info!("log level: {}", general.log_level);
166
167 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 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 let threads = self.worker_threads;
206 tracing::info!("runtime worker threads: {}", threads);
207
208 tracing::info!(
210 "runtime max blocking threads: {}",
211 general.max_blocking_threads
212 );
213
214 let root_dir = helpers::get_valid_dirpath(&general.root)
216 .with_context(|| "root directory was not found or inaccessible")?;
217
218 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 let log_remote_address = general.log_remote_address;
243
244 let log_x_real_ip = general.log_x_real_ip;
246
247 let log_forwarded_for = general.log_forwarded_for;
249
250 let trusted_proxies = general.trusted_proxies;
252
253 let redirect_trailing_slash = general.redirect_trailing_slash;
255 tracing::info!(
256 "redirect trailing slash: enabled={}",
257 redirect_trailing_slash
258 );
259
260 let ignore_hidden_files = general.ignore_hidden_files;
262 tracing::info!("ignore hidden files: enabled={}", ignore_hidden_files);
263
264 let disable_symlinks = general.disable_symlinks;
266 tracing::info!("disable symlinks: enabled={}", disable_symlinks);
267
268 let grace_period = general.grace_period;
270 tracing::info!("grace period before graceful shutdown: {}s", grace_period);
271
272 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 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 #[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 #[cfg(feature = "directory-listing-download")]
312 directory_listing_download::init(&general.directory_listing_download, &mut handler_opts);
313
314 #[cfg(feature = "fallback-page")]
316 fallback_page::init(&general.page_fallback, &mut handler_opts);
317
318 health::init(general.health, &mut handler_opts);
320
321 log_addr::init(general.log_remote_address, &mut handler_opts);
323
324 #[cfg(all(unix, feature = "experimental"))]
326 metrics::init(general.experimental_metrics, &mut handler_opts);
327
328 cors::init(
330 &general.cors_allow_origins,
331 &general.cors_allow_headers,
332 &general.cors_expose_headers,
333 &mut handler_opts,
334 );
335
336 #[cfg(feature = "basic-auth")]
338 basic_auth::init(&general.basic_auth, &mut handler_opts);
339
340 maintenance_mode::init(
342 general.maintenance_mode,
343 general.maintenance_mode_status,
344 general.maintenance_mode_file,
345 &mut handler_opts,
346 );
347
348 #[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 #[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 control_headers::init(general.cache_control_headers, &mut handler_opts);
374
375 security_headers::init(general.security_headers, &mut handler_opts);
377
378 #[cfg(feature = "experimental")]
380 mem_cache::cache::init(&mut handler_opts)?;
381
382 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 #[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 #[cfg(feature = "http2")]
405 if general.http2 {
406 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 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 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 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 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 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 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 #[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 Arc::new(Mutex::new(_cancel_recv))
648 } else {
649 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}