Skip to main content

git_remote_iroh/
client.rs

1use std::{path::PathBuf, str::FromStr};
2
3use anyhow::Context;
4use iroh::{Endpoint, EndpointAddr, SecretKey};
5use iroh_rings::protocol::Status;
6use tokio::io::{
7    AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader, stdin,
8};
9use tracing::{debug, info};
10
11use crate::{GIT_IROH_ALPN, GitOp, auth::SINGLE_REPO_RESOURCE, keys, parse_iroh_url};
12
13pub struct Client {
14    url: String,
15    key: SecretKey,
16}
17
18impl Client {
19    pub fn new(config_dir: &PathBuf, git_dir: &PathBuf, url: &str) -> Result<Self, anyhow::Error> {
20        let (_, key) = keys::load_or_create_key(config_dir, git_dir)?;
21        info!(peer = key.public().to_z32(), "connecting as");
22        Ok(Self {
23            url: url.to_string(),
24            key,
25        })
26    }
27
28    pub async fn run<R: AsyncRead + Unpin, W: AsyncWrite + Unpin>(
29        self,
30        input: R,
31        mut output: W,
32    ) -> Result<(), anyhow::Error> {
33        let bufr = BufReader::new(input);
34        let mut lines = bufr.lines();
35        loop {
36            let line = match lines.next_line().await? {
37                Some(line) => line,
38                None => break,
39            };
40            let command: Vec<&str> = line.split_whitespace().collect();
41            if command.is_empty() {
42                continue;
43            }
44            debug!(?command, "client");
45            match command[0] {
46                "capabilities" => self.capabilities(&mut output).await?,
47                "connect" => {
48                    if command.len() < 2 {
49                        anyhow::bail!("connect: missing <url>");
50                    }
51                    self.connect(&mut output, command[1]).await?
52                }
53                s => anyhow::bail!("unsupported command: {:?}", s),
54            }
55            output.write(b"\n").await?;
56        }
57        Ok(())
58    }
59
60    async fn capabilities<W: AsyncWrite + Unpin>(
61        &self,
62        output: &mut W,
63    ) -> Result<(), anyhow::Error> {
64        output.write(b"connect\n").await?;
65        Ok(())
66    }
67
68    async fn connect<W: AsyncWrite + Unpin>(
69        &self,
70        output: &mut W,
71        cmd: &str,
72    ) -> Result<(), anyhow::Error> {
73        let remote_node_id = parse_iroh_url(self.url.as_str())?;
74        let remote_op = GitOp::from_str(cmd)?;
75        let ep = Endpoint::builder(iroh::endpoint::presets::N0)
76            .secret_key(self.key.clone())
77            .bind()
78            .await?;
79
80        ep.online().await;
81
82        // connect to iroh URL at url
83        let addr = EndpointAddr::new(remote_node_id);
84        let conn = ep.connect(addr, GIT_IROH_ALPN).await?;
85        let (mut send, mut recv) = conn.open_bi().await?;
86
87        // send op request
88        remote_op
89            .write_request(&mut send, SINGLE_REPO_RESOURCE)
90            .await?;
91
92        // read auth response
93        match Status::try_from(recv.read_u8().await?)? {
94            Status::Denied => anyhow::bail!("permission denied"),
95            Status::Allowed => {}
96            s => anyhow::bail!("invalid status byte: {}", s),
97        };
98
99        // Send a single newline to indicate we're connecting the remote stream
100        println!();
101
102        // Connect local stdin/stdout to remote socket send/recv pair
103        let mut stdin = stdin();
104        let (r1, r2) = tokio::join!(
105            tokio::io::copy(&mut stdin, &mut send),
106            tokio::io::copy(&mut recv, output),
107        );
108        r1.context("stdin -> send")?;
109        r2.context("recv -> stdout")?;
110        ep.close().await;
111        Ok(())
112    }
113}