cloudpub_client/
commands.rs

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, // ProxyProtocolNone
191            })
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                                    // Split original address to addr and port
223                                    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, // ProxyProtocolNone
260            })
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            // For FILTER_REDIRECT, expect action_value and optional data
366            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            // For other actions, no action_value, but data can be in parts[1]
381            let data = parts.get(1).unwrap_or(&"").to_string();
382            (None, data)
383        };
384
385        Ok(FilterRule {
386            order: 0, // Will be set based on position in the arguments list
387            action_value,
388            action_type: action_type.into(),
389            data,
390        })
391    }
392}