1use std::{path::PathBuf, str::FromStr as _};
2
3use anyhow::bail;
4use homedir::my_home;
5use iroh::{NodeId, SecretKey};
6
7use crate::{IrohSsh, dot_ssh};
8
9pub async fn info_mode() -> anyhow::Result<()> {
10 let server_key = dot_ssh(&SecretKey::generate(rand::rngs::OsRng), false, false).ok();
11 let service_key = dot_ssh(&SecretKey::generate(rand::rngs::OsRng), false, true).ok();
12
13 if server_key.is_none() && service_key.is_none() {
14 println!(
15 "No keys found, run for server or service:\n 'iroh-ssh server --persist' or '-p' to create it"
16 );
17 println!();
18 println!("(if an iroh-ssh instance is currently running, it is using ephemeral keys)");
19 bail!("No keys found")
20 }
21
22 println!("iroh-ssh version {}", env!("CARGO_PKG_VERSION"));
23 println!("https://github.com/rustonbsd/iroh-ssh");
24 println!();
25
26 if server_key.is_none() && service_key.is_none() {
27 println!("run 'iroh-ssh server --persist' to start the server with persistent keys");
28 println!("run 'iroh-ssh server' to start the server with ephemeral keys");
29 println!(
30 "run 'iroh-ssh service install' to copy the binary, install the service and start the server (always uses persistent keys)"
31 );
32 }
33
34 if let Some(key) = server_key {
35 println!();
36 println!("Your server iroh-ssh nodeid:");
37 println!(" iroh-ssh {}@{}", whoami::username(), key.clone().public());
38 println!();
39 }
40
41 if let Some(key) = service_key {
42 println!();
43 println!("Your service iroh-ssh nodeid:");
44 println!(" iroh-ssh {}@{}", whoami::username(), key.clone().public());
45 println!();
46 }
47
48 Ok(())
49}
50
51pub mod service {
52 use crate::{ServiceParams, install_service, uninstall_service};
53
54 pub async fn install(ssh_port: u16) -> anyhow::Result<()> {
55 if install_service(ServiceParams { ssh_port }).await.is_err() {
56 println!("service install is only supported on linux and windows");
57 anyhow::bail!("service install is only supported on linux and windows");
58 }
59 Ok(())
60 }
61
62 pub async fn uninstall() -> anyhow::Result<()> {
63 if uninstall_service().await.is_err() {
64 println!("service uninstall is only supported on linux or windows");
65 anyhow::bail!("service uninstall is only supported on linux or windows");
66 }
67 Ok(())
68 }
69}
70
71pub async fn server_mode(ssh_port: u16, persist: bool) -> anyhow::Result<()> {
72 let mut iroh_ssh_builder = IrohSsh::builder()
73 .accept_incoming(true)
74 .accept_port(ssh_port);
75 if persist {
76 iroh_ssh_builder = iroh_ssh_builder.dot_ssh_integration(true, false);
77 }
78 let iroh_ssh = iroh_ssh_builder.build().await?;
79
80 println!("Connect to this this machine:");
81 println!(
82 "\n iroh-ssh {}@{}\n",
83 whoami::username(),
84 iroh_ssh.node_id()
85 );
86 if persist {
87 let distro_home = my_home()?.ok_or_else(|| anyhow::anyhow!("home directory not found"))?;
88 let ssh_dir = distro_home.join(".ssh");
89 println!(" (using persistent keys in {})", ssh_dir.display());
90 } else {
91 println!(
92 " warning: (using ephemeral keys, run 'iroh-ssh server --persist' to create persistent keys)"
93 );
94 }
95 println!();
96 println!("client -> iroh-ssh -> direct connect -> iroh-ssh -> local ssh :{ssh_port}");
97
98 println!("Waiting for incoming connections...");
99 println!("Press Ctrl+C to exit");
100 tokio::signal::ctrl_c().await?;
101 Ok(())
102}
103
104pub struct ClientOptions {
105 pub target: String,
106 pub identity_file: Option<PathBuf>,
107 pub local_forward: Option<String>,
108 pub remote_forward: Option<String>,
109}
110
111pub async fn client_mode(
112 client_options: ClientOptions,
113 execute_command: Vec<String>,
114) -> anyhow::Result<()> {
115 let (ssh_user, iroh_node_id) = parse_iroh_target(&client_options.target)?;
116 let iroh_ssh = IrohSsh::builder().accept_incoming(false).build().await?;
117 let mut ssh_process = iroh_ssh
118 .connect(&ssh_user, iroh_node_id, client_options, execute_command)
119 .await?;
120
121 ssh_process.wait().await?;
122
123 Ok(())
124}
125
126fn parse_iroh_target(target: &str) -> anyhow::Result<(String, NodeId)> {
127 let (user, node_id_str) = target
128 .split_once('@')
129 .ok_or_else(|| anyhow::anyhow!("Invalid format, use user@node_id"))?;
130 let node_id = NodeId::from_str(node_id_str)?;
131 Ok((user.to_string(), node_id))
132}