1use anyhow::{bail, Context, Result};
2use clap::builder::TypedValueParser;
3use clap::{Args, Subcommand};
4use cloudpub_common::config::MaskedString;
5use cloudpub_common::protocol::{
6 Acl, Auth, ClientEndpoint, DefaultPort, FilterAction, FilterRule, Header, Protocol, Role,
7};
8use serde::{Deserialize, Serialize};
9use std::net::ToSocketAddrs;
10use std::str::FromStr;
11
12const ROLE_SEP: &str = ":";
13
14#[derive(Subcommand, Debug, Clone)]
15pub enum Commands {
16 #[clap(about = "Set value in the config")]
17 Set(SetArgs),
18 #[clap(about = "Get value from the config")]
19 Get(GetArgs),
20 #[clap(about = "Show all config options and their values")]
21 Options,
22 #[clap(about = "Run all registered services")]
23 Run {
24 #[clap(long, hide = true)]
25 run_as_service: bool,
26 },
27 #[clap(about = "Start publication")]
28 Start(GuidArgs),
29 #[clap(about = "Stop publication")]
30 Stop(GuidArgs),
31 #[clap(about = "Break current operation (used internally)", hide = true)]
32 Break,
33 #[clap(about = "Register service on server")]
34 Register(PublishArgs),
35 #[clap(about = "Register service and run it")]
36 Publish(PublishArgs),
37 #[clap(about = "Unregister service")]
38 Unpublish(GuidArgs),
39 #[clap(about = "List all registered services")]
40 Ls,
41 #[clap(about = "Clean all registered services")]
42 Clean,
43 #[clap(about = "Purge cache")]
44 Purge,
45 #[clap(about = "Ping server and measure roundtrip time")]
46 Ping(PingArg),
47 #[clap(about = "Login to server and save token")]
48 Login(LoginArgs),
49 #[clap(about = "Logout and clear saved token")]
50 Logout,
51 #[clap(about = "Upgrade client to the latest version")]
52 Upgrade,
53 #[clap(about = "Manage system service")]
54 Service {
55 #[clap(subcommand)]
56 action: ServiceAction,
57 },
58}
59
60#[derive(Subcommand, Debug, Clone)]
61pub enum ServiceAction {
62 #[clap(about = "Install as a system service")]
63 Install {
64 #[clap(short, long, help = "Path to config file to use for the service")]
65 conf: Option<String>,
66 },
67
68 #[clap(about = "Uninstall the system service")]
69 Uninstall,
70
71 #[clap(about = "Start the service")]
72 Start,
73
74 #[clap(about = "Stop the service")]
75 Stop,
76
77 #[clap(about = "Get service status")]
78 Status,
79}
80
81#[derive(Args, Debug, Serialize, Deserialize, Clone)]
82pub struct SetArgs {
83 pub key: String,
84 pub value: String,
85}
86
87#[derive(Args, Debug, Serialize, Deserialize, Clone)]
88pub struct PingArg {
89 #[clap(short = 'N', long = "num", help = "Number of parallel pings")]
90 pub num: Option<i32>,
91 #[clap(short = 'B', long = "bare", help = "Output time in µs")]
92 pub bare: bool,
93}
94
95#[derive(Args, Debug, Serialize, Deserialize, Clone)]
96pub struct GetArgs {
97 pub key: String,
98}
99
100#[derive(Args, Debug, Clone)]
101pub struct PublishArgs {
102 #[clap(help = "Protocol to use")]
103 pub protocol: Protocol,
104 #[clap(help = "URL, socket address, port or file path")]
105 pub address: String,
106 #[clap(short = 'U', long = "username", help = "Username")]
107 pub username: Option<String>,
108 #[clap(short = 'P', long = "password", help = "Password")]
109 pub password: Option<MaskedString>,
110 #[clap(short, long, help = "Optional name of the service to publish")]
111 pub name: Option<String>,
112 #[clap(short, long, help = "Authentification type")]
113 pub auth: Option<Auth>,
114 #[clap(short='A', long="acl", help = "Access list", value_parser = AclParser)]
115 pub acl: Vec<Acl>,
116 #[clap(short='H', long="header", help = "HTTP headers", value_parser = HeaderParser)]
117 pub headers: Vec<Header>,
118 #[clap(short='R', long="rule", help = "Filter rules", value_parser = RuleParser)]
119 pub rules: Vec<FilterRule>,
120}
121
122#[derive(Args, Debug, Serialize, Deserialize, Clone)]
123pub struct GuidArgs {
124 pub guid: String,
125}
126
127#[derive(Args, Debug, Serialize, Deserialize, Clone)]
128pub struct LoginArgs {
129 #[clap(
130 help = "Email address (if not provided, will prompt)",
131 required = false
132 )]
133 pub email: Option<String>,
134 #[clap(
135 help = "Password (if not provided, will prompt or use CLO_PASSWORD env var)",
136 env = "CLO_PASSWORD",
137 hide_env_values = true,
138 required = false
139 )]
140 pub password: Option<String>,
141}
142
143impl PublishArgs {
144 pub fn parse(&self) -> Result<ClientEndpoint> {
145 let auth = self.auth.unwrap_or(if self.protocol == Protocol::Webdav {
146 Auth::Basic
147 } else {
148 Auth::None
149 });
150 if self.address.contains("://") {
151 let url = url::Url::parse(&self.address).context(crate::t!("invalid-url"))?;
152 let local_proto =
153 Protocol::from_str(url.scheme()).context(crate::t!("invalid-protocol"))?;
154 let local_addr = url.host_str().unwrap().to_string();
155 let local_port = url
156 .port()
157 .or_else(|| self.protocol.default_port())
158 .context(crate::t!("port-required"))?;
159 let mut local_path = url.path().to_string();
160 if url.query().is_some() {
161 local_path.push('?');
162 local_path.push_str(url.query().unwrap());
163 }
164 let mut username = String::new();
165 if !url.username().is_empty() {
166 username = url.username().to_string();
167 }
168 let mut password = MaskedString(String::new());
169 if let Some(pass) = url.password() {
170 password = MaskedString(pass.to_string());
171 }
172 let mut filter_rules = self.rules.clone();
173 for (index, rule) in filter_rules.iter_mut().enumerate() {
174 rule.order = index as i32;
175 }
176
177 Ok(ClientEndpoint {
178 description: self.name.clone(),
179 local_proto: local_proto.into(),
180 local_addr,
181 local_port: local_port as u32,
182 local_path,
183 nodelay: Some(true),
184 auth: auth.into(),
185 acl: self.acl.clone(),
186 headers: self.headers.clone(),
187 filter_rules,
188 username,
189 password: password.0,
190 proxy_protocol: 0, })
192 } else {
193 let (local_addr, local_port, local_path) = match self.protocol {
194 Protocol::OneC | Protocol::Minecraft | Protocol::Webdav => {
195 (self.address.clone(), 0, String::new())
196 }
197
198 Protocol::Http
199 | Protocol::Https
200 | Protocol::Tcp
201 | Protocol::Udp
202 | Protocol::Rtsp
203 | Protocol::Rdp
204 | Protocol::Vnc
205 | Protocol::Ssh => {
206 if let Ok(port) = self.address.parse::<u16>() {
207 ("localhost".to_string(), port, String::new())
208 } else {
209 let mut address = self.address.split('/').next().unwrap().to_string();
210 let path = self.address[address.len()..].to_string();
211
212 if let Some(port) = self.protocol.default_port() {
213 if !address.contains(":") {
214 address.push(':');
215 address.push_str(port.to_string().as_str());
216 }
217 }
218
219 match address.to_socket_addrs() {
220 Ok(mut addrs) => {
221 if addrs.next().is_some() {
222 let parts = address.split(':').collect::<Vec<&str>>();
224 (parts[0].to_string(), parts[1].parse::<u16>().unwrap(), path)
225 } else {
226 bail!(crate::t!("invalid-address", "address" => address));
227 }
228 }
229 Err(err) => bail!(
230 crate::t!("invalid-address-error", "error" => err.to_string(), "address" => address)
231 ),
232 }
233 }
234 }
235 };
236
237 let mut filter_rules = self.rules.clone();
238 for (index, rule) in filter_rules.iter_mut().enumerate() {
239 rule.order = index as i32;
240 }
241
242 Ok(ClientEndpoint {
243 description: self.name.clone(),
244 local_proto: self.protocol.into(),
245 local_addr,
246 local_port: local_port as u32,
247 local_path,
248 nodelay: Some(true),
249 auth: auth.into(),
250 acl: self.acl.clone(),
251 headers: self.headers.clone(),
252 filter_rules,
253 username: self.username.clone().unwrap_or("".to_string()),
254 password: self
255 .password
256 .clone()
257 .unwrap_or(MaskedString("".to_string()))
258 .to_string(),
259 proxy_protocol: 0, })
261 }
262 }
263}
264
265impl PartialEq for PublishArgs {
266 fn eq(&self, other: &Self) -> bool {
267 self.protocol == other.protocol && self.address == other.address
268 }
269}
270
271const HEADER_SEP: &str = ":";
272
273#[derive(Debug, Clone)]
274struct HeaderParser;
275
276impl TypedValueParser for HeaderParser {
277 type Value = Header;
278
279 fn parse_ref(
280 &self,
281 _cmd: &clap::Command,
282 _arg: Option<&clap::Arg>,
283 value: &std::ffi::OsStr,
284 ) -> Result<Self::Value, clap::Error> {
285 let value = value.to_string_lossy();
286 let parts: Vec<&str> = value.splitn(2, HEADER_SEP).collect();
287 if parts.len() != 2 {
288 return Err(clap::Error::raw(
289 clap::error::ErrorKind::ValueValidation,
290 format!("Invalid Header format (should be 'name:value'): {}", value),
291 ));
292 }
293 Ok(Header {
294 name: parts[0].trim().to_string(),
295 value: parts[1].trim().to_string(),
296 })
297 }
298}
299
300#[derive(Debug, Clone)]
301struct AclParser;
302
303impl TypedValueParser for AclParser {
304 type Value = Acl;
305
306 fn parse_ref(
307 &self,
308 _cmd: &clap::Command,
309 _arg: Option<&clap::Arg>,
310 value: &std::ffi::OsStr,
311 ) -> Result<Self::Value, clap::Error> {
312 let value = value.to_string_lossy();
313 let parts: Vec<&str> = value.split(ROLE_SEP).collect();
314 if parts.len() != 2 {
315 return Err(clap::Error::raw(
316 clap::error::ErrorKind::ValueValidation,
317 format!("Invalid Acl: {}", value),
318 ));
319 }
320 let role = Role::from_str(parts[1]).map_err(|_err| {
321 clap::Error::raw(
322 clap::error::ErrorKind::ValueValidation,
323 format!("Invalid role: {}", parts[1]),
324 )
325 })?;
326 Ok(Acl {
327 user: parts[0].to_string(),
328 role: role.into(),
329 })
330 }
331}
332
333#[derive(Debug, Clone)]
334struct RuleParser;
335
336impl TypedValueParser for RuleParser {
337 type Value = FilterRule;
338
339 fn parse_ref(
340 &self,
341 _cmd: &clap::Command,
342 _arg: Option<&clap::Arg>,
343 value: &std::ffi::OsStr,
344 ) -> Result<Self::Value, clap::Error> {
345 let value = value.to_string_lossy();
346 let parts: Vec<&str> = value.splitn(3, ":").collect();
347 if parts.is_empty() {
348 return Err(clap::Error::raw(
349 clap::error::ErrorKind::ValueValidation,
350 format!(
351 "Invalid rule format (should be 'action_type[:action_value][:data]'): {}",
352 value
353 ),
354 ));
355 }
356
357 let action_type = FilterAction::from_str(parts[0]).map_err(|_| {
358 clap::Error::raw(
359 clap::error::ErrorKind::ValueValidation,
360 format!("Invalid action type: {}", parts[0]),
361 )
362 })?;
363
364 let (action_value, data) = if action_type == FilterAction::FilterRedirect {
365 if parts.len() < 2 {
367 return Err(clap::Error::raw(
368 clap::error::ErrorKind::ValueValidation,
369 format!("FILTER_REDIRECT requires action_value: {}", value),
370 ));
371 }
372 let action_value = if parts[1].is_empty() {
373 None
374 } else {
375 Some(parts[1].to_string())
376 };
377 let data = parts.get(2).unwrap_or(&"").to_string();
378 (action_value, data)
379 } else {
380 let data = parts.get(1).unwrap_or(&"").to_string();
382 (None, data)
383 };
384
385 Ok(FilterRule {
386 order: 0, action_value,
388 action_type: action_type.into(),
389 data,
390 })
391 }
392}