d243dce027
- 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
206 lines
5.9 KiB
Rust
206 lines
5.9 KiB
Rust
use std::sync::OnceLock;
|
|
use std::time::Duration;
|
|
|
|
use moka::sync::Cache;
|
|
use prost::Message;
|
|
|
|
use crate::pb::{ObjectSelector, object_selector};
|
|
|
|
const GLOBAL_CACHE_MAX: u64 = 65_536;
|
|
const CACHE_TTL: Duration = Duration::from_secs(300);
|
|
|
|
static GLOBAL_CACHE: OnceLock<Cache<Vec<u8>, Vec<u8>>> = OnceLock::new();
|
|
|
|
fn cache() -> &'static Cache<Vec<u8>, Vec<u8>> {
|
|
GLOBAL_CACHE.get_or_init(|| {
|
|
Cache::builder()
|
|
.max_capacity(GLOBAL_CACHE_MAX)
|
|
.time_to_live(CACHE_TTL)
|
|
.build()
|
|
})
|
|
}
|
|
|
|
fn cache_key<Req>(namespace: &str, request: &Req) -> Vec<u8>
|
|
where
|
|
Req: Message,
|
|
{
|
|
let mut key = Vec::with_capacity(namespace.len() + 1 + request.encoded_len());
|
|
key.extend_from_slice(namespace.as_bytes());
|
|
key.push(0);
|
|
request
|
|
.encode(&mut key)
|
|
.expect("encoding a prost message into Vec cannot fail");
|
|
key
|
|
}
|
|
|
|
pub(crate) fn cached_response<Req, Res, E, F>(
|
|
namespace: &'static str,
|
|
request: &Req,
|
|
build: F,
|
|
) -> Result<Res, E>
|
|
where
|
|
Req: Message,
|
|
Res: Message + Default,
|
|
F: FnOnce() -> Result<Res, E>,
|
|
{
|
|
let key = cache_key(namespace, request);
|
|
|
|
if let Some(bytes) = cache().get(&key)
|
|
&& let Ok(response) = Res::decode(bytes.as_slice())
|
|
{
|
|
tracing::debug!(
|
|
namespace = %namespace,
|
|
key_len = key.len(),
|
|
"cache hit"
|
|
);
|
|
return Ok(response);
|
|
}
|
|
|
|
tracing::debug!(
|
|
namespace = %namespace,
|
|
key_len = key.len(),
|
|
"cache miss, building response"
|
|
);
|
|
let response = build()?;
|
|
let mut bytes = Vec::with_capacity(response.encoded_len());
|
|
response
|
|
.encode(&mut bytes)
|
|
.expect("encoding a prost message into Vec cannot fail");
|
|
cache().insert(key, bytes);
|
|
Ok(response)
|
|
}
|
|
|
|
pub(crate) fn cached_vec_response<Req, Item, E, F>(
|
|
namespace: &'static str,
|
|
request: &Req,
|
|
build: F,
|
|
) -> Result<Vec<Item>, E>
|
|
where
|
|
Req: Message,
|
|
Item: Message + Default,
|
|
F: FnOnce() -> Result<Vec<Item>, E>,
|
|
{
|
|
let key = cache_key(namespace, request);
|
|
|
|
if let Some(bytes) = cache().get(&key) {
|
|
let mut remaining = bytes.as_slice();
|
|
let mut items = Vec::new();
|
|
let mut valid = true;
|
|
while !remaining.is_empty() {
|
|
match Item::decode_length_delimited(&mut remaining) {
|
|
Ok(item) => items.push(item),
|
|
Err(_) => {
|
|
valid = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if valid {
|
|
tracing::debug!(
|
|
namespace = %namespace,
|
|
key_len = key.len(),
|
|
item_count = items.len(),
|
|
"vec cache hit"
|
|
);
|
|
return Ok(items);
|
|
}
|
|
tracing::warn!(
|
|
namespace = %namespace,
|
|
"vec cache decode failed, rebuilding"
|
|
);
|
|
}
|
|
|
|
tracing::debug!(
|
|
namespace = %namespace,
|
|
key_len = key.len(),
|
|
"vec cache miss, building response"
|
|
);
|
|
let response = build()?;
|
|
let mut bytes = Vec::new();
|
|
for item in &response {
|
|
item.encode_length_delimited(&mut bytes)
|
|
.expect("encoding a prost message into Vec cannot fail");
|
|
}
|
|
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()),
|
|
Some(object_selector::Selector::Oid(_))
|
|
)
|
|
}
|
|
|
|
pub(crate) fn selectors_are_oid(
|
|
left: &Option<ObjectSelector>,
|
|
right: &Option<ObjectSelector>,
|
|
) -> bool {
|
|
selector_is_oid(left) && selector_is_oid(right)
|
|
}
|