1use 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
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, || {})
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)
113 }
114
115 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 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 let general = self.opts.general;
150 let advanced_opts = self.opts.advanced;
152
153 tracing::info!("log level: {}", general.log_level);
154
155 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 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 let threads = self.worker_threads;
194 tracing::info!("runtime worker threads: {}", threads);
195
196 tracing::info!(
198 "runtime max blocking threads: {}",
199 general.max_blocking_threads
200 );
201
202 let root_dir = helpers::get_valid_dirpath(&general.root)
204 .with_context(|| "root directory was not found or inaccessible")?;
205
206 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 let log_remote_address = general.log_remote_address;
231
232 let log_x_real_ip = general.log_x_real_ip;
234
235 let log_forwarded_for = general.log_forwarded_for;
237
238 let trusted_proxies = general.trusted_proxies;
240
241 let redirect_trailing_slash = general.redirect_trailing_slash;
243 tracing::info!(
244 "redirect trailing slash: enabled={}",
245 redirect_trailing_slash
246 );
247
248 let ignore_hidden_files = general.ignore_hidden_files;
250 tracing::info!("ignore hidden files: enabled={}", ignore_hidden_files);
251
252 let disable_symlinks = general.disable_symlinks;
254 tracing::info!("disable symlinks: enabled={}", disable_symlinks);
255
256 let grace_period = general.grace_period;
258 tracing::info!("grace period before graceful shutdown: {}s", grace_period);
259
260 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 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 #[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 #[cfg(feature = "directory-listing-download")]
299 directory_listing_download::init(&general.directory_listing_download, &mut handler_opts);
300
301 #[cfg(feature = "fallback-page")]
303 fallback_page::init(&general.page_fallback, &mut handler_opts);
304
305 health::init(general.health, &mut handler_opts);
307
308 log_addr::init(general.log_remote_address, &mut handler_opts);
310
311 #[cfg(all(unix, feature = "experimental"))]
313 metrics::init(general.experimental_metrics, &mut handler_opts);
314
315 cors::init(
317 &general.cors_allow_origins,
318 &general.cors_allow_headers,
319 &general.cors_expose_headers,
320 &mut handler_opts,
321 );
322
323 #[cfg(feature = "basic-auth")]
325 basic_auth::init(&general.basic_auth, &mut handler_opts);
326
327 maintenance_mode::init(
329 general.maintenance_mode,
330 general.maintenance_mode_status,
331 general.maintenance_mode_file,
332 &mut handler_opts,
333 );
334
335 #[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 #[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 control_headers::init(general.cache_control_headers, &mut handler_opts);
361
362 security_headers::init(general.security_headers, &mut handler_opts);
364
365 #[cfg(feature = "experimental")]
367 mem_cache::cache::init(&mut handler_opts)?;
368
369 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 #[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 #[cfg(feature = "http2")]
392 if general.http2 {
393 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 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 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 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 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 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 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 #[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 Arc::new(Mutex::new(_cancel_recv))
635 } else {
636 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}