//! Iroh server that accepts git push/fetch connections. //! //! Runs as a background process (or integrated into lichen-server). //! When a peer connects, reads the operation header, spawns the //! appropriate git command, and pipes its stdin/stdout over the //! QUIC stream. use std::path::PathBuf; use std::sync::Arc; use anyhow::{Context, Result}; use iroh::endpoint::Endpoint; use iroh::protocol::{AcceptError, ProtocolHandler, Router}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::process::Command; use crate::protocol::{GIT_IROH_ALPN, GitOp}; /// Git pack protocol handler for iroh. /// /// Accepts incoming connections, reads the operation byte, spawns /// `git-receive-pack` or `git-upload-pack`, and pipes the QUIC /// stream to the git process's stdin/stdout. #[derive(Debug, Clone)] pub struct GitIrohHandler { /// the git repo directory to serve repo_dir: PathBuf, } impl GitIrohHandler { pub fn new(repo_dir: PathBuf) -> Self { Self { repo_dir } } } impl ProtocolHandler for GitIrohHandler { async fn accept(&self, conn: iroh::endpoint::Connection) -> Result<(), AcceptError> { let remote = conn.remote_id(); tracing::info!("git-iroh: connection from {}", remote); let (mut send, mut recv) = conn.accept_bi().await?; // read operation byte let op_byte = recv .read_u8() .await .map_err(|e| std::io::Error::other(format!("read op: {}", e)))?; let op = GitOp::from_byte(op_byte).ok_or_else(|| { std::io::Error::other(format!("unknown git op: {}", op_byte)) })?; tracing::info!("git-iroh: {:?} from {}", op, remote); // spawn the git command let mut child = Command::new(op.server_command()) .arg(&self.repo_dir) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::inherit()) .spawn() .map_err(|e| { std::io::Error::other(format!("spawn {}: {}", op.server_command(), e)) })?; let mut child_stdin = child.stdin.take().unwrap(); let mut child_stdout = child.stdout.take().unwrap(); // pipe QUIC recv → git stdin, git stdout → QUIC send let (r1, r2) = tokio::join!( tokio::io::copy(&mut recv, &mut child_stdin), tokio::io::copy(&mut child_stdout, &mut send), ); // close stdin so git process can finish drop(child_stdin); if let Err(e) = r1 { tracing::warn!("git-iroh: recv→stdin error: {}", e); } if let Err(e) = r2 { tracing::warn!("git-iroh: stdout→send error: {}", e); } let status = child.wait().await.map_err(|e| { std::io::Error::other(format!("wait for git: {}", e)) })?; if !status.success() { tracing::warn!("git-iroh: {} exited with {}", op.server_command(), status); } send.finish().map_err(|e| { std::io::Error::other(format!("finish send: {}", e)) })?; tracing::info!("git-iroh: {:?} complete for {}", op, remote); Ok(()) } } /// Starts the git-iroh server on an iroh endpoint. /// /// Returns the Router (keeps the server alive) and the endpoint /// for address info. pub async fn start_server( secret_key: iroh::SecretKey, repo_dir: PathBuf, ) -> Result<(Router, Endpoint)> { let ep = Endpoint::builder(iroh::endpoint::presets::N0) .secret_key(secret_key) .alpns(vec![GIT_IROH_ALPN.to_vec()]) .bind() .await .context("failed to bind iroh endpoint")?; let node_id = ep.id(); tracing::info!("git-iroh server started, node_id={}", node_id); let handler = GitIrohHandler::new(repo_dir); let router = Router::builder(ep.clone()) .accept(GIT_IROH_ALPN, handler) .spawn(); Ok((router, ep)) }