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