gitdataai/libs/git/http/routes.rs
ZhenYi 2b6b4af3db feat(http): improve auth verification and route handling
- Migrate access key auth from custom hash to Argon2 password verification
- Check all un-revoked tokens with expiry validation
- Add branch protection checks to HTTP push handlers
2026-05-15 11:48:33 +08:00

166 lines
5.9 KiB
Rust

use crate::http::HttpAppState;
use crate::http::auth::authorize_repo_access;
use crate::http::handler::GitHttpHandler;
use crate::http::utils::get_repo_model;
use crate::ssh::RepoReceiveSyncTask;
use crate::ssh::push_queue::{PushQueueEvent, PushQueueWaitError, wait_for_push_queue_slot};
use actix_web::{Error, HttpRequest, HttpResponse, web};
use std::path::PathBuf;
use std::time::Duration;
use tokio::time::timeout;
pub async fn info_refs(
req: HttpRequest,
path: web::Path<(String, String)>,
state: web::Data<HttpAppState>,
) -> Result<HttpResponse, Error> {
if !state.rate_limiter.is_read_allowed().await {
return Err(actix_web::error::ErrorTooManyRequests(
"Rate limit exceeded",
));
}
let service_param = req
.query_string()
.split('&')
.find(|s| s.starts_with("service="))
.and_then(|s| s.strip_prefix("service="))
.ok_or_else(|| actix_web::error::ErrorBadRequest("Missing service parameter"))?;
if service_param != "git-upload-pack" && service_param != "git-receive-pack" {
return Err(actix_web::error::ErrorBadRequest("Invalid service"));
}
let path_inner = path.into_inner();
let model = get_repo_model(&path_inner.0, &path_inner.1, &state.db).await?;
let is_write = service_param == "git-receive-pack";
authorize_repo_access(&req, &state.db, &model, is_write).await?;
let storage_path = PathBuf::from(&model.storage_path);
let handler = GitHttpHandler::new(storage_path, model, state.db.clone());
handler.info_refs(service_param).await
}
pub async fn upload_pack(
req: HttpRequest,
path: web::Path<(String, String)>,
payload: web::Payload,
state: web::Data<HttpAppState>,
) -> Result<HttpResponse, Error> {
if !state.rate_limiter.is_read_allowed().await {
return Err(actix_web::error::ErrorTooManyRequests(
"Rate limit exceeded",
));
}
let path_inner = path.into_inner();
let model = get_repo_model(&path_inner.0, &path_inner.1, &state.db).await?;
authorize_repo_access(&req, &state.db, &model, false).await?;
let storage_path = PathBuf::from(&model.storage_path);
let handler = GitHttpHandler::new(storage_path, model, state.db.clone());
handler.upload_pack(payload).await
}
pub async fn receive_pack(
req: HttpRequest,
path: web::Path<(String, String)>,
payload: web::Payload,
state: web::Data<HttpAppState>,
) -> Result<HttpResponse, Error> {
if !state.rate_limiter.is_write_allowed().await {
return Err(actix_web::error::ErrorTooManyRequests(
"Rate limit exceeded",
));
}
let path_inner = path.into_inner();
let model = get_repo_model(&path_inner.0, &path_inner.1, &state.db).await?;
authorize_repo_access(&req, &state.db, &model, true).await?;
let mut push_queue_lease =
match wait_for_push_queue_slot(state.sync.clone(), model.id, |event, request_id| {
let request_id = request_id.to_string();
let repo_name = model.repo_name.clone();
let repo_id = model.id;
match event {
PushQueueEvent::Waiting(position) => {
tracing::info!(
repo = %repo_name,
repo_id = %repo_id,
request_id = %request_id,
position = position.position,
total = position.total,
"http_push_queue_waiting"
);
}
PushQueueEvent::Acquired => {
tracing::info!(
repo = %repo_name,
repo_id = %repo_id,
request_id = %request_id,
"http_push_queue_acquired"
);
}
}
})
.await
{
Ok(lease) => lease,
Err(PushQueueWaitError::Join(e)) => {
tracing::error!(
error = %e,
repo = %model.repo_name,
repo_id = %model.id,
"http_push_queue_join_failed"
);
return Err(actix_web::error::ErrorServiceUnavailable(
"GitData: push queue is temporarily unavailable. Please retry later.",
));
}
Err(PushQueueWaitError::Lock(e)) => {
tracing::error!(
error = %e,
repo = %model.repo_name,
repo_id = %model.id,
"http_push_queue_lock_failed"
);
return Err(actix_web::error::ErrorServiceUnavailable(
"GitData: push queue lock failed. Please retry later.",
));
}
Err(PushQueueWaitError::Timeout) => {
tracing::info!(
repo = %model.repo_name,
repo_id = %model.id,
"http_push_queue_timeout"
);
return Ok(HttpResponse::ServiceUnavailable()
.insert_header(("Retry-After", "5"))
.content_type("text/plain; charset=utf-8")
.body("GitData: push queue timed out. Please retry in a moment.\n"));
}
};
let storage_path = PathBuf::from(&model.storage_path);
let handler = GitHttpHandler::new(storage_path, model.clone(), state.db.clone());
let result = handler.receive_pack(payload).await;
push_queue_lease.release().await;
if result.is_ok() {
let _ = tokio::spawn({
let sync = state.sync.clone();
let repo_uid = model.id;
async move {
let _ = timeout(
Duration::from_secs(5),
sync.send(RepoReceiveSyncTask { repo_uid }),
)
.await;
}
});
}
result
}