//! Read-only clone and fetch over HTTP, by running `git http-backend` as a //! CGI program for the two endpoints the smart protocol needs. Only public //! repositories are reachable, and only fetching is served: the endpoint //! for pushing is never routed, and `git http-backend` refuses pushes over //! HTTP unless a repository is explicitly configured to accept them. use std::sync::Arc; use anyhow::anyhow; use poem::{ Body, Request, Response, Result, error::NotFoundError, handler, http::{HeaderName, HeaderValue, StatusCode, header}, web::{Data, Path}, }; use crate::{App, git::Process, repos::RepoName}; /// The smart protocol endpoints that are served. #[derive(Debug, Clone, Copy)] enum Endpoint { InfoRefs, UploadPack, } impl Endpoint { fn path(self) -> &'static str { match self { Endpoint::InfoRefs => "info/refs", Endpoint::UploadPack => "git-upload-pack", } } } #[handler] pub async fn info_refs( app: Data<&Arc>, Path(repo): Path, req: &Request, ) -> Result { run_backend(&app, &repo, Endpoint::InfoRefs, req, Body::empty()).await } #[handler] pub async fn upload_pack( app: Data<&Arc>, Path(repo): Path, req: &Request, body: Body, ) -> Result { run_backend(&app, &repo, Endpoint::UploadPack, req, body).await } async fn run_backend( app: &App, repo: &str, endpoint: Endpoint, req: &Request, body: Body, ) -> Result { let name = crate::pages::repo_name(repo)?; let visible_name = name.clone(); if !app .blocking(move |store| Ok(store.is_public(&visible_name))) .await? { return Err(NotFoundError.into()); } serve_backend(app, &name, endpoint, req, body).await } async fn serve_backend( app: &App, name: &RepoName, endpoint: Endpoint, req: &Request, body: Body, ) -> Result { let project_root = app.git.root().to_owned(); let path_info = format!("/{}/{}", name.dir_name(), endpoint.path()); let query = req.uri().query().unwrap_or_default().to_owned(); let method = req.method().as_str().to_owned(); let remote_addr = req .remote_addr() .as_socket_addr() .map(|addr| addr.ip().to_string()); let headers = cgi_headers(req); let mut backend = app .git .start("http-backend", Some(body), move |command| { command .arg("http-backend") .env("GIT_PROJECT_ROOT", project_root) .env("PATH_INFO", path_info) .env("QUERY_STRING", query) .env("REQUEST_METHOD", method) .envs(headers); if let Some(addr) = remote_addr { command.env("REMOTE_ADDR", addr); } }) .await?; let mut response = cgi_response(&mut backend).await?; response.set_body(backend.into_body()); Ok(response) } // The status and headers of a CGI response: header lines up to a blank // one, after which the body follows. async fn cgi_response(backend: &mut Process) -> anyhow::Result { let mut response = Response::default(); loop { let line = backend .read_line() .await? .ok_or_else(|| anyhow!("git http-backend ended without a response"))?; if line.is_empty() { return Ok(response); } let (key, value) = line .split_once(':') .ok_or_else(|| anyhow!("malformed CGI header from git http-backend: {line:?}"))?; let value = value.trim(); if key.eq_ignore_ascii_case("Status") { let code = value.split_whitespace().next().unwrap_or_default(); response.set_status(code.parse().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)); } else { response.headers_mut().insert( HeaderName::from_bytes(key.as_bytes())?, HeaderValue::from_str(value)?, ); } } } // The request headers as CGI meta-variables: `Content-Type` and // `Content-Length` under their own names, every other header as // `HTTP_`, so that the backend sees whatever it looks for (the // `Git-Protocol` header that selects protocol v2, for one). `Proxy` is // the exception: as `HTTP_PROXY` it would configure an outbound proxy for // anything the backend runs. fn cgi_headers(req: &Request) -> Vec<(String, String)> { let headers = req.headers(); headers .keys() .filter_map(|name| { let variable = if *name == header::CONTENT_TYPE { "CONTENT_TYPE".to_owned() } else if *name == header::CONTENT_LENGTH { "CONTENT_LENGTH".to_owned() } else if name.as_str() == "proxy" { return None; } else { format!( "HTTP_{}", name.as_str().to_ascii_uppercase().replace('-', "_") ) }; let values: Vec<&str> = headers .get_all(name) .iter() .filter_map(|value| value.to_str().ok()) .collect(); if values.is_empty() { return None; } Some((variable, values.join(", "))) }) .collect() }