use hyper::server::conn::http1::Builder as Http1Builder;
use hyper_util::rt::{TokioIo, TokioTimer};
use std::net::TcpListener;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::watch::Receiver;
use self::idle_timeout::IdleTimeout;
use crate::handler::RequestHandler;
use crate::service::RouterService;
use crate::settings::cli::General;
use crate::{Context, Result, Settings};
mod browser;
mod http1;
#[cfg(feature = "tls")]
mod http1_tls;
#[cfg(feature = "http2")]
mod http2;
mod idle_timeout;
mod listener;
mod opts;
#[cfg(feature = "tls")]
mod redirect;
#[cfg(unix)]
mod uds;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct ServerRunConfig<'a> {
pub addr_str: &'a str,
pub threads: usize,
pub open: bool,
pub open_path: Option<&'a str>,
pub conn: ConnConfig,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(test, derive(Default))]
pub(crate) struct ConnConfig {
pub idle_timeout: Option<Duration>,
}
impl ConnConfig {
fn new(general: &General) -> Self {
Self {
idle_timeout: (general.idle_timeout > 0)
.then(|| Duration::from_secs(general.idle_timeout.into())),
}
}
fn http1_builder(&self) -> Http1Builder {
let mut builder = Http1Builder::new();
builder
.timer(TokioTimer::new())
.header_read_timeout(self.idle_timeout);
builder
}
fn io<T>(&self, io: T) -> TokioIo<IdleTimeout<T>> {
TokioIo::new(IdleTimeout::new(io, self.idle_timeout))
}
}
#[cfg(feature = "tls")]
pub(crate) struct TlsConfig {
pub tls_cert: std::path::PathBuf,
pub tls_key: std::path::PathBuf,
pub https_redirect: bool,
pub https_redirect_host: String,
pub https_redirect_from_port: u16,
pub https_redirect_from_hosts: String,
pub host: String,
pub port: u16,
pub page404: std::path::PathBuf,
pub page50x: std::path::PathBuf,
}
pub(crate) struct ShutdownCtx {
pub grace_period: u8,
pub cancel_recv: Option<Receiver<()>>,
#[cfg(windows)]
pub windows_service: bool,
#[cfg(windows)]
pub ctrl_c_recv: Receiver<()>,
#[cfg(windows)]
pub ctrlc_task: tokio::task::JoinHandle<crate::Result<()>>,
}
const ACCEPT_ERROR_DELAY: Duration = Duration::from_secs(1);
async fn accept_error_backoff(err: &std::io::Error) {
if let Some(delay) = accept_error_delay(err) {
tokio::time::sleep(delay).await;
}
}
fn accept_error_delay(err: &std::io::Error) -> Option<Duration> {
use std::io::ErrorKind;
match err.kind() {
ErrorKind::ConnectionRefused
| ErrorKind::ConnectionAborted
| ErrorKind::ConnectionReset => None,
_ => Some(ACCEPT_ERROR_DELAY),
}
}
pub struct Server {
opts: Settings,
worker_threads: usize,
max_blocking_threads: usize,
pre_bound_listener: Option<(TcpListener, String)>,
}
impl Server {
pub fn new(opts: Settings) -> Result<Server> {
let cpus = std::thread::available_parallelism()
.with_context(|| {
"unable to get current platform cpus or lack of permissions to query available parallelism"
})?
.get();
let worker_threads = match opts.general.threads_multiplier {
0 | 1 => cpus,
n => cpus * n,
};
let max_blocking_threads = opts.general.max_blocking_threads;
Ok(Server {
opts,
worker_threads,
max_blocking_threads,
pre_bound_listener: None,
})
}
pub fn with_pre_bound_listener(mut self, listener: std::net::TcpListener) -> Self {
let addr = listener
.local_addr()
.map(|a| a.to_string())
.unwrap_or_else(|_| "pre-bound".into());
self.pre_bound_listener = Some((listener, addr));
self
}
pub fn run_standalone(self, cancel: Option<Receiver<()>>) -> Result {
self.run_server_on_rt(cancel, || {}, true)
}
#[cfg(windows)]
pub fn run_as_service<F>(self, cancel: Option<Receiver<()>>, cancel_fn: F) -> Result
where
F: FnOnce(),
{
self.run_server_on_rt(cancel, cancel_fn, true)
}
pub fn run_server_on_rt<F>(
self,
cancel_recv: Option<Receiver<()>>,
cancel_fn: F,
exit_on_error: bool,
) -> Result
where
F: FnOnce(),
{
tracing::debug!(
%self.worker_threads,
"initializing tokio runtime with multi-threaded scheduler"
);
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(self.worker_threads)
.max_blocking_threads(self.max_blocking_threads)
.thread_name("static-web-server")
.enable_all()
.build()?;
let res = rt.block_on(async {
tracing::trace!("tokio runtime initialized");
self.start_server(cancel_recv, cancel_fn).await
});
if let Err(err) = &res {
tracing::error!("server failed to start up: {:?}", err);
if exit_on_error {
std::process::exit(1)
}
}
res
}
async fn start_server<F>(self, cancel_recv: Option<Receiver<()>>, cancel_fn: F) -> Result
where
F: FnOnce(),
{
tracing::trace!("starting web server");
tracing::info!(
name = env!("CARGO_PKG_NAME"),
version = env!("CARGO_PKG_VERSION"),
"starting Static Web Server"
);
let general = self.opts.general;
let advanced = self.opts.advanced;
let pre_bound = self.pre_bound_listener;
tracing::info!(log_level = %general.log_level, "log level");
if general.config_file.is_file() {
tracing::info!(path = %general.config_file.display(), "config file used");
} else {
tracing::debug!(
"config file path not found or not a regular file: {}",
general.config_file.display()
);
}
#[cfg(unix)]
let unix_listener_info = if let Some(path) = general.unix_socket.as_ref() {
use crate::server::listener::create_unix_listener;
Some(create_unix_listener(
path,
general.unix_socket_mode,
general.unix_socket_force,
)?)
} else {
None
};
#[cfg(unix)]
let tcp_listener_info = if unix_listener_info.is_none() {
Some(match pre_bound {
Some(pre) => pre,
None => crate::server::listener::create_tcp_listener(&general)?,
})
} else {
None
};
#[cfg(not(unix))]
let tcp_listener_info = Some(match pre_bound {
Some(pre) => pre,
None => crate::server::listener::create_tcp_listener(&general)?,
});
tracing::info!(
worker_threads = self.worker_threads,
"runtime worker threads"
);
tracing::info!(
max_blocking_threads = general.max_blocking_threads,
"runtime max blocking threads"
);
tracing::info!(
grace_period_seconds = general.grace_period,
"grace period before graceful shutdown"
);
tracing::info!(
idle_timeout_seconds = general.idle_timeout,
"connection idle timeout"
);
let conn = ConnConfig::new(&general);
let opts_result = opts::init(&general, advanced)?;
let router_service = RouterService::new(RequestHandler {
opts: Arc::from(opts_result.handler_opts),
});
#[cfg(windows)]
let (sender, ctrl_c_recv) = tokio::sync::watch::channel(());
#[cfg(windows)]
let windows_service = general.windows_service;
#[cfg(windows)]
let ctrlc_task = tokio::spawn(async move {
if !windows_service {
tracing::info!("installing graceful shutdown ctrl+c signal handler");
if let Err(err) = tokio::signal::ctrl_c().await {
return Err(
crate::Error::new(err).context("failed to install ctrl+c signal handler")
);
}
tracing::info!("graceful shutdown ctrl+c signal received");
let _ = sender.send(());
}
Ok::<_, crate::Error>(())
});
let ctx = ShutdownCtx {
grace_period: general.grace_period,
cancel_recv,
#[cfg(windows)]
windows_service,
#[cfg(windows)]
ctrl_c_recv,
#[cfg(windows)]
ctrlc_task,
};
#[cfg(unix)]
if let Some((unix_listener, socket_path, addr_str)) = unix_listener_info {
let run_cfg = ServerRunConfig {
addr_str: &addr_str,
threads: self.worker_threads,
open: false,
open_path: None,
conn,
};
return uds::run(
unix_listener,
socket_path,
router_service,
run_cfg,
ctx,
cancel_fn,
)
.await;
}
let (tcp_listener, addr_str) = tcp_listener_info.unwrap();
let run_cfg = ServerRunConfig {
addr_str: &addr_str,
threads: self.worker_threads,
open: general.open,
open_path: general.open_path.as_deref(),
conn,
};
#[cfg(feature = "tls")]
if general.tls {
let tls_cert = general
.tls_cert
.ok_or_else(|| anyhow!("TLS cert file path is required when --tls is enabled"))?;
let tls_key = general
.tls_key
.ok_or_else(|| anyhow!("TLS key file path is required when --tls is enabled"))?;
let tls_cfg = TlsConfig {
tls_cert,
tls_key,
https_redirect: general.https_redirect,
https_redirect_host: general.https_redirect_host,
https_redirect_from_port: general.https_redirect_from_port,
https_redirect_from_hosts: general.https_redirect_from_hosts,
host: general.host,
port: general.port,
page404: opts_result.page404,
page50x: opts_result.page50x,
};
#[cfg(feature = "http2")]
if general.http2 {
return http2::run(
tcp_listener,
router_service,
run_cfg,
tls_cfg,
ctx,
cancel_fn,
)
.await;
}
return http1_tls::run(
tcp_listener,
router_service,
run_cfg,
tls_cfg,
ctx,
cancel_fn,
)
.await;
}
http1::run(tcp_listener, router_service, run_cfg, ctx, cancel_fn).await
}
}
#[cfg(test)]
mod tests {
use super::{ACCEPT_ERROR_DELAY, ConnConfig, accept_error_delay};
use std::io::{Error, ErrorKind};
use std::time::Duration;
fn conn_config(args: &[&str]) -> ConnConfig {
let settings = crate::Settings::get_unparsed(false, args).expect("settings parse");
ConnConfig::new(&settings.general)
}
#[test]
fn conn_config_idle_timeout_defaults_to_30_seconds() {
let conn = conn_config(&["static-web-server"]);
assert_eq!(conn.idle_timeout, Some(Duration::from_secs(30)));
}
#[test]
fn conn_config_idle_timeout_zero_disables_it() {
let conn = conn_config(&["static-web-server", "--idle-timeout", "0"]);
assert_eq!(conn.idle_timeout, None);
}
#[test]
fn accept_error_delay_skips_per_connection_errors() {
for kind in [
ErrorKind::ConnectionRefused,
ErrorKind::ConnectionAborted,
ErrorKind::ConnectionReset,
] {
assert_eq!(accept_error_delay(&Error::from(kind)), None);
}
}
#[test]
fn accept_error_delay_waits_on_other_errors() {
let emfile = Error::from_raw_os_error(24);
assert_eq!(accept_error_delay(&emfile), Some(ACCEPT_ERROR_DELAY));
}
}