git_remote_iroh/
client.rs1use 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 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 remote_op
89 .write_request(&mut send, SINGLE_REPO_RESOURCE)
90 .await?;
91
92 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 println!();
101
102 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}