mockforge_http/
file_server.rs1use axum::http::{header, StatusCode};
7use axum::response::{IntoResponse, Response};
8use std::path::PathBuf;
9use tracing::{error, warn};
10
11pub async fn serve_mock_file(
13 axum::extract::Path(file_path): axum::extract::Path<String>,
14) -> Result<Response, StatusCode> {
15 if file_path.contains("..") || file_path.contains("//") {
17 warn!("Path traversal attempt detected: {}", file_path);
18 return Err(StatusCode::BAD_REQUEST);
19 }
20
21 let parts: Vec<&str> = file_path.split('/').filter(|s| !s.is_empty()).collect();
23 if parts.is_empty() {
24 warn!("Invalid file path format: {}", file_path);
25 return Err(StatusCode::BAD_REQUEST);
26 }
27
28 let base_dir =
30 std::env::var("MOCKFORGE_MOCK_FILES_DIR").unwrap_or_else(|_| "mock-files".to_string());
31
32 let full_file_path = PathBuf::from(&base_dir).join(&file_path);
34
35 if !full_file_path.exists() {
37 warn!("File not found: {:?}", full_file_path);
38 return Err(StatusCode::NOT_FOUND);
39 }
40
41 let content = match tokio::fs::read(&full_file_path).await {
43 Ok(content) => content,
44 Err(e) => {
45 error!("Failed to read file {:?}: {}", full_file_path, e);
46 return Err(StatusCode::INTERNAL_SERVER_ERROR);
47 }
48 };
49
50 let filename = full_file_path.file_name().and_then(|n| n.to_str()).unwrap_or("file");
52
53 let content_type = full_file_path
55 .extension()
56 .and_then(|ext| ext.to_str())
57 .map(|ext| match ext.to_lowercase().as_str() {
58 "pdf" => "application/pdf",
59 "csv" => "text/csv",
60 "json" => "application/json",
61 "xml" => "application/xml",
62 "txt" => "text/plain",
63 _ => "application/octet-stream",
64 })
65 .unwrap_or("application/octet-stream");
66
67 let headers = [
69 (header::CONTENT_TYPE, content_type),
70 (header::CONTENT_DISPOSITION, &format!("attachment; filename=\"{}\"", filename)),
71 ];
72
73 Ok((StatusCode::OK, headers, content).into_response())
74}
75
76pub fn file_serving_router() -> axum::Router {
78 use axum::routing::get;
79
80 axum::Router::new().route("/mock-files/{*path}", get(serve_mock_file))
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use axum::body::Body;
87 use axum::http::Request;
88 use tower::ServiceExt;
89
90 #[tokio::test]
91 async fn test_serve_mock_file_path_traversal() {
92 use axum::extract::Path;
94 let result = serve_mock_file(Path("../etc/passwd".to_string())).await;
95 assert!(result.is_err());
96 assert_eq!(result.unwrap_err(), StatusCode::BAD_REQUEST);
97 }
98
99 #[tokio::test]
100 async fn test_serve_mock_file_invalid_format() {
101 use axum::extract::Path;
103 let result = serve_mock_file(Path("".to_string())).await;
104 assert!(result.is_err());
105 assert_eq!(result.unwrap_err(), StatusCode::BAD_REQUEST);
106 }
107}