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:
+8
-22
@@ -1,27 +1,9 @@
|
||||
use crate::pb::*;
|
||||
use crate::pb::archive_service_client::ArchiveServiceClient;
|
||||
use crate::pb::*;
|
||||
|
||||
use super::{GitksService, cache, into_status};
|
||||
|
||||
async fn remote_archive_client(
|
||||
svc: &GitksService,
|
||||
header: Option<&RepositoryHeader>,
|
||||
is_write: bool,
|
||||
) -> Result<Option<ArchiveServiceClient<tonic::transport::Channel>>, tonic::Status> {
|
||||
let header = match header {
|
||||
Some(h) => h,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let Some(route) = svc.route_repository(header, is_write).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
tracing::info!(storage_name = %route.storage_name, relative_path = %route.relative_path, actor_name = %route.actor_name, grpc_addr = %route.grpc_addr, "forwarding archive rpc");
|
||||
let endpoint = super::remote_endpoint(&route.grpc_addr).await?;
|
||||
let client = ArchiveServiceClient::connect(endpoint)
|
||||
.await
|
||||
.map_err(|e| tonic::Status::unavailable(e.to_string()))?;
|
||||
Ok(Some(client))
|
||||
}
|
||||
remote_client!(remote_archive_client, ArchiveServiceClient<tonic::transport::Channel>, "archive");
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl archive_service_server::ArchiveService for GitksService {
|
||||
@@ -39,7 +21,9 @@ impl archive_service_server::ArchiveService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_archive_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_archive_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
let resp = client.get_archive(inner).await?;
|
||||
let stream = super::bridge_server_stream(resp.into_inner());
|
||||
return Ok(tonic::Response::new(stream));
|
||||
@@ -64,7 +48,9 @@ impl archive_service_server::ArchiveService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_archive_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_archive_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.list_archive_entries(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
|
||||
+8
-22
@@ -1,27 +1,9 @@
|
||||
use crate::pb::*;
|
||||
use crate::pb::blame_service_client::BlameServiceClient;
|
||||
use crate::pb::*;
|
||||
|
||||
use super::{GitksService, cache, into_status, into_stream};
|
||||
|
||||
async fn remote_blame_client(
|
||||
svc: &GitksService,
|
||||
header: Option<&RepositoryHeader>,
|
||||
is_write: bool,
|
||||
) -> Result<Option<BlameServiceClient<tonic::transport::Channel>>, tonic::Status> {
|
||||
let header = match header {
|
||||
Some(h) => h,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let Some(route) = svc.route_repository(header, is_write).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
tracing::info!(storage_name = %route.storage_name, relative_path = %route.relative_path, actor_name = %route.actor_name, grpc_addr = %route.grpc_addr, "forwarding blame rpc");
|
||||
let endpoint = super::remote_endpoint(&route.grpc_addr).await?;
|
||||
let client = BlameServiceClient::connect(endpoint)
|
||||
.await
|
||||
.map_err(|e| tonic::Status::unavailable(e.to_string()))?;
|
||||
Ok(Some(client))
|
||||
}
|
||||
remote_client!(remote_blame_client, BlameServiceClient<tonic::transport::Channel>, "blame");
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl blame_service_server::BlameService for GitksService {
|
||||
@@ -40,7 +22,9 @@ impl blame_service_server::BlameService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_blame_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_blame_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.blame(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -70,7 +54,9 @@ impl blame_service_server::BlameService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_blame_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_blame_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
let resp = client.stream_blame(inner).await?;
|
||||
let stream = super::bridge_server_stream(resp.into_inner());
|
||||
return Ok(tonic::Response::new(stream));
|
||||
|
||||
+26
-28
@@ -1,27 +1,9 @@
|
||||
use crate::pb::*;
|
||||
use crate::pb::branch_service_client::BranchServiceClient;
|
||||
use crate::pb::*;
|
||||
|
||||
use super::{GitksService, into_status};
|
||||
|
||||
async fn remote_branch_client(
|
||||
svc: &GitksService,
|
||||
header: Option<&RepositoryHeader>,
|
||||
is_write: bool,
|
||||
) -> Result<Option<BranchServiceClient<tonic::transport::Channel>>, tonic::Status> {
|
||||
let header = match header {
|
||||
Some(h) => h,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let Some(route) = svc.route_repository(header, is_write).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
tracing::info!(storage_name = %route.storage_name, relative_path = %route.relative_path, actor_name = %route.actor_name, grpc_addr = %route.grpc_addr, "forwarding branch rpc");
|
||||
let endpoint = super::remote_endpoint(&route.grpc_addr).await?;
|
||||
let client = BranchServiceClient::connect(endpoint)
|
||||
.await
|
||||
.map_err(|e| tonic::Status::unavailable(e.to_string()))?;
|
||||
Ok(Some(client))
|
||||
}
|
||||
remote_client!(remote_branch_client, BranchServiceClient<tonic::transport::Channel>, "branch");
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl branch_service_server::BranchService for GitksService {
|
||||
@@ -36,7 +18,9 @@ impl branch_service_server::BranchService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_branch_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_branch_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.list_branches(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -60,7 +44,9 @@ impl branch_service_server::BranchService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_branch_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_branch_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.get_branch(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -83,7 +69,9 @@ impl branch_service_server::BranchService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_branch_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_branch_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.create_branch(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -108,7 +96,9 @@ impl branch_service_server::BranchService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_branch_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_branch_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.delete_branch(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -134,7 +124,9 @@ impl branch_service_server::BranchService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_branch_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_branch_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.rename_branch(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -159,7 +151,9 @@ impl branch_service_server::BranchService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_branch_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_branch_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.update_branch_target(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -184,7 +178,9 @@ impl branch_service_server::BranchService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_branch_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_branch_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.set_branch_upstream(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -210,7 +206,9 @@ impl branch_service_server::BranchService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_branch_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_branch_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.compare_branch(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
|
||||
+80
-30
@@ -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()),
|
||||
|
||||
+26
-28
@@ -1,27 +1,9 @@
|
||||
use crate::pb::*;
|
||||
use crate::pb::commit_service_client::CommitServiceClient;
|
||||
use crate::pb::*;
|
||||
|
||||
use super::{GitksService, cache, into_status};
|
||||
|
||||
async fn remote_commit_client(
|
||||
svc: &GitksService,
|
||||
header: Option<&RepositoryHeader>,
|
||||
is_write: bool,
|
||||
) -> Result<Option<CommitServiceClient<tonic::transport::Channel>>, tonic::Status> {
|
||||
let header = match header {
|
||||
Some(h) => h,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let Some(route) = svc.route_repository(header, is_write).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
tracing::info!(storage_name = %route.storage_name, relative_path = %route.relative_path, actor_name = %route.actor_name, grpc_addr = %route.grpc_addr, "forwarding commit rpc");
|
||||
let endpoint = super::remote_endpoint(&route.grpc_addr).await?;
|
||||
let client = CommitServiceClient::connect(endpoint)
|
||||
.await
|
||||
.map_err(|e| tonic::Status::unavailable(e.to_string()))?;
|
||||
Ok(Some(client))
|
||||
}
|
||||
remote_client!(remote_commit_client, CommitServiceClient<tonic::transport::Channel>, "commit");
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl commit_service_server::CommitService for GitksService {
|
||||
@@ -36,7 +18,9 @@ impl commit_service_server::CommitService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_commit_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_commit_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.list_commits(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -65,7 +49,9 @@ impl commit_service_server::CommitService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_commit_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_commit_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.get_commit(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -93,7 +79,9 @@ impl commit_service_server::CommitService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_commit_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_commit_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.get_commit_ancestors(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -123,7 +111,9 @@ impl commit_service_server::CommitService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_commit_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_commit_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.create_commit(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -131,7 +121,9 @@ impl commit_service_server::CommitService for GitksService {
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let resp = gb.create_commit(inner).map_err(into_status)?;
|
||||
let commit_hex = resp.commit.as_ref()
|
||||
let commit_hex = resp
|
||||
.commit
|
||||
.as_ref()
|
||||
.and_then(|c| c.oid.as_ref().map(|o| o.hex.as_str()).or(Some("?")))
|
||||
.unwrap_or("?");
|
||||
tracing::info!(%repo, %branch, %commit_hex, "commit created");
|
||||
@@ -151,7 +143,9 @@ impl commit_service_server::CommitService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_commit_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_commit_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.revert_commit(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -176,7 +170,9 @@ impl commit_service_server::CommitService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_commit_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_commit_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.cherry_pick_commit(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -200,7 +196,9 @@ impl commit_service_server::CommitService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_commit_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_commit_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.compare_commits(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
|
||||
+14
-24
@@ -1,27 +1,9 @@
|
||||
use crate::pb::*;
|
||||
use crate::pb::diff_service_client::DiffServiceClient;
|
||||
use crate::pb::*;
|
||||
|
||||
use super::{GitksService, cache, into_status, into_stream};
|
||||
|
||||
async fn remote_diff_client(
|
||||
svc: &GitksService,
|
||||
header: Option<&RepositoryHeader>,
|
||||
is_write: bool,
|
||||
) -> Result<Option<DiffServiceClient<tonic::transport::Channel>>, tonic::Status> {
|
||||
let header = match header {
|
||||
Some(h) => h,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let Some(route) = svc.route_repository(header, is_write).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
tracing::info!(storage_name = %route.storage_name, relative_path = %route.relative_path, actor_name = %route.actor_name, grpc_addr = %route.grpc_addr, "forwarding diff rpc");
|
||||
let endpoint = super::remote_endpoint(&route.grpc_addr).await?;
|
||||
let client = DiffServiceClient::connect(endpoint)
|
||||
.await
|
||||
.map_err(|e| tonic::Status::unavailable(e.to_string()))?;
|
||||
Ok(Some(client))
|
||||
}
|
||||
remote_client!(remote_diff_client, DiffServiceClient<tonic::transport::Channel>, "diff");
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl diff_service_server::DiffService for GitksService {
|
||||
@@ -39,7 +21,9 @@ impl diff_service_server::DiffService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_diff_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_diff_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.get_diff(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -68,7 +52,9 @@ impl diff_service_server::DiffService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_diff_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_diff_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.get_commit_diff(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -97,7 +83,9 @@ impl diff_service_server::DiffService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_diff_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_diff_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
let resp = client.get_patch(inner).await?;
|
||||
let stream = super::bridge_server_stream(resp.into_inner());
|
||||
return Ok(tonic::Response::new(stream));
|
||||
@@ -127,7 +115,9 @@ impl diff_service_server::DiffService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_diff_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_diff_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.get_diff_stats(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
|
||||
+17
-25
@@ -1,27 +1,9 @@
|
||||
use crate::pb::*;
|
||||
use crate::pb::merge_service_client::MergeServiceClient;
|
||||
use crate::pb::*;
|
||||
|
||||
use super::{GitksService, into_status};
|
||||
|
||||
async fn remote_merge_client(
|
||||
svc: &GitksService,
|
||||
header: Option<&RepositoryHeader>,
|
||||
is_write: bool,
|
||||
) -> Result<Option<MergeServiceClient<tonic::transport::Channel>>, tonic::Status> {
|
||||
let header = match header {
|
||||
Some(h) => h,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let Some(route) = svc.route_repository(header, is_write).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
tracing::info!(storage_name = %route.storage_name, relative_path = %route.relative_path, actor_name = %route.actor_name, grpc_addr = %route.grpc_addr, "forwarding merge rpc");
|
||||
let endpoint = super::remote_endpoint(&route.grpc_addr).await?;
|
||||
let client = MergeServiceClient::connect(endpoint)
|
||||
.await
|
||||
.map_err(|e| tonic::Status::unavailable(e.to_string()))?;
|
||||
Ok(Some(client))
|
||||
}
|
||||
remote_client!(remote_merge_client, MergeServiceClient<tonic::transport::Channel>, "merge");
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl merge_service_server::MergeService for GitksService {
|
||||
@@ -36,7 +18,9 @@ impl merge_service_server::MergeService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_merge_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_merge_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.check_merge(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -60,7 +44,9 @@ impl merge_service_server::MergeService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_merge_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_merge_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.merge(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -84,7 +70,9 @@ impl merge_service_server::MergeService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_merge_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_merge_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.list_merge_conflicts(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -108,7 +96,9 @@ impl merge_service_server::MergeService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_merge_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_merge_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.resolve_merge_conflicts(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -133,7 +123,9 @@ impl merge_service_server::MergeService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_merge_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_merge_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.rebase(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
|
||||
+97
-22
@@ -1,3 +1,35 @@
|
||||
/// Generate a `remote_<service>_client` helper function that resolves a repository
|
||||
/// route and returns a connected gRPC client for the given service.
|
||||
macro_rules! remote_client {
|
||||
($fn_name:ident, $client:ty, $svc_label:literal) => {
|
||||
async fn $fn_name(
|
||||
svc: &super::GitksService,
|
||||
header: Option<&crate::pb::RepositoryHeader>,
|
||||
is_write: bool,
|
||||
) -> Result<Option<$client>, tonic::Status> {
|
||||
let header = match header {
|
||||
Some(h) => h,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let Some(route) = svc.route_repository(header, is_write).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
tracing::info!(
|
||||
storage_name = %route.storage_name,
|
||||
relative_path = %route.relative_path,
|
||||
actor_name = %route.actor_name,
|
||||
grpc_addr = %route.grpc_addr,
|
||||
concat!("forwarding ", $svc_label, " rpc")
|
||||
);
|
||||
let endpoint = super::remote_endpoint(&route.grpc_addr).await?;
|
||||
let client = <$client>::connect(endpoint)
|
||||
.await
|
||||
.map_err(|e| tonic::Status::unavailable(e.to_string()))?;
|
||||
Ok(Some(client))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mod archive;
|
||||
mod blame;
|
||||
mod branch;
|
||||
@@ -11,9 +43,9 @@ mod repository_maint;
|
||||
mod tag;
|
||||
mod tree;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use gix::discover::is_git;
|
||||
use ractor::{ActorCell, ActorRef};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
|
||||
use crate::actor::message::{GitNodeMessage, RouteDecision};
|
||||
@@ -34,7 +66,11 @@ pub struct GitksService {
|
||||
|
||||
impl GitksService {
|
||||
pub fn new(repo_prefix: PathBuf) -> Self {
|
||||
Self { repo_prefix, node_actor: None, grpc_addr: String::new() }
|
||||
Self {
|
||||
repo_prefix,
|
||||
node_actor: None,
|
||||
grpc_addr: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_actor(mut self, node_actor: ActorRef<GitNodeMessage>) -> Self {
|
||||
@@ -74,20 +110,23 @@ impl GitksService {
|
||||
if local.as_ref().is_some_and(|actor| actor == &member) {
|
||||
continue;
|
||||
}
|
||||
if let Some(decision) = query_find_primary(member.clone(), header.clone()).await? {
|
||||
if decision.found && !decision.grpc_addr.is_empty() {
|
||||
primary = Some(decision);
|
||||
if is_write {
|
||||
return Ok(primary);
|
||||
}
|
||||
if let Some(decision) = query_find_primary(member.clone(), header.clone()).await?
|
||||
&& decision.found
|
||||
&& !decision.grpc_addr.is_empty()
|
||||
{
|
||||
primary = Some(decision);
|
||||
if is_write {
|
||||
return Ok(primary);
|
||||
}
|
||||
}
|
||||
if !is_write && replica.is_none() {
|
||||
if let Some(decision) = query_find_replica(member.clone(), header.clone()).await? {
|
||||
if decision.found && !decision.grpc_addr.is_empty() && decision.role == ROLE_REPLICA {
|
||||
replica = Some(decision);
|
||||
}
|
||||
}
|
||||
if !is_write
|
||||
&& replica.is_none()
|
||||
&& let Some(decision) = query_find_replica(member.clone(), header.clone()).await?
|
||||
&& decision.found
|
||||
&& !decision.grpc_addr.is_empty()
|
||||
&& decision.role == ROLE_REPLICA
|
||||
{
|
||||
replica = Some(decision);
|
||||
}
|
||||
}
|
||||
if let Some(p) = primary {
|
||||
@@ -142,20 +181,56 @@ 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);
|
||||
// Path traversal check
|
||||
let canonical = candidate
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| candidate.clone());
|
||||
// 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")
|
||||
})?;
|
||||
|
||||
let parent_canon = parent
|
||||
.canonicalize()
|
||||
.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();
|
||||
|
||||
if !constructed_str.starts_with(&*prefix_str) {
|
||||
return Err(tonic::Status::invalid_argument(
|
||||
"path traversal detected: relative_path escapes repo prefix",
|
||||
));
|
||||
}
|
||||
|
||||
constructed
|
||||
}
|
||||
};
|
||||
|
||||
// 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",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(canonical)
|
||||
}
|
||||
|
||||
@@ -166,6 +241,9 @@ impl GitksService {
|
||||
old_oid: &str,
|
||||
new_oid: &str,
|
||||
) {
|
||||
// Invalidate caches that depend on this repository
|
||||
crate::server::cache::invalidate_repo(relative_path);
|
||||
|
||||
if let Some(ref actor) = self.node_actor {
|
||||
let event = crate::actor::message::RefUpdateEvent {
|
||||
relative_path: relative_path.to_string(),
|
||||
@@ -189,14 +267,11 @@ impl GitksService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub async fn remote_endpoint(addr: &str) -> Result<tonic::transport::Endpoint, tonic::Status> {
|
||||
let uri: tonic::codegen::http::Uri = addr
|
||||
.parse()
|
||||
.map_err(|e| tonic::Status::invalid_argument(format!("invalid URI: {e}")))?;
|
||||
tonic::transport::Endpoint::new(uri)
|
||||
.map_err(|e| tonic::Status::internal(e.to_string()))
|
||||
tonic::transport::Endpoint::new(uri).map_err(|e| tonic::Status::internal(e.to_string()))
|
||||
}
|
||||
|
||||
pub(super) fn bridge_server_stream<T: Send + 'static>(
|
||||
|
||||
+43
-31
@@ -1,30 +1,12 @@
|
||||
use tokio_stream::StreamExt;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
|
||||
use crate::pb::*;
|
||||
use crate::pb::pack_service_client::PackServiceClient;
|
||||
use crate::pb::*;
|
||||
|
||||
use super::{GitksService, into_status};
|
||||
|
||||
async fn remote_pack_client(
|
||||
svc: &GitksService,
|
||||
header: Option<&RepositoryHeader>,
|
||||
is_write: bool,
|
||||
) -> Result<Option<PackServiceClient<tonic::transport::Channel>>, tonic::Status> {
|
||||
let header = match header {
|
||||
Some(h) => h,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let Some(route) = svc.route_repository(header, is_write).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
tracing::info!(storage_name = %route.storage_name, relative_path = %route.relative_path, actor_name = %route.actor_name, grpc_addr = %route.grpc_addr, "forwarding pack rpc");
|
||||
let endpoint = super::remote_endpoint(&route.grpc_addr).await?;
|
||||
let client = PackServiceClient::connect(endpoint)
|
||||
.await
|
||||
.map_err(|e| tonic::Status::unavailable(e.to_string()))?;
|
||||
Ok(Some(client))
|
||||
}
|
||||
remote_client!(remote_pack_client, PackServiceClient<tonic::transport::Channel>, "pack");
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl pack_service_server::PackService for GitksService {
|
||||
@@ -43,7 +25,9 @@ impl pack_service_server::PackService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_pack_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_pack_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.advertise_refs(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -70,19 +54,27 @@ impl pack_service_server::PackService for GitksService {
|
||||
let gb = match self.resolve(first.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_pack_client(self, first.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_pack_client(self, first.repository.as_ref(), false).await?
|
||||
{
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(16);
|
||||
let _ = tx.send(first).await;
|
||||
tokio::spawn(async move {
|
||||
use tokio_stream::StreamExt;
|
||||
while let Some(msg) = stream.next().await {
|
||||
match msg {
|
||||
Ok(m) => { if tx.send(m).await.is_err() { break; } }
|
||||
Ok(m) => {
|
||||
if tx.send(m).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
let resp = client.upload_pack(tokio_stream::wrappers::ReceiverStream::new(rx)).await?;
|
||||
let resp = client
|
||||
.upload_pack(tokio_stream::wrappers::ReceiverStream::new(rx))
|
||||
.await?;
|
||||
let out = super::bridge_server_stream(resp.into_inner());
|
||||
return Ok(tonic::Response::new(out));
|
||||
}
|
||||
@@ -123,19 +115,27 @@ impl pack_service_server::PackService for GitksService {
|
||||
let gb = match self.resolve(first.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_pack_client(self, first.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_pack_client(self, first.repository.as_ref(), false).await?
|
||||
{
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(16);
|
||||
let _ = tx.send(first).await;
|
||||
tokio::spawn(async move {
|
||||
use tokio_stream::StreamExt;
|
||||
while let Some(msg) = stream.next().await {
|
||||
match msg {
|
||||
Ok(m) => { if tx.send(m).await.is_err() { break; } }
|
||||
Ok(m) => {
|
||||
if tx.send(m).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
let resp = client.receive_pack(tokio_stream::wrappers::ReceiverStream::new(rx)).await?;
|
||||
let resp = client
|
||||
.receive_pack(tokio_stream::wrappers::ReceiverStream::new(rx))
|
||||
.await?;
|
||||
let out = super::bridge_server_stream(resp.into_inner());
|
||||
return Ok(tonic::Response::new(out));
|
||||
}
|
||||
@@ -172,7 +172,9 @@ impl pack_service_server::PackService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_pack_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_pack_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
let resp = client.pack_objects(inner).await?;
|
||||
let stream = super::bridge_server_stream(resp.into_inner());
|
||||
return Ok(tonic::Response::new(stream));
|
||||
@@ -201,7 +203,13 @@ impl pack_service_server::PackService for GitksService {
|
||||
let gb = match self.resolve(inputs.first().and_then(|r| r.repository.as_ref())) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_pack_client(self, inputs.first().and_then(|r| r.repository.as_ref()), false).await? {
|
||||
if let Some(mut client) = remote_pack_client(
|
||||
self,
|
||||
inputs.first().and_then(|r| r.repository.as_ref()),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return client.index_pack(tokio_stream::iter(inputs)).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -224,7 +232,9 @@ impl pack_service_server::PackService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_pack_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_pack_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.list_packfiles(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -247,7 +257,9 @@ impl pack_service_server::PackService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_pack_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_pack_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.fsck(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
|
||||
+50
-40
@@ -1,27 +1,9 @@
|
||||
use crate::pb::*;
|
||||
use crate::pb::repository_service_client::RepositoryServiceClient;
|
||||
use crate::pb::*;
|
||||
|
||||
use super::{GitksService, git_cmd, into_status, repository_maint, remote_endpoint};
|
||||
use super::{GitksService, git_cmd, into_status, repository_maint};
|
||||
|
||||
async fn remote_repository_client(
|
||||
svc: &GitksService,
|
||||
header: Option<&RepositoryHeader>,
|
||||
is_write: bool,
|
||||
) -> Result<Option<RepositoryServiceClient<tonic::transport::Channel>>, tonic::Status> {
|
||||
let header = match header {
|
||||
Some(h) => h,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let Some(route) = svc.route_repository(header, is_write).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
tracing::info!(storage_name = %route.storage_name, relative_path = %route.relative_path, actor_name = %route.actor_name, grpc_addr = %route.grpc_addr, "forwarding repository rpc");
|
||||
let endpoint = remote_endpoint(&route.grpc_addr).await?;
|
||||
let client = RepositoryServiceClient::connect(endpoint)
|
||||
.await
|
||||
.map_err(|e| tonic::Status::unavailable(e.to_string()))?;
|
||||
Ok(Some(client))
|
||||
}
|
||||
remote_client!(remote_repository_client, RepositoryServiceClient<tonic::transport::Channel>, "repository");
|
||||
|
||||
fn default_branch_name(gb: &crate::bare::GitBare) -> String {
|
||||
git_cmd(gb, &["symbolic-ref", "HEAD"])
|
||||
@@ -48,7 +30,9 @@ impl repository_service_server::RepositoryService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_repository_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_repository_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.get_repository(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -95,10 +79,11 @@ impl repository_service_server::RepositoryService for GitksService {
|
||||
let span = tracing::info_span!("repo.delete_repository", %repo);
|
||||
let _enter = span.enter();
|
||||
let bare_dir = self.resolve_for_init(inner.repository.as_ref())?;
|
||||
if !bare_dir.exists() {
|
||||
if let Some(mut client) = remote_repository_client(self, inner.repository.as_ref(), true).await? {
|
||||
return client.delete_repository(inner).await;
|
||||
}
|
||||
if !bare_dir.exists()
|
||||
&& let Some(mut client) =
|
||||
remote_repository_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.delete_repository(inner).await;
|
||||
}
|
||||
tracing::warn!(%repo, path = %bare_dir.display(), "deleting repository");
|
||||
std::fs::remove_dir_all(&bare_dir).map_err(|e| tonic::Status::internal(e.to_string()))?;
|
||||
@@ -117,10 +102,11 @@ impl repository_service_server::RepositoryService for GitksService {
|
||||
let _enter = span.enter();
|
||||
let bare_dir = self.resolve_for_init(inner.repository.as_ref())?;
|
||||
let exists = bare_dir.exists() && bare_dir.is_dir() && bare_dir.join("HEAD").exists();
|
||||
if !exists {
|
||||
if let Some(mut client) = remote_repository_client(self, inner.repository.as_ref(), false).await? {
|
||||
return client.repository_exists(inner).await;
|
||||
}
|
||||
if !exists
|
||||
&& let Some(mut client) =
|
||||
remote_repository_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.repository_exists(inner).await;
|
||||
}
|
||||
Ok(tonic::Response::new(RepositoryExistsResponse { exists }))
|
||||
}
|
||||
@@ -136,7 +122,9 @@ impl repository_service_server::RepositoryService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_repository_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_repository_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.get_object_format(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -159,7 +147,9 @@ impl repository_service_server::RepositoryService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_repository_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_repository_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.get_default_branch(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -183,7 +173,9 @@ impl repository_service_server::RepositoryService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_repository_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_repository_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.set_default_branch(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -213,7 +205,9 @@ impl repository_service_server::RepositoryService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_repository_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_repository_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.get_repository_config(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -238,6 +232,8 @@ impl repository_service_server::RepositoryService for GitksService {
|
||||
}
|
||||
} else {
|
||||
for key in &inner.keys {
|
||||
crate::sanitize::validate_config_key(key)
|
||||
.map_err(|e| tonic::Status::invalid_argument(e.to_string()))?;
|
||||
let out = git_cmd(&gb, &["config", "--get-all", key])?;
|
||||
if out.status.success() {
|
||||
let vals: Vec<String> = String::from_utf8_lossy(&out.stdout)
|
||||
@@ -270,7 +266,9 @@ impl repository_service_server::RepositoryService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_repository_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_repository_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.set_repository_config(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -278,6 +276,8 @@ impl repository_service_server::RepositoryService for GitksService {
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
for entry in &inner.entries {
|
||||
crate::sanitize::validate_config_key(&entry.key)
|
||||
.map_err(|e| tonic::Status::invalid_argument(e.to_string()))?;
|
||||
if entry.values.is_empty() {
|
||||
git_cmd(&gb, &["config", "--unset-all", &entry.key])?;
|
||||
} else {
|
||||
@@ -305,7 +305,9 @@ impl repository_service_server::RepositoryService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_repository_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_repository_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.get_repository_statistics(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -326,7 +328,9 @@ impl repository_service_server::RepositoryService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_repository_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_repository_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.check_repository_health(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -349,7 +353,9 @@ impl repository_service_server::RepositoryService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_repository_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_repository_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.garbage_collect(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -372,7 +378,9 @@ impl repository_service_server::RepositoryService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_repository_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_repository_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.repack(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -400,7 +408,9 @@ impl repository_service_server::RepositoryService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_repository_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_repository_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.write_commit_graph(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
|
||||
@@ -39,17 +39,14 @@ fn dir_size(gb: &crate::bare::GitBare) -> u64 {
|
||||
}
|
||||
|
||||
fn count_refs(gb: &crate::bare::GitBare) -> u64 {
|
||||
let out = git_cmd(gb, &["for-each-ref", "--format=%(refname)"]).unwrap_or_else(|_| {
|
||||
std::process::Output {
|
||||
status: Default::default(),
|
||||
stdout: Vec::new(),
|
||||
stderr: Vec::new(),
|
||||
}
|
||||
});
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.filter(|l| !l.is_empty())
|
||||
.count() as u64
|
||||
let out = git_cmd(gb, &["for-each-ref", "--format=%(refname)"]).ok();
|
||||
out.map(|o| {
|
||||
String::from_utf8_lossy(&o.stdout)
|
||||
.lines()
|
||||
.filter(|l| !l.is_empty())
|
||||
.count() as u64
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn file_len(path: &std::path::Path) -> u64 {
|
||||
|
||||
+17
-25
@@ -1,27 +1,9 @@
|
||||
use crate::pb::*;
|
||||
use crate::pb::tag_service_client::TagServiceClient;
|
||||
use crate::pb::*;
|
||||
|
||||
use super::{GitksService, into_status};
|
||||
|
||||
async fn remote_tag_client(
|
||||
svc: &GitksService,
|
||||
header: Option<&RepositoryHeader>,
|
||||
is_write: bool,
|
||||
) -> Result<Option<TagServiceClient<tonic::transport::Channel>>, tonic::Status> {
|
||||
let header = match header {
|
||||
Some(h) => h,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let Some(route) = svc.route_repository(header, is_write).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
tracing::info!(storage_name = %route.storage_name, relative_path = %route.relative_path, actor_name = %route.actor_name, grpc_addr = %route.grpc_addr, "forwarding tag rpc");
|
||||
let endpoint = super::remote_endpoint(&route.grpc_addr).await?;
|
||||
let client = TagServiceClient::connect(endpoint)
|
||||
.await
|
||||
.map_err(|e| tonic::Status::unavailable(e.to_string()))?;
|
||||
Ok(Some(client))
|
||||
}
|
||||
remote_client!(remote_tag_client, TagServiceClient<tonic::transport::Channel>, "tag");
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl tag_service_server::TagService for GitksService {
|
||||
@@ -36,7 +18,9 @@ impl tag_service_server::TagService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_tag_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_tag_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.list_tags(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -60,7 +44,9 @@ impl tag_service_server::TagService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_tag_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_tag_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.get_tag(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -83,7 +69,9 @@ impl tag_service_server::TagService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_tag_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_tag_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.create_tag(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -108,7 +96,9 @@ impl tag_service_server::TagService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_tag_client(self, inner.repository.as_ref(), true).await? {
|
||||
if let Some(mut client) =
|
||||
remote_tag_client(self, inner.repository.as_ref(), true).await?
|
||||
{
|
||||
return client.delete_tag(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -133,7 +123,9 @@ impl tag_service_server::TagService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_tag_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_tag_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.verify_tag(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
|
||||
+20
-26
@@ -1,27 +1,9 @@
|
||||
use crate::pb::*;
|
||||
use crate::pb::tree_service_client::TreeServiceClient;
|
||||
use crate::pb::*;
|
||||
|
||||
use super::{GitksService, cache, into_status, into_stream};
|
||||
|
||||
async fn remote_tree_client(
|
||||
svc: &GitksService,
|
||||
header: Option<&RepositoryHeader>,
|
||||
is_write: bool,
|
||||
) -> Result<Option<TreeServiceClient<tonic::transport::Channel>>, tonic::Status> {
|
||||
let header = match header {
|
||||
Some(h) => h,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let Some(route) = svc.route_repository(header, is_write).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
tracing::info!(storage_name = %route.storage_name, relative_path = %route.relative_path, actor_name = %route.actor_name, grpc_addr = %route.grpc_addr, "forwarding tree rpc");
|
||||
let endpoint = super::remote_endpoint(&route.grpc_addr).await?;
|
||||
let client = TreeServiceClient::connect(endpoint)
|
||||
.await
|
||||
.map_err(|e| tonic::Status::unavailable(e.to_string()))?;
|
||||
Ok(Some(client))
|
||||
}
|
||||
remote_client!(remote_tree_client, TreeServiceClient<tonic::transport::Channel>, "tree");
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl tree_service_server::TreeService for GitksService {
|
||||
@@ -39,7 +21,9 @@ impl tree_service_server::TreeService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_tree_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_tree_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.list_tree(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -68,7 +52,9 @@ impl tree_service_server::TreeService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_tree_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_tree_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.get_tree(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -97,7 +83,9 @@ impl tree_service_server::TreeService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_tree_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_tree_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.get_blob(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -125,7 +113,9 @@ impl tree_service_server::TreeService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_tree_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_tree_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
let resp = client.get_raw_blob(inner).await?;
|
||||
let stream = super::bridge_server_stream(resp.into_inner());
|
||||
return Ok(tonic::Response::new(stream));
|
||||
@@ -159,7 +149,9 @@ impl tree_service_server::TreeService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_tree_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_tree_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.get_file_metadata(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
@@ -187,7 +179,9 @@ impl tree_service_server::TreeService for GitksService {
|
||||
let gb = match self.resolve(inner.repository.as_ref()) {
|
||||
Ok(gb) => gb,
|
||||
Err(err) if err.code() == tonic::Code::NotFound => {
|
||||
if let Some(mut client) = remote_tree_client(self, inner.repository.as_ref(), false).await? {
|
||||
if let Some(mut client) =
|
||||
remote_tree_client(self, inner.repository.as_ref(), false).await?
|
||||
{
|
||||
return client.find_files(inner).await;
|
||||
}
|
||||
return Err(err);
|
||||
|
||||
Reference in New Issue
Block a user