use std::path::{Component, Path, PathBuf}; use axum::http::{StatusCode, header}; use axum::response::{IntoResponse, Response}; use crate::smart::error::{SmartError, SmartResult}; pub async fn serve( bare: &Path, request_path: &str, ) -> SmartResult { let file = resolve_within_root(bare, request_path)?; match tokio::fs::read(&file).await { Ok(bytes) => Ok(( StatusCode::OK, [(header::CONTENT_TYPE, guess_content_type(request_path))], bytes, ) .into_response()), Err(e) if e.kind() == std::io::ErrorKind::NotFound => { Err(SmartError::NotFound(format!("file not found: {request_path}"))) } Err(e) => Err(SmartError::Io(e)), } } fn resolve_within_root(root: &Path, request_path: &str) -> SmartResult { let mut base = root.to_path_buf(); for component in Path::new(request_path).components() { match component { Component::Normal(part) => base.push(part), Component::CurDir => {} Component::ParentDir | Component::RootDir | Component::Prefix(_) => { return Err(SmartError::Forbidden("path traversal denied".into())); } } } let canonical = base .canonicalize() .map_err(|_| SmartError::NotFound("file not found".into()))?; let root_canonical = root .canonicalize() .map_err(|_| SmartError::NotFound("repository not found".into()))?; if canonical.starts_with(&root_canonical) { Ok(canonical) } else { Err(SmartError::Forbidden("path escapes repository root".into())) } } fn guess_content_type(path: &str) -> &'static str { if path.ends_with("/info/refs") || path.ends_with("info/refs") { "text/plain; charset=utf-8" } else { "application/octet-stream" } }