use std::{sync::Arc, time::Instant}; use actix_web::{App, HttpResponse, HttpServer, dev::Service, web}; use cache::AppCache; use config::AppConfig; use db::database::AppDatabase; use sqlx; pub mod action; pub mod auth; pub mod handler; pub mod lfs; pub mod lfs_routes; pub mod rate_limit; pub mod routes; pub mod utils; const REQUEST_LOG_EXCLUDED_PATHS: &[&str] = &[ "/health", "/live", "/ready", "/metrics", "/favicon.ico", "/robots.txt", ]; fn should_log_request(path: &str) -> bool { !REQUEST_LOG_EXCLUDED_PATHS.contains(&path) } #[derive(Clone)] pub struct HttpAppState { pub db: AppDatabase, pub cache: AppCache, pub sync: crate::sync::ReceiveSyncService, pub rate_limiter: Arc, pub config: AppConfig, pub git_state: crate::AppGitState, } async fn robots(state: web::Data) -> HttpResponse { let sitemap_url = state .config .git_http_domain() .map(|d| format!("{}/sitemap.xml", d.trim_end_matches('/'))) .unwrap_or_default(); let body = if sitemap_url.is_empty() { "User-agent: *\nDisallow: /\n".to_string() } else { format!("User-agent: *\nDisallow: /\n\nSitemap: {sitemap_url}\n") }; HttpResponse::Ok() .content_type("text/plain; charset=utf-8") .body(body) } async fn health(state: web::Data) -> HttpResponse { let db_ok = sqlx::query("SELECT 1") .execute(state.db.reader()) .await .is_ok(); let cache_ok = state.cache.ping_cluster().await.is_ok(); if db_ok && cache_ok { HttpResponse::Ok().json(serde_json::json!({ "status": "ok", "db": "ok", "cache": "ok", })) } else { HttpResponse::ServiceUnavailable().json(serde_json::json!({ "status": "unhealthy", "db": if db_ok { "ok" } else { "error" }, "cache": if cache_ok { "ok" } else { "error" }, })) } } 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/action", web::get().to(action::action_poll), ) .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), ) .route( "/{namespace}/{repo_name}.git/graphql", web::post().to(crate::graphql::graphql_handle), ); } pub async fn run_http( config: AppConfig, db: AppDatabase, cache: AppCache, redis_pool: deadpool_redis::cluster::Pool, ) -> anyhow::Result<()> { let sync = crate::sync::ReceiveSyncService::new(redis_pool); let rate_limiter = Arc::new(rate_limit::RateLimiter::new( rate_limit::RateLimitConfig::default(), )); let _cleanup = rate_limiter.clone().start_cleanup(); let git_state = crate::AppGitState { cache: cache.clone(), db: db.clone(), }; let state = HttpAppState { db: db.clone(), cache: cache.clone(), sync, rate_limiter, config: config.clone(), git_state, }; let http_port = config.git_http_port()?; tracing::info!("Starting git HTTP server on 0.0.0.0:{}", http_port); let server = HttpServer::new(move || { App::new() .app_data(web::Data::new(state.clone())) .wrap_fn(|req, srv| { let should_log = should_log_request(req.path()); let method = req.method().clone(); let path = req.path().to_owned(); let peer_addr = req.connection_info().peer_addr().map(str::to_owned); let started_at = Instant::now(); let fut = srv.call(req); async move { match fut.await { Ok(res) => { if should_log { tracing::info!( method = %method, path = %path, status = res.status().as_u16(), elapsed_ms = started_at.elapsed().as_millis(), peer_addr = peer_addr.as_deref().unwrap_or("-"), "http request" ); } Ok(res) } Err(err) => { if should_log { tracing::warn!( method = %method, path = %path, elapsed_ms = started_at.elapsed().as_millis(), peer_addr = peer_addr.as_deref().unwrap_or("-"), error = %err, "http request failed" ); } Err(err) } } } }) .configure(git_http_cfg) }) .bind(format!("0.0.0.0:{}", http_port))? .run(); let result = server.await; if let Err(e) = result { tracing::error!("HTTP server error: {}", e); } tracing::info!("Git HTTP server stopped"); Ok(()) }