refactor(bare): enhance security and performance optimizations

- Remove unnecessary sorting in advertise_refs for deterministic output
- Add path traversal detection and validation in bare_dir construction
- Implement symlink resolution checks to prevent security vulnerabilities
- Refactor cache system with CRC validation and improved metrics
- Integrate repo-specific cache invalidation using indexed keys
- Add comprehensive unit tests for commit operations and diff functionality
- Move configuration constants to centralized config module
- Optimize string operations in disk cache random value generation
- Enhance license detection algorithm with cleaner matching logic
- Streamline argument processing in various git operations
- Update dependencies including crc32fast and flate2 for performance
- Add signal handling capability to tokio runtime configuration
This commit is contained in:
zhenyi
2026-06-12 15:04:12 +08:00
parent e386f44ee2
commit 10a4398e81
41 changed files with 1373 additions and 365 deletions
+61 -25
View File
@@ -30,6 +30,8 @@ use gix::discover::is_git;
use std::path::{Path, PathBuf};
use tokio_stream::wrappers::ReceiverStream;
use tonic::codec::CompressionEncoding;
use crate::bare::GitBare;
use crate::error::{GitError, GitResult};
use crate::pb::{
@@ -151,25 +153,20 @@ impl GitksService {
if relative_path.is_empty() {
return Err(tonic::Status::invalid_argument("relative_path is required"));
}
// Validate early to reject '..' and other traversal patterns
crate::sanitize::validate_relative_path(relative_path)
.map_err(|e| tonic::Status::invalid_argument(e.to_string()))?;
let candidate = self.repo_prefix.join(relative_path);
// Canonicalize repo_prefix (which should exist) for a reliable check
let prefix_canon = self
.repo_prefix
.canonicalize()
.unwrap_or_else(|_| self.repo_prefix.clone());
// Unified path validation to avoid TOCTOU
let canonical = match candidate.canonicalize() {
Ok(canon) => {
// Path exists and was canonicalized
canon
}
Err(_) => {
// Path doesn't exist yet — validate via parent
let parent = candidate.parent().unwrap_or(&self.repo_prefix);
let filename = candidate.file_name().ok_or_else(|| {
tonic::Status::invalid_argument("invalid path: missing filename")
@@ -180,7 +177,6 @@ impl GitksService {
.unwrap_or_else(|_| parent.to_path_buf());
let constructed = parent_canon.join(filename);
// String-level verification for non-existent paths
let constructed_str = constructed.to_string_lossy();
let prefix_str = prefix_canon.to_string_lossy();
@@ -194,13 +190,19 @@ impl GitksService {
}
};
// Final check: canonical must be under prefix
if !canonical.starts_with(&prefix_canon) {
return Err(tonic::Status::invalid_argument(
"path traversal detected: relative_path escapes repo prefix",
));
}
let double_canon = canonical.canonicalize().unwrap_or_else(|_| canonical.clone());
if canonical != double_canon {
return Err(tonic::Status::invalid_argument(
"path resolved to different target (possible symlink race)",
));
}
Ok(canonical)
}
@@ -211,10 +213,8 @@ impl GitksService {
_old_oid: &str,
_new_oid: &str,
) {
// Invalidate moka caches
crate::server::cache::invalidate_repo(relative_path);
// Invalidate disk cache
if let Some(ref pc) = self.pack_cache {
pc.invalidate_repo(relative_path);
}
@@ -328,10 +328,8 @@ pub(crate) fn git_cmd(gb: &GitBare, args: &[&str]) -> GitResult<std::process::Ou
let elapsed = start.elapsed();
let elapsed_ms = elapsed.as_millis() as u64;
// Record metrics
crate::metrics::record_git_cmd(cmd_name, elapsed);
// Slow operation warning
if elapsed.as_secs() >= 1 {
tracing::warn!(
repo = %gb.bare_dir.display(),
@@ -343,11 +341,12 @@ pub(crate) fn git_cmd(gb: &GitBare, args: &[&str]) -> GitResult<std::process::Ou
if !result.status.success() {
let stderr_str = String::from_utf8_lossy(&result.stderr);
let sanitized = crate::sanitize::sanitize_git_stderr(stderr_str.trim());
tracing::warn!(
repo = %gb.bare_dir.display(),
command = cmd_name,
status = ?result.status.code(),
stderr = %stderr_str.trim(),
stderr = %sanitized,
elapsed_ms,
"git subprocess exited with non-zero status"
);
@@ -386,6 +385,19 @@ fn structured_git_error(stderr: &str, code: Option<i32>) -> GitError {
pub async fn serve(
addr: std::net::SocketAddr,
svc: GitksService,
) -> Result<(), tonic::transport::Error> {
serve_with_shutdown(addr, svc, std::future::pending()).await
}
/// Start the gRPC server and block until the shutdown signal fires.
///
/// The `shutdown` future should resolve when the process should stop
/// (e.g. on SIGTERM/SIGINT). All in-flight requests are drained before
/// the server returns.
pub async fn serve_with_shutdown(
addr: std::net::SocketAddr,
svc: GitksService,
shutdown: impl std::future::Future<Output = ()>,
) -> Result<(), tonic::transport::Error> {
let span = tracing::info_span!("gitks.server", %addr);
let _enter = span.enter();
@@ -393,18 +405,42 @@ pub async fn serve(
let (health_reporter, health_service) = tonic_health::server::health_reporter();
let repo_svc = repository_service_server::RepositoryServiceServer::new(svc.clone());
let archive_svc = archive_service_server::ArchiveServiceServer::new(svc.clone());
let blame_svc = blame_service_server::BlameServiceServer::new(svc.clone());
let branch_svc = branch_service_server::BranchServiceServer::new(svc.clone());
let commit_svc = commit_service_server::CommitServiceServer::new(svc.clone());
let diff_svc = diff_service_server::DiffServiceServer::new(svc.clone());
let merge_svc = merge_service_server::MergeServiceServer::new(svc.clone());
let pack_svc = pack_service_server::PackServiceServer::new(svc.clone());
let ref_svc = ref_service_server::RefServiceServer::new(svc.clone());
let remote_svc = remote_service_server::RemoteServiceServer::new(svc.clone());
let tag_svc = tag_service_server::TagServiceServer::new(svc.clone());
let tree_svc = tree_service_server::TreeServiceServer::new(svc);
let repo_svc = repository_service_server::RepositoryServiceServer::new(svc.clone())
.send_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Gzip);
let archive_svc = archive_service_server::ArchiveServiceServer::new(svc.clone())
.send_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Gzip);
let blame_svc = blame_service_server::BlameServiceServer::new(svc.clone())
.send_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Gzip);
let branch_svc = branch_service_server::BranchServiceServer::new(svc.clone())
.send_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Gzip);
let commit_svc = commit_service_server::CommitServiceServer::new(svc.clone())
.send_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Gzip);
let diff_svc = diff_service_server::DiffServiceServer::new(svc.clone())
.send_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Gzip);
let merge_svc = merge_service_server::MergeServiceServer::new(svc.clone())
.send_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Gzip);
let pack_svc = pack_service_server::PackServiceServer::new(svc.clone())
.send_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Gzip);
let ref_svc = ref_service_server::RefServiceServer::new(svc.clone())
.send_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Gzip);
let remote_svc = remote_service_server::RemoteServiceServer::new(svc.clone())
.send_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Gzip);
let tag_svc = tag_service_server::TagServiceServer::new(svc.clone())
.send_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Gzip);
let tree_svc = tree_service_server::TreeServiceServer::new(svc)
.send_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Gzip);
health_reporter
.set_serving::<repository_service_server::RepositoryServiceServer<GitksService>>()
@@ -458,5 +494,5 @@ pub async fn serve(
.add_service(tag_svc)
.add_service(tree_svc);
tracing::info!("server ready, starting to accept connections");
server.serve(addr).await
server.serve_with_shutdown(addr, shutdown).await
}