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) => { tracing::debug!(path = %request_path, bytes = bytes.len(), "serving dumb http object"); 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(_) => { tracing::warn!(path = %request_path, "dumb http path traversal denied"); 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 { tracing::warn!(path = %request_path, "dumb http path escapes repository root"); 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" } }