Skip to main content

git_remote_iroh/
lib.rs

1use std::{fmt::Display, fs::create_dir_all, path::PathBuf, str::FromStr};
2
3mod auth;
4mod cli;
5mod client;
6mod keys;
7mod server;
8
9use anyhow::Context;
10
11pub use cli::{Args, ClientArgs};
12pub use client::Client;
13pub use server::Server;
14use tokio::io::{AsyncWrite, AsyncWriteExt};
15
16/// ALPN for git-remote-iroh connections.
17pub const GIT_IROH_ALPN: &[u8] = b"git/iroh/0";
18
19fn config_dir() -> Result<PathBuf, anyhow::Error> {
20    let config_dir = dirs::config_local_dir()
21        .unwrap_or(PathBuf::from(
22            std::env::var("HOME").context("HOME not set")?,
23        ))
24        .join("git-remote-iroh");
25    create_dir_all(&config_dir)?;
26    Ok(config_dir)
27}
28
29fn keys_dir(config_dir: &PathBuf) -> PathBuf {
30    config_dir.join("keys")
31}
32
33/// Git operation client request.
34#[derive(Debug, Clone, Copy, PartialEq)]
35pub enum GitOp {
36    /// Client is doing a fetch. Server runs git-upload-pack.
37    ClientFetchServerPush,
38    /// Client is going a push. Server runs git-fetch-pack.
39    ClientPushServerFetch,
40}
41
42impl GitOp {
43    /// Return the resource ID that will be authorized for the operation on
44    /// the git repository path. The operation must currently be bound to the
45    /// authorized resource_id, otherwise the transfer function has no way to
46    /// know what was authorized.
47    pub fn resource_id<S: Display>(self, resource: S) -> Vec<u8> {
48        format!(
49            "{}:{}",
50            match self {
51                GitOp::ClientFetchServerPush => "fetch",
52                GitOp::ClientPushServerFetch => "push",
53            },
54            resource
55        )
56        .as_bytes()
57        .to_vec()
58    }
59
60    pub async fn write_request<W: AsyncWrite + Unpin, S: Display>(
61        self,
62        w: &mut W,
63        resource: S,
64    ) -> Result<(), anyhow::Error> {
65        let resource_id = self.resource_id(resource);
66        w.write_u16_le(resource_id.len().try_into()?).await?;
67        w.write(resource_id.as_slice()).await?;
68        w.write_u8(match self {
69            // Client requesting a push is a read operation.
70            GitOp::ClientFetchServerPush => 1u8,
71            // Client requesting a fetch is a write operation.
72            GitOp::ClientPushServerFetch => 2u8,
73        })
74        .await?;
75        Ok(())
76    }
77
78    pub fn from_resource_id(resource_id: &[u8]) -> Result<GitOp, anyhow::Error> {
79        Ok(match resource_id.split(|c| *c == b':').next() {
80            Some(b"fetch") => GitOp::ClientFetchServerPush,
81            Some(b"push") => GitOp::ClientPushServerFetch,
82            _ => anyhow::bail!("invalid resource"),
83        })
84    }
85
86    pub fn to_byte(self) -> u8 {
87        match self {
88            GitOp::ClientFetchServerPush => 1,
89            GitOp::ClientPushServerFetch => 2,
90        }
91    }
92
93    pub fn from_byte(b: u8) -> Option<Self> {
94        match b {
95            1 => Some(GitOp::ClientFetchServerPush),
96            2 => Some(GitOp::ClientPushServerFetch),
97            _ => None,
98        }
99    }
100}
101
102impl FromStr for GitOp {
103    type Err = anyhow::Error;
104
105    fn from_str(s: &str) -> Result<Self, Self::Err> {
106        Ok(match s {
107            "git-upload-pack" => GitOp::ClientFetchServerPush,
108            "git-receive-pack" => GitOp::ClientPushServerFetch,
109            _ => anyhow::bail!("unsupported command {s}"),
110        })
111    }
112}
113
114impl Display for GitOp {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        match self {
117            GitOp::ClientPushServerFetch => f.write_str("git-receive-pack"),
118            GitOp::ClientFetchServerPush => f.write_str("git-upload-pack"),
119        }
120    }
121}
122
123/// Parses `iroh://<node-id>` URL.
124fn parse_iroh_url(url: &str) -> Result<iroh::PublicKey, anyhow::Error> {
125    let node_id_str = url.strip_prefix("iroh://").ok_or(anyhow::anyhow!(
126        "invalid iroh URL (expected iroh://): {}",
127        url
128    ))?;
129
130    Ok(node_id_str.parse::<iroh::PublicKey>()?)
131}