refactor(server): replace custom remote clients with macro-based implementation

- Replaced manual remote client functions with remote_client! macro for archive, blame, branch, commit, and diff services
- Simplified remote client creation logic using declarative macro approach
- Maintained same functionality while reducing code duplication across services

security(bare): enhance path traversal protection with comprehensive validation

- Added early relative_path validation to prevent path traversal attacks
- Implemented unified path validation to avoid TOCTOU race conditions
- Enhanced canonicalization checks for both existing and non-existent paths
- Added detailed logging for path traversal detection attempts

feat(cache): migrate from CLruCache to Moka with TTL and invalidation support

- Replaced clru dependency with moka for improved caching capabilities
- Added 300-second time-to-live for cache entries
- Implemented repository-specific cache invalidation mechanism
- Enhanced cache operations with thread-safe async support

refactor(commit): improve security validation for commit operations

- Added ref name validation to prevent command injection in cherry_pick_commit
- Implemented revision validation for commit selectors
- Added comprehensive input validation for create_commit parameters
- Enhanced file path validation to prevent traversal
This commit is contained in:
zhenyi
2026-06-08 09:43:57 +08:00
parent 8c95eb230d
commit d243dce027
60 changed files with 1746 additions and 561 deletions
+80 -30
View File
@@ -1,22 +1,22 @@
use std::num::NonZeroUsize;
use std::sync::{Mutex, OnceLock};
use std::sync::OnceLock;
use std::time::Duration;
use clru::CLruCache;
use moka::sync::Cache;
use prost::Message;
use crate::pb::{ObjectSelector, object_selector};
const GLOBAL_CACHE_MAX: usize = 65_545;
const GLOBAL_CACHE_MAX: u64 = 65_536;
const CACHE_TTL: Duration = Duration::from_secs(300);
type Cache = CLruCache<Vec<u8>, Vec<u8>>;
static GLOBAL_CACHE: OnceLock<Cache<Vec<u8>, Vec<u8>>> = OnceLock::new();
static GLOBAL_CACHE: OnceLock<Mutex<Cache>> = OnceLock::new();
fn cache() -> &'static Mutex<Cache> {
fn cache() -> &'static Cache<Vec<u8>, Vec<u8>> {
GLOBAL_CACHE.get_or_init(|| {
let capacity =
NonZeroUsize::new(GLOBAL_CACHE_MAX).expect("cache capacity must be non-zero");
Mutex::new(CLruCache::new(capacity))
Cache::builder()
.max_capacity(GLOBAL_CACHE_MAX)
.time_to_live(CACHE_TTL)
.build()
})
}
@@ -45,11 +45,7 @@ where
{
let key = cache_key(namespace, request);
if let Some(bytes) = cache()
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&key)
.cloned()
if let Some(bytes) = cache().get(&key)
&& let Ok(response) = Res::decode(bytes.as_slice())
{
tracing::debug!(
@@ -70,10 +66,7 @@ where
response
.encode(&mut bytes)
.expect("encoding a prost message into Vec cannot fail");
cache()
.lock()
.unwrap_or_else(|e| e.into_inner())
.put(key, bytes);
cache().insert(key, bytes);
Ok(response)
}
@@ -89,12 +82,7 @@ where
{
let key = cache_key(namespace, request);
if let Some(bytes) = cache()
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&key)
.cloned()
{
if let Some(bytes) = cache().get(&key) {
let mut remaining = bytes.as_slice();
let mut items = Vec::new();
let mut valid = true;
@@ -133,13 +121,75 @@ where
item.encode_length_delimited(&mut bytes)
.expect("encoding a prost message into Vec cannot fail");
}
cache()
.lock()
.unwrap_or_else(|e| e.into_inner())
.put(key, bytes);
cache().insert(key, bytes);
Ok(response)
}
/// Invalidate all cache entries related to a specific repository.
/// Called when refs are updated (create branch, create commit, etc.)
/// so that stale data is not served.
pub(crate) fn invalidate_repo(relative_path: &str) {
let c = cache();
// Encode the relative_path to match how it appears in cache keys
let target_path_bytes = relative_path.as_bytes();
// Remove all keys that reference this repository
// Cache keys are: namespace\0 + prost-encoded request
let keys_to_remove: Vec<std::sync::Arc<Vec<u8>>> = c
.iter()
.filter_map(|(key, _)| {
// Find the null byte separator
if let Some(null_pos) = key.iter().position(|&b| b == 0) {
let encoded_request = &key[null_pos + 1..];
// Check if this encoded request contains the repository path
// We use a sliding window to find the path bytes in the encoded protobuf
// This is conservative but correct: we may invalidate slightly more than
// necessary, but we won't miss any entries for this repository.
//
// The encoded protobuf format embeds string fields as length-prefixed data,
// so the relative_path bytes should appear verbatim somewhere in the message.
if contains_subslice(encoded_request, target_path_bytes) {
return Some(key);
}
} else {
// Malformed key without separator, remove it to be safe
tracing::warn!("found cache key without null separator, removing");
return Some(key);
}
None
})
.collect();
let removed = keys_to_remove.len();
for key in keys_to_remove {
c.invalidate(key.as_ref());
}
if removed > 0 {
tracing::debug!(
relative_path = %relative_path,
entries_removed = removed,
"cache invalidated for repository"
);
}
}
/// Check if a byte slice contains a subslice
fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() {
return true;
}
if needle.len() > haystack.len() {
return false;
}
haystack
.windows(needle.len())
.any(|window| window == needle)
}
pub(crate) fn selector_is_oid(selector: &Option<ObjectSelector>) -> bool {
matches!(
selector.as_ref().and_then(|s| s.selector.as_ref()),