mod archive; mod blame; mod branch; mod cache; mod commit; mod diff; mod merge; mod pack; mod repository; mod repository_maint; mod tag; mod tree; use std::path::PathBuf; use tokio_stream::wrappers::ReceiverStream; use crate::bare::GitBare; use crate::error::GitError; use crate::pb::{ archive_service_server, blame_service_server, branch_service_server, commit_service_server, diff_service_server, merge_service_server, pack_service_server, repository_service_server, tag_service_server, tree_service_server, }; #[derive(Clone)] pub struct GitksService { /// Root prefix path for all repositories pub repo_prefix: PathBuf, } impl GitksService { fn repo_label(&self, header: Option<&crate::pb::RepositoryHeader>) -> String { header .and_then(|h| { if h.relative_path.is_empty() { None } else { Some(h.relative_path.clone()) } }) .unwrap_or_else(|| "unknown".into()) } pub(crate) fn resolve( &self, header: Option<&crate::pb::RepositoryHeader>, ) -> Result { let header = header.ok_or_else(|| tonic::Status::invalid_argument("repository is required"))?; let header = self.prefixed_header(header); let gb = GitBare::from_repository_header(&header).map_err(into_status)?; tracing::debug!( repo = %gb.bare_dir.display(), "resolved repository" ); Ok(gb) } pub(crate) fn resolve_for_init( &self, header: Option<&crate::pb::RepositoryHeader>, ) -> Result { let header = header.ok_or_else(|| tonic::Status::invalid_argument("repository is required"))?; let relative_path = header.relative_path.trim(); if relative_path.is_empty() { return Err(tonic::Status::invalid_argument("relative_path is required")); } let candidate = self.repo_prefix.join(relative_path); // Path traversal check let canonical = candidate .canonicalize() .unwrap_or_else(|_| candidate.clone()); let prefix_canon = self .repo_prefix .canonicalize() .unwrap_or_else(|_| self.repo_prefix.clone()); if !canonical.starts_with(&prefix_canon) { return Err(tonic::Status::invalid_argument( "path traversal detected: relative_path escapes repo prefix", )); } Ok(canonical) } /// Inject repo_prefix as storage_path into the client-provided header fn prefixed_header(&self, header: &crate::pb::RepositoryHeader) -> crate::pb::RepositoryHeader { crate::pb::RepositoryHeader { storage_path: self.repo_prefix.to_string_lossy().into_owned(), relative_path: header.relative_path.clone(), storage_name: header.storage_name.clone(), } } } pub(crate) fn into_status(e: GitError) -> tonic::Status { match &e { GitError::NotFound(_) | GitError::ObjectNotFound(_) | GitError::RefNotFound(_) | GitError::RepoNotFound => tonic::Status::not_found(e.to_string()), GitError::InvalidArgument(_) => tonic::Status::invalid_argument(e.to_string()), GitError::PermissionDenied(_) => tonic::Status::permission_denied(e.to_string()), GitError::Locked(_) => tonic::Status::failed_precondition(e.to_string()), GitError::AuthFailed(_) => tonic::Status::unauthenticated(e.to_string()), GitError::NotBareRepository => tonic::Status::failed_precondition(e.to_string()), _ => tonic::Status::internal(e.to_string()), } } impl From for tonic::Status { fn from(e: GitError) -> Self { into_status(e) } } pub(crate) fn into_stream( items: Vec, ) -> ReceiverStream> { let (tx, rx) = tokio::sync::mpsc::channel(items.len().max(1)); for item in items { let _ = tx.try_send(Ok(item)); } ReceiverStream::new(rx) } pub(crate) fn git_cmd(gb: &GitBare, args: &[&str]) -> Result { let mut full_args: Vec = vec![ "--git-dir".into(), gb.bare_dir.to_string_lossy().into_owned(), ]; full_args.extend(args.iter().map(|s| s.to_string())); tracing::debug!( repo = %gb.bare_dir.display(), args = %full_args.iter().skip(2).cloned().collect::>().join(" "), "spawning git subprocess" ); let result = std::process::Command::new("git") .args(&full_args) .output() .map_err(|e| { tracing::error!( repo = %gb.bare_dir.display(), error = %e, "failed to spawn git subprocess" ); tonic::Status::internal(e.to_string()) })?; if !result.status.success() { let stderr = String::from_utf8_lossy(&result.stderr); tracing::warn!( repo = %gb.bare_dir.display(), status = ?result.status.code(), stderr = %stderr.trim(), "git subprocess exited with non-zero status" ); } Ok(result) } pub async fn serve( addr: std::net::SocketAddr, repo_prefix: PathBuf, ) -> Result<(), tonic::transport::Error> { let span = tracing::info_span!("gitks.server", %addr); let _enter = span.enter(); let svc = GitksService { repo_prefix }; tracing::info!("registering gRPC services"); let server = tonic::transport::Server::builder() .add_service(repository_service_server::RepositoryServiceServer::new( svc.clone(), )) .add_service(archive_service_server::ArchiveServiceServer::new( svc.clone(), )) .add_service(blame_service_server::BlameServiceServer::new(svc.clone())) .add_service(branch_service_server::BranchServiceServer::new(svc.clone())) .add_service(commit_service_server::CommitServiceServer::new(svc.clone())) .add_service(diff_service_server::DiffServiceServer::new(svc.clone())) .add_service(merge_service_server::MergeServiceServer::new(svc.clone())) .add_service(pack_service_server::PackServiceServer::new(svc.clone())) .add_service(tag_service_server::TagServiceServer::new(svc.clone())) .add_service(tree_service_server::TreeServiceServer::new(svc)); tracing::info!("server ready, starting to accept connections"); server.serve(addr).await }