refactor(git,gingress-proxy): apply rustfmt formatting

This commit is contained in:
ZhenYi 2026-05-14 10:02:00 +08:00
parent 2dcb5b3028
commit 4c4c33f970
28 changed files with 579 additions and 262 deletions

View File

@ -44,19 +44,21 @@ impl GIngressProxy {
/// Match a request to a route rule based on host and path.
fn match_route(cfg: &crate::config::ProxyConfig, host: &str, path: &str) -> Option<String> {
cfg.routes.get(host).and_then(|rules| {
rules.iter().find(|r| match r.path_type {
crate::config::PathType::Prefix | crate::config::PathType::ImplementationSpecific => {
path.starts_with(&r.path)
}
crate::config::PathType::Exact => path == r.path,
cfg.routes
.get(host)
.and_then(|rules| {
rules.iter().find(|r| match r.path_type {
crate::config::PathType::Prefix
| crate::config::PathType::ImplementationSpecific => path.starts_with(&r.path),
crate::config::PathType::Exact => path == r.path,
})
})
.map(|r| {
format!(
"upstream:{}/{}:{}",
r.backend.namespace, r.backend.name, r.backend.port
)
})
}).map(|r| {
format!(
"upstream:{}/{}:{}",
r.backend.namespace, r.backend.name, r.backend.port
)
})
}
}
@ -128,7 +130,11 @@ impl ProxyHttp for GIngressProxy {
}
None => pingora::Error::e_explain(
pingora::ErrorType::InternalError,
format!("no upstream found for host '{}' path '{}'", host, session.req_header().uri.path()),
format!(
"no upstream found for host '{}' path '{}'",
host,
session.req_header().uri.path()
),
),
}
}
@ -143,11 +149,7 @@ impl ProxyHttp for GIngressProxy {
.unwrap()
.run_pre(session, ctx)
.map_err(|e| {
pingora::Error::because(
pingora::ErrorType::InternalError,
"pre-filter failed",
e,
)
pingora::Error::because(pingora::ErrorType::InternalError, "pre-filter failed", e)
})?;
Ok(false)
}

View File

