1use crate::error::ServerError;
15use crate::request::Request;
16use crate::response::Response;
17use crossbeam_channel::{Receiver, Sender, unbounded};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20use std::fs;
21use std::io;
22use std::net::{IpAddr, TcpListener, TcpStream};
23use std::path::{Path, PathBuf};
24use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
25#[cfg(test)]
26use std::sync::mpsc;
27use std::sync::{Arc, Mutex, Once, OnceLock, RwLock};
28use std::thread;
29use std::time::{Duration, Instant, UNIX_EPOCH};
30
31static SHUTDOWN_SIGNAL_SLOT: OnceLock<
32 Mutex<Option<Arc<ShutdownSignal>>>,
33> = OnceLock::new();
34static SIGNAL_HANDLER_INSTALL: Once = Once::new();
35const RATE_LIMIT_SHARDS: usize = 16;
40type RateLimitShard = Mutex<HashMap<IpAddr, Vec<Instant>>>;
41type RateLimitTable = [RateLimitShard; RATE_LIMIT_SHARDS];
42static RATE_LIMIT_STATE: OnceLock<RateLimitTable> = OnceLock::new();
43
44const ETAG_CACHE_MAX: usize = 256;
48type EtagCache = RwLock<HashMap<(u64, u64), Arc<str>>>;
49static ETAG_CACHE: OnceLock<EtagCache> = OnceLock::new();
50static METRIC_REQUESTS_TOTAL: AtomicUsize = AtomicUsize::new(0);
51static METRIC_RESPONSES_4XX: AtomicUsize = AtomicUsize::new(0);
52static METRIC_RESPONSES_5XX: AtomicUsize = AtomicUsize::new(0);
53static METRIC_RATE_LIMITED: AtomicUsize = AtomicUsize::new(0);
54
55#[doc(alias = "http server")]
77#[doc(alias = "static file server")]
78#[derive(
79 Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize,
80)]
81pub struct Server {
82 address: String,
83 document_root: PathBuf,
84 #[serde(skip, default)]
88 canonical_document_root: PathBuf,
89 cors_enabled: Option<bool>,
90 cors_origins: Option<Vec<String>>,
91 custom_headers: Option<HashMap<String, String>>,
92 request_timeout: Option<Duration>,
93 connection_timeout: Option<Duration>,
94 rate_limit_per_minute: Option<usize>,
95 static_cache_ttl_secs: Option<u64>,
96 max_buffered_body_bytes: Option<u64>,
102}
103
104#[doc(alias = "builder")]
132#[doc(alias = "configuration")]
133#[derive(Clone, Debug, Default)]
134pub struct ServerBuilder {
135 address: Option<String>,
136 document_root: Option<PathBuf>,
137 cors_enabled: Option<bool>,
138 cors_origins: Option<Vec<String>>,
139 custom_headers: Option<HashMap<String, String>>,
140 request_timeout: Option<Duration>,
141 connection_timeout: Option<Duration>,
142 rate_limit_per_minute: Option<usize>,
143 static_cache_ttl_secs: Option<u64>,
144 max_buffered_body_bytes: Option<u64>,
145}
146
147impl ServerBuilder {
148 #[doc(alias = "new builder")]
164 pub fn new() -> Self {
165 Self::default()
166 }
167
168 #[doc(alias = "bind address")]
187 pub fn address(mut self, address: &str) -> Self {
188 self.address = Some(address.to_string());
189 self
190 }
191
192 #[doc(alias = "document root")]
211 pub fn document_root(mut self, path: &str) -> Self {
212 self.document_root = Some(PathBuf::from(path));
213 self
214 }
215
216 pub fn enable_cors(mut self) -> Self {
218 self.cors_enabled = Some(true);
219 self
220 }
221
222 pub fn disable_cors(mut self) -> Self {
224 self.cors_enabled = Some(false);
225 self
226 }
227
228 pub fn cors_origins(mut self, origins: Vec<String>) -> Self {
230 self.cors_origins = Some(origins);
231 self.cors_enabled = Some(true); self
233 }
234
235 pub fn custom_header(mut self, name: &str, value: &str) -> Self {
237 let mut headers = self.custom_headers.unwrap_or_default();
238 let _ = headers.insert(name.to_string(), value.to_string());
239 self.custom_headers = Some(headers);
240 self
241 }
242
243 pub fn custom_headers(
245 mut self,
246 headers: HashMap<String, String>,
247 ) -> Self {
248 self.custom_headers = Some(headers);
249 self
250 }
251
252 pub fn request_timeout(mut self, timeout: Duration) -> Self {
254 self.request_timeout = Some(timeout);
255 self
256 }
257
258 pub fn connection_timeout(mut self, timeout: Duration) -> Self {
260 self.connection_timeout = Some(timeout);
261 self
262 }
263
264 pub fn rate_limit_per_minute(mut self, requests: usize) -> Self {
266 self.rate_limit_per_minute = Some(requests.max(1));
267 self
268 }
269
270 pub fn static_cache_ttl_secs(mut self, ttl: u64) -> Self {
272 self.static_cache_ttl_secs = Some(ttl);
273 self
274 }
275
276 pub fn max_buffered_body_bytes(mut self, bytes: u64) -> Self {
298 self.max_buffered_body_bytes = Some(bytes);
299 self
300 }
301
302 #[doc(alias = "finalize")]
326 pub fn build(self) -> Result<Server, &'static str> {
327 let address = self.address.ok_or("Address is required")?;
328 let document_root =
329 self.document_root.ok_or("Document root is required")?;
330 let canonical_document_root = fs::canonicalize(&document_root)
333 .unwrap_or_else(|_| document_root.clone());
334
335 Ok(Server {
336 address,
337 document_root,
338 canonical_document_root,
339 cors_enabled: self.cors_enabled,
340 cors_origins: self.cors_origins,
341 custom_headers: self.custom_headers,
342 request_timeout: self.request_timeout,
343 connection_timeout: self.connection_timeout,
344 rate_limit_per_minute: self.rate_limit_per_minute,
345 static_cache_ttl_secs: self.static_cache_ttl_secs,
346 max_buffered_body_bytes: self.max_buffered_body_bytes,
347 })
348 }
349}
350
351#[derive(Debug, Clone)]
353pub struct ShutdownSignal {
354 pub should_shutdown: Arc<AtomicBool>,
356 pub active_connections: Arc<AtomicUsize>,
358 pub shutdown_timeout: Duration,
360}
361
362impl Default for ShutdownSignal {
363 fn default() -> Self {
364 Self::new(Duration::from_secs(30))
365 }
366}
367
368impl ShutdownSignal {
369 pub fn new(shutdown_timeout: Duration) -> Self {
371 Self {
372 should_shutdown: Arc::new(AtomicBool::new(false)),
373 active_connections: Arc::new(AtomicUsize::new(0)),
374 shutdown_timeout,
375 }
376 }
377
378 pub fn shutdown(&self) {
380 self.should_shutdown.store(true, Ordering::SeqCst);
381 println!(
382 "🛑 Shutdown signal received. Waiting for active connections to finish..."
383 );
384 }
385
386 pub fn is_shutdown_requested(&self) -> bool {
388 self.should_shutdown.load(Ordering::SeqCst)
389 }
390
391 pub fn connection_started(&self) {
393 let _ = self.active_connections.fetch_add(1, Ordering::SeqCst);
394 }
395
396 pub fn connection_finished(&self) {
398 let _ = self.active_connections.fetch_sub(1, Ordering::SeqCst);
399 }
400
401 pub fn active_connection_count(&self) -> usize {
403 self.active_connections.load(Ordering::SeqCst)
404 }
405
406 pub fn wait_for_shutdown(&self) -> bool {
408 let start_time = Instant::now();
409
410 while self.active_connection_count() > 0
411 && start_time.elapsed() < self.shutdown_timeout
412 {
413 let remaining = self
414 .shutdown_timeout
415 .saturating_sub(start_time.elapsed());
416 println!(
417 "⏳ Waiting for {} active connection(s) to finish... ({:.1}s remaining)",
418 self.active_connection_count(),
419 remaining.as_secs_f32()
420 );
421
422 thread::sleep(remaining.min(Duration::from_millis(50)));
424 }
425
426 let remaining_connections = self.active_connection_count();
427 if remaining_connections > 0 {
428 println!(
429 "⚠️ Shutdown timeout reached. {} connection(s) will be forcibly terminated.",
430 remaining_connections
431 );
432 false
433 } else {
434 println!("✅ All connections closed gracefully.");
435 true
436 }
437 }
438}
439
440pub struct ThreadPool {
442 workers: Vec<Worker>,
443 sender: Sender<Job>,
444}
445
446impl std::fmt::Debug for ThreadPool {
447 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448 f.debug_struct("ThreadPool")
449 .field("workers", &self.workers)
450 .field("sender", &"<Sender<Job>>")
451 .finish()
452 }
453}
454
455type Job = Box<dyn FnOnce() + Send + 'static>;
457
458#[derive(Debug)]
460struct Worker {
461 id: usize,
462 thread: Option<thread::JoinHandle<()>>,
463}
464
465impl ThreadPool {
466 pub fn new(size: usize) -> ThreadPool {
474 assert!(size > 0);
475
476 let (sender, receiver) = unbounded();
481
482 let mut workers = Vec::with_capacity(size);
483
484 for id in 0..size {
485 workers.push(Worker::new(id, receiver.clone()));
486 }
487
488 ThreadPool { workers, sender }
490 }
491
492 pub fn execute<F>(&self, f: F)
497 where
498 F: FnOnce() + Send + 'static,
499 {
500 let job = Box::new(f);
501 self.sender.send(job).unwrap();
502 }
503}
504
505impl Drop for ThreadPool {
506 fn drop(&mut self) {
507 let (replacement_sender, _replacement_receiver) = unbounded();
512 let old_sender =
513 std::mem::replace(&mut self.sender, replacement_sender);
514 drop(old_sender);
515
516 for worker in &mut self.workers {
517 println!("Shutting down worker {}", worker.id);
518
519 if let Some(thread) = worker.thread.take() {
520 thread.join().unwrap();
521 }
522 }
523 }
524}
525
526impl Worker {
527 fn new(id: usize, receiver: Receiver<Job>) -> Worker {
528 let thread = thread::spawn(move || {
529 while let Ok(job) = receiver.recv() {
530 job();
531 }
532 println!("Worker {id} disconnected; shutting down.");
533 });
534
535 Worker {
536 id,
537 thread: Some(thread),
538 }
539 }
540}
541
542#[derive(Debug)]
544pub struct ConnectionPool {
545 max_connections: usize,
546 active_connections: Arc<AtomicUsize>,
547}
548
549impl ConnectionPool {
550 pub fn new(max_connections: usize) -> Self {
552 Self {
554 max_connections,
555 active_connections: Arc::new(AtomicUsize::new(0)),
556 }
557 }
558
559 pub fn acquire(&self) -> Result<ConnectionGuard, io::Error> {
561 let mut current =
562 self.active_connections.load(Ordering::SeqCst);
563 let reserved = loop {
564 if current >= self.max_connections {
565 break Err(());
566 }
567 match self.active_connections.compare_exchange_weak(
568 current,
569 current + 1,
570 Ordering::SeqCst,
571 Ordering::SeqCst,
572 ) {
573 Ok(_) => break Ok(()),
574 Err(actual) => current = actual,
575 }
576 };
577 if reserved.is_err() {
578 return Err(io::Error::new(
579 io::ErrorKind::WouldBlock,
580 "Connection pool exhausted",
581 ));
582 }
583 Ok(ConnectionGuard {
584 pool: Arc::clone(&self.active_connections),
585 })
586 }
587
588 pub fn active_count(&self) -> usize {
590 self.active_connections.load(Ordering::SeqCst)
591 }
592}
593
594#[derive(Debug)]
596pub struct ConnectionGuard {
597 pool: Arc<AtomicUsize>,
598}
599
600impl Drop for ConnectionGuard {
601 fn drop(&mut self) {
602 let _ = self.pool.fetch_sub(1, Ordering::SeqCst);
603 }
604}
605
606impl Server {
607 #[doc(alias = "constructor")]
625 pub fn new(address: &str, document_root: &str) -> Self {
626 let document_root = PathBuf::from(document_root);
627 let canonical_document_root = fs::canonicalize(&document_root)
628 .unwrap_or_else(|_| document_root.clone());
629 Server {
630 address: address.to_string(),
631 document_root,
632 canonical_document_root,
633 cors_enabled: None,
634 cors_origins: None,
635 custom_headers: None,
636 request_timeout: None,
637 connection_timeout: None,
638 rate_limit_per_minute: None,
639 static_cache_ttl_secs: None,
640 max_buffered_body_bytes: None,
641 }
642 }
643
644 pub fn builder() -> ServerBuilder {
663 ServerBuilder::new()
664 }
665
666 #[doc(alias = "listen")]
688 #[doc(alias = "serve")]
689 pub fn start(&self) -> io::Result<()> {
690 let listener = TcpListener::bind(&self.address)?;
691 println!("❯ Server is now running at http://{}", self.address);
692 println!(" Document root: {}", self.document_root.display());
693 println!(" Press Ctrl+C to stop the server.");
694
695 Self::run_basic_accept_loop(listener.incoming(), self.clone());
696
697 Ok(())
698 }
699
700 #[doc(alias = "graceful shutdown")]
723 pub fn start_with_graceful_shutdown(
724 &self,
725 shutdown_timeout: Duration,
726 ) -> io::Result<()> {
727 let shutdown = Arc::new(ShutdownSignal::new(shutdown_timeout));
728 self.start_with_shutdown_signal(shutdown)
729 }
730
731 #[doc(alias = "shutdown signal")]
753 pub fn start_with_shutdown_signal(
754 &self,
755 shutdown: Arc<ShutdownSignal>,
756 ) -> io::Result<()> {
757 self.start_with_shutdown_signal_and_ready(shutdown, |_| {})
758 }
759
760 pub fn start_with_shutdown_signal_and_ready<F>(
774 &self,
775 shutdown: Arc<ShutdownSignal>,
776 on_ready: F,
777 ) -> io::Result<()>
778 where
779 F: FnOnce(String),
780 {
781 Self::install_signal_handlers(shutdown.clone());
783
784 let listener = TcpListener::bind(&self.address)?;
785 let bound_address = listener.local_addr()?.to_string();
786 on_ready(bound_address.clone());
787 println!("❯ Server is now running at http://{}", bound_address);
788 println!(" Document root: {}", self.document_root.display());
789 println!(" Press Ctrl+C to stop the server gracefully.");
790
791 listener.set_nonblocking(true)?;
793
794 const MIN_IDLE_SLEEP: Duration = Duration::from_micros(100);
800 const MAX_IDLE_SLEEP: Duration = Duration::from_millis(5);
801 let mut idle_sleep = MIN_IDLE_SLEEP;
802
803 loop {
804 if shutdown.is_shutdown_requested() {
806 println!(
807 "🛑 Shutdown requested. Stopping new connections..."
808 );
809 break;
810 }
811
812 match listener.accept() {
813 Ok((stream, _addr)) => {
814 idle_sleep = MIN_IDLE_SLEEP;
815 Self::run_tracked_accept(
816 stream,
817 self.clone(),
818 shutdown.clone(),
819 );
820 }
821 Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
822 thread::sleep(idle_sleep);
823 idle_sleep = (idle_sleep * 2).min(MAX_IDLE_SLEEP);
824 }
825 Err(e) => Self::log_listener_error(e),
826 }
827 }
828
829 let graceful = shutdown.wait_for_shutdown();
831
832 if graceful {
833 println!("✅ Server shut down gracefully.");
834 } else {
835 println!(
836 "⚠️ Server shut down with active connections remaining."
837 );
838 }
839
840 Ok(())
841 }
842
843 fn install_signal_handlers(shutdown: Arc<ShutdownSignal>) {
849 let slot =
850 SHUTDOWN_SIGNAL_SLOT.get_or_init(|| Mutex::new(None));
851
852 if let Ok(mut guard) = slot.lock() {
854 *guard = Some(shutdown);
855 }
856
857 SIGNAL_HANDLER_INSTALL.call_once(|| {
859 let _ = ctrlc::set_handler(Self::handle_shutdown_signal);
860 });
861 }
862
863 fn handle_shutdown_signal() {
864 if let Some(slot) = SHUTDOWN_SIGNAL_SLOT.get() {
865 Self::trigger_shutdown_from_slot(slot);
866 }
867 }
868
869 fn trigger_shutdown_from_slot(
870 slot: &Mutex<Option<Arc<ShutdownSignal>>>,
871 ) {
872 if let Ok(guard) = slot.lock()
873 && let Some(shutdown_signal) = guard.as_ref()
874 {
875 shutdown_signal.shutdown();
876 }
877 }
878
879 pub fn start_with_thread_pool(
892 &self,
893 thread_pool_size: usize,
894 ) -> io::Result<()> {
895 let thread_pool = ThreadPool::new(thread_pool_size);
896 let listener = TcpListener::bind(&self.address)?;
897
898 println!("❯ Server is now running at http://{}", self.address);
899 println!(" Document root: {}", self.document_root.display());
900 println!(" Thread pool size: {} workers", thread_pool_size);
901 println!(" Press Ctrl+C to stop the server.");
902
903 Self::run_thread_pool_accept_loop(
904 listener.incoming(),
905 self.clone(),
906 &thread_pool,
907 );
908
909 Ok(())
910 }
911
912 pub fn start_with_pooling(
928 &self,
929 thread_pool_size: usize,
930 max_connections: usize,
931 ) -> io::Result<()> {
932 let thread_pool = ThreadPool::new(thread_pool_size);
933 let connection_pool =
934 Arc::new(ConnectionPool::new(max_connections));
935 let listener = TcpListener::bind(&self.address)?;
936
937 println!("❯ Server is now running at http://{}", self.address);
938 println!(" Document root: {}", self.document_root.display());
939 println!(" Thread pool size: {} workers", thread_pool_size);
940 println!(" Max concurrent connections: {}", max_connections);
941 println!(" Press Ctrl+C to stop the server.");
942
943 Self::run_pooling_accept_loop(
944 listener.incoming(),
945 self.clone(),
946 &thread_pool,
947 connection_pool,
948 );
949
950 Ok(())
951 }
952
953 fn log_connection_result(result: Result<(), ServerError>) {
954 if let Err(error) = result {
955 eprintln!("Error handling connection: {}", error);
956 }
957 }
958
959 fn log_listener_error(error: io::Error) {
960 eprintln!("Connection error: {}", error);
961 }
962
963 fn run_tracked_accept(
964 stream: TcpStream,
965 server: Server,
966 shutdown: Arc<ShutdownSignal>,
967 ) {
968 shutdown.connection_started();
969 let _ = thread::spawn(move || {
970 let result =
971 handle_connection_tracked(stream, &server, &shutdown);
972 shutdown.connection_finished();
973 Self::log_connection_result(result);
974 });
975 }
976
977 fn run_basic_accept_loop<I>(incoming: I, server: Server)
978 where
979 I: IntoIterator<Item = io::Result<TcpStream>>,
980 {
981 for stream in incoming {
982 match stream {
983 Ok(stream) => {
984 let server = server.clone();
985 let _ = thread::spawn(move || {
986 Self::log_connection_result(handle_connection(
987 stream, &server,
988 ));
989 });
990 }
991 Err(error) => Self::log_listener_error(error),
992 }
993 }
994 }
995
996 fn run_thread_pool_accept_loop<I>(
997 incoming: I,
998 server: Server,
999 thread_pool: &ThreadPool,
1000 ) where
1001 I: IntoIterator<Item = io::Result<TcpStream>>,
1002 {
1003 for stream in incoming {
1004 match stream {
1005 Ok(stream) => {
1006 let server = server.clone();
1007 thread_pool.execute(move || {
1008 Self::log_connection_result(handle_connection(
1009 stream, &server,
1010 ));
1011 });
1012 }
1013 Err(error) => Self::log_listener_error(error),
1014 }
1015 }
1016 }
1017
1018 fn run_pooling_accept_loop<I>(
1019 incoming: I,
1020 server: Server,
1021 thread_pool: &ThreadPool,
1022 connection_pool: Arc<ConnectionPool>,
1023 ) where
1024 I: IntoIterator<Item = io::Result<TcpStream>>,
1025 {
1026 for stream in incoming {
1027 match stream {
1028 Ok(stream) => {
1029 let server = server.clone();
1030 let pool_clone = Arc::clone(&connection_pool);
1031 thread_pool.execute(move || match pool_clone.acquire() {
1032 Ok(_guard) => Self::log_connection_result(
1033 handle_connection(stream, &server),
1034 ),
1035 Err(_) => {
1036 if let Err(error) =
1037 send_service_unavailable(stream)
1038 {
1039 eprintln!(
1040 "Error sending service unavailable: {}",
1041 error
1042 );
1043 }
1044 }
1045 });
1046 }
1047 Err(error) => Self::log_listener_error(error),
1048 }
1049 }
1050 }
1051
1052 pub fn cors_enabled(&self) -> Option<bool> {
1056 self.cors_enabled
1057 }
1058
1059 pub fn cors_origins(&self) -> &Option<Vec<String>> {
1061 &self.cors_origins
1062 }
1063
1064 pub fn custom_headers(&self) -> &Option<HashMap<String, String>> {
1066 &self.custom_headers
1067 }
1068
1069 pub fn request_timeout(&self) -> Option<Duration> {
1071 self.request_timeout
1072 }
1073
1074 pub fn connection_timeout(&self) -> Option<Duration> {
1076 self.connection_timeout
1077 }
1078
1079 pub fn address(&self) -> &str {
1081 &self.address
1082 }
1083
1084 pub fn document_root(&self) -> &PathBuf {
1086 &self.document_root
1087 }
1088
1089 pub fn canonical_document_root(&self) -> &Path {
1096 if self.canonical_document_root.as_os_str().is_empty() {
1097 &self.document_root
1098 } else {
1099 &self.canonical_document_root
1100 }
1101 }
1102
1103 pub fn max_buffered_body_bytes(&self) -> u64 {
1108 self.max_buffered_body_bytes
1109 .unwrap_or(DEFAULT_MAX_BUFFERED_BODY_BYTES)
1110 }
1111}
1112
1113fn send_service_unavailable(mut stream: TcpStream) -> io::Result<()> {
1123 let mut response = Response::new(
1124 503,
1125 "SERVICE UNAVAILABLE",
1126 b"Service temporarily unavailable. Please try again later."
1127 .to_vec(),
1128 );
1129
1130 response.add_header("Content-Type", "text/plain");
1131 response.add_header("Retry-After", "1"); response.add_header("Connection", "close");
1133
1134 response.send(&mut stream).map_err(|e| {
1135 use std::io::Error;
1136 Error::other(format!("Failed to send response: {}", e))
1137 })?;
1138 Ok(())
1139}
1140
1141pub(crate) const MAX_KEEPALIVE_REQUESTS: usize = 100;
1146
1147pub(crate) const KEEPALIVE_IDLE_TIMEOUT: Duration =
1153 Duration::from_secs(5);
1154
1155pub const DEFAULT_MAX_BUFFERED_BODY_BYTES: u64 = 64 * 1024 * 1024;
1171
1172#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1181pub(crate) enum ConnectionPolicy {
1182 KeepAlive,
1183 Close,
1184}
1185
1186impl ConnectionPolicy {
1187 pub(crate) fn header_value(self) -> &'static str {
1188 match self {
1189 ConnectionPolicy::KeepAlive => "keep-alive",
1190 ConnectionPolicy::Close => "close",
1191 }
1192 }
1193
1194 pub(crate) fn from_request(request: &Request) -> Self {
1195 let connection_header = request
1196 .header("connection")
1197 .map(|h| h.trim().to_ascii_lowercase());
1198 match request.version() {
1199 "HTTP/1.1" => match connection_header.as_deref() {
1200 Some("close") => ConnectionPolicy::Close,
1201 _ => ConnectionPolicy::KeepAlive,
1202 },
1203 _ => match connection_header.as_deref() {
1204 Some("keep-alive") => ConnectionPolicy::KeepAlive,
1205 _ => ConnectionPolicy::Close,
1206 },
1207 }
1208 }
1209}
1210
1211pub(crate) fn handle_connection(
1227 mut stream: TcpStream,
1228 server: &Server,
1229) -> Result<(), ServerError> {
1230 let _ = stream.set_nodelay(true);
1233 let timeout =
1234 server.request_timeout.unwrap_or(Duration::from_secs(30));
1235 stream.set_read_timeout(Some(timeout))?;
1236 stream.set_write_timeout(Some(timeout))?;
1237
1238 let peer_ip = stream.peer_addr().ok().map(|addr| addr.ip());
1239
1240 for i in 0..MAX_KEEPALIVE_REQUESTS {
1241 if i > 0 {
1242 stream.set_read_timeout(Some(KEEPALIVE_IDLE_TIMEOUT))?;
1247 }
1248 let (mut response, policy) =
1249 build_response_for_stream(server, &stream, peer_ip);
1250 response.set_connection_header(policy.header_value());
1254
1255 if response.send(&mut stream).is_err() {
1256 return Ok(());
1258 }
1259 if policy == ConnectionPolicy::Close {
1260 return Ok(());
1261 }
1262 }
1263 Ok(())
1264}
1265
1266fn handle_connection_tracked(
1281 mut stream: TcpStream,
1282 server: &Server,
1283 _shutdown: &ShutdownSignal,
1284) -> Result<(), ServerError> {
1285 stream.set_nonblocking(false)?;
1287 let _ = stream.set_nodelay(true);
1289
1290 let timeout =
1292 server.connection_timeout.unwrap_or(Duration::from_secs(30));
1293 stream.set_read_timeout(Some(timeout))?;
1294 stream.set_write_timeout(Some(timeout))?;
1295
1296 let peer_ip = stream.peer_addr().ok().map(|addr| addr.ip());
1297
1298 for i in 0..MAX_KEEPALIVE_REQUESTS {
1299 if i > 0 {
1300 stream.set_read_timeout(Some(KEEPALIVE_IDLE_TIMEOUT))?;
1301 }
1302 let (mut response, policy) =
1303 build_response_for_stream(server, &stream, peer_ip);
1304 response.set_connection_header(policy.header_value());
1305 if response.send(&mut stream).is_err() {
1306 return Ok(());
1307 }
1308 if policy == ConnectionPolicy::Close {
1309 return Ok(());
1310 }
1311 }
1312 Ok(())
1313}
1314
1315fn build_response_for_stream(
1316 server: &Server,
1317 stream: &TcpStream,
1318 peer_ip: Option<IpAddr>,
1319) -> (Response, ConnectionPolicy) {
1320 match Request::from_stream(stream) {
1321 Ok(request) => {
1322 let policy = ConnectionPolicy::from_request(&request);
1327 if request.path() == "/metrics" && request.method() == "GET"
1328 {
1329 return (generate_metrics_response(), policy);
1330 }
1331 if let Some(ip) = peer_ip
1332 && is_rate_limited(server, ip)
1333 {
1334 let _ =
1335 METRIC_RATE_LIMITED.fetch_add(1, Ordering::Relaxed);
1336 return (generate_too_many_requests_response(), policy);
1337 }
1338 (
1339 build_response_for_request_with_metrics(
1340 server, &request,
1341 ),
1342 policy,
1343 )
1344 }
1345 Err(error) => (
1346 response_from_error(&error, &server.document_root),
1347 ConnectionPolicy::Close,
1348 ),
1349 }
1350}
1351
1352pub(crate) fn build_response_for_request_with_metrics(
1357 server: &Server,
1358 request: &Request,
1359) -> Response {
1360 let response = build_response_for_request(server, request);
1361 record_metrics(&response);
1362 response
1363}
1364
1365pub(crate) fn build_response_for_request(
1367 server: &Server,
1368 request: &Request,
1369) -> Response {
1370 let generated = match request.method() {
1371 "GET" => generate_response_with_cache(
1372 request,
1373 &server.document_root,
1374 &server.canonical_document_root,
1375 server.static_cache_ttl_secs,
1376 server.max_buffered_body_bytes(),
1377 ),
1378 "HEAD" => {
1379 generate_head_response(request, &server.document_root)
1380 }
1381 "OPTIONS" => generate_options_response(request),
1382 _ => Ok(generate_method_not_allowed_response()),
1383 };
1384 match generated {
1385 Ok(response) => {
1386 apply_response_policies(response, server, request)
1387 }
1388 Err(error) => {
1389 response_from_error(&error, &server.document_root)
1390 }
1391 }
1392}
1393
1394fn record_metrics(response: &Response) {
1395 let _ = METRIC_REQUESTS_TOTAL.fetch_add(1, Ordering::Relaxed);
1396 if (400..500).contains(&response.status_code) {
1397 let _ = METRIC_RESPONSES_4XX.fetch_add(1, Ordering::Relaxed);
1398 } else if response.status_code >= 500 {
1399 let _ = METRIC_RESPONSES_5XX.fetch_add(1, Ordering::Relaxed);
1400 }
1401}
1402
1403fn generate_metrics_response() -> Response {
1404 let body = format!(
1405 "http_handle_requests_total {}\nhttp_handle_responses_4xx_total {}\nhttp_handle_responses_5xx_total {}\nhttp_handle_rate_limited_total {}\n",
1406 METRIC_REQUESTS_TOTAL.load(Ordering::Relaxed),
1407 METRIC_RESPONSES_4XX.load(Ordering::Relaxed),
1408 METRIC_RESPONSES_5XX.load(Ordering::Relaxed),
1409 METRIC_RATE_LIMITED.load(Ordering::Relaxed),
1410 );
1411 let mut response = Response::new(200, "OK", body.into_bytes());
1412 response.add_header("Content-Type", "text/plain; version=0.0.3");
1413 response
1414}
1415
1416fn generate_too_many_requests_response() -> Response {
1417 let mut response = Response::new(
1418 429,
1419 "TOO MANY REQUESTS",
1420 b"Rate limit exceeded".to_vec(),
1421 );
1422 response.add_header("Content-Type", "text/plain");
1423 response.add_header("Retry-After", "60");
1424 response
1425}
1426
1427fn rate_limit_shard_index(ip: IpAddr) -> usize {
1428 use std::collections::hash_map::DefaultHasher;
1429 use std::hash::{Hash, Hasher};
1430 let mut h = DefaultHasher::new();
1431 ip.hash(&mut h);
1432 (h.finish() as usize) & (RATE_LIMIT_SHARDS - 1)
1433}
1434
1435fn rate_limit_table() -> &'static RateLimitTable {
1436 RATE_LIMIT_STATE.get_or_init(|| {
1437 std::array::from_fn(|_| Mutex::new(HashMap::new()))
1438 })
1439}
1440
1441fn is_rate_limited(server: &Server, ip: IpAddr) -> bool {
1442 let Some(limit) = server.rate_limit_per_minute else {
1443 return false;
1444 };
1445 let now = Instant::now();
1446 let shard = &rate_limit_table()[rate_limit_shard_index(ip)];
1450 let mut guard = match shard.lock() {
1451 Ok(guard) => guard,
1452 Err(poisoned) => poisoned.into_inner(),
1453 };
1454 let hits = guard.entry(ip).or_default();
1455 hits.retain(|timestamp| {
1456 now.duration_since(*timestamp) <= Duration::from_secs(60)
1457 });
1458 if hits.len() >= limit {
1459 return true;
1460 }
1461 hits.push(now);
1462 false
1463}
1464
1465fn generate_response(
1476 request: &Request,
1477 document_root: &Path,
1478) -> Result<Response, ServerError> {
1479 let canonical = fs::canonicalize(document_root)
1481 .unwrap_or_else(|_| document_root.to_path_buf());
1482 generate_response_with_cache(
1483 request,
1484 document_root,
1485 &canonical,
1486 None,
1487 DEFAULT_MAX_BUFFERED_BODY_BYTES,
1488 )
1489}
1490
1491fn generate_response_with_cache(
1492 request: &Request,
1493 document_root: &Path,
1494 canonical_root: &Path,
1495 cache_ttl_secs: Option<u64>,
1496 max_buffered_body_bytes: u64,
1497) -> Result<Response, ServerError> {
1498 let mut path = PathBuf::from(document_root);
1499 let request_path = request.path().trim_start_matches('/');
1500
1501 if request_path.is_empty() {
1502 path.push("index.html");
1504 } else {
1505 for component in request_path.split('/') {
1506 if component == ".." {
1507 let _ = path.pop();
1508 } else {
1509 path.push(component);
1510 }
1511 }
1512 }
1513
1514 let within_root = fs::canonicalize(&path)
1515 .map(|candidate| candidate.starts_with(canonical_root))
1516 .unwrap_or_else(|_| path.starts_with(document_root));
1517 if !within_root {
1518 return Err(ServerError::forbidden("Access denied"));
1519 }
1520
1521 if path.is_file() {
1522 serve_file_response(
1523 request,
1524 &path,
1525 cache_ttl_secs,
1526 max_buffered_body_bytes,
1527 )
1528 } else if path.is_dir() {
1529 path.push("index.html");
1531 if path.is_file() {
1532 serve_file_response(
1533 request,
1534 &path,
1535 cache_ttl_secs,
1536 max_buffered_body_bytes,
1537 )
1538 } else {
1539 generate_404_response(document_root)
1540 }
1541 } else {
1542 generate_404_response(document_root)
1543 }
1544}
1545
1546fn serve_file_response(
1547 request: &Request,
1548 path: &Path,
1549 cache_ttl_secs: Option<u64>,
1550 max_buffered_body_bytes: u64,
1551) -> Result<Response, ServerError> {
1552 let mut serving_path = path.to_path_buf();
1553 let mut content_encoding: Option<&'static str> = None;
1554 if let Some(encoding) = request.header("accept-encoding") {
1555 if encoding.contains("br") {
1556 let candidate =
1557 PathBuf::from(format!("{}.br", path.display()));
1558 if candidate.is_file() {
1559 serving_path = candidate;
1560 content_encoding = Some("br");
1561 }
1562 }
1563 if content_encoding.is_none()
1564 && (encoding.contains("zstd") || encoding.contains("zst"))
1565 {
1566 let candidate =
1567 PathBuf::from(format!("{}.zst", path.display()));
1568 if candidate.is_file() {
1569 serving_path = candidate;
1570 content_encoding = Some("zstd");
1571 }
1572 }
1573 if content_encoding.is_none() && encoding.contains("gzip") {
1574 let candidate =
1575 PathBuf::from(format!("{}.gz", path.display()));
1576 if candidate.is_file() {
1577 serving_path = candidate;
1578 content_encoding = Some("gzip");
1579 }
1580 }
1581 }
1582
1583 let serving_metadata =
1589 fs::metadata(&serving_path).map_err(ServerError::from)?;
1590 if serving_metadata.len() > max_buffered_body_bytes {
1591 return Err(ServerError::Custom(format!(
1592 "file exceeds in-memory serve cap ({} > {} bytes); \
1593 override via ServerBuilder::max_buffered_body_bytes or \
1594 wait for v0.1 streaming",
1595 serving_metadata.len(),
1596 max_buffered_body_bytes
1597 )));
1598 }
1599 let contents = fs::read(&serving_path)?;
1600 let metadata = fs::metadata(path)?;
1601 let etag = compute_etag(&metadata);
1602 if request
1603 .header("if-none-match")
1604 .is_some_and(|candidate| candidate == &*etag)
1605 {
1606 let mut response =
1607 Response::new(304, "NOT MODIFIED", Vec::new());
1608 response.add_header("ETag", &etag);
1609 return Ok(response);
1610 }
1611
1612 let content_type = get_content_type(path);
1613 let mut response = if let Some((start, end)) =
1614 parse_range_header(request.header("range"), contents.len())
1615 {
1616 let body = contents[start..=end].to_vec();
1617 let mut partial = Response::new(206, "PARTIAL CONTENT", body);
1618 partial.add_header(
1619 "Content-Range",
1620 &format!("bytes {}-{}/{}", start, end, contents.len()),
1621 );
1622 partial
1623 } else {
1624 Response::new(200, "OK", contents)
1625 };
1626
1627 response.add_header("Content-Type", content_type);
1628 response.add_header("ETag", &etag);
1629 response.add_header("Accept-Ranges", "bytes");
1630 if let Some(encoding) = content_encoding {
1631 response.add_header("Content-Encoding", encoding);
1632 response.add_header("Vary", "Accept-Encoding");
1633 }
1634 if let Some(ttl) = cache_ttl_secs {
1635 response.add_header(
1636 "Cache-Control",
1637 &format!("public, max-age={ttl}"),
1638 );
1639 }
1640 Ok(response)
1641}
1642
1643fn etag_cache() -> &'static EtagCache {
1644 ETAG_CACHE.get_or_init(|| RwLock::new(HashMap::new()))
1645}
1646
1647fn compute_etag(metadata: &fs::Metadata) -> Arc<str> {
1648 let modified = metadata
1649 .modified()
1650 .ok()
1651 .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
1652 .map_or(0_u64, |duration| duration.as_secs());
1653 let len = metadata.len();
1654 let key = (len, modified);
1655
1656 let cache = etag_cache();
1657 if let Ok(read) = cache.read()
1658 && let Some(etag) = read.get(&key)
1659 {
1660 return Arc::clone(etag);
1661 }
1662
1663 let etag: Arc<str> =
1667 Arc::from(format!("W/\"{:x}-{:x}\"", len, modified));
1668
1669 if let Ok(mut write) = cache.write() {
1670 if write.len() >= ETAG_CACHE_MAX {
1671 let drop_count = ETAG_CACHE_MAX / 4;
1677 let to_remove: Vec<_> =
1678 write.keys().take(drop_count).copied().collect();
1679 for k in to_remove {
1680 let _ = write.remove(&k);
1681 }
1682 }
1683 let _ = write.insert(key, Arc::clone(&etag));
1684 }
1685
1686 etag
1687}
1688
1689fn parse_range_header(
1690 header: Option<&str>,
1691 total_len: usize,
1692) -> Option<(usize, usize)> {
1693 let header = header?;
1694 let value = header.strip_prefix("bytes=")?;
1695 let (start_str, end_str) = value.split_once('-')?;
1696 if start_str.is_empty() && end_str.is_empty() {
1697 return None;
1698 }
1699 if start_str.is_empty() {
1700 let suffix_len = end_str.parse::<usize>().ok()?;
1701 if suffix_len == 0 || suffix_len > total_len {
1702 return None;
1703 }
1704 return Some((total_len - suffix_len, total_len - 1));
1705 }
1706 let start = start_str.parse::<usize>().ok()?;
1707 let end = if end_str.is_empty() {
1708 total_len.checked_sub(1)?
1709 } else {
1710 end_str.parse::<usize>().ok()?
1711 };
1712 if start > end || end >= total_len {
1713 return None;
1714 }
1715 Some((start, end))
1716}
1717
1718fn generate_404_response(
1728 document_root: &Path,
1729) -> Result<Response, ServerError> {
1730 let not_found_path = document_root.join("404/index.html");
1731 let contents = if not_found_path.is_file() {
1732 fs::read(not_found_path)?
1733 } else {
1734 b"404 Not Found".to_vec()
1735 };
1736 let mut response = Response::new(404, "NOT FOUND", contents);
1737 response.add_header("Content-Type", "text/html");
1738 Ok(response)
1739}
1740
1741fn generate_head_response(
1756 request: &Request,
1757 document_root: &Path,
1758) -> Result<Response, ServerError> {
1759 let full_response = generate_response(request, document_root)?;
1761
1762 let mut head_response = Response::new(
1764 full_response.status_code,
1765 &full_response.status_text,
1766 Vec::new(), );
1768
1769 for (name, value) in &full_response.headers {
1771 head_response.add_header(name, value);
1772 }
1773
1774 let content_length = full_response.body.len().to_string();
1776 head_response.add_header("Content-Length", &content_length);
1777
1778 Ok(head_response)
1779}
1780
1781fn generate_options_response(
1794 _request: &Request,
1795) -> Result<Response, ServerError> {
1796 let mut response = Response::new(200, "OK", Vec::new());
1797 response.add_header("Allow", "GET, HEAD, OPTIONS");
1798 response.add_header("Content-Length", "0");
1799 Ok(response)
1800}
1801
1802fn generate_method_not_allowed_response() -> Response {
1811 let mut response = Response::new(
1812 405,
1813 "METHOD NOT ALLOWED",
1814 b"Method Not Allowed".to_vec(),
1815 );
1816 response.add_header("Allow", "GET, HEAD, OPTIONS");
1817 response.add_header("Content-Type", "text/plain");
1818 response.add_header("Content-Length", "18");
1819 response
1820}
1821
1822fn response_from_error(
1823 error: &ServerError,
1824 document_root: &Path,
1825) -> Response {
1826 match error {
1827 ServerError::InvalidRequest(message) => {
1828 let mut response = Response::new(
1829 400,
1830 "BAD REQUEST",
1831 message.as_bytes().to_vec(),
1832 );
1833 response.add_header("Content-Type", "text/plain");
1834 response
1835 }
1836 ServerError::Forbidden(message) => {
1837 let mut response = Response::new(
1838 403,
1839 "FORBIDDEN",
1840 message.as_bytes().to_vec(),
1841 );
1842 response.add_header("Content-Type", "text/plain");
1843 response
1844 }
1845 ServerError::NotFound(_) => {
1846 generate_404_response(document_root).unwrap_or_else(|_| {
1847 let mut response = Response::new(
1848 404,
1849 "NOT FOUND",
1850 b"404 Not Found".to_vec(),
1851 );
1852 response.add_header("Content-Type", "text/plain");
1853 response
1854 })
1855 }
1856 ServerError::Io(_)
1857 | ServerError::Custom(_)
1858 | ServerError::TaskFailed(_) => {
1859 let mut response = Response::new(
1860 500,
1861 "INTERNAL SERVER ERROR",
1862 b"Internal Server Error".to_vec(),
1863 );
1864 response.add_header("Content-Type", "text/plain");
1865 response
1866 }
1867 }
1868}
1869
1870fn apply_response_policies(
1871 mut response: Response,
1872 server: &Server,
1873 request: &Request,
1874) -> Response {
1875 if let Some(headers) = server.custom_headers.as_ref() {
1876 for (name, value) in headers {
1877 response.add_header(name, value);
1878 }
1879 }
1880
1881 if server.cors_enabled.unwrap_or(false) {
1882 let allow_origin = server
1883 .cors_origins
1884 .as_ref()
1885 .and_then(|origins| origins.first())
1886 .map(String::as_str)
1887 .unwrap_or("*");
1888 response
1889 .add_header("Access-Control-Allow-Origin", allow_origin);
1890 response.add_header(
1891 "Access-Control-Allow-Methods",
1892 "GET, HEAD, OPTIONS",
1893 );
1894 response.add_header("Access-Control-Allow-Headers", "*");
1895
1896 if request.method().eq_ignore_ascii_case("OPTIONS") {
1897 response.add_header("Access-Control-Max-Age", "600");
1898 }
1899 }
1900
1901 if let Some(ttl) = server.static_cache_ttl_secs {
1902 let has_cache_control =
1903 response.headers.iter().any(|(name, _)| {
1904 name.eq_ignore_ascii_case("cache-control")
1905 });
1906 if !has_cache_control {
1907 if is_probably_immutable_asset_path(request.path()) {
1908 response.add_header(
1909 "Cache-Control",
1910 "public, max-age=31536000, immutable",
1911 );
1912 } else {
1913 response.add_header(
1914 "Cache-Control",
1915 &format!("public, max-age={ttl}"),
1916 );
1917 }
1918 }
1919 }
1920
1921 response
1922}
1923
1924fn is_probably_immutable_asset_path(path: &str) -> bool {
1925 let file = path.rsplit('/').next().unwrap_or(path);
1926 let Some((stem, _ext)) = file.rsplit_once('.') else {
1927 return false;
1928 };
1929 let Some(hash) = stem.rsplit('-').next() else {
1930 return false;
1931 };
1932 hash.len() >= 8 && hash.chars().all(|c| c.is_ascii_hexdigit())
1933}
1934
1935fn get_content_type(path: &Path) -> &'static str {
1945 match path.extension().and_then(std::ffi::OsStr::to_str) {
1946 Some("html") | Some("htm") => "text/html",
1948 Some("css") => "text/css",
1949 Some("js") | Some("mjs") => "application/javascript",
1950 Some("ts") => "application/typescript",
1951 Some("json") => "application/json",
1952 Some("xml") => "application/xml",
1953 Some("txt") => "text/plain",
1954 Some("md") | Some("markdown") => "text/markdown",
1955 Some("yaml") | Some("yml") => "application/x-yaml",
1956 Some("toml") => "application/toml",
1957
1958 Some("png") => "image/png",
1960 Some("jpg") | Some("jpeg") => "image/jpeg",
1961 Some("gif") => "image/gif",
1962 Some("svg") => "image/svg+xml",
1963 Some("ico") => "image/x-icon",
1964
1965 Some("webp") => "image/webp",
1967 Some("avif") => "image/avif",
1968 Some("heic") | Some("heif") => "image/heic",
1969 Some("jxl") => "image/jxl",
1970 Some("bmp") => "image/bmp",
1971 Some("tiff") | Some("tif") => "image/tiff",
1972
1973 Some("wasm") => "application/wasm",
1975
1976 Some("woff") => "font/woff",
1978 Some("woff2") => "font/woff2",
1979 Some("ttf") => "font/ttf",
1980 Some("otf") => "font/otf",
1981 Some("eot") => "application/vnd.ms-fontobject",
1982
1983 Some("mp3") => "audio/mpeg",
1985 Some("wav") => "audio/wav",
1986 Some("ogg") => "audio/ogg",
1987 Some("opus") => "audio/opus",
1988 Some("flac") => "audio/flac",
1989 Some("m4a") => "audio/mp4",
1990 Some("aac") => "audio/aac",
1991
1992 Some("mp4") => "video/mp4",
1994 Some("webm") => "video/webm",
1995 Some("av1") => "video/av1",
1996 Some("avi") => "video/x-msvideo",
1997 Some("mov") => "video/quicktime",
1998
1999 Some("pdf") => "application/pdf",
2001 Some("zip") => "application/zip",
2002 Some("tar") => "application/x-tar",
2003 Some("gz") => "application/gzip",
2004
2005 Some("map") => "application/json", Some("webmanifest") => "application/manifest+json",
2008
2009 _ => "application/octet-stream",
2011 }
2012}
2013
2014#[cfg(test)]
2015mod tests {
2016 use super::*;
2017 use std::fs::File;
2018 use std::io;
2019 use std::io::Read;
2020 use std::io::Write;
2021 use std::net::{TcpListener, TcpStream};
2022 use tempfile::TempDir;
2023
2024 fn setup_test_directory() -> TempDir {
2025 let temp_dir = TempDir::new().unwrap();
2026 let root_path = temp_dir.path();
2027
2028 let mut index_file =
2030 File::create(root_path.join("index.html")).unwrap();
2031 index_file
2032 .write_all(b"<html><body>Hello, World!</body></html>")
2033 .unwrap();
2034
2035 fs::create_dir(root_path.join("404")).unwrap();
2037 let mut not_found_file =
2038 File::create(root_path.join("404/index.html")).unwrap();
2039 not_found_file
2040 .write_all(b"<html><body>404 Not Found</body></html>")
2041 .unwrap();
2042
2043 fs::create_dir(root_path.join("subdir")).unwrap();
2045 let mut subdir_index_file =
2046 File::create(root_path.join("subdir/index.html")).unwrap();
2047 subdir_index_file
2048 .write_all(b"<html><body>Subdirectory Index</body></html>")
2049 .unwrap();
2050
2051 temp_dir
2052 }
2053
2054 fn roundtrip_handle_connection(
2055 server: &Server,
2056 request: &[u8],
2057 ) -> String {
2058 let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
2059 let addr = listener.local_addr().expect("addr");
2060 let server_clone = server.clone();
2061 let handle = thread::spawn(move || {
2062 let (stream, _) = listener.accept().expect("accept");
2063 handle_connection(stream, &server_clone).expect("handle");
2064 });
2065
2066 let mut client = TcpStream::connect(addr).expect("connect");
2067 let request_text = std::str::from_utf8(request).unwrap_or("");
2074 if request_text.to_ascii_lowercase().contains("connection:") {
2075 client.write_all(request).expect("write");
2076 } else {
2077 let with_close =
2080 if let Some(idx) = request_text.rfind("\r\n\r\n") {
2081 let (head, tail) = request_text.split_at(idx);
2082 format!("{head}\r\nConnection: close{tail}")
2083 } else {
2084 request_text.to_string()
2085 };
2086 client.write_all(with_close.as_bytes()).expect("write");
2087 }
2088 let mut response = String::new();
2089 let _ = client.read_to_string(&mut response).expect("read");
2090 handle.join().expect("join");
2091 response
2092 }
2093
2094 fn connected_pair() -> (TcpStream, TcpStream) {
2095 let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
2096 let addr = listener.local_addr().expect("addr");
2097 let client = TcpStream::connect(addr).expect("connect");
2098 let (server, _) = listener.accept().expect("accept");
2099 (server, client)
2100 }
2101
2102 #[test]
2103 fn test_server_creation() {
2104 let server = Server::new("127.0.0.1:8080", "/var/www");
2105 assert_eq!(server.address, "127.0.0.1:8080");
2106 assert_eq!(server.document_root, PathBuf::from("/var/www"));
2107 }
2108
2109 #[test]
2110 fn test_get_content_type() {
2111 assert_eq!(
2112 get_content_type(Path::new("test.html")),
2113 "text/html"
2114 );
2115 assert_eq!(
2116 get_content_type(Path::new("page.htm")),
2117 "text/html"
2118 );
2119 assert_eq!(
2120 get_content_type(Path::new("style.css")),
2121 "text/css"
2122 );
2123 assert_eq!(
2124 get_content_type(Path::new("script.js")),
2125 "application/javascript"
2126 );
2127 assert_eq!(
2128 get_content_type(Path::new("data.json")),
2129 "application/json"
2130 );
2131 assert_eq!(
2132 get_content_type(Path::new("image.png")),
2133 "image/png"
2134 );
2135 assert_eq!(
2136 get_content_type(Path::new("photo.jpg")),
2137 "image/jpeg"
2138 );
2139 assert_eq!(
2140 get_content_type(Path::new("animation.gif")),
2141 "image/gif"
2142 );
2143 assert_eq!(
2144 get_content_type(Path::new("icon.svg")),
2145 "image/svg+xml"
2146 );
2147 assert_eq!(
2148 get_content_type(Path::new("unknown.xyz")),
2149 "application/octet-stream"
2150 );
2151 }
2152
2153 #[test]
2154 fn test_modern_content_types() {
2155 assert_eq!(
2157 get_content_type(Path::new("image.webp")),
2158 "image/webp"
2159 );
2160 assert_eq!(
2161 get_content_type(Path::new("image.avif")),
2162 "image/avif"
2163 );
2164 assert_eq!(
2165 get_content_type(Path::new("image.heic")),
2166 "image/heic"
2167 );
2168 assert_eq!(
2169 get_content_type(Path::new("image.heif")),
2170 "image/heic"
2171 );
2172 assert_eq!(
2173 get_content_type(Path::new("image.jxl")),
2174 "image/jxl"
2175 );
2176
2177 assert_eq!(
2179 get_content_type(Path::new("module.wasm")),
2180 "application/wasm"
2181 );
2182
2183 assert_eq!(
2185 get_content_type(Path::new("script.ts")),
2186 "application/typescript"
2187 );
2188 assert_eq!(
2189 get_content_type(Path::new("module.mjs")),
2190 "application/javascript"
2191 );
2192 assert_eq!(
2193 get_content_type(Path::new("README.md")),
2194 "text/markdown"
2195 );
2196 assert_eq!(
2197 get_content_type(Path::new("config.yaml")),
2198 "application/x-yaml"
2199 );
2200 assert_eq!(
2201 get_content_type(Path::new("config.yml")),
2202 "application/x-yaml"
2203 );
2204 assert_eq!(
2205 get_content_type(Path::new("Cargo.toml")),
2206 "application/toml"
2207 );
2208
2209 assert_eq!(
2211 get_content_type(Path::new("audio.opus")),
2212 "audio/opus"
2213 );
2214 assert_eq!(
2215 get_content_type(Path::new("audio.flac")),
2216 "audio/flac"
2217 );
2218
2219 assert_eq!(
2221 get_content_type(Path::new("video.av1")),
2222 "video/av1"
2223 );
2224
2225 assert_eq!(
2227 get_content_type(Path::new("script.js.map")),
2228 "application/json"
2229 );
2230 assert_eq!(
2231 get_content_type(Path::new("manifest.webmanifest")),
2232 "application/manifest+json"
2233 );
2234 }
2235
2236 #[test]
2237 fn test_generate_response() {
2238 let temp_dir = setup_test_directory();
2239 let document_root = temp_dir.path();
2240
2241 let root_request = Request {
2243 method: "GET".to_string(),
2244 path: "/".to_string(),
2245 version: "HTTP/1.1".to_string(),
2246 headers: Vec::new(),
2247 };
2248
2249 let root_response =
2250 generate_response(&root_request, document_root).unwrap();
2251 assert_eq!(root_response.status_code, 200);
2252 assert_eq!(root_response.status_text, "OK");
2253 assert!(
2254 root_response.body.starts_with(
2255 b"<html><body>Hello, World!</body></html>"
2256 )
2257 );
2258
2259 let file_request = Request {
2261 method: "GET".to_string(),
2262 path: "/index.html".to_string(),
2263 version: "HTTP/1.1".to_string(),
2264 headers: Vec::new(),
2265 };
2266
2267 let file_response =
2268 generate_response(&file_request, document_root).unwrap();
2269 assert_eq!(file_response.status_code, 200);
2270 assert_eq!(file_response.status_text, "OK");
2271 assert!(
2272 file_response.body.starts_with(
2273 b"<html><body>Hello, World!</body></html>"
2274 )
2275 );
2276
2277 let subdir_request = Request {
2279 method: "GET".to_string(),
2280 path: "/subdir/".to_string(),
2281 version: "HTTP/1.1".to_string(),
2282 headers: Vec::new(),
2283 };
2284
2285 let subdir_response =
2286 generate_response(&subdir_request, document_root).unwrap();
2287 assert_eq!(subdir_response.status_code, 200);
2288 assert_eq!(subdir_response.status_text, "OK");
2289 assert!(subdir_response.body.starts_with(
2290 b"<html><body>Subdirectory Index</body></html>"
2291 ));
2292
2293 let not_found_request = Request {
2295 method: "GET".to_string(),
2296 path: "/nonexistent.html".to_string(),
2297 version: "HTTP/1.1".to_string(),
2298 headers: Vec::new(),
2299 };
2300
2301 let not_found_response =
2302 generate_response(¬_found_request, document_root)
2303 .unwrap();
2304 assert_eq!(not_found_response.status_code, 404);
2305 assert_eq!(not_found_response.status_text, "NOT FOUND");
2306 assert!(
2307 not_found_response.body.starts_with(
2308 b"<html><body>404 Not Found</body></html>"
2309 )
2310 );
2311
2312 let traversal_request = Request {
2314 method: "GET".to_string(),
2315 path: "/../outside.html".to_string(),
2316 version: "HTTP/1.1".to_string(),
2317 headers: Vec::new(),
2318 };
2319
2320 let traversal_response =
2321 generate_response(&traversal_request, document_root);
2322 assert!(matches!(
2323 traversal_response,
2324 Err(ServerError::Forbidden(_))
2325 ));
2326 }
2327
2328 #[test]
2329 fn test_server_builder() {
2330 let server = Server::builder()
2332 .address("127.0.0.1:8080")
2333 .document_root("/var/www")
2334 .enable_cors()
2335 .custom_header("X-Custom", "test-value")
2336 .request_timeout(Duration::from_secs(30))
2337 .build()
2338 .unwrap();
2339
2340 assert_eq!(server.address, "127.0.0.1:8080");
2341 assert_eq!(server.document_root, PathBuf::from("/var/www"));
2342 assert_eq!(server.cors_enabled, Some(true));
2343 assert_eq!(
2344 server.request_timeout,
2345 Some(Duration::from_secs(30))
2346 );
2347
2348 let headers = server.custom_headers.unwrap();
2350 assert_eq!(
2351 headers.get("X-Custom"),
2352 Some(&"test-value".to_string())
2353 );
2354
2355 let result = ServerBuilder::new().build();
2357 assert!(result.is_err());
2358
2359 let server_with_origins = Server::builder()
2361 .address("127.0.0.1:9000")
2362 .document_root("/tmp")
2363 .cors_origins(vec!["https://example.com".to_string()])
2364 .build()
2365 .unwrap();
2366
2367 assert_eq!(server_with_origins.cors_enabled, Some(true));
2368 assert_eq!(
2369 server_with_origins.cors_origins,
2370 Some(vec!["https://example.com".to_string()])
2371 );
2372 }
2373
2374 #[test]
2375 fn test_graceful_shutdown() {
2376 let shutdown = ShutdownSignal::new(Duration::from_secs(5));
2378
2379 assert!(!shutdown.is_shutdown_requested());
2381 assert_eq!(shutdown.active_connection_count(), 0);
2382
2383 shutdown.connection_started();
2385 assert_eq!(shutdown.active_connection_count(), 1);
2386
2387 shutdown.connection_started();
2388 assert_eq!(shutdown.active_connection_count(), 2);
2389
2390 shutdown.connection_finished();
2391 assert_eq!(shutdown.active_connection_count(), 1);
2392
2393 shutdown.connection_finished();
2394 assert_eq!(shutdown.active_connection_count(), 0);
2395
2396 shutdown.shutdown();
2398 assert!(shutdown.is_shutdown_requested());
2399
2400 let graceful = shutdown.wait_for_shutdown();
2402 assert!(graceful);
2403 }
2404
2405 #[test]
2406 fn test_shutdown_signal_timeout() {
2407 let shutdown = ShutdownSignal::new(Duration::from_millis(100));
2408
2409 shutdown.connection_started();
2411 shutdown.shutdown();
2412
2413 let graceful = shutdown.wait_for_shutdown();
2415 assert!(!graceful); }
2417
2418 #[test]
2419 fn test_thread_pool() {
2420 use std::sync::Arc;
2421 use std::sync::atomic::AtomicUsize;
2422 use std::sync::mpsc;
2423
2424 let pool = ThreadPool::new(4);
2425 let counter = Arc::new(AtomicUsize::new(0));
2426 let (tx, rx) = mpsc::channel();
2427
2428 for _ in 0..10 {
2430 let counter_clone = Arc::clone(&counter);
2431 let tx_clone = tx.clone();
2432
2433 pool.execute(move || {
2434 let _ = counter_clone.fetch_add(1, Ordering::SeqCst);
2435 tx_clone.send(()).unwrap();
2436 });
2437 }
2438
2439 for _ in 0..10 {
2441 rx.recv().unwrap();
2442 }
2443
2444 assert_eq!(counter.load(Ordering::SeqCst), 10);
2445 }
2446
2447 #[test]
2448 fn test_connection_pool() {
2449 let pool = ConnectionPool::new(2);
2450 assert_eq!(pool.active_count(), 0);
2451
2452 let guard1 = pool.acquire().unwrap();
2454 assert_eq!(pool.active_count(), 1);
2455
2456 let guard2 = pool.acquire().unwrap();
2458 assert_eq!(pool.active_count(), 2);
2459
2460 let result = pool.acquire();
2462 assert!(result.is_err());
2463 assert_eq!(pool.active_count(), 2);
2464
2465 drop(guard1);
2467 assert_eq!(pool.active_count(), 1);
2468
2469 let _guard3 = pool.acquire().unwrap();
2471 assert_eq!(pool.active_count(), 2);
2472
2473 drop(guard2);
2475 drop(_guard3);
2476 assert_eq!(pool.active_count(), 0);
2477 }
2478
2479 #[test]
2480 fn test_thread_pool_concurrent_execution() {
2481 use std::sync::Arc;
2482 use std::sync::atomic::AtomicUsize;
2483 use std::sync::mpsc;
2484 use std::time::Instant;
2485
2486 let pool = ThreadPool::new(4);
2487 let counter = Arc::new(AtomicUsize::new(0));
2488 let (tx, rx) = mpsc::channel();
2489
2490 let start_time = Instant::now();
2491
2492 for i in 0..100 {
2494 let counter_clone = Arc::clone(&counter);
2495 let tx_clone = tx.clone();
2496
2497 pool.execute(move || {
2498 thread::sleep(Duration::from_millis(10));
2500 let _ = counter_clone.fetch_add(1, Ordering::SeqCst);
2501 tx_clone.send(i).unwrap();
2502 });
2503 }
2504
2505 for _ in 0..100 {
2507 let _ = rx.recv().unwrap();
2508 }
2509
2510 let elapsed = start_time.elapsed();
2511 assert_eq!(counter.load(Ordering::SeqCst), 100);
2512
2513 assert!(
2515 elapsed.as_millis() < 800,
2516 "Thread pool should provide concurrency benefits"
2517 );
2518 }
2519
2520 #[test]
2521 fn test_connection_pool_backpressure() {
2522 let pool = ConnectionPool::new(2);
2523
2524 let _guard1 = pool.acquire().unwrap();
2526 let _guard2 = pool.acquire().unwrap();
2527 assert_eq!(pool.active_count(), 2);
2528
2529 let result = pool.acquire();
2531 assert!(result.is_err());
2532 assert_eq!(
2533 result.unwrap_err().kind(),
2534 io::ErrorKind::WouldBlock
2535 );
2536 }
2537
2538 #[test]
2539 fn test_service_unavailable_response() {
2540 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2542 let addr = listener.local_addr().unwrap();
2543
2544 let _ = thread::spawn(move || {
2545 let (stream, _) = listener.accept().unwrap();
2546 send_service_unavailable(stream).unwrap();
2547 });
2548
2549 let mut client_stream = TcpStream::connect(addr).unwrap();
2550 let mut response = String::new();
2551 let _ = client_stream.read_to_string(&mut response).unwrap();
2552
2553 assert!(response.contains("503"));
2554 assert!(response.contains("SERVICE UNAVAILABLE"));
2555 assert!(response.contains("Service temporarily unavailable"));
2556 assert!(response.contains("Retry-After: 1"));
2557 }
2558
2559 #[test]
2560 fn test_service_unavailable_send_failure_is_mapped() {
2561 use std::net::Shutdown;
2562
2563 let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
2564 let addr = listener.local_addr().expect("addr");
2565
2566 let t = thread::spawn(move || {
2567 let (stream, _) = listener.accept().expect("accept");
2568 stream.shutdown(Shutdown::Write).expect("shutdown");
2569 let err =
2570 send_service_unavailable(stream).expect_err("err");
2571 assert!(
2572 err.to_string().contains("Failed to send response")
2573 );
2574 });
2575
2576 let _client = TcpStream::connect(addr).expect("connect");
2577 t.join().expect("join");
2578 }
2579
2580 #[test]
2581 fn test_response_from_error_variants() {
2582 let temp_dir = setup_test_directory();
2583 let root = temp_dir.path();
2584
2585 let bad = response_from_error(
2586 &ServerError::InvalidRequest("bad".to_string()),
2587 root,
2588 );
2589 assert_eq!(bad.status_code, 400);
2590
2591 let forbidden = response_from_error(
2592 &ServerError::Forbidden("no".to_string()),
2593 root,
2594 );
2595 assert_eq!(forbidden.status_code, 403);
2596
2597 let not_found = response_from_error(
2598 &ServerError::NotFound("missing".to_string()),
2599 root,
2600 );
2601 assert_eq!(not_found.status_code, 404);
2602
2603 let internal = response_from_error(
2604 &ServerError::TaskFailed("boom".to_string()),
2605 root,
2606 );
2607 assert_eq!(internal.status_code, 500);
2608 }
2609
2610 #[test]
2611 fn test_apply_response_policies_with_cors_and_headers() {
2612 let mut headers = HashMap::new();
2613 let _ = headers
2614 .insert("X-App".to_string(), "http-handle".to_string());
2615 let server = Server::builder()
2616 .address("127.0.0.1:0")
2617 .document_root(".")
2618 .enable_cors()
2619 .cors_origins(vec!["https://example.com".to_string()])
2620 .custom_headers(headers)
2621 .build()
2622 .expect("server builder");
2623
2624 let request = Request {
2625 method: "OPTIONS".to_string(),
2626 path: "/".to_string(),
2627 version: "HTTP/1.1".to_string(),
2628 headers: Vec::new(),
2629 };
2630 let response = apply_response_policies(
2631 Response::new(200, "OK", Vec::new()),
2632 &server,
2633 &request,
2634 );
2635
2636 let has_origin = response.headers.iter().any(|(k, v)| {
2637 k.eq_ignore_ascii_case("Access-Control-Allow-Origin")
2638 && v == "https://example.com"
2639 });
2640 let has_custom = response
2641 .headers
2642 .iter()
2643 .any(|(k, v)| k == "X-App" && v == "http-handle");
2644 let has_max_age = response.headers.iter().any(|(k, _)| {
2645 k.eq_ignore_ascii_case("Access-Control-Max-Age")
2646 });
2647
2648 assert!(has_origin);
2649 assert!(has_custom);
2650 assert!(has_max_age);
2651 }
2652
2653 #[test]
2654 fn test_thread_pool_debug_representation() {
2655 let pool = ThreadPool::new(1);
2656 let rendered = format!("{pool:?}");
2657 assert!(rendered.contains("ThreadPool"));
2658 assert!(rendered.contains("<Sender<Job>>"));
2659 }
2660
2661 #[test]
2662 fn test_server_getters_expose_builder_config() {
2663 let mut headers = HashMap::new();
2664 let _ =
2665 headers.insert("X-Test".to_string(), "value".to_string());
2666
2667 let server = Server::builder()
2668 .address("127.0.0.1:9001")
2669 .document_root("/tmp")
2670 .enable_cors()
2671 .cors_origins(vec!["https://example.com".to_string()])
2672 .custom_headers(headers)
2673 .request_timeout(Duration::from_secs(5))
2674 .connection_timeout(Duration::from_secs(7))
2675 .build()
2676 .expect("server");
2677
2678 assert_eq!(server.cors_enabled(), Some(true));
2679 assert_eq!(
2680 server.cors_origins(),
2681 &Some(vec!["https://example.com".to_string()])
2682 );
2683 assert_eq!(
2684 server.request_timeout(),
2685 Some(Duration::from_secs(5))
2686 );
2687 assert_eq!(
2688 server.connection_timeout(),
2689 Some(Duration::from_secs(7))
2690 );
2691 assert_eq!(server.address(), "127.0.0.1:9001");
2692 assert_eq!(server.document_root(), &PathBuf::from("/tmp"));
2693 assert_eq!(
2694 server
2695 .custom_headers()
2696 .as_ref()
2697 .and_then(|h| h.get("X-Test")),
2698 Some(&"value".to_string())
2699 );
2700 }
2701
2702 #[test]
2703 fn test_start_variants_return_bind_errors_for_in_use_address() {
2704 let occupied = TcpListener::bind("127.0.0.1:0").expect("bind");
2705 let addr = occupied.local_addr().expect("addr").to_string();
2706 let server = Server::new(&addr, ".");
2707
2708 assert!(server.start().is_err());
2709 assert!(
2710 server
2711 .start_with_graceful_shutdown(Duration::from_millis(1))
2712 .is_err()
2713 );
2714 assert!(server.start_with_thread_pool(1).is_err());
2715 assert!(server.start_with_pooling(1, 1).is_err());
2716 }
2717
2718 #[test]
2719 fn test_start_with_shutdown_signal_and_ready_reports_bound_address()
2720 {
2721 let root = setup_test_directory();
2722 let server = Server::builder()
2723 .address("127.0.0.1:0")
2724 .document_root(root.path().to_str().expect("path"))
2725 .build()
2726 .expect("server");
2727
2728 let (ready_tx, ready_rx) = mpsc::channel::<String>();
2729 let shutdown =
2730 Arc::new(ShutdownSignal::new(Duration::from_secs(2)));
2731 let shutdown_for_server = shutdown.clone();
2732 let server_for_thread = server.clone();
2733
2734 let handle = thread::spawn(move || {
2735 server_for_thread
2736 .start_with_shutdown_signal_and_ready(
2737 shutdown_for_server,
2738 move |addr| {
2739 let _ = ready_tx.send(addr);
2740 },
2741 )
2742 .expect("server run");
2743 });
2744
2745 let bound_addr = ready_rx
2746 .recv_timeout(Duration::from_secs(2))
2747 .expect("bound address");
2748 assert!(bound_addr.starts_with("127.0.0.1:"));
2749 assert_ne!(bound_addr, "127.0.0.1:0");
2750
2751 let mut stream =
2752 TcpStream::connect(&bound_addr).expect("connect");
2753 stream
2754 .write_all(
2755 b"GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n",
2756 )
2757 .expect("write");
2758 let mut response = String::new();
2759 let _ = stream.read_to_string(&mut response);
2760 assert!(response.starts_with("HTTP/1.1 200 OK"));
2761
2762 shutdown.shutdown();
2763 handle.join().expect("join");
2764 }
2765
2766 #[test]
2767 fn test_generate_response_falls_back_to_builtin_404_without_page() {
2768 let temp_dir = TempDir::new().expect("tmp");
2769 fs::write(temp_dir.path().join("index.html"), b"index")
2770 .expect("write");
2771 fs::create_dir(temp_dir.path().join("empty-dir")).expect("dir");
2772
2773 let request = Request {
2774 method: "GET".to_string(),
2775 path: "/empty-dir/".to_string(),
2776 version: "HTTP/1.1".to_string(),
2777 headers: Vec::new(),
2778 };
2779
2780 let response = generate_response(&request, temp_dir.path())
2781 .expect("response");
2782 assert_eq!(response.status_code, 404);
2783 assert_eq!(response.body, b"404 Not Found".to_vec());
2784 }
2785
2786 #[cfg(unix)]
2787 #[test]
2788 fn test_response_from_error_not_found_fallback_when_custom_404_unreadable()
2789 {
2790 use std::os::unix::fs::PermissionsExt;
2791
2792 let temp_dir = TempDir::new().expect("tmp");
2793 let custom_404_dir = temp_dir.path().join("404");
2794 fs::create_dir(&custom_404_dir).expect("create 404 dir");
2795 let custom_404 = custom_404_dir.join("index.html");
2796 fs::write(&custom_404, b"custom").expect("write 404");
2797 fs::set_permissions(
2798 &custom_404,
2799 fs::Permissions::from_mode(0o000),
2800 )
2801 .expect("chmod");
2802
2803 let response = response_from_error(
2804 &ServerError::NotFound("missing".to_string()),
2805 temp_dir.path(),
2806 );
2807
2808 assert_eq!(response.status_code, 404);
2809 assert_eq!(response.status_text, "NOT FOUND");
2810 assert_eq!(response.body, b"404 Not Found".to_vec());
2811 }
2812
2813 #[test]
2814 fn test_handle_connection_options_and_parse_error_paths() {
2815 let root = setup_test_directory();
2816 let root_str = root.path().to_str().expect("root path");
2817 let server = Server::builder()
2818 .address("127.0.0.1:0")
2819 .document_root(root_str)
2820 .build()
2821 .expect("server");
2822
2823 let options_response = roundtrip_handle_connection(
2824 &server,
2825 b"OPTIONS / HTTP/1.1\r\nHost: localhost\r\n\r\n",
2826 );
2827 assert!(options_response.starts_with("HTTP/1.1 200 OK"));
2828 assert!(options_response.contains("Allow: GET, HEAD, OPTIONS"));
2829
2830 let head_response = roundtrip_handle_connection(
2831 &server,
2832 b"HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n",
2833 );
2834 assert!(head_response.starts_with("HTTP/1.1 200 OK"));
2835 assert!(head_response.contains("Content-Length:"));
2836
2837 let method_not_allowed = roundtrip_handle_connection(
2838 &server,
2839 b"POST / HTTP/1.1\r\nHost: localhost\r\n\r\n",
2840 );
2841 assert!(
2842 method_not_allowed
2843 .starts_with("HTTP/1.1 405 METHOD NOT ALLOWED")
2844 );
2845
2846 let traversal_response = roundtrip_handle_connection(
2847 &server,
2848 b"GET /../outside HTTP/1.1\r\nHost: localhost\r\n\r\n",
2849 );
2850 assert!(
2851 traversal_response.starts_with("HTTP/1.1 403 FORBIDDEN")
2852 );
2853
2854 let bad_response =
2855 roundtrip_handle_connection(&server, b"BAD\r\n\r\n");
2856 assert!(bad_response.starts_with("HTTP/1.1 400 BAD REQUEST"));
2857 }
2858
2859 #[test]
2860 fn test_generate_response_supports_etag_and_range() {
2861 let temp_dir = setup_test_directory();
2862 let root = temp_dir.path();
2863
2864 let headers =
2865 vec![("range".to_string(), "bytes=0-4".to_string())];
2866 let range_request = Request {
2867 method: "GET".to_string(),
2868 path: "/index.html".to_string(),
2869 version: "HTTP/1.1".to_string(),
2870 headers,
2871 };
2872 let range_response =
2873 generate_response(&range_request, root).expect("range");
2874 assert_eq!(range_response.status_code, 206);
2875 assert!(range_response.body.starts_with(b"<html"));
2876 let etag = range_response
2877 .headers
2878 .iter()
2879 .find(|(name, _)| name.eq_ignore_ascii_case("etag"))
2880 .map(|(_, value)| value.clone())
2881 .expect("etag");
2882
2883 let headers = vec![("if-none-match".to_string(), etag)];
2884 let conditional_request = Request {
2885 method: "GET".to_string(),
2886 path: "/index.html".to_string(),
2887 version: "HTTP/1.1".to_string(),
2888 headers,
2889 };
2890 let conditional_response =
2891 generate_response(&conditional_request, root)
2892 .expect("conditional");
2893 assert_eq!(conditional_response.status_code, 304);
2894 assert!(conditional_response.body.is_empty());
2895 }
2896
2897 #[test]
2898 fn test_metrics_and_rate_limiting() {
2899 let root = setup_test_directory();
2900 let server = Server::builder()
2901 .address("127.0.0.1:0")
2902 .document_root(root.path().to_str().expect("path"))
2903 .rate_limit_per_minute(1)
2904 .build()
2905 .expect("server");
2906
2907 let _ = roundtrip_handle_connection(
2908 &server,
2909 b"GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n",
2910 );
2911 let limited = roundtrip_handle_connection(
2912 &server,
2913 b"GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n",
2914 );
2915 assert!(limited.starts_with("HTTP/1.1 429 TOO MANY REQUESTS"));
2916
2917 let metrics = roundtrip_handle_connection(
2918 &server,
2919 b"GET /metrics HTTP/1.1\r\nHost: localhost\r\n\r\n",
2920 );
2921 assert!(metrics.starts_with("HTTP/1.1 200 OK"));
2922 assert!(metrics.contains("http_handle_requests_total"));
2923 }
2924
2925 #[test]
2926 fn test_trigger_shutdown_from_slot_helper() {
2927 let shutdown =
2928 Arc::new(ShutdownSignal::new(Duration::from_secs(1)));
2929 let slot = Mutex::new(Some(shutdown.clone()));
2930 assert!(!shutdown.is_shutdown_requested());
2931 Server::trigger_shutdown_from_slot(&slot);
2932 assert!(shutdown.is_shutdown_requested());
2933 }
2934
2935 #[test]
2936 fn test_handle_shutdown_signal_helper() {
2937 let shutdown =
2938 Arc::new(ShutdownSignal::new(Duration::from_secs(1)));
2939 let slot =
2940 SHUTDOWN_SIGNAL_SLOT.get_or_init(|| Mutex::new(None));
2941 if let Ok(mut guard) = slot.lock() {
2942 *guard = Some(shutdown.clone());
2943 }
2944 Server::handle_shutdown_signal();
2945 assert!(shutdown.is_shutdown_requested());
2946 }
2947
2948 #[test]
2949 fn test_accept_loop_helpers_cover_ok_and_err_paths() {
2950 let root = setup_test_directory();
2951 let server = Server::builder()
2952 .address("127.0.0.1:0")
2953 .document_root(root.path().to_str().expect("path"))
2954 .build()
2955 .expect("server");
2956
2957 Server::run_basic_accept_loop(
2958 vec![Err(io::Error::other("incoming failed"))],
2959 server.clone(),
2960 );
2961 let pool = ThreadPool::new(1);
2962 Server::run_thread_pool_accept_loop(
2963 vec![Err(io::Error::other("incoming failed"))],
2964 server.clone(),
2965 &pool,
2966 );
2967 Server::run_pooling_accept_loop(
2968 vec![Err(io::Error::other("incoming failed"))],
2969 server.clone(),
2970 &pool,
2971 Arc::new(ConnectionPool::new(1)),
2972 );
2973
2974 let (server_stream, mut client) = connected_pair();
2975 client.write_all(b"BAD\r\n\r\n").expect("write");
2976 Server::run_basic_accept_loop(
2977 vec![Ok(server_stream)],
2978 server.clone(),
2979 );
2980 let mut response = String::new();
2981 let _ = client.read_to_string(&mut response).expect("read");
2982 assert!(response.starts_with("HTTP/1.1 400 BAD REQUEST"));
2983
2984 let (server_stream, mut client) = connected_pair();
2985 client.write_all(b"BAD\r\n\r\n").expect("write");
2986 Server::run_thread_pool_accept_loop(
2987 vec![Ok(server_stream)],
2988 server.clone(),
2989 &pool,
2990 );
2991 let mut response = String::new();
2992 let _ = client.read_to_string(&mut response).expect("read");
2993 assert!(response.starts_with("HTTP/1.1 400 BAD REQUEST"));
2994
2995 let (server_stream, mut client) = connected_pair();
2996 client.write_all(b"BAD\r\n\r\n").expect("write");
2997 Server::run_pooling_accept_loop(
2998 vec![Ok(server_stream)],
2999 server.clone(),
3000 &pool,
3001 Arc::new(ConnectionPool::new(1)),
3002 );
3003 let mut response = String::new();
3004 let _ = client.read_to_string(&mut response).expect("read");
3005 assert!(response.starts_with("HTTP/1.1 400 BAD REQUEST"));
3006
3007 let (server_stream, mut client) = connected_pair();
3008 Server::run_pooling_accept_loop(
3009 vec![Ok(server_stream)],
3010 server,
3011 &pool,
3012 Arc::new(ConnectionPool::new(0)),
3013 );
3014 let mut response = String::new();
3015 let _ = client.read_to_string(&mut response).expect("read");
3016 assert!(
3017 response.starts_with("HTTP/1.1 503 SERVICE UNAVAILABLE")
3018 );
3019 }
3020
3021 #[test]
3022 fn test_immutable_cache_control_policy() {
3023 let root = setup_test_directory();
3024 let server = Server::builder()
3025 .address("127.0.0.1:0")
3026 .document_root(root.path().to_str().expect("path"))
3027 .static_cache_ttl_secs(60)
3028 .build()
3029 .expect("server");
3030
3031 let request = Request {
3032 method: "GET".to_string(),
3033 path: "/assets/app-abcdef12.js".to_string(),
3034 version: "HTTP/1.1".to_string(),
3035 headers: Vec::new(),
3036 };
3037 let response = apply_response_policies(
3038 Response::new(200, "OK", b"ok".to_vec()),
3039 &server,
3040 &request,
3041 );
3042 assert!(response.headers.iter().any(|(name, value)| {
3043 name.eq_ignore_ascii_case("cache-control")
3044 && value.contains("immutable")
3045 }));
3046 }
3047
3048 #[test]
3049 fn test_zstd_precompressed_asset_is_served() {
3050 let root = setup_test_directory();
3051 let file = root.path().join("index.html.zst");
3052 fs::write(&file, b"zstd-data").expect("write");
3053
3054 let headers = vec![(
3055 "accept-encoding".to_string(),
3056 "zstd,gzip".to_string(),
3057 )];
3058 let request = Request {
3059 method: "GET".to_string(),
3060 path: "/index.html".to_string(),
3061 version: "HTTP/1.1".to_string(),
3062 headers,
3063 };
3064
3065 let response = generate_response_with_cache(
3066 &request,
3067 root.path(),
3068 &fs::canonicalize(root.path()).expect("canonicalize"),
3069 None,
3070 DEFAULT_MAX_BUFFERED_BODY_BYTES,
3071 )
3072 .expect("response");
3073 assert!(response.headers.iter().any(|(name, value)| {
3074 name.eq_ignore_ascii_case("content-encoding")
3075 && value.eq_ignore_ascii_case("zstd")
3076 }));
3077 assert_eq!(response.body, b"zstd-data");
3078 }
3079
3080 #[test]
3081 fn test_brotli_precompressed_asset_is_served() {
3082 let root = setup_test_directory();
3083 fs::write(root.path().join("index.html.br"), b"brotli-encoded")
3084 .expect("write br");
3085
3086 let headers = vec![(
3087 "accept-encoding".to_string(),
3088 "br, gzip".to_string(),
3089 )];
3090 let request = Request {
3091 method: "GET".to_string(),
3092 path: "/index.html".to_string(),
3093 version: "HTTP/1.1".to_string(),
3094 headers,
3095 };
3096
3097 let response = generate_response_with_cache(
3098 &request,
3099 root.path(),
3100 &fs::canonicalize(root.path()).expect("canonicalize"),
3101 None,
3102 DEFAULT_MAX_BUFFERED_BODY_BYTES,
3103 )
3104 .expect("response");
3105 assert!(response.headers.iter().any(|(name, value)| {
3106 name.eq_ignore_ascii_case("content-encoding")
3107 && value.eq_ignore_ascii_case("br")
3108 }));
3109 assert_eq!(response.body, b"brotli-encoded");
3110 }
3111
3112 #[test]
3113 fn test_gzip_precompressed_asset_is_served() {
3114 let root = setup_test_directory();
3115 fs::write(root.path().join("index.html.gz"), b"gzdata")
3116 .expect("write gz");
3117
3118 let headers =
3119 vec![("accept-encoding".to_string(), "gzip".to_string())];
3120 let request = Request {
3121 method: "GET".to_string(),
3122 path: "/index.html".to_string(),
3123 version: "HTTP/1.1".to_string(),
3124 headers,
3125 };
3126
3127 let response = generate_response_with_cache(
3128 &request,
3129 root.path(),
3130 &fs::canonicalize(root.path()).expect("canonicalize"),
3131 None,
3132 DEFAULT_MAX_BUFFERED_BODY_BYTES,
3133 )
3134 .expect("response");
3135 assert!(response.headers.iter().any(|(name, value)| {
3136 name.eq_ignore_ascii_case("content-encoding")
3137 && value.eq_ignore_ascii_case("gzip")
3138 }));
3139 assert_eq!(response.body, b"gzdata");
3140 }
3141
3142 #[test]
3143 fn test_serve_file_response_applies_cache_ttl() {
3144 let root = setup_test_directory();
3145 let request = Request {
3146 method: "GET".to_string(),
3147 path: "/index.html".to_string(),
3148 version: "HTTP/1.1".to_string(),
3149 headers: Vec::new(),
3150 };
3151
3152 let response = generate_response_with_cache(
3153 &request,
3154 root.path(),
3155 &fs::canonicalize(root.path()).expect("canonicalize"),
3156 Some(600),
3157 DEFAULT_MAX_BUFFERED_BODY_BYTES,
3158 )
3159 .expect("response");
3160 assert!(response.headers.iter().any(|(name, value)| {
3161 name.eq_ignore_ascii_case("cache-control")
3162 && value.contains("max-age=600")
3163 }));
3164 }
3165
3166 #[test]
3167 fn test_parse_range_header_covers_all_branches() {
3168 assert!(parse_range_header(None, 100).is_none());
3170 assert!(parse_range_header(Some("items=0-1"), 100).is_none());
3171 assert!(
3172 parse_range_header(Some("bytes=no-dash"), 100).is_none()
3173 );
3174 assert!(parse_range_header(Some("bytes=-"), 100).is_none());
3176 assert_eq!(
3178 parse_range_header(Some("bytes=-10"), 100),
3179 Some((90, 99))
3180 );
3181 assert!(parse_range_header(Some("bytes=-0"), 100).is_none());
3183 assert!(parse_range_header(Some("bytes=-500"), 100).is_none());
3184 assert_eq!(
3186 parse_range_header(Some("bytes=10-"), 100),
3187 Some((10, 99))
3188 );
3189 assert!(parse_range_header(Some("bytes=0-"), 0).is_none());
3191 assert!(parse_range_header(Some("bytes=50-10"), 100).is_none());
3193 assert!(
3195 parse_range_header(Some("bytes=0-9999"), 100).is_none()
3196 );
3197 assert_eq!(
3199 parse_range_header(Some("bytes=0-9"), 100),
3200 Some((0, 9))
3201 );
3202 assert!(parse_range_header(Some("bytes=abc-9"), 100).is_none());
3204 assert!(parse_range_header(Some("bytes=0-abc"), 100).is_none());
3205 assert!(parse_range_header(Some("bytes=-abc"), 100).is_none());
3206 }
3207
3208 #[test]
3209 fn test_non_immutable_cache_control_policy_uses_ttl() {
3210 let root = setup_test_directory();
3211 let server = Server::builder()
3212 .address("127.0.0.1:0")
3213 .document_root(root.path().to_str().expect("path"))
3214 .static_cache_ttl_secs(90)
3215 .build()
3216 .expect("server");
3217
3218 let request = Request {
3219 method: "GET".to_string(),
3220 path: "/index.html".to_string(),
3221 version: "HTTP/1.1".to_string(),
3222 headers: Vec::new(),
3223 };
3224 let response = apply_response_policies(
3225 Response::new(200, "OK", b"ok".to_vec()),
3226 &server,
3227 &request,
3228 );
3229 assert!(response.headers.iter().any(|(name, value)| {
3230 name.eq_ignore_ascii_case("cache-control")
3231 && value == "public, max-age=90"
3232 }));
3233 }
3234
3235 #[test]
3236 fn test_cache_control_policy_respects_existing_header() {
3237 let root = setup_test_directory();
3238 let server = Server::builder()
3239 .address("127.0.0.1:0")
3240 .document_root(root.path().to_str().expect("path"))
3241 .static_cache_ttl_secs(90)
3242 .build()
3243 .expect("server");
3244
3245 let mut existing = Response::new(200, "OK", b"ok".to_vec());
3246 existing.add_header("Cache-Control", "no-store");
3247
3248 let request = Request {
3249 method: "GET".to_string(),
3250 path: "/anything.txt".to_string(),
3251 version: "HTTP/1.1".to_string(),
3252 headers: Vec::new(),
3253 };
3254 let response =
3255 apply_response_policies(existing, &server, &request);
3256 let header = response
3257 .headers
3258 .iter()
3259 .find(|(name, _)| {
3260 name.eq_ignore_ascii_case("cache-control")
3261 })
3262 .map(|(_, value)| value.clone())
3263 .expect("cache-control");
3264 assert_eq!(header, "no-store");
3265 }
3266
3267 #[test]
3268 fn test_is_probably_immutable_asset_path_edge_cases() {
3269 assert!(is_probably_immutable_asset_path(
3270 "/assets/app-abcdef12.js"
3271 ));
3272 assert!(!is_probably_immutable_asset_path("/noext"));
3274 assert!(!is_probably_immutable_asset_path(
3276 "/assets/app-zzzzzzzz.js"
3277 ));
3278 assert!(!is_probably_immutable_asset_path("/assets/app-ab.js"));
3280 }
3281
3282 #[test]
3283 fn test_record_metrics_tracks_5xx_responses() {
3284 let before = METRIC_RESPONSES_5XX.load(Ordering::Relaxed);
3285 let response =
3286 Response::new(503, "SERVICE UNAVAILABLE", b"down".to_vec());
3287 record_metrics(&response);
3288 let after = METRIC_RESPONSES_5XX.load(Ordering::Relaxed);
3289 assert!(after > before);
3290 }
3291
3292 #[test]
3293 fn test_rate_limit_recovers_from_poisoned_mutex() {
3294 let ip: IpAddr = "127.0.0.1".parse().expect("ip");
3297 let shard = &rate_limit_table()[rate_limit_shard_index(ip)];
3298 let _ = std::panic::catch_unwind(|| {
3299 let _guard = shard.lock().expect("lock");
3300 panic!("intentional to poison");
3301 });
3302 assert!(shard.is_poisoned());
3303
3304 let root = setup_test_directory();
3305 let server = Server::builder()
3306 .address("127.0.0.1:0")
3307 .document_root(root.path().to_str().expect("path"))
3308 .rate_limit_per_minute(10)
3309 .build()
3310 .expect("server");
3311 let _ = is_rate_limited(&server, ip);
3314
3315 shard.clear_poison();
3317 }
3318
3319 #[test]
3320 fn test_log_connection_result_handles_error() {
3321 Server::log_connection_result(Err(
3324 ServerError::invalid_request("boom"),
3325 ));
3326 }
3327
3328 #[test]
3329 fn test_start_with_shutdown_signal_reports_active_connections_on_timeout()
3330 {
3331 let root = setup_test_directory();
3332 let server = Server::builder()
3333 .address("127.0.0.1:0")
3334 .document_root(root.path().to_str().expect("path"))
3335 .build()
3336 .expect("server");
3337
3338 let shutdown =
3341 Arc::new(ShutdownSignal::new(Duration::from_millis(50)));
3342 let (ready_tx, ready_rx) = mpsc::channel::<String>();
3343 let shutdown_for_server = shutdown.clone();
3344 let server_clone = server.clone();
3345 let handle = thread::spawn(move || {
3346 server_clone
3347 .start_with_shutdown_signal_and_ready(
3348 shutdown_for_server,
3349 move |addr| {
3350 let _ = ready_tx.send(addr);
3351 },
3352 )
3353 .expect("server start");
3354 });
3355
3356 let addr = ready_rx
3357 .recv_timeout(Duration::from_secs(2))
3358 .expect("ready");
3359
3360 let _holder = TcpStream::connect(&addr).expect("connect");
3364 thread::sleep(Duration::from_millis(20));
3365 shutdown.shutdown();
3366
3367 handle.join().expect("join server thread");
3368 }
3369
3370 #[test]
3371 fn test_start_with_thread_pool_serves_one_connection() {
3372 let root = setup_test_directory();
3373 let probe = TcpListener::bind("127.0.0.1:0").expect("probe");
3374 let addr = probe.local_addr().expect("addr");
3375 drop(probe);
3376
3377 let server = Server::builder()
3378 .address(&addr.to_string())
3379 .document_root(root.path().to_str().expect("path"))
3380 .build()
3381 .expect("server");
3382
3383 let _handle = thread::spawn(move || {
3384 let _ = server.start_with_thread_pool(2);
3385 });
3386
3387 let mut stream = None;
3389 for _ in 0..50 {
3390 if let Ok(s) = TcpStream::connect(addr.to_string()) {
3391 stream = Some(s);
3392 break;
3393 }
3394 thread::sleep(Duration::from_millis(20));
3395 }
3396 let mut stream = stream.expect("server did not bind");
3397 stream
3398 .write_all(
3399 b"GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n",
3400 )
3401 .expect("write");
3402 let mut response = String::new();
3403 let _ = stream.read_to_string(&mut response).expect("read");
3404 assert!(response.starts_with("HTTP/1.1 200 OK"));
3405 }
3407
3408 #[test]
3409 fn test_start_with_pooling_serves_one_connection() {
3410 let root = setup_test_directory();
3411 let probe = TcpListener::bind("127.0.0.1:0").expect("probe");
3412 let addr = probe.local_addr().expect("addr");
3413 drop(probe);
3414
3415 let server = Server::builder()
3416 .address(&addr.to_string())
3417 .document_root(root.path().to_str().expect("path"))
3418 .build()
3419 .expect("server");
3420
3421 let _handle = thread::spawn(move || {
3422 let _ = server.start_with_pooling(2, 4);
3423 });
3424
3425 let mut stream = None;
3426 for _ in 0..50 {
3427 if let Ok(s) = TcpStream::connect(addr.to_string()) {
3428 stream = Some(s);
3429 break;
3430 }
3431 thread::sleep(Duration::from_millis(20));
3432 }
3433 let mut stream = stream.expect("server did not bind");
3434 stream
3435 .write_all(
3436 b"GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n",
3437 )
3438 .expect("write");
3439 let mut response = String::new();
3440 let _ = stream.read_to_string(&mut response).expect("read");
3441 assert!(response.starts_with("HTTP/1.1 200 OK"));
3442 }
3443
3444 #[test]
3453 fn test_server_builder_disable_cors_setter() {
3454 let server = Server::builder()
3455 .address("127.0.0.1:0")
3456 .document_root(".")
3457 .enable_cors()
3458 .disable_cors()
3459 .build()
3460 .expect("server");
3461 assert_eq!(server.cors_enabled(), Some(false));
3462 }
3463
3464 #[test]
3467 fn test_server_builder_max_buffered_body_bytes_override() {
3468 let server = Server::builder()
3469 .address("127.0.0.1:0")
3470 .document_root(".")
3471 .max_buffered_body_bytes(1_000_000)
3472 .build()
3473 .expect("server");
3474 assert_eq!(server.max_buffered_body_bytes(), 1_000_000);
3475 }
3476
3477 #[test]
3479 fn test_server_max_buffered_body_bytes_default_fallback() {
3480 let server = Server::builder()
3481 .address("127.0.0.1:0")
3482 .document_root(".")
3483 .build()
3484 .expect("server");
3485 assert_eq!(
3486 server.max_buffered_body_bytes(),
3487 DEFAULT_MAX_BUFFERED_BODY_BYTES
3488 );
3489 }
3490
3491 #[test]
3493 fn test_shutdown_signal_default_constructor() {
3494 let signal = ShutdownSignal::default();
3495 assert_eq!(signal.shutdown_timeout, Duration::from_secs(30));
3496 assert!(!signal.is_shutdown_requested());
3497 }
3498
3499 #[test]
3503 fn test_serve_file_response_rejects_oversize_body() {
3504 let root = setup_test_directory();
3505 let request = Request {
3506 method: "GET".to_string(),
3507 path: "/index.html".to_string(),
3508 version: "HTTP/1.1".to_string(),
3509 headers: Vec::new(),
3510 };
3511 let canonical =
3512 fs::canonicalize(root.path()).expect("canonicalize");
3513 let err = generate_response_with_cache(
3514 &request,
3515 root.path(),
3516 &canonical,
3517 None,
3518 1, )
3520 .expect_err("oversized file must be rejected");
3521 assert!(
3522 err.to_string().contains("exceeds in-memory serve cap"),
3523 "unexpected error: {err}"
3524 );
3525 }
3526
3527 #[test]
3531 fn test_etag_cache_evicts_when_full() {
3532 let cache = etag_cache();
3533 if let Ok(mut write) = cache.write() {
3535 for i in 0..(ETAG_CACHE_MAX + 1) as u64 {
3536 let _ = write.insert(
3537 (i, i),
3538 Arc::<str>::from(format!("W/\"{i:x}-{i:x}\"")),
3539 );
3540 }
3541 }
3542 let len_before =
3544 cache.read().map(|r| r.len()).unwrap_or_default();
3545 assert!(len_before >= ETAG_CACHE_MAX);
3546
3547 let temp = std::env::temp_dir();
3551 let metadata = fs::metadata(&temp).expect("metadata");
3552 let _ = compute_etag(&metadata);
3553
3554 let len_after =
3555 cache.read().map(|r| r.len()).unwrap_or_default();
3556 assert!(
3559 len_after <= ETAG_CACHE_MAX,
3560 "cache len {len_after} exceeds cap {ETAG_CACHE_MAX}"
3561 );
3562 }
3563
3564 #[test]
3571 fn connection_policy_handles_http_1_0_explicit_keepalive_and_default_close()
3572 {
3573 let keepalive = Request {
3574 method: "GET".into(),
3575 path: "/".into(),
3576 version: "HTTP/1.0".into(),
3577 headers: vec![("connection".into(), "keep-alive".into())],
3578 };
3579 assert_eq!(
3580 ConnectionPolicy::from_request(&keepalive),
3581 ConnectionPolicy::KeepAlive
3582 );
3583
3584 let bare = Request {
3585 method: "GET".into(),
3586 path: "/".into(),
3587 version: "HTTP/1.0".into(),
3588 headers: Vec::new(),
3589 };
3590 assert_eq!(
3591 ConnectionPolicy::from_request(&bare),
3592 ConnectionPolicy::Close
3593 );
3594 }
3595
3596 #[test]
3603 fn canonical_document_root_falls_back_when_cache_is_empty() {
3604 let mut server = Server {
3610 document_root: PathBuf::from("/tmp/some-root"),
3611 canonical_document_root: PathBuf::new(),
3612 ..Server::default()
3613 };
3614 assert_eq!(
3615 server.canonical_document_root(),
3616 Path::new("/tmp/some-root")
3617 );
3618
3619 server.canonical_document_root =
3622 PathBuf::from("/canonical/elsewhere");
3623 assert_eq!(
3624 server.canonical_document_root(),
3625 Path::new("/canonical/elsewhere")
3626 );
3627 }
3628}