1use crate::error::Error;
2use crate::file_service::{FileService, FileMetadata};
3use crate::handler::{RangeRequest, CompressionType, compress, parse_accept_encoding, should_skip_compression};
4use crate::mime_types::MimeDetector;
5use crate::path_security::PathValidator;
6use crate::utils::format_bytes;
7use crate::Config;
8use http_body_util::Full;
9use hyper::{Request, Response, header};
10use hyper::body::{Bytes, Incoming};
11use std::convert::Infallible;
12use std::path::PathBuf;
13use std::sync::Arc;
14use std::fs;
15use std::time::SystemTime;
16
17#[derive(Clone)]
19pub struct Handler {
20 config: Arc<Config>,
21 path_validator: PathValidator,
22}
23
24impl Handler {
25 pub fn new(config: Arc<Config>) -> Self {
26 let path_validator = PathValidator::new(config.root.clone());
27 Self {
28 config,
29 path_validator,
30 }
31 }
32
33 pub async fn handle_request(&self, req: Request<Incoming>) -> std::result::Result<Response<Full<Bytes>>, Infallible> {
34 let path = req.uri().path();
35
36 let clean_path = path.strip_prefix('/').unwrap_or(path);
38 let decoded_path = urlencoding::decode(clean_path).unwrap_or_else(|_| clean_path.to_string().into());
39
40 let full_path = self.config.root.join(decoded_path.as_ref());
41
42 if FileService::is_directory(&full_path) {
44 let index_path = full_path.join("index.html");
45 if index_path.exists() {
46 return self.serve_file(&index_path, req).await;
47 }
48 if self.config.enable_indexing {
50 return Ok(self.serve_directory_index(&full_path));
51 }
52 return Ok(self.serve_not_found());
53 }
54
55 match self.path_validator.validate(&full_path) {
57 Ok(_) => self.serve_file(&full_path, req).await,
58 Err(Error::NotFound(_)) => Ok(self.serve_not_found()),
59 Err(Error::Forbidden(_)) => Ok(self.serve_forbidden()),
60 Err(_) => Ok(self.serve_internal_error()),
61 }
62 }
63
64 async fn serve_file(&self, path: &PathBuf, req: Request<Incoming>) -> std::result::Result<Response<Full<Bytes>>, Infallible> {
65 let range_header = req.headers().get("Range")
67 .and_then(|v| v.to_str().ok());
68
69 let if_none_match = req.headers().get("If-None-Match")
71 .and_then(|v| v.to_str().ok());
72
73 let compression_type = if self.config.enable_compression {
75 parse_accept_encoding(req.headers())
76 } else {
77 CompressionType::None
78 };
79
80 match FileService::read_file(path) {
81 Ok(content) => {
82 let file_size = content.len() as u64;
83
84 let etag = self.generate_etag(path, file_size);
86
87 if let Some(if_none_match) = if_none_match {
89 if if_none_match == etag {
90 let last_modified = self.generate_last_modified(path);
92 return Ok(Response::builder()
93 .status(304)
94 .header("Content-Type", MimeDetector::detect(path).to_string())
95 .header("ETag", etag)
96 .header("Last-Modified", last_modified)
97 .body(Full::new(Bytes::new()))
98 .unwrap());
99 }
100 }
101
102 if let Some(range_header) = range_header {
104 match RangeRequest::parse(range_header, file_size) {
106 Ok(Some(range)) => {
107 let byte_range = range.to_range();
109 let range_content = content[byte_range.clone()].to_vec();
110 let range_size = range_content.len() as u64;
111
112 let content_range_str = format!("bytes {}-{}/{}",
113 range.start, range.end.unwrap_or(file_size - 1), file_size);
114 let last_modified = self.generate_last_modified(path);
115
116 return Ok(Response::builder()
117 .status(206)
118 .header("Content-Type", MimeDetector::detect(path).to_string())
119 .header("Content-Range", content_range_str)
120 .header("Content-Length", range_size.to_string())
121 .header("Accept-Ranges", "bytes")
122 .header("ETag", etag)
123 .header("Last-Modified", last_modified)
124 .header("Cache-Control", "public, max-age=86400")
125 .body(Full::new(Bytes::from(range_content)))
126 .unwrap());
127 }
128 Ok(None) => {
129 return Ok(self.serve_file_with_etag(path, content, etag, compression_type));
131 }
132 Err(_) => {
133 return Ok(self.serve_file_with_etag(path, content, etag, compression_type));
135 }
136 }
137 } else {
138 Ok(self.serve_file_with_etag(path, content, etag, compression_type))
140 }
141 }
142 Err(Error::NotFound(_)) => Ok(self.serve_not_found()),
143 Err(Error::Forbidden(_)) => Ok(self.serve_forbidden()),
144 Err(_) => Ok(self.serve_internal_error()),
145 }
146 }
147
148 fn generate_etag(&self, path: &PathBuf, file_size: u64) -> String {
150 let modified = fs::metadata(path)
151 .and_then(|m| m.modified())
152 .unwrap_or(SystemTime::UNIX_EPOCH);
153
154 let modified_secs = modified
155 .duration_since(SystemTime::UNIX_EPOCH)
156 .unwrap_or_default()
157 .as_secs();
158
159 format!(r#""{}-{}""#, modified_secs, file_size)
160 }
161
162 fn generate_last_modified(&self, path: &PathBuf) -> String {
164 let modified = fs::metadata(path)
165 .and_then(|m| m.modified())
166 .unwrap_or(SystemTime::UNIX_EPOCH);
167
168 let duration = modified
169 .duration_since(SystemTime::UNIX_EPOCH)
170 .unwrap_or_default();
171
172 let datetime = time::OffsetDateTime::from_unix_timestamp(duration.as_secs() as i64)
173 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
174
175 let format = time::format_description::parse("[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT")
177 .expect("Invalid format description");
178 datetime
179 .format(&format)
180 .unwrap_or_else(|_| "Thu, 01 Jan 1970 00:00:00 GMT".to_string())
181 }
182
183 fn serve_file_with_etag(&self, path: &PathBuf, content: Vec<u8>, etag: String, compression_type: CompressionType) -> Response<Full<Bytes>> {
184 let mime = MimeDetector::detect(path);
185 let content_type = mime.to_string();
186
187 let (final_content, content_encoding) = if compression_type != CompressionType::None
189 && !should_skip_compression(&content_type) {
190 match compress(&content, compression_type) {
191 Ok(compressed) => {
192 if compressed.len() < content.len() {
194 (compressed, Some(compression_type))
196 } else {
197 (content, None)
199 }
200 }
201 Err(_) => {
202 (content, None)
204 }
205 }
206 } else {
207 (content, None)
209 };
210
211 let last_modified = self.generate_last_modified(path);
212
213 let mut builder = Response::builder()
215 .status(200)
216 .header("Content-Type", content_type)
217 .header("Content-Length", final_content.len().to_string())
218 .header("Accept-Ranges", "bytes")
219 .header("ETag", etag)
220 .header("Last-Modified", last_modified)
221 .header("Cache-Control", "public, max-age=86400");
222
223 if let Some(encoding) = content_encoding {
225 let encoding_value = match encoding {
226 CompressionType::Gzip => "gzip",
227 CompressionType::Brotli => "br",
228 CompressionType::None => unreachable!(),
229 };
230 builder = builder.header(header::CONTENT_ENCODING, encoding_value);
231 builder = builder.header(header::VARY, "Accept-Encoding");
232 }
233
234 builder.body(Full::new(Bytes::from(final_content))).unwrap()
235 }
236
237 fn serve_not_found(&self) -> Response<Full<Bytes>> {
238 Response::builder()
239 .status(404)
240 .header("Content-Type", "text/plain")
241 .body(Full::new(Bytes::from("404 Not Found")))
242 .unwrap()
243 }
244
245 fn serve_forbidden(&self) -> Response<Full<Bytes>> {
246 Response::builder()
247 .status(403)
248 .header("Content-Type", "text/plain")
249 .body(Full::new(Bytes::from("403 Forbidden")))
250 .unwrap()
251 }
252
253 fn serve_internal_error(&self) -> Response<Full<Bytes>> {
254 Response::builder()
255 .status(500)
256 .header("Content-Type", "text/plain")
257 .body(Full::new(Bytes::from("500 Internal Server Error")))
258 .unwrap()
259 }
260
261 fn serve_directory_index(&self, path: &PathBuf) -> Response<Full<Bytes>> {
262 let files = match FileService::list_directory(path) {
263 Ok(files) => files,
264 Err(_) => return self.serve_internal_error(),
265 };
266
267 let html = self.generate_directory_html(path, &files);
268 Response::builder()
269 .status(200)
270 .header("Content-Type", "text/html; charset=utf-8")
271 .body(Full::new(Bytes::from(html)))
272 .unwrap()
273 }
274
275 fn generate_directory_html(&self, path: &PathBuf, files: &[FileMetadata]) -> String {
276 let path_str = path.display();
277 let mut html = format!(r#"<!DOCTYPE html>
278<html>
279<head>
280 <title>Index of {}</title>
281 <style>
282 body {{ font-family: Arial, sans-serif; margin: 40px; }}
283 h1 {{ margin-bottom: 20px; }}
284 table {{ border-collapse: collapse; width: 100%; }}
285 th, td {{ text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }}
286 th {{ background-color: #f2f2f2; }}
287 a {{ color: #0066cc; text-decoration: none; }}
288 a:hover {{ text-decoration: underline; }}
289 </style>
290</head>
291<body>
292 <h1>Index of {}</h1>
293 <table>
294 <thead>
295 <tr>
296 <th>Name</th>
297 <th>Type</th>
298 <th>Size</th>
299 </tr>
300 </thead>
301 <tbody>
302"#, path_str, path_str);
303
304 if path != &self.config.root {
306 html.push_str(&format!(
307 r#"<tr>
308 <td><a href="../">../</a></td>
309 <td>Directory</td>
310 <td>-</td>
311 </tr>"#
312 ));
313 }
314
315 for file in files {
316 let name = &file.name;
317 let file_type = if file.is_dir { "Directory" } else { "File" };
318 let size = if file.is_dir { "-" } else { &format_bytes(file.size) };
319
320 let link = if file.is_dir {
321 format!("{}/", name)
322 } else {
323 name.clone()
324 };
325
326 html.push_str(&format!(
327 r#"<tr>
328 <td><a href="{}">{}</a></td>
329 <td>{}</td>
330 <td>{}</td>
331 </tr>"#,
332 link, name, file_type, size
333 ));
334 }
335
336 html.push_str(r#"
337 </tbody>
338 </table>
339</body>
340</html>"#);
341
342 html
343 }
344}
345
346pub async fn handle_request(
348 req: Request<Incoming>,
349) -> std::result::Result<Response<Full<Bytes>>, Infallible> {
350 let config = Arc::new(Config::default());
351 let handler = Handler::new(config);
352 handler.handle_request(req).await
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn test_handler_creation() {
361 let config = Arc::new(Config::default());
362 let handler = Handler::new(config);
363 assert_eq!(handler.config.port, 8080);
365 }
366
367 #[test]
368 fn test_serve_not_found() {
369 let config = Arc::new(Config::default());
370 let handler = Handler::new(config);
371
372 let response = handler.serve_not_found();
373 assert_eq!(response.status(), 404);
374 }
375
376 #[test]
377 fn test_serve_forbidden() {
378 let config = Arc::new(Config::default());
379 let handler = Handler::new(config);
380
381 let response = handler.serve_forbidden();
382 assert_eq!(response.status(), 403);
383 }
384
385 #[test]
386 fn test_serve_internal_error() {
387 let config = Arc::new(Config::default());
388 let handler = Handler::new(config);
389
390 let response = handler.serve_internal_error();
391 assert_eq!(response.status(), 500);
392 }
393
394 #[test]
395 fn test_generate_etag() {
396 let config = Arc::new(Config::default());
397 let handler = Handler::new(config);
398
399 let temp_dir = tempfile::TempDir::new().unwrap();
400 let test_file = temp_dir.path().join("test.txt");
401 std::fs::write(&test_file, "test content").unwrap();
402
403 let file_size = std::fs::metadata(&test_file).unwrap().len();
404 let etag = handler.generate_etag(&test_file, file_size);
405
406 assert!(etag.starts_with('"'));
408 assert!(etag.ends_with('"'));
409 }
410
411 #[test]
412 fn test_handler_with_custom_config() {
413 let config = Arc::new(Config {
414 port: 3000,
415 root: "/tmp".into(),
416 enable_indexing: true,
417 enable_compression: false,
418 log_level: "info".into(),
419 enable_tls: false,
420 tls_cert: None,
421 tls_key: None,
422 connection_timeout_secs: 30,
423 max_connections: 1000,
424 enable_health_check: true,
425 enable_cors: true,
426 cors_allowed_origins: vec!["*".to_string()],
427 cors_allowed_methods: vec!["GET".to_string()],
428 cors_allowed_headers: vec![],
429 cors_allow_credentials: false,
430 cors_exposed_headers: vec![],
431 cors_max_age: Some(86400),
432 enable_security: true,
433 rate_limit_max_requests: 100,
434 rate_limit_window_secs: 60,
435 ip_allowlist: vec![],
436 ip_blocklist: vec![],
437 max_body_size: 10 * 1024 * 1024,
438 max_headers: 100,
439 management: None,
440 auto_tls: None,
441 });
442 let handler = Handler::new(config);
443
444 assert_eq!(handler.config.port, 3000);
445 assert_eq!(handler.config.root, PathBuf::from("/tmp"));
446 assert!(handler.config.enable_indexing);
447 assert!(!handler.config.enable_compression);
448 }
449
450 #[test]
451 fn test_serve_file_with_etag() {
452 let config = Arc::new(Config::default());
453 let handler = Handler::new(config);
454
455 let temp_dir = tempfile::TempDir::new().unwrap();
456 let test_file = temp_dir.path().join("test.txt");
457 std::fs::write(&test_file, "test content").unwrap();
458
459 let content = std::fs::read(&test_file).unwrap();
460 let etag = handler.generate_etag(&test_file, content.len() as u64);
461 let etag_clone = etag.clone();
462 let response = handler.serve_file_with_etag(&test_file, content, etag, CompressionType::None);
463
464 assert_eq!(response.status(), 200);
465 assert_eq!(response.headers().get("ETag").unwrap().to_str().unwrap(), etag_clone);
466 }
467
468 #[test]
469 fn test_generate_etag_nonexistent_file() {
470 let config = Arc::new(Config::default());
471 let handler = Handler::new(config);
472
473 let temp_dir = tempfile::TempDir::new().unwrap();
474 let nonexistent_file = temp_dir.path().join("nonexistent.txt");
475
476 let etag = handler.generate_etag(&nonexistent_file, 100);
478
479 assert!(etag.starts_with('"'));
480 assert!(etag.ends_with('"'));
481 }
482
483 #[test]
484 fn test_generate_last_modified() {
485 let config = Arc::new(Config::default());
486 let handler = Handler::new(config);
487
488 let temp_dir = tempfile::TempDir::new().unwrap();
489 let test_file = temp_dir.path().join("test.txt");
490 std::fs::write(&test_file, "content").unwrap();
491
492 let last_modified = handler.generate_last_modified(&test_file);
493
494 assert!(last_modified.contains("GMT"));
495 assert!(last_modified.len() > 0);
496 }
497
498 #[test]
499 fn test_generate_directory_html_empty() {
500 let config = Arc::new(Config::default());
501 let handler = Handler::new(config);
502
503 let temp_dir = tempfile::TempDir::new().unwrap();
504 let files: Vec<FileMetadata> = vec![];
505
506 let html = handler.generate_directory_html(&temp_dir.path().to_path_buf(), &files);
507
508 assert!(html.contains("Index of"));
509 assert!(html.contains("</html>"));
510 }
511
512 #[test]
513 fn test_generate_directory_html_with_files() {
514 let config = Arc::new(Config::default());
515 let handler = Handler::new(config);
516
517 let temp_dir = tempfile::TempDir::new().unwrap();
518 let files = vec![
519 FileMetadata {
520 path: temp_dir.path().join("file1.txt").display().to_string(),
521 name: "file1.txt".to_string(),
522 size: 100,
523 is_dir: false,
524 },
525 FileMetadata {
526 path: temp_dir.path().join("dir1").display().to_string(),
527 name: "dir1".to_string(),
528 size: 0,
529 is_dir: true,
530 },
531 ];
532
533 let html = handler.generate_directory_html(&temp_dir.path().to_path_buf(), &files);
534
535 assert!(html.contains("file1.txt"));
536 assert!(html.contains("dir1"));
537 assert!(html.contains("File"));
538 assert!(html.contains("Directory"));
539 assert!(html.contains("100.00 B"));
540 }
541
542 #[test]
543 fn test_handler_root_path() {
544 let config = Arc::new(Config::default());
545 let handler = Handler::new(config);
546
547 assert_eq!(handler.config.root, PathBuf::from("."));
548 }
549
550 #[test]
551 fn test_serve_file_with_etag_includes_headers() {
552 let config = Arc::new(Config::default());
553 let handler = Handler::new(config);
554
555 let temp_dir = tempfile::TempDir::new().unwrap();
556 let test_file = temp_dir.path().join("test.html");
557 std::fs::write(&test_file, "<html>test</html>").unwrap();
558
559 let content = std::fs::read(&test_file).unwrap();
560 let etag = handler.generate_etag(&test_file, content.len() as u64);
561 let response = handler.serve_file_with_etag(&test_file, content, etag, CompressionType::None);
562
563 assert!(response.headers().get("Content-Type").is_some());
564 assert!(response.headers().get("Content-Length").is_some());
565 assert!(response.headers().get("ETag").is_some());
566 assert!(response.headers().get("Accept-Ranges").is_some());
567 }
568
569 #[test]
570 fn test_serve_file_compression_disabled() {
571 let temp_dir = tempfile::TempDir::new().unwrap();
572 let config = Arc::new(Config {
573 root: temp_dir.path().to_path_buf(),
574 enable_compression: false,
575 ..Default::default()
576 });
577 let handler = Handler::new(config);
578
579 let content = "Hello, World! ".repeat(100);
581 let test_file = temp_dir.path().join("test.txt");
582 std::fs::write(&test_file, &content).unwrap();
583
584 let file_content = std::fs::read(&test_file).unwrap();
585 let etag = handler.generate_etag(&test_file, file_content.len() as u64);
586 let response = handler.serve_file_with_etag(&test_file, file_content, etag, CompressionType::None);
587
588 assert_eq!(response.status(), 200);
589 assert!(response.headers().get("Content-Encoding").is_none());
591 }
592
593 #[test]
594 fn test_handler_clone() {
595 let config = Arc::new(Config::default());
596 let handler = Handler::new(config);
597 let _cloned = handler.clone();
598 }
600
601 #[test]
602 fn test_compression_type_none() {
603 let config = Arc::new(Config {
604 enable_compression: false,
605 ..Default::default()
606 });
607 let handler = Handler::new(config);
608
609 let temp_dir = tempfile::TempDir::new().unwrap();
610 let test_file = temp_dir.path().join("test.txt");
611 std::fs::write(&test_file, "test content for compression").unwrap();
612
613 let content = std::fs::read(&test_file).unwrap();
614 let etag = handler.generate_etag(&test_file, content.len() as u64);
615 let response = handler.serve_file_with_etag(&test_file, content, etag, CompressionType::None);
616
617 assert_eq!(response.status(), 200);
618 assert!(response.headers().get("Content-Encoding").is_none());
620 }
621
622 #[test]
623 fn test_serve_file_with_gzip_compression() {
624 let config = Arc::new(Config {
625 enable_compression: true,
626 ..Default::default()
627 });
628 let handler = Handler::new(config);
629
630 let temp_dir = tempfile::TempDir::new().unwrap();
631 let test_file = temp_dir.path().join("test.txt");
632 let content = "Hello, World! ".repeat(100);
634 std::fs::write(&test_file, &content).unwrap();
635
636 let file_content = std::fs::read(&test_file).unwrap();
637 let etag = handler.generate_etag(&test_file, file_content.len() as u64);
638 let response = handler.serve_file_with_etag(&test_file, file_content, etag, CompressionType::Gzip);
639
640 assert_eq!(response.status(), 200);
641 }
642
643 #[test]
644 fn test_serve_file_with_brotli_compression() {
645 let config = Arc::new(Config {
646 enable_compression: true,
647 ..Default::default()
648 });
649 let handler = Handler::new(config);
650
651 let temp_dir = tempfile::TempDir::new().unwrap();
652 let test_file = temp_dir.path().join("test.txt");
653 let content = "Hello, World! ".repeat(100);
654 std::fs::write(&test_file, &content).unwrap();
655
656 let file_content = std::fs::read(&test_file).unwrap();
657 let etag = handler.generate_etag(&test_file, file_content.len() as u64);
658 let response = handler.serve_file_with_etag(&test_file, file_content, etag, CompressionType::Brotli);
659
660 assert_eq!(response.status(), 200);
661 }
662
663 #[test]
664 fn test_serve_file_skip_compression_binary() {
665 let config = Arc::new(Config {
666 enable_compression: true,
667 ..Default::default()
668 });
669 let handler = Handler::new(config);
670
671 let temp_dir = tempfile::TempDir::new().unwrap();
672 let test_file = temp_dir.path().join("test.png");
674 let content = vec![0u8; 1000]; std::fs::write(&test_file, &content).unwrap();
676
677 let file_content = std::fs::read(&test_file).unwrap();
678 let etag = handler.generate_etag(&test_file, file_content.len() as u64);
679 let response = handler.serve_file_with_etag(&test_file, file_content, etag, CompressionType::Gzip);
680
681 assert_eq!(response.status(), 200);
682 }
683
684 #[test]
685 fn test_generate_last_modified_nonexistent_file() {
686 let config = Arc::new(Config::default());
687 let handler = Handler::new(config);
688
689 let temp_dir = tempfile::TempDir::new().unwrap();
690 let nonexistent_file = temp_dir.path().join("nonexistent.txt");
691
692 let last_modified = handler.generate_last_modified(&nonexistent_file);
694 assert!(last_modified.contains("GMT"));
695 }
696
697 #[test]
698 fn test_compression_skipped_for_small_files() {
699 let config = Arc::new(Config {
700 enable_compression: true,
701 ..Default::default()
702 });
703 let handler = Handler::new(config);
704
705 let temp_dir = tempfile::TempDir::new().unwrap();
706 let test_file = temp_dir.path().join("test.txt");
707 let content = "Hi";
709 std::fs::write(&test_file, content).unwrap();
710
711 let file_content = std::fs::read(&test_file).unwrap();
712 let etag = handler.generate_etag(&test_file, file_content.len() as u64);
713 let response = handler.serve_file_with_etag(&test_file, file_content, etag, CompressionType::Gzip);
714
715 assert_eq!(response.status(), 200);
716 }
717
718 #[test]
719 fn test_serve_file_cache_control_headers() {
720 let config = Arc::new(Config::default());
721 let handler = Handler::new(config);
722
723 let temp_dir = tempfile::TempDir::new().unwrap();
724 let test_file = temp_dir.path().join("test.txt");
725 std::fs::write(&test_file, "test content").unwrap();
726
727 let content = std::fs::read(&test_file).unwrap();
728 let etag = handler.generate_etag(&test_file, content.len() as u64);
729 let response = handler.serve_file_with_etag(&test_file, content, etag, CompressionType::None);
730
731 let cache_control = response.headers().get("Cache-Control").unwrap().to_str().unwrap();
733 assert!(cache_control.contains("public"));
734 assert!(cache_control.contains("max-age"));
735 }
736
737 #[test]
738 fn test_serve_file_with_vary_header() {
739 let config = Arc::new(Config {
740 enable_compression: true,
741 ..Default::default()
742 });
743 let handler = Handler::new(config);
744
745 let temp_dir = tempfile::TempDir::new().unwrap();
746 let test_file = temp_dir.path().join("test.txt");
747 let content = "Hello, World! ".repeat(100);
749 std::fs::write(&test_file, &content).unwrap();
750
751 let file_content = std::fs::read(&test_file).unwrap();
752 let etag = handler.generate_etag(&test_file, file_content.len() as u64);
753 let response = handler.serve_file_with_etag(&test_file, file_content, etag, CompressionType::Gzip);
754
755 assert_eq!(response.status(), 200);
756 if let Some(vary) = response.headers().get("Vary") {
758 assert!(vary.to_str().unwrap().contains("Accept-Encoding"));
759 }
760 }
761
762 #[test]
763 fn test_generate_directory_html_with_parent_link() {
764 let temp_dir = tempfile::TempDir::new().unwrap();
765 let config = Arc::new(Config {
766 root: temp_dir.path().to_path_buf(),
767 ..Default::default()
768 });
769 let handler = Handler::new(config);
770
771 let subdir = temp_dir.path().join("subdir");
773 std::fs::create_dir(&subdir).unwrap();
774
775 let files = vec![
776 FileMetadata {
777 path: subdir.join("file1.txt").display().to_string(),
778 name: "file1.txt".to_string(),
779 size: 100,
780 is_dir: false,
781 },
782 ];
783
784 let html = handler.generate_directory_html(&subdir, &files);
786
787 assert!(html.contains("../"));
789 assert!(html.contains("Parent Directory") || html.contains("../"));
790 }
791
792 #[test]
793 fn test_serve_file_with_brotli_has_vary_header() {
794 let config = Arc::new(Config {
795 enable_compression: true,
796 ..Default::default()
797 });
798 let handler = Handler::new(config);
799
800 let temp_dir = tempfile::TempDir::new().unwrap();
801 let test_file = temp_dir.path().join("test.css");
802 let content = "body { color: red; } ".repeat(50);
803 std::fs::write(&test_file, &content).unwrap();
804
805 let file_content = std::fs::read(&test_file).unwrap();
806 let etag = handler.generate_etag(&test_file, file_content.len() as u64);
807 let response = handler.serve_file_with_etag(&test_file, file_content, etag, CompressionType::Brotli);
808
809 assert_eq!(response.status(), 200);
810 if let Some(encoding) = response.headers().get("Content-Encoding") {
812 assert_eq!(encoding.to_str().unwrap(), "br");
813 }
814 }
815
816 #[test]
817 fn test_etag_format() {
818 let config = Arc::new(Config::default());
819 let handler = Handler::new(config);
820
821 let temp_dir = tempfile::TempDir::new().unwrap();
822 let test_file = temp_dir.path().join("test.txt");
823 std::fs::write(&test_file, "content").unwrap();
824
825 let metadata = std::fs::metadata(&test_file).unwrap();
826 let file_size = metadata.len();
827 let etag = handler.generate_etag(&test_file, file_size);
828
829 assert!(etag.starts_with('"'));
831 assert!(etag.ends_with('"'));
832 assert!(etag.contains('-'));
833
834 let inner = &etag[1..etag.len()-1];
836 let parts: Vec<&str> = inner.split('-').collect();
837 assert_eq!(parts.len(), 2);
838
839 assert!(parts[0].parse::<u64>().is_ok());
841 assert!(parts[1].parse::<u64>().is_ok());
842 }
843
844 #[test]
845 fn test_generate_directory_html_special_chars() {
846 let config = Arc::new(Config::default());
847 let handler = Handler::new(config);
848
849 let temp_dir = tempfile::TempDir::new().unwrap();
850
851 let files = vec![
853 FileMetadata {
854 path: temp_dir.path().join("file with spaces & symbols.txt").display().to_string(),
855 name: "file with spaces & symbols.txt".to_string(),
856 size: 100,
857 is_dir: false,
858 },
859 ];
860
861 let html = handler.generate_directory_html(&temp_dir.path().to_path_buf(), &files);
862 assert!(html.contains("file with spaces & symbols.txt"));
863 }
864
865 #[test]
866 fn test_serve_file_with_uncompressible_content_type() {
867 let config = Arc::new(Config {
868 enable_compression: true,
869 ..Default::default()
870 });
871 let handler = Handler::new(config);
872
873 let temp_dir = tempfile::TempDir::new().unwrap();
874 let test_file = temp_dir.path().join("test.png");
876 let content = vec![0u8; 1000]; std::fs::write(&test_file, &content).unwrap();
878
879 let file_content = std::fs::read(&test_file).unwrap();
880 let etag = handler.generate_etag(&test_file, file_content.len() as u64);
881 let response = handler.serve_file_with_etag(&test_file, file_content, etag, CompressionType::Gzip);
882
883 assert_eq!(response.status(), 200);
884 assert!(response.headers().get("Content-Encoding").is_none());
886 }
887
888 #[test]
889 fn test_directory_listing_error_returns_500() {
890 let temp_dir = tempfile::TempDir::new().unwrap();
891 let config = Arc::new(Config {
892 root: temp_dir.path().to_path_buf(),
893 enable_indexing: true,
894 ..Default::default()
895 });
896 let handler = Handler::new(config);
897
898 let not_a_dir = temp_dir.path().join("not_a_dir");
900 std::fs::write(¬_a_dir, "I'm a file").unwrap();
901
902 let response = handler.serve_directory_index(¬_a_dir);
903 assert_eq!(response.status(), 500);
905 }
906
907 #[test]
908 fn test_last_modified_format() {
909 let config = Arc::new(Config::default());
910 let handler = Handler::new(config);
911
912 let temp_dir = tempfile::TempDir::new().unwrap();
913 let test_file = temp_dir.path().join("test.txt");
914 std::fs::write(&test_file, "content").unwrap();
915
916 let last_modified = handler.generate_last_modified(&test_file);
917
918 assert!(last_modified.contains("GMT"));
920 let parts: Vec<&str> = last_modified.split_whitespace().collect();
922 assert!(parts.len() >= 5);
923 }
924
925 #[test]
926 fn test_handler_config_values() {
927 let config = Arc::new(Config {
928 port: 9090,
929 root: "/custom/path".into(),
930 enable_indexing: true,
931 enable_compression: false,
932 ..Default::default()
933 });
934 let handler = Handler::new(config.clone());
935
936 assert_eq!(handler.config.port, 9090);
937 assert_eq!(handler.config.root, PathBuf::from("/custom/path"));
938 assert!(handler.config.enable_indexing);
939 assert!(!handler.config.enable_compression);
940 }
941}