feat(server): add repository prefix path configuration and service struct

- Add REPO_PREFIX_PATH environment variable support in Dockerfile and main.rs
- Introduce GitksService struct with repo_prefix field to manage repository paths
- Implement resolve and resolve_for_init methods for repository path handling
- Add path traversal protection and validation for repository operations
- Update all service implementations to use self.resolve instead of global resolve
- Modify serve function to accept repo_prefix parameter and pass to GitksService
- Remove global resolve functions and integrate them into GitksService struct
- Add proper initialization of repo directory from environment variable
This commit is contained in:
zhenyi
2026-06-04 14:18:12 +08:00
parent 4a87ea475d
commit 729604f13b
13 changed files with 166 additions and 115 deletions
+70 -43
View File
@@ -21,8 +21,62 @@ use crate::pb::{
tag_service_server, tree_service_server,
};
#[derive(Clone, Copy)]
pub struct GitksService;
#[derive(Clone)]
pub struct GitksService {
/// 所有仓库的根路径前缀
pub(crate) repo_prefix: PathBuf,
}
impl GitksService {
pub(crate) fn resolve(
&self,
header: Option<&crate::pb::RepositoryHeader>,
) -> Result<GitBare, tonic::Status> {
let header =
header.ok_or_else(|| tonic::Status::invalid_argument("repository is required"))?;
let header = self.prefixed_header(header);
GitBare::from_repository_header(&header).map_err(into_status)
}
pub(crate) fn resolve_for_init(
&self,
header: Option<&crate::pb::RepositoryHeader>,
) -> Result<PathBuf, tonic::Status> {
let header =
header.ok_or_else(|| tonic::Status::invalid_argument("repository is required"))?;
let relative_path = header.relative_path.trim();
if relative_path.is_empty() {
return Err(tonic::Status::invalid_argument("relative_path is required"));
}
let candidate = self.repo_prefix.join(relative_path);
// 路径穿越检查
let canonical = candidate
.canonicalize()
.unwrap_or_else(|_| candidate.clone());
let prefix_canon = self
.repo_prefix
.canonicalize()
.unwrap_or_else(|_| self.repo_prefix.clone());
if !canonical.starts_with(&prefix_canon) {
return Err(tonic::Status::invalid_argument(
"path traversal detected: relative_path escapes repo prefix",
));
}
Ok(canonical)
}
/// 将客户端传入的 header 注入 repo_prefix 作为 storage_path
fn prefixed_header(
&self,
header: &crate::pb::RepositoryHeader,
) -> crate::pb::RepositoryHeader {
crate::pb::RepositoryHeader {
storage_path: self.repo_prefix.to_string_lossy().into_owned(),
relative_path: header.relative_path.clone(),
storage_name: header.storage_name.clone(),
}
}
}
pub(crate) fn into_status(e: GitError) -> tonic::Status {
match &e {
@@ -45,36 +99,6 @@ impl From<GitError> for tonic::Status {
}
}
pub(crate) fn resolve(
header: Option<&crate::pb::RepositoryHeader>,
) -> Result<GitBare, tonic::Status> {
let header = header.ok_or_else(|| tonic::Status::invalid_argument("repository is required"))?;
GitBare::from_repository_header(header).map_err(into_status)
}
pub(crate) fn resolve_for_init(
header: Option<&crate::pb::RepositoryHeader>,
) -> Result<PathBuf, tonic::Status> {
let header = header.ok_or_else(|| tonic::Status::invalid_argument("repository is required"))?;
let storage_path = header.storage_path.trim();
let relative_path = header.relative_path.trim();
if storage_path.is_empty() {
return Err(tonic::Status::invalid_argument("storage_path is required"));
}
let p = std::path::Path::new(storage_path);
if !p.is_absolute() {
return Err(tonic::Status::invalid_argument(
"storage_path must be an absolute path",
));
}
let base = PathBuf::from(p);
Ok(if !relative_path.is_empty() {
base.join(relative_path)
} else {
base
})
}
pub(crate) fn into_stream<T: Send + 'static>(
items: Vec<T>,
) -> ReceiverStream<Result<T, tonic::Status>> {
@@ -97,18 +121,21 @@ pub(crate) fn git_cmd(gb: &GitBare, args: &[&str]) -> Result<std::process::Outpu
.map_err(|e| tonic::Status::internal(e.to_string()))
}
pub async fn serve(addr: std::net::SocketAddr) -> Result<(), tonic::transport::Error> {
let svc = GitksService;
pub async fn serve(
addr: std::net::SocketAddr,
repo_prefix: PathBuf,
) -> Result<(), tonic::transport::Error> {
let svc = GitksService { repo_prefix };
tonic::transport::Server::builder()
.add_service(repository_service_server::RepositoryServiceServer::new(svc))
.add_service(archive_service_server::ArchiveServiceServer::new(svc))
.add_service(blame_service_server::BlameServiceServer::new(svc))
.add_service(branch_service_server::BranchServiceServer::new(svc))
.add_service(commit_service_server::CommitServiceServer::new(svc))
.add_service(diff_service_server::DiffServiceServer::new(svc))
.add_service(merge_service_server::MergeServiceServer::new(svc))
.add_service(pack_service_server::PackServiceServer::new(svc))
.add_service(tag_service_server::TagServiceServer::new(svc))
.add_service(repository_service_server::RepositoryServiceServer::new(svc.clone()))
.add_service(archive_service_server::ArchiveServiceServer::new(svc.clone()))
.add_service(blame_service_server::BlameServiceServer::new(svc.clone()))
.add_service(branch_service_server::BranchServiceServer::new(svc.clone()))
.add_service(commit_service_server::CommitServiceServer::new(svc.clone()))
.add_service(diff_service_server::DiffServiceServer::new(svc.clone()))
.add_service(merge_service_server::MergeServiceServer::new(svc.clone()))
.add_service(pack_service_server::PackServiceServer::new(svc.clone()))
.add_service(tag_service_server::TagServiceServer::new(svc.clone()))
.add_service(tree_service_server::TreeServiceServer::new(svc))
.serve(addr)
.await