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 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 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 let data = parts.get(1).unwrap_or(&"").to_string();
371 (None, data)
372 };
373
374 Ok(FilterRule {
375 order: 0, action_value,
377 action_type: action_type.into(),
378 data,
379 })
380 }
381}