1use clap::{Args, Parser, Subcommand};
2use futures::StreamExt;
3
4use std::{net::IpAddr, time::Duration};
5use thiserror::Error;
6
7use mousehop_ipc::{
8 ClientHandle, ConnectionError, FrontendEvent, FrontendRequest, IpcError, Position,
9 connect_async,
10};
11
12#[derive(Debug, Error)]
13pub enum CliError {
14 #[error("could not connect: `{0}` - is the service running?")]
16 ServiceNotRunning(#[from] ConnectionError),
17 #[error("error communicating with service: {0}")]
18 Ipc(#[from] IpcError),
19}
20
21#[derive(Parser, Clone, Debug, PartialEq, Eq)]
22#[command(name = "mousehop-cli", about = "Mousehop CLI interface")]
23pub struct CliArgs {
24 #[command(subcommand)]
25 command: CliSubcommand,
26}
27
28#[derive(Args, Clone, Debug, PartialEq, Eq)]
29struct Client {
30 #[arg(long)]
31 hostname: Option<String>,
32 #[arg(long)]
33 port: Option<u16>,
34 #[arg(long)]
35 ips: Option<Vec<IpAddr>>,
36 #[arg(long)]
37 enter_hook: Option<String>,
38}
39
40#[derive(Clone, Subcommand, Debug, PartialEq, Eq)]
41enum CliSubcommand {
42 AddClient(Client),
44 RemoveClient { id: ClientHandle },
46 Activate { id: ClientHandle },
48 Deactivate { id: ClientHandle },
50 List,
52 SetHost {
54 id: ClientHandle,
55 host: Option<String>,
56 },
57 SetPort { id: ClientHandle, port: u16 },
59 SetPosition { id: ClientHandle, pos: Position },
61 SetIps { id: ClientHandle, ips: Vec<IpAddr> },
63 EnableCapture,
65 EnableEmulation,
67 AuthorizeKey {
69 description: String,
70 sha256_fingerprint: String,
71 },
72 RemoveAuthorizedKey { sha256_fingerprint: String },
74 SaveConfig,
76}
77
78pub async fn run(args: CliArgs) -> Result<(), CliError> {
79 execute(args.command).await?;
80 Ok(())
81}
82
83async fn execute(cmd: CliSubcommand) -> Result<(), CliError> {
84 let (mut rx, mut tx) = connect_async(Some(Duration::from_millis(500))).await?;
85 match cmd {
86 CliSubcommand::AddClient(Client {
87 hostname,
88 port,
89 ips,
90 enter_hook,
91 }) => {
92 tx.request(FrontendRequest::Create).await?;
93 while let Some(e) = rx.next().await {
94 if let FrontendEvent::Created(handle, _, _) = e? {
95 if let Some(hostname) = hostname {
96 tx.request(FrontendRequest::UpdateHostname(handle, Some(hostname)))
97 .await?;
98 }
99 if let Some(port) = port {
100 tx.request(FrontendRequest::UpdatePort(handle, port))
101 .await?;
102 }
103 if let Some(ips) = ips {
104 tx.request(FrontendRequest::UpdateFixIps(handle, ips))
105 .await?;
106 }
107 if let Some(enter_hook) = enter_hook {
108 tx.request(FrontendRequest::UpdateEnterHook(handle, Some(enter_hook)))
109 .await?;
110 }
111 break;
112 }
113 }
114 }
115 CliSubcommand::RemoveClient { id } => tx.request(FrontendRequest::Delete(id)).await?,
116 CliSubcommand::Activate { id } => tx.request(FrontendRequest::Activate(id, true)).await?,
117 CliSubcommand::Deactivate { id } => {
118 tx.request(FrontendRequest::Activate(id, false)).await?
119 }
120 CliSubcommand::List => {
121 tx.request(FrontendRequest::Enumerate()).await?;
122 while let Some(e) = rx.next().await {
123 if let FrontendEvent::Enumerate(clients) = e? {
124 for (handle, config, state) in clients {
125 let host = config.hostname.unwrap_or("unknown".to_owned());
126 let port = config.port;
127 let pos = config.pos;
128 let active = state.active;
129 let ips = state.ips;
130 println!(
131 "id {handle}: {host}:{port} ({pos}) active: {active}, ips: {ips:?}"
132 );
133 }
134 break;
135 }
136 }
137 }
138 CliSubcommand::SetHost { id, host } => {
139 tx.request(FrontendRequest::UpdateHostname(id, host))
140 .await?
141 }
142 CliSubcommand::SetPort { id, port } => {
143 tx.request(FrontendRequest::UpdatePort(id, port)).await?
144 }
145 CliSubcommand::SetPosition { id, pos } => {
146 tx.request(FrontendRequest::UpdatePosition(id, pos)).await?
147 }
148 CliSubcommand::SetIps { id, ips } => {
149 tx.request(FrontendRequest::UpdateFixIps(id, ips)).await?
150 }
151 CliSubcommand::EnableCapture => tx.request(FrontendRequest::EnableCapture).await?,
152 CliSubcommand::EnableEmulation => tx.request(FrontendRequest::EnableEmulation).await?,
153 CliSubcommand::AuthorizeKey {
154 description,
155 sha256_fingerprint,
156 } => {
157 tx.request(FrontendRequest::AuthorizeKey(
158 description,
159 sha256_fingerprint,
160 ))
161 .await?
162 }
163 CliSubcommand::RemoveAuthorizedKey { sha256_fingerprint } => {
164 tx.request(FrontendRequest::RemoveAuthorizedKey(sha256_fingerprint))
165 .await?
166 }
167 CliSubcommand::SaveConfig => tx.request(FrontendRequest::SaveConfiguration).await?,
168 }
169 Ok(())
170}