diff --git a/Cargo.lock b/Cargo.lock index f90c5aa..1eba49b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,9 +34,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" dependencies = [ "anstyle", "anstyle-parse", @@ -54,30 +54,30 @@ checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.10" +version = "4.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272" +checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" dependencies = [ "clap_builder", "clap_derive", @@ -182,9 +182,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.9" +version = "4.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1" +checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" dependencies = [ "anstream", "anstyle", @@ -201,7 +201,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.43", ] [[package]] @@ -264,12 +264,12 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.4.1" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e95fbd621905b854affdc67943b043a0fbb6ed7385fd5a25650d19a8a6cfdf" +checksum = "b467862cc8610ca6fc9a1532d7777cee0804e678ab45410897b9396495994a0b" dependencies = [ "nix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -591,9 +591,9 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" @@ -612,9 +612,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "linux-raw-sys" @@ -636,9 +636,9 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "mime" @@ -735,9 +735,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" @@ -762,7 +762,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.43", ] [[package]] @@ -826,7 +826,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.43", ] [[package]] @@ -866,9 +866,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" dependencies = [ "unicode-ident", ] @@ -980,17 +980,17 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1033,7 +1033,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.43", ] [[package]] @@ -1144,9 +1144,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" dependencies = [ "proc-macro2", "quote", @@ -1176,35 +1176,35 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.51" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.51" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.43", ] [[package]] @@ -1297,7 +1297,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.43", ] [[package]] @@ -1445,7 +1445,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.43", "wasm-bindgen-shared", ] @@ -1479,7 +1479,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.43", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/src/errors.rs b/src/errors.rs index 78d42fe..2fbfad6 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -30,6 +30,10 @@ pub enum SevaError { UriTooLong, #[error("Missing value for header: {0}")] MissingHeaderValue(HeaderName), + #[error("timed out while reading data")] + ReadTimeOut, + #[error("timed out while writing data")] + WriteTimeOut, } #[derive(Error, Debug)] @@ -53,10 +57,18 @@ impl fmt::Display for ParsingError { pub trait IoErrorUtils { fn kind(&self) -> io::ErrorKind; + fn is_addr_in_use(&self) -> bool { + self.kind() == io::ErrorKind::AddrInUse + } + fn is_blocking(&self) -> bool { self.kind() == io::ErrorKind::WouldBlock } + fn is_timed_out(&self) -> bool { + self.kind() == io::ErrorKind::TimedOut + } + fn is_not_found(&self) -> bool { self.kind() == io::ErrorKind::NotFound } diff --git a/src/main.rs b/src/main.rs index fe82aab..a76171c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,6 +43,10 @@ struct Args { /// Log Level #[arg(long, default_value = "INFO")] log_level: Level, + + /// Timeout duration in seconds + #[arg(long, default_value = "60")] + timeout: u64, } fn expand_tilde(path: &str) -> String { @@ -81,7 +85,8 @@ fn main() -> errors::Result<()> { port = args.port, ); - let mut server = server::HttpServer::new(&args.host, args.port, dir, false)?; + let mut server = + server::HttpServer::new(&args.host, args.port, dir, false, args.timeout)?; server.run()?; Ok(()) } diff --git a/src/server.rs b/src/server.rs index 042c33a..7d923dd 100644 --- a/src/server.rs +++ b/src/server.rs @@ -9,6 +9,7 @@ use std::{ atomic::{AtomicBool, Ordering}, Arc, }, + time::{Duration, Instant}, }; use bytes::{BufMut, BytesMut}; @@ -16,7 +17,7 @@ use chrono::Local; use clap::crate_version; use contracts::debug_requires; use handlebars::Handlebars; -use tracing::{debug, error, info, trace}; +use tracing::{debug, error, info, trace, warn}; use crate::{ errors::{IoErrorUtils, ParsingError, Result, SevaError}, @@ -56,6 +57,7 @@ pub struct HttpServer { listener: TcpListener, shutdown: Arc, test_mode: bool, + timeout: Duration, } impl HttpServer { @@ -64,10 +66,12 @@ impl HttpServer { port: u16, dir: PathBuf, test_mode: bool, + timeout: u64, ) -> Result { debug!("binding to {host} on port: {port}"); let listener = TcpListener::bind((host, port))?; listener.set_nonblocking(true)?; + let shutdown = Arc::new(AtomicBool::new(false)); let s = shutdown.clone(); if !test_mode { @@ -78,6 +82,7 @@ impl HttpServer { listener, shutdown, test_mode, + timeout: Duration::from_secs(timeout), }) } fn shut_down(&mut self) -> Result<()> { @@ -86,22 +91,23 @@ impl HttpServer { } pub fn run(&mut self) -> Result<()> { - let start = std::time::Instant::now(); + let start = Instant::now(); loop { match self.listener.accept() { Ok((stream, client_addr)) => { + stream.set_read_timeout(Some(self.timeout))?; + stream.set_write_timeout(Some(self.timeout))?; let dir = self.dir.clone(); - let mut handler = RequestHandler::new(stream, client_addr, dir); + let mut handler = + RequestHandler::new(stream, client_addr, dir, self.timeout); match handler.handle() { Ok(_) => {} - // TODO: return 500 Err(e) => { error!("got error while handling request: {e}") } } } Err(e) => { - // handle error if !e.is_blocking() { error!("failed to accept new tcp connection. Reason: {e}"); } @@ -111,8 +117,7 @@ impl HttpServer { self.shut_down()?; break; } - if self.test_mode && start.elapsed() > std::time::Duration::from_secs(1) - { + if self.test_mode && start.elapsed() > Duration::from_secs(1) { break; } } @@ -124,17 +129,20 @@ struct RequestHandler { stream: TcpStream, client_addr: SocketAddr, dir: PathBuf, + timeout: Duration, } impl RequestHandler { fn new( stream: TcpStream, client_addr: SocketAddr, dir: PathBuf, + timeout: Duration, ) -> RequestHandler { Self { stream, client_addr, dir, + timeout, } } @@ -153,6 +161,12 @@ impl RequestHandler { Some(&format!("invalid range: {hdr}")), )? } + SevaError::ReadTimeOut => { + warn!("timed out while reading request data") + } + SevaError::WriteTimeOut => { + warn!("timed out while writing request data") + } _ => { error!("internal server error: {e}"); self.send_error( @@ -353,16 +367,21 @@ impl RequestHandler { //TODO: optimize fn read_line(&mut self, buf: &mut BytesMut, limit: usize) -> Result<()> { let mut sz = 0usize; + let t = Instant::now(); loop { if sz == limit { break; } else { let mut b = [0u8; 1]; - //TODO: possible to get stuck infinitely - // fix by adding timeout loop { match self.stream.read_exact(&mut b) { Ok(_) => break, + Err(e) if e.is_blocking() && t.elapsed() > self.timeout => { + return Err(SevaError::ReadTimeOut) + } + Err(e) if e.is_timed_out() => { + return Err(SevaError::ReadTimeOut) + } Err(e) if e.is_blocking() => continue, Err(e) => return Err(SevaError::Io(e)), } @@ -388,13 +407,22 @@ impl RequestHandler { self.send_headers(response.headers)?; self.end_headers()?; + let t = Instant::now(); + let bytes_sent = if request.method == HttpMethod::Head { 0 } else { trace!("RequestHandler::send_response body io::copy"); // TODO: can we do this w/o blocking self.stream.set_nonblocking(false)?; - let count = io::copy(&mut response.body, &mut self.stream)? as usize; + let count = + io::copy(&mut response.body, &mut self.stream).map_err(|e| { + if e.is_timed_out() || t.elapsed() > self.timeout { + SevaError::WriteTimeOut + } else { + SevaError::Io(e) + } + })? as usize; self.stream.set_nonblocking(true)?; count }; @@ -580,10 +608,20 @@ mod tests { .collect() } + fn create_server() -> Result<(HttpServer, u16)> { + loop { + let path = current_dir()?; + let port = get_random_port(); + match HttpServer::new("127.0.0.1", port, path, true, 30) { + Ok(svr) => return Ok((svr, port)), + Err(SevaError::Io(e)) if e.is_addr_in_use() => continue, + Err(e) => return Err(e), + } + } + } + fn start_server() -> Result { - let path = current_dir()?; - let port = get_random_port(); - let mut server = HttpServer::new("127.0.0.1", port, path, true)?; + let (mut server, port) = create_server()?; thread::spawn(move || server.run()); let subscriber = FmtSubscriber::builder() .with_max_level(Level::INFO) @@ -600,7 +638,7 @@ mod tests { fn shutdown_works() -> Result<()> { let path = current_dir()?; let port = get_random_port(); - let mut server = HttpServer::new("127.0.0.1", port, path, false)?; + let mut server = HttpServer::new("127.0.0.1", port, path, false, 30)?; let shutdown = server.shutdown.clone(); let handle = thread::spawn(move || server.run()); shutdown.store(true, Ordering::SeqCst);