use axum::extract::{Query, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use serde::Deserialize;
use super::super::server::AppState;
#[derive(Deserialize)]
pub struct FileQuery {
pub path: String,
}
pub async fn handler(
Query(params): Query<FileQuery>,
State(state): State<AppState>,
) -> impl IntoResponse {
let file_path = state.project_root.join(¶ms.path);
let canonical = match file_path.canonicalize() {
Ok(p) => p,
Err(_) => {
return (StatusCode::NOT_FOUND, "File not found".to_string()).into_response();
}
};
let project_root = match state.project_root.canonicalize() {
Ok(p) => p,
Err(_) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Project root not accessible".to_string(),
)
.into_response();
}
};
if !canonical.starts_with(&project_root) {
return (
StatusCode::BAD_REQUEST,
"Path outside project root".to_string(),
)
.into_response();
}
match tokio::fs::read_to_string(&canonical).await {
Ok(content) => (
StatusCode::OK,
[(
axum::http::header::CONTENT_TYPE,
"text/plain; charset=utf-8",
)],
content,
)
.into_response(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
(StatusCode::NOT_FOUND, "File not found".to_string()).into_response()
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to read file: {}", e),
)
.into_response(),
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_traversal_paths_detected() {
let malicious_paths = vec!["../etc/passwd", "../../secrets", "src/../../../root"];
for path in malicious_paths {
assert!(path.contains("..") || path.starts_with('/'));
}
}
#[test]
fn test_safe_paths_accepted() {
let safe_paths = vec!["src/main.rs", "Cargo.toml", "web/dist/index.html"];
for path in safe_paths {
assert!(!path.contains("..") && !path.starts_with('/'));
}
}
}