Skip to content
Clone
69f12aa
59 lines 2.1 KB Rust
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<Response> {
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<PathBuf> {
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"
}
}