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 = "Send a bug report with the client logs to support")]
54 Report(ReportArgs),
55 #[clap(about = "Manage system service")]
56 Service {
57 #[clap(subcommand)]
58 action: ServiceAction,
59 },
60}
61
62#[derive(Subcommand, Debug, Clone)]
63pub enum ServiceAction {
64 #[clap(about = "Install as a system service")]
65 Install {
66 #[clap(short, long, help = "Path to config file to use for the service")]
67 conf: Option<String>,
68 },
69
70 #[clap(about = "Uninstall the system service")]
71 Uninstall,
72
73 #[clap(about = "Start the service")]
74 Start,
75
76 #[clap(about = "Stop the service")]
77 Stop,
78
79 #[clap(about = "Get service status")]
80 Status,
81}
82
83#[derive(Args, Debug, Serialize, Deserialize, Clone)]
84pub struct ReportArgs {
85 #[clap(
86 short,
87 long,
88 help = "What went wrong (optional, sent to support with the logs)"
89 )]
90 pub message: Option<String>,
91}
92
93#[derive(Args, Debug, Serialize, Deserialize, Clone)]
94pub struct SetArgs {
95 pub key: String,
96 pub value: String,
97}
98
99#[derive(Args, Debug, Serialize, Deserialize, Clone)]
100pub struct PingArg {
101 #[clap(short = 'N', long = "num", help = "Number of parallel pings")]
102 pub num: Option<i32>,
103 #[clap(short = 'B', long = "bare", help = "Output time in µs")]
104 pub bare: bool,
105}
106
107#[derive(Args, Debug, Serialize, Deserialize, Clone)]
108pub struct GetArgs {
109 pub key: String,
110}
111
112#[derive(Args, Debug, Clone)]
113pub struct PublishArgs {
114 #[clap(help = "Protocol to use")]
115 pub protocol: Protocol,
116 #[clap(help = "URL, socket address, port or file path")]
117 pub address: String,
118 #[clap(short = 'U', long = "username", help = "Username")]
119 pub username: Option<String>,
120 #[clap(short = 'P', long = "password", help = "Password")]
121 pub password: Option<MaskedString>,
122 #[clap(short, long, help = "Optional name of the service to publish")]
123 pub name: Option<String>,
124 #[clap(short, long, help = "Authentification type")]
125 pub auth: Option<Auth>,
126 #[clap(short='A', long="acl", help = "Access list", value_parser = AclParser)]
127 pub acl: Vec<Acl>,
128 #[clap(short='H', long="header", help = "HTTP headers", value_parser = HeaderParser)]
129 pub headers: Vec<Header>,
130 #[clap(short='R', long="rule", help = "Filter rules", value_parser = RuleParser)]
131 pub rules: Vec<FilterRule>,
132}
133
134#[derive(Args, Debug, Serialize, Deserialize, Clone)]
135pub struct GuidArgs {
136 pub guid: String,
137}
138
139#[derive(Args, Debug, Serialize, Deserialize, Clone)]
140pub struct LoginArgs {
141 #[clap(
142 help = "Email address (if not provided, will prompt)",
143 required = false
144 )]
145 pub email: Option<String>,
146 #[clap(
147 help = "Password (if not provided, will prompt or use CLO_PASSWORD env var)",
148 env = "CLO_PASSWORD",
149 hide_env_values = true,
150 required = false
151 )]
152 pub password: Option<String>,
153}
154
155impl PublishArgs {
156 pub fn parse(&self) -> Result<ClientEndpoint> {
157 let auth = self.auth.map(Into::into);
162 if self.address.contains("://") {
163 let url = url::Url::parse(&self.address).context(crate::t!("invalid-url"))?;
164 let local_proto =
165 Protocol::from_str(url.scheme()).context(crate::t!("invalid-protocol"))?;
166 let local_addr = url.host_str().unwrap().to_string();
167 let local_port = url
168 .port()
169 .or_else(|| self.protocol.default_port())
170 .context(crate::t!("port-required"))?;
171 let mut local_path = url.path().to_string();
172 if url.query().is_some() {
173 local_path.push('?');
174 local_path.push_str(url.query().unwrap());
175 }
176 let mut username = None;
177 if !url.username().is_empty() {
178 username = Some(url.username().to_string());
179 }
180 let password = url.password().map(|pass| MaskedString(pass.to_string()));
181 let mut filter_rules = self.rules.clone();
182 for (index, rule) in filter_rules.iter_mut().enumerate() {
183 rule.order = index as i32;
184 }
185
186 Ok(ClientEndpoint {
187 description: self.name.clone(),
188 local_proto: local_proto.into(),
189 local_addr,
190 local_port: local_port as u32,
191 local_path,
192 nodelay: Some(true),
193 auth,
194 acl: self.acl.clone(),
195 headers: self.headers.clone(),
196 filter_rules,
197 username,
198 password: password.map(|p| p.0),
199 proxy_protocol: None,
202 })
203 } else {
204 let (local_addr, local_port, local_path) = match self.protocol {
205 Protocol::OneC | Protocol::Minecraft | Protocol::Webdav => {
206 (self.address.clone(), 0, String::new())
207 }
208
209 Protocol::Http
210 | Protocol::Https
211 | Protocol::Tcp
212 | Protocol::Udp
213 | Protocol::Rtsp
214 | Protocol::Rdp
215 | Protocol::Vnc
216 | Protocol::Ssh => {
217 if let Ok(port) = self.address.parse::<u16>() {
218 ("localhost".to_string(), port, String::new())
219 } else {
220 let mut address = self.address.split('/').next().unwrap().to_string();
221 let path = self.address[address.len()..].to_string();
222
223 if let Some(port) = self.protocol.default_port() {
224 if !address.contains(":") {
225 address.push(':');
226 address.push_str(port.to_string().as_str());
227 }
228 }
229
230 match address.to_socket_addrs() {
231 Ok(mut addrs) => {
232 if addrs.next().is_some() {
233 let parts = address.split(':').collect::<Vec<&str>>();
235 (parts[0].to_string(), parts[1].parse::<u16>().unwrap(), path)
236 } else {
237 bail!(crate::t!("invalid-address", "address" => address));
238 }
239 }
240 Err(err) => bail!(
241 crate::t!("invalid-address-error", "error" => err.to_string(), "address" => address)
242 ),
243 }
244 }
245 }
246 };
247
248 let mut filter_rules = self.rules.clone();
249 for (index, rule) in filter_rules.iter_mut().enumerate() {
250 rule.order = index as i32;
251 }
252
253 Ok(ClientEndpoint {
254 description: self.name.clone(),
255 local_proto: self.protocol.into(),
256 local_addr,
257 local_port: local_port as u32,
258 local_path,
259 nodelay: Some(true),
260 auth,
261 acl: self.acl.clone(),
262 headers: self.headers.clone(),
263 filter_rules,
264 username: self.username.clone(),
265 password: self.password.clone().map(|p| p.to_string()),
266 proxy_protocol: None,
269 })
270 }
271 }
272}
273
274impl PartialEq for PublishArgs {
275 fn eq(&self, other: &Self) -> bool {
276 self.protocol == other.protocol && self.address == other.address
277 }
278}
279
280const HEADER_SEP: &str = ":";
281
282#[derive(Debug, Clone)]
283struct HeaderParser;
284
285impl TypedValueParser for HeaderParser {
286 type Value = Header;
287
288 fn parse_ref(
289 &self,
290 _cmd: &clap::Command,
291 _arg: Option<&clap::Arg>,
292 value: &std::ffi::OsStr,
293 ) -> Result<Self::Value, clap::Error> {
294 let value = value.to_string_lossy();
295 let parts: Vec<&str> = value.splitn(2, HEADER_SEP).collect();
296 if parts.len() != 2 {
297 return Err(clap::Error::raw(
298 clap::error::ErrorKind::ValueValidation,
299 format!("Invalid Header format (should be 'name:value'): {}", value),
300 ));
301 }
302 Ok(Header {
303 name: parts[0].trim().to_string(),
304 value: parts[1].trim().to_string(),
305 })
306 }
307}
308
309#[derive(Debug, Clone)]
310struct AclParser;
311
312impl TypedValueParser for AclParser {
313 type Value = Acl;
314
315 fn parse_ref(
316 &self,
317 _cmd: &clap::Command,
318 _arg: Option<&clap::Arg>,
319 value: &std::ffi::OsStr,
320 ) -> Result<Self::Value, clap::Error> {
321 let value = value.to_string_lossy();
322 let parts: Vec<&str> = value.split(ROLE_SEP).collect();
323 if parts.len() != 2 {
324 return Err(clap::Error::raw(
325 clap::error::ErrorKind::ValueValidation,
326 format!("Invalid Acl: {}", value),
327 ));
328 }
329 let role = Role::from_str(parts[1]).map_err(|_err| {
330 clap::Error::raw(
331 clap::error::ErrorKind::ValueValidation,
332 format!("Invalid role: {}", parts[1]),
333 )
334 })?;
335 Ok(Acl {
336 user: parts[0].to_string(),
337 role: role.into(),
338 })
339 }
340}
341
342#[derive(Debug, Clone)]
343struct RuleParser;
344
345impl TypedValueParser for RuleParser {
346 type Value = FilterRule;
347
348 fn parse_ref(
349 &self,
350 _cmd: &clap::Command,
351 _arg: Option<&clap::Arg>,
352 value: &std::ffi::OsStr,
353 ) -> Result<Self::Value, clap::Error> {
354 let value = value.to_string_lossy();
355 let parts: Vec<&str> = value.splitn(3, ":").collect();
356 if parts.is_empty() {
357 return Err(clap::Error::raw(
358 clap::error::ErrorKind::ValueValidation,
359 format!(
360 "Invalid rule format (should be 'action_type[:action_value][:data]'): {}",
361 value
362 ),
363 ));
364 }
365
366 let action_type = FilterAction::from_str(parts[0]).map_err(|_| {
367 clap::Error::raw(
368 clap::error::ErrorKind::ValueValidation,
369 format!("Invalid action type: {}", parts[0]),
370 )
371 })?;
372
373 let (action_value, data) = if action_type == FilterAction::FilterRedirect {
374 if parts.len() < 2 {
376 return Err(clap::Error::raw(
377 clap::error::ErrorKind::ValueValidation,
378 format!("FILTER_REDIRECT requires action_value: {}", value),
379 ));
380 }
381 let action_value = if parts[1].is_empty() {
382 None
383 } else {
384 Some(parts[1].to_string())
385 };
386 let data = parts.get(2).unwrap_or(&"").to_string();
387 (action_value, data)
388 } else {
389 let data = parts.get(1).unwrap_or(&"").to_string();
391 (None, data)
392 };
393
394 Ok(FilterRule {
395 order: 0, action_value,
397 action_type: action_type.into(),
398 data,
399 ..Default::default()
400 })
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 fn publish_args(protocol: Protocol, address: &str, auth: Option<Auth>) -> PublishArgs {
409 PublishArgs {
410 protocol,
411 address: address.to_string(),
412 username: None,
413 password: None,
414 name: None,
415 auth,
416 acl: Vec::new(),
417 headers: Vec::new(),
418 rules: Vec::new(),
419 }
420 }
421
422 #[test]
428 fn omitted_auth_flag_is_not_sent() {
429 let endpoint = publish_args(Protocol::Http, "8080", None).parse().unwrap();
430 assert_eq!(endpoint.auth, None);
431 assert_eq!(endpoint.username, None);
432 assert_eq!(endpoint.password, None);
433 }
434
435 #[test]
439 fn omitted_auth_flag_is_not_sent_for_webdav() {
440 let endpoint = publish_args(Protocol::Webdav, "/srv/files", None)
441 .parse()
442 .unwrap();
443 assert_eq!(endpoint.auth, None);
444 }
445
446 #[test]
447 fn explicit_auth_flag_is_sent() {
448 let endpoint = publish_args(Protocol::Http, "8080", Some(Auth::Basic))
449 .parse()
450 .unwrap();
451 assert_eq!(endpoint.auth, Some(Auth::Basic.into()));
452
453 let endpoint = publish_args(Protocol::Http, "8080", Some(Auth::None))
455 .parse()
456 .unwrap();
457 assert_eq!(endpoint.auth, Some(Auth::None.into()));
458 }
459
460 #[test]
465 fn proxy_protocol_is_never_sent() {
466 let endpoint = publish_args(Protocol::Tcp, "5432", None).parse().unwrap();
467 assert_eq!(endpoint.proxy_protocol, None);
468
469 let endpoint = publish_args(Protocol::Http, "http://localhost:8080", None)
470 .parse()
471 .unwrap();
472 assert_eq!(endpoint.proxy_protocol, None);
473 }
474
475 #[test]
477 fn credentials_from_url_are_sent() {
478 let endpoint = publish_args(Protocol::Http, "http://alice:secret@localhost:8080", None)
479 .parse()
480 .unwrap();
481 assert_eq!(endpoint.username, Some("alice".to_string()));
482 assert_eq!(endpoint.password, Some("secret".to_string()));
483 }
484}