Files
gitks/bare.rs
T
zhenyi 10a4398e81 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
2026-06-12 15:04:12 +08:00

180 lines
6.9 KiB
Rust

use std::path::{Path, PathBuf};
use crate::error::{GitError, GitResult};
use crate::pb::RepositoryHeader;
#[derive(Debug)]
pub struct GitBare {
pub bare_dir: PathBuf,
}
impl GitBare {
pub fn new(bare_dir: PathBuf) -> Self {
Self { bare_dir }
}
pub fn gix_repo(&self) -> GitResult<gix::Repository> {
tracing::debug!(repo = %self.bare_dir.display(), "opening gix repository");
gix::open(&self.bare_dir).map_err(|e| {
tracing::error!(repo = %self.bare_dir.display(), error = %e, "failed to open gix repository");
GitError::Internal(format!("failed to open gix repository: {e}"))
})
}
pub fn from_repository_header(header: &RepositoryHeader) -> GitResult<Self> {
let storage_path = header.storage_path.trim();
let relative_path = header.relative_path.trim();
let storage_name = header.storage_name.trim();
let _ = storage_name; // reserved for future sharding logic
// Validate relative_path early to prevent path traversal
if !relative_path.is_empty() {
crate::sanitize::validate_relative_path(relative_path)?;
}
let base = if !storage_path.is_empty() {
let p = Path::new(storage_path);
if !p.is_absolute() {
return Err(GitError::InvalidArgument(
"storage_path must be an absolute path".into(),
));
}
PathBuf::from(p)
} else if !relative_path.is_empty() {
return Err(GitError::InvalidArgument(
"relative_path requires storage_path to be set".into(),
));
} else {
return Err(GitError::InvalidArgument("empty repository path".into()));
};
let bare_dir = if !relative_path.is_empty() && !storage_path.is_empty() {
let candidate = base.join(relative_path);
let base_canon = base.canonicalize().unwrap_or_else(|_| base.clone());
// Validate that relative_path itself contains no traversal patterns
// before any filesystem access (mitigates TOCTOU)
if relative_path.contains("..") {
return Err(GitError::InvalidArgument(format!(
"path traversal detected: relative_path contains '..': {relative_path}"
)));
}
// Reject symlinks in relative_path components
if relative_path.contains('\0') {
return Err(GitError::InvalidArgument(
"relative_path contains null byte".into(),
));
}
let canonical = match candidate.canonicalize() {
Ok(canon) => canon,
Err(_) => {
// Path doesn't exist yet; validate via parent
let parent = candidate.parent().unwrap_or(&base);
let filename = candidate.file_name().ok_or_else(|| {
GitError::InvalidArgument("invalid path: missing filename".into())
})?;
let parent_canon = parent
.canonicalize()
.unwrap_or_else(|_| parent.to_path_buf());
let constructed = parent_canon.join(filename);
let constructed_str = constructed.to_string_lossy();
let base_str = base_canon.to_string_lossy();
if !constructed_str.starts_with(&*base_str) {
tracing::warn!(
relative_path = %relative_path,
base = %base_canon.display(),
"path traversal attempt detected (parent check)"
);
return Err(GitError::InvalidArgument(format!(
"path traversal detected: {relative_path} escapes storage root"
)));
}
constructed
}
};
if !canonical.starts_with(&base_canon) {
tracing::warn!(
relative_path = %relative_path,
canonical = %canonical.display(),
base = %base_canon.display(),
"path traversal attempt detected"
);
return Err(GitError::InvalidArgument(format!(
"path traversal detected: {relative_path} escapes storage root"
)));
}
// Verify the resolved path has no symlinks in its components
// by checking that canonicalization is idempotent
let double_canon = canonical.canonicalize().unwrap_or_else(|_| canonical.clone());
if canonical != double_canon {
return Err(GitError::InvalidArgument(
"path resolved to different target (possible symlink race)".into(),
));
}
canonical
} else if !storage_path.is_empty() {
base.canonicalize().unwrap_or(base)
} else {
return Err(GitError::InvalidArgument("empty repository path".into()));
};
if !bare_dir.exists() {
tracing::warn!(path = %bare_dir.display(), "repository not found");
return Err(GitError::RepoNotFound);
}
if !bare_dir.is_dir() {
return Err(GitError::InvalidArgument(format!(
"not a directory: {}",
bare_dir.display()
)));
}
let head_path = bare_dir.join("HEAD");
if !head_path.exists() {
let git_dir = bare_dir.join(".git");
if git_dir.is_dir() && git_dir.join("HEAD").exists() {
tracing::debug!(path = %git_dir.display(), "resolved non-bare repo via .git subdir");
return Ok(Self { bare_dir: git_dir });
}
return Err(GitError::NotBareRepository);
}
Ok(Self { bare_dir })
}
pub fn object_format(&self) -> crate::pb::ObjectFormat {
let repo = self.gix_repo().ok();
let kind = repo
.as_ref()
.map(|r| r.object_hash())
.unwrap_or(gix::hash::Kind::Sha1);
match kind {
gix::hash::Kind::Sha1 => crate::pb::ObjectFormat::Sha1,
gix::hash::Kind::Sha256 => crate::pb::ObjectFormat::Sha256,
_ => crate::pb::ObjectFormat::Unspecified,
}
}
/// Convert a hex object id to a protobuf Oid.
///
/// `Oid.value` is the binary hash bytes, while `Oid.hex` keeps the printable
/// lowercase representation for clients that prefer string IDs.
pub fn oid_to_pb(&self, hex: impl Into<String>) -> crate::pb::Oid {
let hex = hex.into().to_lowercase();
let format = self.object_format();
crate::pb::Oid {
value: crate::oid::hex_to_bytes(&hex).unwrap_or_default(),
hex,
format: format as i32,
}
}
}