use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Semaphore;
use super::auth::Credentials;
use crate::exit::ExitCode;
use crate::http;
mod answer;
mod response;
#[cfg(all(test, unix))]
mod testing;
use answer::{answer, path_of};
use response::log_access;
const READ_TIMEOUT: Duration = Duration::from_secs(5);
const MAX_CONNECTIONS: usize = 512;
pub const CONNECTION_DEADLINE: Duration = Duration::from_secs(60);
pub struct ServeConfig {
pub root: PathBuf,
pub bind: SocketAddr,
pub spa: bool,
pub listing: bool,
pub hidden: bool,
pub auth: Option<Credentials>,
pub follow_symlinks: bool,
pub connection_deadline: Duration,
}
pub async fn run(cfg: ServeConfig) -> ExitCode {
let listener = match TcpListener::bind(cfg.bind).await {
Ok(listener) => listener,
Err(err) => {
eprintln!("shep serve: could not bind {}: {err}", cfg.bind);
return ExitCode::Failure;
}
};
let mut sigterm = match crate::shutdown::Terminate::install() {
Ok(sigterm) => sigterm,
Err(err) => {
eprintln!("shep serve: could not install a SIGTERM handler: {err}");
return ExitCode::Failure;
}
};
let cfg = Arc::new(cfg);
let semaphore = Arc::new(Semaphore::new(MAX_CONNECTIONS));
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
_ = sigterm.recv() => {}
() = accept_forever(listener, cfg, semaphore) => {}
}
ExitCode::Success
}
async fn accept_forever(listener: TcpListener, cfg: Arc<ServeConfig>, semaphore: Arc<Semaphore>) {
loop {
match listener.accept().await {
Ok((stream, _peer)) => {
let Ok(permit) = Arc::clone(&semaphore).try_acquire_owned() else {
drop(stream);
continue;
};
let cfg = Arc::clone(&cfg);
tokio::spawn(async move {
let _permit = permit;
let deadline = cfg.connection_deadline;
let _ = tokio::time::timeout(deadline, handle_connection(stream, cfg)).await;
});
}
Err(err) => {
eprintln!("shep serve: accept failed: {err}");
}
}
}
}
async fn handle_connection(mut stream: TcpStream, cfg: Arc<ServeConfig>) {
let peer = stream.peer_addr().ok();
let request = match http::read_request(&mut stream, READ_TIMEOUT).await {
Ok(request) => request,
Err(_err) => return,
};
let mut logged_path = path_of(&request.target).to_string();
let (status, bytes) = answer(&mut stream, &cfg, &request, &mut logged_path).await;
log_access(peer, &request.method, &logged_path, status, bytes);
}
#[cfg(all(test, unix))]
mod tests {
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use super::MAX_CONNECTIONS;
use super::testing::{config, serve_on_free_port, write_tree};
#[tokio::test]
async fn connections_beyond_the_cap_are_closed_rather_than_queued_forever() {
let outer = tempfile::tempdir().unwrap();
let root = outer.path().join("www");
write_tree(&root, &[("index.html", "<h1>home</h1>")]);
let server = serve_on_free_port(config(&root)).await;
let mut held = Vec::with_capacity(MAX_CONNECTIONS);
for _ in 0..MAX_CONNECTIONS {
let stream =
tokio::time::timeout(Duration::from_secs(5), TcpStream::connect(server.addr()))
.await
.expect("connect must not hang")
.unwrap();
held.push(stream);
}
tokio::time::sleep(Duration::from_millis(200)).await;
let mut over_cap =
tokio::time::timeout(Duration::from_secs(5), TcpStream::connect(server.addr()))
.await
.expect("connect must not hang")
.unwrap();
let mut buf = Vec::new();
tokio::time::timeout(Duration::from_millis(500), over_cap.read_to_end(&mut buf))
.await
.expect("a connection beyond the cap must close promptly rather than hang")
.unwrap();
assert!(
buf.is_empty(),
"nothing should be written to a refused connection: {buf:?}"
);
drop(held);
}
#[tokio::test]
async fn a_connection_that_stops_reading_is_dropped_at_the_deadline() {
let outer = tempfile::tempdir().unwrap();
let root = outer.path().join("www");
let big = "a".repeat(8 * 1024 * 1024);
write_tree(&root, &[("big.txt", &big)]);
let mut cfg = config(&root);
cfg.connection_deadline = Duration::from_millis(300);
let server = serve_on_free_port(cfg).await;
let mut stream = TcpStream::connect(server.addr()).await.unwrap();
stream
.write_all(b"GET /big.txt HTTP/1.1\r\nHost: localhost\r\n\r\n")
.await
.unwrap();
let mut rest = Vec::new();
tokio::time::timeout(Duration::from_secs(30), stream.read_to_end(&mut rest))
.await
.expect("the deadline must close the connection rather than hang forever")
.unwrap();
}
}