@ -5,9 +5,9 @@
use crate::config::ConfigStore;
use anyhow::Context;
use rustls::ServerConfig;
use rustls::server::ResolvesServerCert;
use rustls::sign::CertifiedKey;
use rustls::ServerConfig;
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
@ -43,12 +43,7 @@ impl SniResolver {
}
/// Add a certificate for a specific hostname.
pub fn add_cert(
&mut self,
host: &str,
cert_pem: &str,
key_pem: &str,
) -> anyhow::Result<()> {
pub fn add_cert(&mut self, host: &str, cert_pem: &str, key_pem: &str) -> anyhow::Result<()> {
let cert_chain = rustls_pemfile::certs(&mut cert_pem.as_bytes())
.collect::<Result<Vec<_>, _>>()
.context("Failed to parse certificate PEM")?;

View File

@ -267,7 +267,12 @@ impl GitDomain {
let obj = match self.repo().find_object(oid, None) {
Ok(o) => o,
Err(e) => {
tracing::warn!("archive_skip_missing_object oid={} path={} error={}", oid, full_path, e);
tracing::warn!(
"archive_skip_missing_object oid={} path={} error={}",
oid,
full_path,
e
);
continue;
}
};
@ -385,7 +390,12 @@ impl GitDomain {
let obj = match self.repo().find_object(oid, None) {
Ok(o) => o,
Err(e) => {
tracing::warn!("archive_skip_missing_object oid={} path={} error={}", oid, full_path, e);
tracing::warn!(
"archive_skip_missing_object oid={} path={} error={}",
oid,
full_path,
e
);
continue;
}
};
@ -464,7 +474,12 @@ impl GitDomain {
let obj = match self.repo().find_object(oid, None) {
Ok(o) => o,
Err(e) => {
tracing::warn!("archive_skip_missing_object oid={} path={} error={}", oid, full_path, e);
tracing::warn!(
"archive_skip_missing_object oid={} path={} error={}",
oid,
full_path,
e
);
continue;
}
};
@ -528,7 +543,12 @@ impl GitDomain {
let obj = match self.repo().find_object(oid, None) {
Ok(o) => o,
Err(e) => {
tracing::warn!("archive_list_skip_missing_object oid={} path={} error={}", oid, full_path, e);
tracing::warn!(
"archive_list_skip_missing_object oid={} path={} error={}",
oid,
full_path,
e
);
continue;
}
};

View File

@ -71,7 +71,7 @@ impl GitDomain {
pub fn branch_delete_remote(&self, name: &str) -> GitResult<()> {
let full_name = format!("refs/remotes/{}", name);
let mut branch = self
let mut branch = self
.repo()
.find_branch(&full_name, BranchType::Local)
.map_err(|_e| GitError::RefNotFound(full_name.clone()))?;

View File

@ -16,7 +16,11 @@ impl GitDomain {
let mut branches = Vec::with_capacity(16);
// Keep head_name as full ref for comparison with branch names
let head_name = self.repo.head().ok().and_then(|r| r.name().map(String::from));
let head_name = self
.repo
.head()
.ok()
.and_then(|r| r.name().map(String::from));
for branch_result in self
.repo()
@ -94,7 +98,11 @@ impl GitDomain {
self.repo
.find_branch(full_name, git2::BranchType::Local)
.ok()
.or_else(|| self.repo.find_branch(full_name, git2::BranchType::Remote).ok())
.or_else(|| {
self.repo
.find_branch(full_name, git2::BranchType::Remote)
.ok()
})
})
.ok_or_else(|| GitError::RefNotFound(name.to_string()))?;
@ -139,9 +147,7 @@ impl GitDomain {
};
candidates.iter().any(|full_name| {
self.repo
.find_branch(full_name, BranchType::Local)
.is_ok()
self.repo.find_branch(full_name, BranchType::Local).is_ok()
|| self
.repo()
.find_branch(full_name, BranchType::Remote)

View File

@ -206,7 +206,9 @@ impl GitDomain {
return Ok(CommitOid::from_git2(target));
}
}
return Err(GitError::InvalidOid("cannot resolve: HEAD (detached or empty)".into()));
return Err(GitError::InvalidOid(
"cannot resolve: HEAD (detached or empty)".into(),
));
}
if let Ok(reference) = self.repo.find_reference(rev) {

View File

@ -21,7 +21,8 @@ impl GitDomain {
let o = oid
.to_oid()
.map_err(|_| GitError::InvalidOid(oid.to_string()))?;
let obj = self.repo()
let obj = self
.repo()
.find_object(o, None)
.map_err(|e| GitError::Internal(e.to_string()))?;
Some(
@ -36,7 +37,8 @@ impl GitDomain {
let o = oid
.to_oid()
.map_err(|_| GitError::InvalidOid(oid.to_string()))?;
let obj = self.repo()
let obj = self
.repo()
.find_object(o, None)
.map_err(|e| GitError::Internal(e.to_string()))?;
Some(
@ -196,11 +198,21 @@ impl GitDomain {
.to_oid()
.map_err(|_| GitError::InvalidOid(new_tree.to_string()))?;
let old_obj = self.repo().find_object(old_oid, None).map_err(|e| GitError::Internal(e.to_string()))?;
let new_obj = self.repo().find_object(new_oid, None).map_err(|e| GitError::Internal(e.to_string()))?;
let old_tree = old_obj.peel_to_tree().map_err(|e| GitError::Internal(e.to_string()))?;
let new_tree = new_obj.peel_to_tree().map_err(|e| GitError::Internal(e.to_string()))?;
let old_obj = self
.repo()
.find_object(old_oid, None)
.map_err(|e| GitError::Internal(e.to_string()))?;
let new_obj = self
.repo()
.find_object(new_oid, None)
.map_err(|e| GitError::Internal(e.to_string()))?;
let old_tree = old_obj
.peel_to_tree()
.map_err(|e| GitError::Internal(e.to_string()))?;
let new_tree = new_obj
.peel_to_tree()
.map_err(|e| GitError::Internal(e.to_string()))?;
let diff = self
.repo()
@ -222,11 +234,21 @@ impl GitDomain {
.to_oid()
.map_err(|_| GitError::InvalidOid(new_tree.to_string()))?;
let old_obj = self.repo().find_object(old_oid, None).map_err(|e| GitError::Internal(e.to_string()))?;
let new_obj = self.repo().find_object(new_oid, None).map_err(|e| GitError::Internal(e.to_string()))?;
let old_tree = old_obj.peel_to_tree().map_err(|e| GitError::Internal(e.to_string()))?;
let new_tree = new_obj.peel_to_tree().map_err(|e| GitError::Internal(e.to_string()))?;
let old_obj = self
.repo()
.find_object(old_oid, None)
.map_err(|e| GitError::Internal(e.to_string()))?;
let new_obj = self
.repo()
.find_object(new_oid, None)
.map_err(|e| GitError::Internal(e.to_string()))?;
let old_tree = old_obj
.peel_to_tree()
.map_err(|e| GitError::Internal(e.to_string()))?;
let new_tree = new_obj
.peel_to_tree()
.map_err(|e| GitError::Internal(e.to_string()))?;
let diff = self
.repo()

View File

@ -4,5 +4,8 @@ use models::TagEmbedInput;
/// Defined here to avoid git → agent dependency.
#[async_trait::async_trait]
pub trait TagEmbedder: Send + Sync {
async fn embed_tags_batch(&self, tags: Vec<TagEmbedInput>) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
async fn embed_tags_batch(
&self,
tags: Vec<TagEmbedInput>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
}

View File

@ -12,8 +12,8 @@ pub mod sync;
pub mod webhook_dispatch;
pub use embed::TagEmbedder;
pub use pool::{HookWorker, PoolConfig, RedisConsumer};
pub use pool::types::{HookTask, TaskType};
pub use pool::{HookWorker, PoolConfig, RedisConsumer};
/// Hook service that manages the Redis-backed task queue worker.
/// Multiple gitserver pods can run concurrently — the worker acquires a
@ -28,12 +28,7 @@ pub struct HookService {
}
impl HookService {
pub fn new(
db: AppDatabase,
cache: AppCache,
redis_pool: RedisPool,
config: AppConfig,
) -> Self {
pub fn new(db: AppDatabase, cache: AppCache, redis_pool: RedisPool, config: AppConfig) -> Self {
Self {
db,
cache,

View File

@ -8,8 +8,25 @@ use std::time::Duration;
/// NATS consumer function type: returns (task, ack_fn) pairs.
pub type NatsHookConsumeFn = Arc<
dyn Fn(String, usize) -> Pin<Box<dyn Future<Output = anyhow::Result<Vec<(Vec<u8>, Box<dyn Fn() -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send>> + Send>)>>> + Send>>
+ Send
dyn Fn(
String,
usize,
) -> Pin<
Box<
dyn Future<
Output = anyhow::Result<
Vec<(
Vec<u8>,
Box<
dyn Fn() -> Pin<
Box<dyn Future<Output = anyhow::Result<()>> + Send>,
> + Send,
>,
)>,
>,
> + Send,
>,
> + Send
+ Sync,
>;
@ -101,7 +118,11 @@ impl RedisConsumer {
match serde_json::from_slice::<HookTask>(&data) {
Ok(task) => {
let task_json = String::from_utf8_lossy(&data).to_string();
tracing::debug!("task dequeued from NATS task_id={} task_type={}", task.id, task.task_type);
tracing::debug!(
"task dequeued from NATS task_id={} task_type={}",
task.id,
task.task_type
);
// Store ack_fn for later use - we'll need to refactor to support async ack
// For now, we'll ack immediately after processing in the worker
@ -141,12 +162,21 @@ impl RedisConsumer {
Some(json) => {
match serde_json::from_str::<HookTask>(&json) {
Ok(task) => {
tracing::debug!("task dequeued task_id={} task_type={} queue={}", task.id, task.task_type, queue_key);
tracing::debug!(
"task dequeued task_id={} task_type={} queue={}",
task.id,
task.task_type,
queue_key
);
Ok(Some((task, json)))
}
Err(e) => {
// Malformed task — remove from work queue and discard
tracing::warn!("malformed task JSON, discarding error={} queue={}", e, work_key);
tracing::warn!(
"malformed task JSON, discarding error={} queue={}",
e,
work_key
);
let _ = self.ack_raw(&work_key, &json).await;
Ok(None)
}
@ -190,7 +220,8 @@ impl RedisConsumer {
queue_key: &str,
task_json: &str,
) -> Result<(), GitError> {
self.nak_with_retry(work_key, queue_key, task_json, task_json).await
self.nak_with_retry(work_key, queue_key, task_json, task_json)
.await
}
/// Negative acknowledge with a different (updated) task JSON — used to

View File

@ -6,8 +6,8 @@ use crate::hook::sync::HookMetaDataSync;
use db::cache::AppCache;
use db::database::AppDatabase;
use metrics::counter;
use models::repos::repo_tag;
use models::EntityTrait;
use models::repos::repo_tag;
use sea_orm::{ColumnTrait, QueryFilter};
use std::sync::Arc;
use std::time::Duration;
@ -100,8 +100,12 @@ impl HookWorker {
work_key: &str,
queue_key: &str,
) {
tracing::info!("task started task_id={} task_type={} repo_id={}",
task.id, task.task_type, task.repo_id);
tracing::info!(
"task started task_id={} task_type={} repo_id={}",
task.id,
task.task_type,
task.repo_id
);
counter!("hook_tasks_total", "task_type" => task.task_type.to_string()).increment(1);
@ -113,7 +117,8 @@ impl HookWorker {
match result {
Ok(()) => {
counter!("hook_tasks_success_total", "task_type" => task.task_type.to_string()).increment(1);
counter!("hook_tasks_success_total", "task_type" => task.task_type.to_string())
.increment(1);
if let Err(e) = self.consumer.ack(work_key, task_json).await {
tracing::warn!("failed to ack task: {}", e);
}
@ -125,19 +130,31 @@ impl HookWorker {
if is_locked {
counter!("hook_tasks_locked_total").increment(1);
// Another worker holds the lock — requeue without counting as retry.
tracing::info!("repo locked by another worker, requeueing task_id={}", task.id);
tracing::info!(
"repo locked by another worker, requeueing task_id={}",
task.id
);
if let Err(nak_err) = self.consumer.nak(work_key, queue_key, task_json).await {
tracing::warn!("failed to requeue locked task: {}", nak_err);
}
} else {
counter!("hook_tasks_failed_total", "task_type" => task.task_type.to_string()).increment(1);
tracing::warn!("task failed task_id={} task_type={} repo_id={} error={}",
task.id, task.task_type, task.repo_id, e);
counter!("hook_tasks_failed_total", "task_type" => task.task_type.to_string())
.increment(1);
tracing::warn!(
"task failed task_id={} task_type={} repo_id={} error={}",
task.id,
task.task_type,
task.repo_id,
e
);
if task.retry_count >= self.max_retries {
counter!("hook_tasks_exhausted_total").increment(1);
tracing::warn!("task exhausted retries, discarding task_id={} retry_count={}",
task.id, task.retry_count);
tracing::warn!(
"task exhausted retries, discarding task_id={} retry_count={}",
task.id,
task.retry_count
);
let _ = self.consumer.ack(work_key, task_json).await;
} else {
counter!("hook_tasks_retried_total").increment(1);
@ -197,9 +214,8 @@ impl HookWorker {
// Run full sync (internally acquires/releases per-repo lock)
let sync_clone = sync.clone();
tokio::task::spawn_blocking(move || {
let result = tokio::runtime::Handle::current().block_on(async {
sync_clone.sync().await
});
let result =
tokio::runtime::Handle::current().block_on(async { sync_clone.sync().await });
match result {
Ok(()) => Ok::<(), GitError>(()),
Err(e) => Err(GitError::Internal(e.to_string())),

View File

@ -91,7 +91,11 @@ impl HookMetaDataSync {
} else if is_branch && !is_remote {
// Try to get upstream branch name from the reference's upstream target
let upstream: Option<String> = if reference.target().is_some() {
if let Ok(branch) = self.domain.repo().find_branch(&name, git2::BranchType::Local) {
if let Ok(branch) = self
.domain
.repo()
.find_branch(&name, git2::BranchType::Local)
{
if let Ok(upstream_ref) = branch.upstream() {
if let Some(upstream_name) = upstream_ref.name().ok().flatten() {
Some(upstream_name.to_string())
@ -132,8 +136,7 @@ impl HookMetaDataSync {
.all(txn)
.await
.map_err(|e| GitError::IoError(format!("failed to query branches: {}", e)))?;
let mut existing_names: HashSet<String> =
existing.iter().map(|r| r.name.clone()).collect();
let mut existing_names: HashSet<String> = existing.iter().map(|r| r.name.clone()).collect();
let (branches, _) = self.collect_git_refs()?;
@ -155,7 +158,10 @@ impl HookMetaDataSync {
if current_default.is_none() {
// Prefer known branch names over first-come
for preferred in PREFERRED_BRANCHES {
if branches.iter().any(|b| b.shorthand == *preferred && b.is_branch && !b.is_remote) {
if branches
.iter()
.any(|b| b.shorthand == *preferred && b.is_branch && !b.is_remote)
{
auto_detected_branch = Some(ToString::to_string(preferred));
break;
}

View File

@ -8,11 +8,11 @@ pub mod tag;
use db::cache::AppCache;
use db::database::AppDatabase;
use models::ActiveModelTrait;
use models::RepoId;
use models::projects::project_skill::ActiveModel as SkillActiveModel;
use models::projects::project_skill::{Column as SkillCol, Entity as SkillEntity};
use models::repos::repo::Model as RepoModel;
use models::ActiveModelTrait;
use models::RepoId;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, Set};
use std::collections::HashMap;
use std::path::Path;
@ -160,7 +160,9 @@ fn scan_skills_from_tree(
match entry.kind() {
Some(git2::ObjectType::Tree) => {
if !name.starts_with('.') {
if let Ok(subtree) = entry.to_object(git_repo).and_then(|o| o.peel_to_tree()) {
if let Ok(subtree) =
entry.to_object(git_repo).and_then(|o| o.peel_to_tree())
{
stack.push((subtree, entry_path));
}
}
@ -406,16 +408,19 @@ impl HookMetaDataSync {
};
// Deduplicate by {repo_id}+{blob_hash}, keep latest by commit_sha
let mut deduped: std::collections::HashMap<String, DiscoveredSkill> = std::collections::HashMap::new();
let mut deduped: std::collections::HashMap<String, DiscoveredSkill> =
std::collections::HashMap::new();
for skill in discovered {
let key = if let Some(ref hash) = skill.blob_hash {
format!("{}:{}", self.repo.id, hash)
} else {
format!("{}:{}:slug", self.repo.id, skill.slug)
};
format!("{}:{}", self.repo.id, hash)
} else {
format!("{}:{}:slug", self.repo.id, skill.slug)
};
match deduped.get(&key) {
Some(existing) => {
if skill.commit_sha.as_ref().unwrap_or(&String::new()) > existing.commit_sha.as_ref().unwrap_or(&String::new()) {
if skill.commit_sha.as_ref().unwrap_or(&String::new())
> existing.commit_sha.as_ref().unwrap_or(&String::new())
{
deduped.insert(key, skill);
}
}
@ -428,7 +433,11 @@ impl HookMetaDataSync {
let existing_by_hash: HashMap<_, _> = existing
.into_iter()
.map(|s| {
let key = format!("{}:{}", s.repo_id.unwrap_or_default(), s.blob_hash.clone().unwrap_or_default());
let key = format!(
"{}:{}",
s.repo_id.unwrap_or_default(),
s.blob_hash.clone().unwrap_or_default()
);
(key, s)
})
.collect();

View File

@ -1,6 +1,6 @@
use crate::hook::sync::commit::TagTip;
use crate::GitError;
use crate::hook::sync::HookMetaDataSync;
use crate::hook::sync::commit::TagTip;
use db::database::AppTransaction;
use models::repos::repo_tag;
use sea_orm::prelude::Expr;
@ -79,9 +79,15 @@ impl HookMetaDataSync {
.filter(repo_tag::Column::Repo.eq(repo_id))
.filter(repo_tag::Column::Name.eq(&tag.name))
.col_expr(repo_tag::Column::Oid, Expr::value(&tag.target_oid))
.col_expr(repo_tag::Column::Description, Expr::value(tag.description.clone()))
.col_expr(
repo_tag::Column::Description,
Expr::value(tag.description.clone()),
)
.col_expr(repo_tag::Column::TaggerName, Expr::value(&tag.tagger_name))
.col_expr(repo_tag::Column::TaggerEmail, Expr::value(&tag.tagger_email))
.col_expr(
repo_tag::Column::TaggerEmail,
Expr::value(&tag.tagger_email),
)
.exec(txn)
.await
.map_err(|e| GitError::IoError(format!("failed to update tag: {}", e)))?;

View File

@ -309,15 +309,28 @@ pub async fn dispatch_repo_webhooks(
.await
{
Ok(Ok(())) => {
tracing::info!("push webhook delivered webhook_id={} url={}", webhook_id, url);
tracing::info!(
"push webhook delivered webhook_id={} url={}",
webhook_id,
url
);
let _ = touch_webhook(db, webhook_id, true).await;
}
Ok(Err(e)) => {
tracing::warn!("push webhook delivery failed webhook_id={} url={} error={}", webhook_id, url, e);
tracing::warn!(
"push webhook delivery failed webhook_id={} url={} error={}",
webhook_id,
url,
e
);
let _ = touch_webhook(db, webhook_id, false).await;
}
Err(_) => {
tracing::warn!("push webhook timed out webhook_id={} url={}", webhook_id, url);
tracing::warn!(
"push webhook timed out webhook_id={} url={}",
webhook_id,
url
);
let _ = touch_webhook(db, webhook_id, false).await;
}
}
@ -363,15 +376,28 @@ pub async fn dispatch_repo_webhooks(
.await
{
Ok(Ok(())) => {
tracing::info!("tag webhook delivered webhook_id={} url={}", webhook_id, url);
tracing::info!(
"tag webhook delivered webhook_id={} url={}",
webhook_id,
url
);
let _ = touch_webhook(db, webhook_id, true).await;
}
Ok(Err(e)) => {
tracing::warn!("tag webhook delivery failed webhook_id={} url={} error={}", webhook_id, url, e);
tracing::warn!(
"tag webhook delivery failed webhook_id={} url={} error={}",
webhook_id,
url,
e
);
let _ = touch_webhook(db, webhook_id, false).await;
}
Err(_) => {
tracing::warn!("tag webhook timed out webhook_id={} url={}", webhook_id, url);
tracing::warn!(
"tag webhook timed out webhook_id={} url={}",
webhook_id,
url
);
let _ = touch_webhook(db, webhook_id, false).await;
}
}
@ -380,10 +406,14 @@ pub async fn dispatch_repo_webhooks(
}
}
async fn touch_webhook(db: &AppDatabase, webhook_id: i64, success: bool) -> Result<(), sea_orm::DbErr> {
async fn touch_webhook(
db: &AppDatabase,
webhook_id: i64,
success: bool,
) -> Result<(), sea_orm::DbErr> {
use models::repos::repo_webhook::{Column as RwCol, Entity as RepoWebhookEntity};
use models::{ColumnTrait, EntityTrait, QueryFilter};
use sea_orm::{sea_query::Expr, ExprTrait};
use sea_orm::{ExprTrait, sea_query::Expr};
let result: Result<sea_orm::UpdateResult, sea_orm::DbErr> = if success {
RepoWebhookEntity::update_many()

View File

@ -98,7 +98,12 @@ impl GitHttpHandler {
mut payload: web::Payload,
) -> Result<HttpResponse, Error> {
let started = Instant::now();
tracing::info!("git_rpc_started service={} repo={} repo_id={}", service, self.repo.repo_name, self.repo.id.to_string());
tracing::info!(
"git_rpc_started service={} repo={} repo_id={}",
service,
self.repo.repo_name,
self.repo.id.to_string()
);
let mut child = tokio::process::Command::new("git")
.arg(service)
.arg("--stateless-rpc")
@ -140,7 +145,12 @@ impl GitHttpHandler {
// Reject oversized pre-PACK data to prevent memory exhaustion
if pre_pack.len() + bytes.len() > PRE_PACK_LIMIT {
tracing::warn!("git_rpc_payload_too_large service={} repo={} repo_id={}", service, self.repo.repo_name, self.repo.id.to_string());
tracing::warn!(
"git_rpc_payload_too_large service={} repo={} repo_id={}",
service,
self.repo.repo_name,
self.repo.id.to_string()
);
return Err(actix_web::error::ErrorPayloadTooLarge(format!(
"Ref negotiation exceeds {} byte limit",
PRE_PACK_LIMIT
@ -151,7 +161,12 @@ impl GitHttpHandler {
pre_pack.extend_from_slice(&bytes[..pos]);
if let Err(msg) = check_branch_protection(&branch_protects, &pre_pack) {
tracing::warn!("branch_protection_violation repo={} repo_id={} message={}", self.repo.repo_name, self.repo.id.to_string(), msg);
tracing::warn!(
"branch_protection_violation repo={} repo_id={} message={}",
self.repo.repo_name,
self.repo.id.to_string(),
msg
);
return Err(actix_web::error::ErrorForbidden(msg));
}
@ -212,7 +227,14 @@ impl GitHttpHandler {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let ms = started.elapsed().as_millis() as u64;
tracing::error!("git_rpc_failed service={} repo={} repo_id={} duration_ms={} stderr={}", service, self.repo.repo_name, self.repo.id.to_string(), ms, stderr.to_string());
tracing::error!(
"git_rpc_failed service={} repo={} repo_id={} duration_ms={} stderr={}",
service,
self.repo.repo_name,
self.repo.id.to_string(),
ms,
stderr.to_string()
);
return Err(actix_web::error::ErrorInternalServerError(format!(
"Git command failed: {}",
stderr
@ -220,7 +242,14 @@ impl GitHttpHandler {
}
let ms = started.elapsed().as_millis() as u64;
tracing::info!("git_rpc_completed service={} repo={} repo_id={} duration_ms={} bytes_out={}", service, self.repo.repo_name, self.repo.id.to_string(), ms, output.stdout.len());
tracing::info!(
"git_rpc_completed service={} repo={} repo_id={} duration_ms={} bytes_out={}",
service,
self.repo.repo_name,
self.repo.id.to_string(),
ms,
output.stdout.len()
);
Ok(HttpResponse::Ok()
.content_type(format!("application/x-git-{}-result", service))

View File

@ -294,8 +294,13 @@ impl LfsHandler {
let token = uuid::Uuid::now_v7().to_string();
crate::http::lfs_routes::store_lfs_token(
cache, &token, self.model.id, user_uid, "upload",
).await;
cache,
&token,
self.model.id,
user_uid,
"upload",
)
.await;
let mut headers = HashMap::new();
headers.insert("authorization".to_string(), format!("Bearer {}", token));
@ -320,8 +325,13 @@ impl LfsHandler {
let token = uuid::Uuid::now_v7().to_string();
crate::http::lfs_routes::store_lfs_token(
cache, &token, self.model.id, user_uid, "download",
).await;
cache,
&token,
self.model.id,
user_uid,
"download",
)
.await;
let mut headers = HashMap::new();
headers.insert("authorization".to_string(), format!("Bearer {}", token));
@ -461,10 +471,7 @@ impl LfsHandler {
Ok(HttpResponse::Ok().finish())
}
pub async fn download_object(
&self,
oid: &str,
) -> Result<HttpResponse, GitError> {
pub async fn download_object(&self, oid: &str) -> Result<HttpResponse, GitError> {
if !is_valid_lfs_oid(oid) {
return Err(GitError::InvalidOid(format!("Invalid OID format: {}", oid)));
}
@ -481,7 +488,10 @@ impl LfsHandler {
let expected_base = self.get_lfs_storage_path();
let obj_path = PathBuf::from(&obj.storage_path);
if !obj_path.starts_with(&expected_base) {
tracing::error!("LFS object path outside storage directory: {}", obj.storage_path);
tracing::error!(
"LFS object path outside storage directory: {}",
obj.storage_path
);
return Err(GitError::AuthFailed("Invalid object path".to_string()));
}

View File

@ -36,10 +36,7 @@ fn hash_token(token: &str) -> Result<String, argon2::password_hash::Error> {
}
/// Derive the acting user from the authenticated bearer token.
async fn user_uid(
req: &HttpRequest,
db: &db::database::AppDatabase,
) -> Result<uuid::Uuid, Error> {
async fn user_uid(req: &HttpRequest, db: &db::database::AppDatabase) -> Result<uuid::Uuid, Error> {
let auth_header = req
.headers()
.get("authorization")
@ -61,8 +58,8 @@ async fn user_uid(
.await
.map_err(|_| actix_web::error::ErrorUnauthorized("Authentication failed"))?;
let token_model = token_model
.ok_or_else(|| actix_web::error::ErrorUnauthorized("Invalid token"))?;
let token_model =
token_model.ok_or_else(|| actix_web::error::ErrorUnauthorized("Invalid token"))?;
if let Some(expires_at) = token_model.expires_at {
if expires_at < chrono::Utc::now() {
@ -124,7 +121,9 @@ async fn validate_lfs_token(
let operation = parts[2];
if repo_id != expected_repo_id {
return Err(actix_web::error::ErrorUnauthorized("Token not valid for this repo"));
return Err(actix_web::error::ErrorUnauthorized(
"Token not valid for this repo",
));
}
if operation != expected_operation {
return Err(actix_web::error::ErrorUnauthorized(
@ -133,7 +132,8 @@ async fn validate_lfs_token(
}
// Consume the token (one-time use)
let _: Result<(), redis::RedisError> = conn.del(format!("lfs:token:{}", token)).await;
let _: Result<(), redis::RedisError> =
conn.del(format!("lfs:token:{}", token)).await;
return Ok(user_uid);
}
@ -252,7 +252,9 @@ pub async fn lfs_download(
Ok(response) => Ok(response),
Err(GitError::NotFound(_)) => Err(actix_web::error::ErrorNotFound("Object not found")),
Err(GitError::AuthFailed(_)) => Err(actix_web::error::ErrorUnauthorized("Unauthorized")),
Err(_e) => Err(actix_web::error::ErrorInternalServerError("Download failed")),
Err(_e) => Err(actix_web::error::ErrorInternalServerError(
"Download failed",
)),
}
}
@ -295,7 +297,9 @@ pub async fn lfs_lock_list(
match handler.list_locks(maybe_oid).await {
Ok(list) => Ok(HttpResponse::Ok().json(list)),
Err(_e) => Err(actix_web::error::ErrorInternalServerError("Lock list failed")),
Err(_e) => Err(actix_web::error::ErrorInternalServerError(
"Lock list failed",
)),
}
}
@ -317,7 +321,9 @@ pub async fn lfs_lock_get(
match handler.get_lock(&lock_path).await {
Ok(lock) => Ok(HttpResponse::Ok().json(lock)),
Err(GitError::NotFound(_)) => Err(actix_web::error::ErrorNotFound("Lock not found")),
Err(_e) => Err(actix_web::error::ErrorInternalServerError("Lock get failed")),
Err(_e) => Err(actix_web::error::ErrorInternalServerError(
"Lock get failed",
)),
}
}
@ -337,6 +343,8 @@ pub async fn lfs_lock_delete(
Ok(()) => Ok(HttpResponse::NoContent().finish()),
Err(GitError::PermissionDenied(_)) => Err(actix_web::error::ErrorForbidden("Not allowed")),
Err(GitError::NotFound(_)) => Err(actix_web::error::ErrorNotFound("Lock not found")),
Err(_e) => Err(actix_web::error::ErrorInternalServerError("Lock delete failed")),
Err(_e) => Err(actix_web::error::ErrorInternalServerError(
"Lock delete failed",
)),
}
}
}

View File

@ -1,5 +1,5 @@
use crate::hook::HookService;
use actix_web::{App, HttpServer, HttpResponse, web};
use actix_web::{App, HttpResponse, HttpServer, web};
use config::AppConfig;
use db::cache::AppCache;
use db::database::AppDatabase;
@ -71,45 +71,45 @@ pub fn git_http_cfg(cfg: &mut web::ServiceConfig) {
cfg.route("/robots.txt", web::get().to(robots))
.route("/health", web::get().to(health))
.route(
"/{namespace}/{repo_name}.git/info/refs",
web::get().to(routes::info_refs),
)
.route(
"/{namespace}/{repo_name}.git/git-upload-pack",
web::post().to(routes::upload_pack),
)
.route(
"/{namespace}/{repo_name}.git/git-receive-pack",
web::post().to(routes::receive_pack),
)
.route(
"/{namespace}/{repo_name}.git/info/lfs/objects/batch",
web::post().to(lfs_routes::lfs_batch),
)
.route(
"/{namespace}/{repo_name}.git/info/lfs/objects/{oid}",
web::put().to(lfs_routes::lfs_upload),
)
.route(
"/{namespace}/{repo_name}.git/info/lfs/objects/{oid}",
web::get().to(lfs_routes::lfs_download),
)
.route(
"/{namespace}/{repo_name}.git/info/lfs/locks",
web::post().to(lfs_routes::lfs_lock_create),
)
.route(
"/{namespace}/{repo_name}.git/info/lfs/locks",
web::get().to(lfs_routes::lfs_lock_list),
)
.route(
"/{namespace}/{repo_name}.git/info/lfs/locks/{id}",
web::get().to(lfs_routes::lfs_lock_get),
)
.route(
"/{namespace}/{repo_name}.git/info/lfs/locks/{id}",
web::delete().to(lfs_routes::lfs_lock_delete),
);
"/{namespace}/{repo_name}.git/info/refs",
web::get().to(routes::info_refs),
)
.route(
"/{namespace}/{repo_name}.git/git-upload-pack",
web::post().to(routes::upload_pack),
)
.route(
"/{namespace}/{repo_name}.git/git-receive-pack",
web::post().to(routes::receive_pack),
)
.route(
"/{namespace}/{repo_name}.git/info/lfs/objects/batch",
web::post().to(lfs_routes::lfs_batch),
)
.route(
"/{namespace}/{repo_name}.git/info/lfs/objects/{oid}",
web::put().to(lfs_routes::lfs_upload),
)
.route(
"/{namespace}/{repo_name}.git/info/lfs/objects/{oid}",
web::get().to(lfs_routes::lfs_download),
)
.route(
"/{namespace}/{repo_name}.git/info/lfs/locks",
web::post().to(lfs_routes::lfs_lock_create),
)
.route(
"/{namespace}/{repo_name}.git/info/lfs/locks",
web::get().to(lfs_routes::lfs_lock_list),
)
.route(
"/{namespace}/{repo_name}.git/info/lfs/locks/{id}",
web::get().to(lfs_routes::lfs_lock_get),
)
.route(
"/{namespace}/{repo_name}.git/info/lfs/locks/{id}",
web::delete().to(lfs_routes::lfs_lock_delete),
);
}
pub async fn run_http(config: AppConfig) -> anyhow::Result<()> {

View File

@ -58,13 +58,21 @@ impl RateLimiter {
}
pub async fn is_read_allowed(&self) -> bool {
self.is_allowed("global:read", BucketOp::Read, self.config.read_requests_per_window)
.await
self.is_allowed(
"global:read",
BucketOp::Read,
self.config.read_requests_per_window,
)
.await
}
pub async fn is_write_allowed(&self) -> bool {
self.is_allowed("global:write", BucketOp::Write, self.config.write_requests_per_window)
.await
self.is_allowed(
"global:write",
BucketOp::Write,
self.config.write_requests_per_window,
)
.await
}
pub async fn is_repo_write_allowed(&self, repo_path: &str) -> bool {

View File

@ -1,6 +1,6 @@
use actix_web::{Error, HttpRequest};
use argon2::password_hash::{SaltString, PasswordHasher};
use argon2::Argon2;
use argon2::password_hash::{PasswordHasher, SaltString};
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use db::database::AppDatabase;

View File

@ -36,9 +36,9 @@ pub use diff::types::{
};
pub use domain::GitDomain;
pub use error::{GitError, GitResult};
pub use hook::pool::types::{HookTask, TaskType};
pub use hook::pool::PoolConfig;
pub use hook::pool::HookWorker;
pub use hook::pool::PoolConfig;
pub use hook::pool::types::{HookTask, TaskType};
pub use hook::sync::HookMetaDataSync;
pub use lfs::types::{LfsConfig, LfsEntry, LfsOid, LfsPointer};
pub use merge::types::{MergeAnalysisResult, MergeOptions, MergePreferenceResult, MergeheadInfo};

View File

@ -111,7 +111,10 @@ impl SshAuthService {
} else {
fingerprint.clone()
};
tracing::info!("looking up user with SSH key fingerprint={}", fingerprint_preview);
tracing::info!(
"looking up user with SSH key fingerprint={}",
fingerprint_preview
);
let ssh_key = user_ssh_key::Entity::find()
.filter(user_ssh_key::Column::Fingerprint.eq(&fingerprint))
@ -128,7 +131,11 @@ impl SshAuthService {
};
if self.is_key_expired(&ssh_key) {
tracing::warn!("SSH key expired key_id={} expires_at={:?}", ssh_key.id, ssh_key.expires_at);
tracing::warn!(
"SSH key expired key_id={} expires_at={:?}",
ssh_key.id,
ssh_key.expires_at
);
return Ok(None);
}
@ -138,7 +145,11 @@ impl SshAuthService {
.await?;
if let Some(ref user) = user_model {
tracing::info!("user authenticated via SSH key user={} key={}", user.username, ssh_key.title);
tracing::info!(
"user authenticated via SSH key user={} key={}",
user.username,
ssh_key.title
);
self.update_key_last_used_async(ssh_key.id);
}
@ -158,15 +169,16 @@ impl SshAuthService {
let db_clone = self.db.clone();
tokio::spawn(async move {
if let Err(e) = Self::update_key_last_used_sync(db_clone, key_id).await {
tracing::warn!("failed to update key last_used key_id={} error={}", key_id, e);
tracing::warn!(
"failed to update key last_used key_id={} error={}",
key_id,
e
);
}
});
}
async fn update_key_last_used_sync(
db: AppDatabase,
key_id: i64,
) -> Result<(), DbErr> {
async fn update_key_last_used_sync(db: AppDatabase, key_id: i64) -> Result<(), DbErr> {
let key = user_ssh_key::Entity::find_by_id(key_id)
.one(db.reader())
.await?;
@ -191,7 +203,11 @@ impl SshAuthService {
is_write: bool,
) -> bool {
if repo.created_by == user.uid {
tracing::info!("user is repo owner user={} repo={}", user.username, repo.repo_name);
tracing::info!(
"user is repo owner user={} repo={}",
user.username,
repo.repo_name
);
return true;
}
@ -205,7 +221,11 @@ impl SshAuthService {
.await
.unwrap_or(false)
{
tracing::info!("user has collaborator access user={} repo={}", user.username, repo.repo_name);
tracing::info!(
"user has collaborator access user={} repo={}",
user.username,
repo.repo_name
);
return true;
}
@ -215,11 +235,20 @@ impl SshAuthService {
.await
.unwrap_or(false)
{
tracing::info!("user has project member access user={} repo={}", user.username, repo.repo_name);
tracing::info!(
"user has project member access user={} repo={}",
user.username,
repo.repo_name
);
return true;
}
tracing::warn!("access denied user={} repo={} is_write={}", user.username, repo.repo_name, is_write);
tracing::warn!(
"access denied user={} repo={} is_write={}",
user.username,
repo.repo_name,
is_write
);
false
}

View File

@ -5,8 +5,7 @@ use models::repos::repo_branch_protect;
/// (e.g. "refs/heads/main" matches "refs/heads/main" and "refs/heads/main/*"
/// but NOT "refs/heads/main-v2").
fn ref_matches_protection(ref_name: &str, protection_branch: &str) -> bool {
ref_name == protection_branch
|| ref_name.starts_with(&format!("{}/", protection_branch))
ref_name == protection_branch || ref_name.starts_with(&format!("{}/", protection_branch))
}
/// Granular branch protection check (same logic as HTTP handler).

View File

@ -1,5 +1,5 @@
use russh::server::Handle;
use russh::ChannelId;
use russh::server::Handle;
use std::future::Future;
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncReadExt};

View File

@ -53,9 +53,15 @@ pub fn build_git_command(service: GitService, path: PathBuf) -> tokio::process::
cmd.current_dir(cwd);
match service {
GitService::UploadPack => { cmd.arg("upload-pack"); }
GitService::ReceivePack => { cmd.arg("receive-pack"); }
GitService::UploadArchive => { cmd.arg("upload-archive"); }
GitService::UploadPack => {
cmd.arg("upload-pack");
}
GitService::ReceivePack => {
cmd.arg("receive-pack");
}
GitService::UploadArchive => {
cmd.arg("upload-archive");
}
}
cmd.arg(".")

View File

@ -2,6 +2,10 @@ use crate::ssh::ReceiveSyncService;
use crate::ssh::RepoReceiveSyncTask;
use crate::ssh::SshTokenService;
use crate::ssh::authz::SshAuthService;
use crate::ssh::branch_protect::check_branch_protection;
use crate::ssh::forward::forward;
use crate::ssh::git_service::{GitService, build_git_command, parse_git_command, parse_repo_path};
use crate::ssh::ref_update::RefUpdate;
use db::cache::AppCache;
use db::database::AppDatabase;
use models::repos::{repo, repo_branch_protect};
@ -9,11 +13,6 @@ use models::users::user;
use russh::keys::{Certificate, PublicKey};
use russh::server::{Auth, Msg, Session};
use russh::{Channel, ChannelId, Disconnect};
use crate::ssh::ref_update::RefUpdate;
use crate::ssh::git_service::{GitService, parse_git_command, parse_repo_path, build_git_command};
use crate::ssh::branch_protect::check_branch_protection;
use crate::ssh::forward::forward;
use tokio_util::bytes::Bytes;
use sea_orm::ColumnTrait;
use sea_orm::EntityTrait;
use sea_orm::QueryFilter;
@ -23,6 +22,7 @@ use std::net::SocketAddr;
use std::path::PathBuf;
use std::process::Stdio;
use std::time::Duration;
use tokio_util::bytes::Bytes;
const PRE_PACK_LIMIT: usize = 1_048_576;
use tokio::io::AsyncWriteExt;
@ -131,7 +131,6 @@ impl russh::server::Handler for SSHandle {
.map(|addr| format!("{}", addr))
.unwrap_or_else(|| "unknown".to_string());
if token.is_empty() {
tracing::warn!("auth_rejected_empty_token client={}", client_info);
return Err(russh::Error::NotAuthenticated);
@ -151,7 +150,11 @@ impl russh::server::Handler for SSHandle {
}
};
tracing::info!("auth_token_success user={} client={}", user_model.username, client_info);
tracing::info!(
"auth_token_success user={} client={}",
user_model.username,
client_info
);
self.operator = Some(user_model);
Ok(Auth::Accept)
}
@ -173,12 +176,19 @@ impl russh::server::Handler for SSHandle {
.unwrap_or_else(|| "unknown".to_string());
if user != "git" {
tracing::warn!("auth_rejected_invalid_username user={} client={}", user, client_info);
tracing::warn!(
"auth_rejected_invalid_username user={} client={}",
user,
client_info
);
return Err(russh::Error::NotAuthenticated);
}
let public_key_str = public_key.to_string();
if public_key_str.len() < 32 {
tracing::warn!("auth_rejected_invalid_key_length key_length={}", public_key_str.len());
tracing::warn!(
"auth_rejected_invalid_key_length key_length={}",
public_key_str.len()
);
return Err(russh::Error::NotAuthenticated);
}
@ -195,7 +205,11 @@ impl russh::server::Handler for SSHandle {
}
};
tracing::info!("auth_publickey_success user={} client={}", user_model.username, client_info);
tracing::info!(
"auth_publickey_success user={} client={}",
user_model.username,
client_info
);
self.operator = Some(user_model);
Ok(Auth::Accept)
}
@ -210,12 +224,19 @@ impl russh::server::Handler for SSHandle {
.unwrap_or_else(|| "unknown".to_string());
if user != "git" {
tracing::warn!("auth_rejected_invalid_username user={} client={}", user, client_info);
tracing::warn!(
"auth_rejected_invalid_username user={} client={}",
user,
client_info
);
return Err(russh::Error::NotAuthenticated);
}
let public_key_str = certificate.to_string();
if public_key_str.len() < 32 {
tracing::warn!("auth_rejected_invalid_key_length key_length={}", public_key_str.len());
tracing::warn!(
"auth_rejected_invalid_key_length key_length={}",
public_key_str.len()
);
return Err(russh::Error::NotAuthenticated);
}
@ -232,7 +253,11 @@ impl russh::server::Handler for SSHandle {
}
};
tracing::info!("auth_publickey_success user={} client={}", user_model.username, client_info);
tracing::info!(
"auth_publickey_success user={} client={}",
user_model.username,
client_info
);
self.operator = Some(user_model);
Ok(Auth::Accept)
}
@ -245,7 +270,11 @@ impl russh::server::Handler for SSHandle {
channel: ChannelId,
_: &mut Session,
) -> Result<(), Self::Error> {
tracing::info!("channel_close channel={:?} client={:?}", channel, self.client_addr);
tracing::info!(
"channel_close channel={:?} client={:?}",
channel,
self.client_addr
);
self.cleanup_channel(channel);
Ok(())
}
@ -255,14 +284,22 @@ impl russh::server::Handler for SSHandle {
channel: ChannelId,
_: &mut Session,
) -> Result<(), Self::Error> {
tracing::info!("channel_eof channel={:?} client={:?}", channel, self.client_addr);
tracing::info!(
"channel_eof channel={:?} client={:?}",
channel,
self.client_addr
);
if let Some(eof) = self.eof.get(&channel) {
let _ = eof.send(true).await;
}
if let Some(mut stdin) = self.stdin.remove(&channel) {
tracing::info!("Closing stdin channel={:?} client={:?}", channel, self.client_addr);
tracing::info!(
"Closing stdin channel={:?} client={:?}",
channel,
self.client_addr
);
// Use timeout so we never block the SSH event loop waiting for git.
let _ = tokio::time::timeout(Duration::from_secs(5), async {
if let Err(e) = stdin.flush().await {
@ -271,9 +308,17 @@ impl russh::server::Handler for SSHandle {
let _ = stdin.shutdown().await;
})
.await;
tracing::info!("stdin closed channel={:?} client={:?}", channel, self.client_addr);
tracing::info!(
"stdin closed channel={:?} client={:?}",
channel,
self.client_addr
);
} else {
tracing::warn!("stdin already removed channel={:?} client={:?}", channel, self.client_addr);
tracing::warn!(
"stdin already removed channel={:?} client={:?}",
channel,
self.client_addr
);
}
Ok(())
@ -288,7 +333,11 @@ impl russh::server::Handler for SSHandle {
.client_addr
.map(|addr| format!("{}", addr))
.unwrap_or_else(|| "unknown".to_string());
tracing::info!("channel_open_session channel={:?} client={}", channel, client_info);
tracing::info!(
"channel_open_session channel={:?} client={}",
channel,
client_info
);
if let Err(e) = session.flush() {
tracing::warn!(error = %e, "ssh_session_flush_failed");
}
@ -306,7 +355,13 @@ impl russh::server::Handler for SSHandle {
_modes: &[(russh::Pty, u32)],
session: &mut Session,
) -> Result<(), Self::Error> {
tracing::warn!("pty_request not supported channel={:?} term={} cols={} rows={}", channel, term, col_width, row_height);
tracing::warn!(
"pty_request not supported channel={:?} term={} cols={} rows={}",
channel,
term,
col_width,
row_height
);
if let Err(e) = session.flush() {
tracing::warn!(error = %e, "ssh_session_flush_failed");
}
@ -341,11 +396,8 @@ impl russh::server::Handler for SSHandle {
if bf.len() + data.len() > PRE_PACK_LIMIT {
tracing::warn!("ssh_pre_pack_too_large channel={:?}", channel);
let msg = "remote: Ref negotiation exceeds size limit\r\n";
let _ = session.extended_data(
channel,
1,
Bytes::copy_from_slice(msg.as_bytes()),
);
let _ =
session.extended_data(channel, 1, Bytes::copy_from_slice(msg.as_bytes()));
let _ = session.exit_status_request(channel, 1);
let _ = session.eof(channel);
let _ = session.close(channel);
@ -376,8 +428,7 @@ impl russh::server::Handler for SSHandle {
if let Some(msg) =
check_branch_protection(&branch_protect_roles, r#ref)
{
let full_msg =
format!("remote: {}\r\n", msg);
let full_msg = format!("remote: {}\r\n", msg);
let _ = session.extended_data(
channel,
1,
@ -444,8 +495,7 @@ impl russh::server::Handler for SSHandle {
);
tracing::info!("shell_request user={}", user.username);
let _ = session
.data(channel_id, Bytes::copy_from_slice(welcome_msg.as_bytes()));
let _ = session.data(channel_id, Bytes::copy_from_slice(welcome_msg.as_bytes()));
let _ = session.exit_status_request(channel_id, 0);
let _ = session.eof(channel_id);
let _ = session.close(channel_id);
@ -453,8 +503,7 @@ impl russh::server::Handler for SSHandle {
} else {
tracing::warn!("shell_request_unauthenticated channel={:?}", channel_id);
let msg = "Authentication required\r\n";
let _ = session
.data(channel_id, Bytes::copy_from_slice(msg.as_bytes()));
let _ = session.data(channel_id, Bytes::copy_from_slice(msg.as_bytes()));
let _ = session.exit_status_request(channel_id, 1);
let _ = session.eof(channel_id);
let _ = session.close(channel_id);
@ -473,18 +522,21 @@ impl russh::server::Handler for SSHandle {
.map(|addr| format!("{}", addr))
.unwrap_or_else(|| "unknown".to_string());
tracing::info!("exec_request received channel={:?} client={}", channel_id, client_info);
tracing::info!(
"exec_request received channel={:?} client={}",
channel_id,
client_info
);
let git_shell_cmd = match std::str::from_utf8(data) {
Ok(cmd) => cmd.trim(),
Err(e) => {
tracing::error!("invalid_command_encoding error={}", e);
let _ = session
.disconnect(
Disconnect::ServiceNotAvailable,
"Invalid command encoding",
"",
);
let _ = session.disconnect(
Disconnect::ServiceNotAvailable,
"Invalid command encoding",
"",
);
return Err(russh::Error::Disconnect);
}
};
@ -493,8 +545,7 @@ impl russh::server::Handler for SSHandle {
None => {
tracing::error!("invalid_git_command command={}", git_shell_cmd);
let msg = format!("Invalid git command: {}", git_shell_cmd);
let _ = session
.disconnect(Disconnect::ServiceNotAvailable, &msg, "");
let _ = session.disconnect(Disconnect::ServiceNotAvailable, &msg, "");
return Err(russh::Error::Disconnect);
}
};
@ -504,8 +555,7 @@ impl russh::server::Handler for SSHandle {
None => {
let msg = format!("Invalid repository path: {}", path);
tracing::error!("invalid_repo_path path={}", path);
let _ = session
.disconnect(Disconnect::ServiceNotAvailable, &msg, "");
let _ = session.disconnect(Disconnect::ServiceNotAvailable, &msg, "");
return Err(russh::Error::Disconnect);
}
};
@ -516,8 +566,8 @@ impl russh::server::Handler for SSHandle {
Err(e) => {
// Log the detailed error internally; client receives generic message.
tracing::error!("repo_fetch_error error={}", e);
let _ = session
.disconnect(Disconnect::ServiceNotAvailable, "Repository not found", "");
let _ =
session.disconnect(Disconnect::ServiceNotAvailable, "Repository not found", "");
return Err(russh::Error::Disconnect);
}
};
@ -546,20 +596,34 @@ impl russh::server::Handler for SSHandle {
if is_write { "write" } else { "read" },
repo.repo_name
);
tracing::error!("access_denied user={} repo={} is_write={}", operator.username, repo.repo_name, is_write);
tracing::error!(
"access_denied user={} repo={} is_write={}",
operator.username,
repo.repo_name,
is_write
);
let _ = session.disconnect(Disconnect::ByApplication, &msg, "");
return Err(russh::Error::Disconnect);
}
tracing::info!("access_granted user={} repo={} is_write={}", operator.username, repo.repo_name, is_write);
tracing::info!(
"access_granted user={} repo={} is_write={}",
operator.username,
repo.repo_name,
is_write
);
let repo_path = PathBuf::from(&repo.storage_path);
if !repo_path.exists() {
tracing::error!("repo_path_not_found path={}", repo.storage_path);
}
let mut cmd = build_git_command(service, repo_path);
tracing::info!("spawn_git_process service={:?} path={}", service, repo.storage_path);
tracing::info!(
"spawn_git_process service={:?} path={}",
service,
repo.storage_path
);
let mut shell = match cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
@ -583,7 +647,10 @@ impl russh::server::Handler for SSHandle {
None => {
tracing::error!("stdin pipe unavailable for channel={:?}", channel_id);
let _ = session_handle.channel_failure(channel_id).await;
return Err(russh::Error::IO(io::Error::new(io::ErrorKind::Other, "stdin unavailable")));
return Err(russh::Error::IO(io::Error::new(
io::ErrorKind::Other,
"stdin unavailable",
)));
}
};
self.stdin.insert(channel_id, stdin);
@ -591,14 +658,20 @@ impl russh::server::Handler for SSHandle {
Some(s) => s,
None => {
tracing::error!("stdout pipe unavailable for channel={:?}", channel_id);
return Err(russh::Error::IO(io::Error::new(io::ErrorKind::Other, "stdout unavailable")));
return Err(russh::Error::IO(io::Error::new(
io::ErrorKind::Other,
"stdout unavailable",
)));
}
};
let mut shell_stderr = match shell.stderr.take() {
Some(s) => s,
None => {
tracing::error!("stderr pipe unavailable for channel={:?}", channel_id);
return Err(russh::Error::IO(io::Error::new(io::ErrorKind::Other, "stderr unavailable")));
return Err(russh::Error::IO(io::Error::new(
io::ErrorKind::Other,
"stderr unavailable",
)));
}
};
@ -607,7 +680,7 @@ impl russh::server::Handler for SSHandle {
let repo_uid = repo.id;
let should_sync = service == GitService::ReceivePack;
let sync = self.sync.clone();
let fut = async move {
tracing::info!(channel = ?channel_id, "git_task_started");

View File

@ -40,12 +40,7 @@ impl SSHHandle {
}
});
}
pub fn new(
db: AppDatabase,
app: AppConfig,
cache: AppCache,
redis_pool: RedisPool,
) -> Self {
pub fn new(db: AppDatabase, app: AppConfig, cache: AppCache, redis_pool: RedisPool) -> Self {
SSHHandle {
db,
app,
@ -72,10 +67,7 @@ impl SSHHandle {
)
})?;
tracing::info!(
"Hex decoded to {} bytes",
private_key_bytes.len()
);
tracing::info!("Hex decoded to {} bytes", private_key_bytes.len());
let private_key_pem = std::str::from_utf8(&private_key_bytes)
.with_context(|| "Decoded SSH private key is not valid UTF-8")?;
@ -98,7 +90,8 @@ impl SSHHandle {
}
Err(e) => {
tracing::info!(
"ssh-key from_openssh failed: {}, trying direct russh parse", e
"ssh-key from_openssh failed: {}, trying direct russh parse",
e
);
PrivateKey::from_str(private_key_pem).with_context(|| {
format!("Failed to parse SSH private key with both methods")
@ -119,9 +112,7 @@ impl SSHHandle {
config.keepalive_interval = Some(Duration::from_secs(60));
config.keepalive_max = 3;
tracing::info!(
"SSH server configured with methods: {:?}", config.methods
);
tracing::info!("SSH server configured with methods: {:?}", config.methods);
let token_service = SshTokenService::new(self.db.clone());
let mut server = server::SSHServer::new(
self.db.clone(),
@ -161,7 +152,17 @@ pub struct ReceiveSyncService {
pool: deadpool_redis::cluster::Pool,
redis_prefix: String,
/// Optional NATS publish function: (subject, payload) -> Result<sequence, error>
nats_publish: Option<Arc<dyn Fn(String, Vec<u8>) -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<u64>> + Send>> + Send + Sync>>,
nats_publish: Option<
Arc<
dyn Fn(
String,
Vec<u8>,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = anyhow::Result<u64>> + Send>,
> + Send
+ Sync,
>,
>,
}
impl ReceiveSyncService {
@ -175,7 +176,15 @@ impl ReceiveSyncService {
pub fn with_nats(
pool: deadpool_redis::cluster::Pool,
nats_publish: Arc<dyn Fn(String, Vec<u8>) -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<u64>> + Send>> + Send + Sync>,
nats_publish: Arc<
dyn Fn(
String,
Vec<u8>,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = anyhow::Result<u64>> + Send>,
> + Send
+ Sync,
>,
) -> Self {
Self {
pool,
@ -243,8 +252,11 @@ impl ReceiveSyncService {
.query_async::<()>(&mut conn)
.await
{
tracing::error!("failed to enqueue sync task repo_id={} error={}",
task.repo_uid, e);
tracing::error!(
"failed to enqueue sync task repo_id={} error={}",
task.repo_uid,
e
);
} else {
tracing::info!(repo_id = %task.repo_uid, "hook task queued to Redis");
metrics::counter!("hook_task_queued_total", "backend" => "redis").increment(1);