#![forbid(unsafe_code)] use actix_web::{ get, http::header::{HeaderValue, LOCATION}, middleware, web, App, HttpResponse, HttpServer, Responder, }; use clap::Parser; use env_logger::Env; use git2::{ReferenceType, Repository}; use log::error; use std::collections::HashMap; use std::sync::Mutex; fn ensure_repo(repos_dir: &str, username: &str, repo_name: &str) -> RepoOr404 { let not_found = lilgit::render_404( format!("404 - {}", repo_name), format!("Could not find repo {}", repo_name), ); let not_found_result = RepoOr404::R404(not_found); if repo_name.contains("/") { return not_found_result; } if repo_name.contains("\\") { return not_found_result; } let repo = Repository::open(&format!("{}/{}/{}", repos_dir, username, repo_name)); repo.map_or_else(|_| not_found_result, |v| RepoOr404::Repo(v)) } enum RepoOr404 { Repo(Repository), R404(String), } #[get("/~{username}/{repo_name}/info/refs")] async fn get_info_refs( app_data: web::Data<lilgit::AppState>, path: web::Path<(String, String)>, ) -> impl Responder { let (username, repo_name) = path.into_inner(); if let Some(response) = lilgit::guard_username_configured(&username, &app_data) { return response; } let repo = match ensure_repo(&app_data.config.repos_dir, &username, &repo_name) { RepoOr404::Repo(r) => r, RepoOr404::R404(r) => { return HttpResponse::NotFound().body(r); } }; let references = match repo.references() { Ok(x) => x, Err(x) => { error!("Error rendering index listing: {:?}", x); return HttpResponse::InternalServerError().body("Error rendering index listing"); } }; let branch_references_str = references.fold("".to_string(), |acc, curr| { let reference = match curr { Ok(x) => x, _ => return acc, }; if !reference.is_branch() { return acc; } if reference.kind() != Some(ReferenceType::Direct) { return acc; } let target = match reference.target() { Some(x) => x, _ => return acc, }; let name = match reference.name() { Some(x) => x, _ => return acc, }; format!("{}\n{}\t{}", acc, target, name) }); HttpResponse::Ok().body(format!("{}\n", branch_references_str.trim())) } #[get("/~{username}/{repo_name}/HEAD")] async fn get_head( app_data: web::Data<lilgit::AppState>, path: web::Path<(String, String)>, ) -> impl Responder { let (username, repo_name) = path.into_inner(); if let Some(response) = lilgit::guard_username_configured(&username, &app_data) { return response; } let repo = match ensure_repo(&app_data.config.repos_dir, &username, &repo_name) { RepoOr404::Repo(r) => r, RepoOr404::R404(r) => { return HttpResponse::NotFound().body(r); } }; let s500 = HttpResponse::InternalServerError().body("Error getting repo head"); let head = match repo.head() { Ok(x) => x, Err(x) => { error!("Error getting repo head: {:?}", x); return s500; } }; if !head.is_branch() { return s500; } if head.kind() != Some(ReferenceType::Direct) { return s500; } let name = match head.name() { Some(x) => x, _ => return s500, }; HttpResponse::Ok().body(format!("ref: {}\n", name)) } #[get("/~{username}/{repo_name}")] async fn get_repo( app_data: web::Data<lilgit::AppState>, path: web::Path<(String, String)>, ) -> impl Responder { let (username, repo_name) = path.into_inner(); if let Some(response) = lilgit::guard_username_configured(&username, &app_data) { return response; } let repo = match ensure_repo(&app_data.config.repos_dir, &username, &repo_name) { RepoOr404::Repo(r) => r, RepoOr404::R404(r) => { return HttpResponse::NotFound().body(r); } }; let repo_config = app_data.config.get_repo_config(&username, &repo_name); match lilgit::listing::render_repo_index( &repo, &username, &repo_name, repo_config, app_data.config.folder_depth, ) { Ok(x) => HttpResponse::Ok().content_type("text/html").body(x), Err(x) => { error!("Error rendering index listing: {:?}", x); HttpResponse::InternalServerError().body("Error rendering index listing") } } } #[get("/~{username}/{repo_name}/tree/{tail:.*}")] async fn get_file( app_data: web::Data<lilgit::AppState>, path: web::Path<(String, String, String)>, ) -> impl Responder { let (username, repo_name, file_path) = path.into_inner(); let repo = match ensure_repo(&app_data.config.repos_dir, &username, &repo_name) { RepoOr404::Repo(r) => r, RepoOr404::R404(r) => { return HttpResponse::NotFound().body(r); } }; if let Ok(obj) = repo.revparse_single(&format!("HEAD:{}", file_path)) { if obj.kind() == Some(git2::ObjectType::Tree) { let render_result = lilgit::listing::render_repo_folder( &repo, &username, &repo_name, app_data.config.folder_depth, &file_path, ); return match render_result { Ok(x) => HttpResponse::Ok().content_type("text/html").body(x), Err(_) => { return HttpResponse::NotFound().body("404 not found"); } }; } if obj.kind() == Some(git2::ObjectType::Blob) { if let Some(blob) = obj.as_blob() { if blob.is_binary() { return lilgit::file::render_binary_object(blob, &file_path); } } } }; let render_result = lilgit::file::render_file(&repo, &username, &repo_name, &file_path); match render_result { Ok(x) => HttpResponse::Ok().content_type("text/html").body(x), Err(_) => HttpResponse::NotFound().body(lilgit::render_404( format!("Could not render {}", file_path), String::from(""), )), } } #[get("/~{username}/{repo_name}/tree")] async fn get_tree_redirect(path: web::Path<(String, String)>) -> impl Responder { let (username, repo_name) = path.into_inner(); let mut redirect = HttpResponse::PermanentRedirect().body(()); let header_value = HeaderValue::from_str(&format!("/~{}/{}", &username, &repo_name)) .unwrap_or(HeaderValue::from_static("/")); redirect.headers_mut().insert(LOCATION, header_value); redirect } /// Lilgit repo server #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { /// Port to bind to #[arg(short, long, default_value_t = 8080)] port: u16, /// Ipv4 address to bind to #[arg(short, long, default_value_t = String::from("127.0.0.1"))] ip: String, /// folder containing repositories #[arg(short, long, default_value_t = String::from("./"))] repos_dir: String, /// Optional max number of folders to render at one time #[arg(short, long)] folder_depth: Option<usize>, /// Optional config file providing extra metadata like descriptions of repos #[arg(short, long)] config: Option<String>, } #[actix_web::main] async fn main() -> std::io::Result<()> { let args = Args::parse(); env_logger::init_from_env(Env::default().default_filter_or("info")); let config_path = args.config.clone(); let app_data = web::Data::new(lilgit::AppState { config: lilgit::get_config(&config_path), line_counts: Mutex::new(HashMap::new()), }); HttpServer::new(move || { App::new() .wrap(middleware::NormalizePath::trim()) .wrap(middleware::Compress::default()) .wrap(middleware::Logger::default()) .app_data(app_data.clone()) .service(lilgit::get_index) .service(get_info_refs) .service(get_repo) .service(lilgit::user::get_repos) .service(lilgit::r#static::get_static_asset) .service(get_file) .service(get_tree_redirect) .service(get_head) }) .bind((args.ip, args.port))? .run() .await }