diff --git a/.github/actions-rs/grcov.yml b/.github/actions-rs/grcov.yml index 82e7ae2..037f9bb 100644 --- a/.github/actions-rs/grcov.yml +++ b/.github/actions-rs/grcov.yml @@ -8,6 +8,7 @@ ignore: - "../*" - src/main.rs - src/activity_manager.rs + - src/cli_opts.rs - src/support.rs - src/system/notifications.rs - "src/ui/activities/*" diff --git a/CHANGELOG.md b/CHANGELOG.md index ff03c0a..823e10d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog - [Changelog](#changelog) + - [0.9.0](#090) - [0.8.2](#082) - [0.8.1](#081) - [0.8.0](#080) @@ -24,6 +25,25 @@ --- +## 0.9.0 + +Released on FIXME: + +> 🏖️ Tenerife Update 🍹 + +- **Bookmark name as hostname for CLI arguments** + - It is now possible to provide the name of the bookmark you want to connect to, instead of the address in command line arguments + + To do so it is enough to run termscp as follows: + + ```sh + termscp -b + ``` + + If the password is stored in the bookmark, it will be used, otherwise you will be prompted to type the password in. +- Dependencies: + - Updated `tui-realm` to `1.6.0` + ## 0.8.2 Released on 26/04/2022 diff --git a/Cargo.lock b/Cargo.lock index dca9ded..10e36bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,45 +441,20 @@ dependencies = [ [[package]] name = "crossterm" -version = "0.20.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d" +checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" dependencies = [ "bitflags", - "crossterm_winapi 0.8.0", + "crossterm_winapi", "libc", - "mio 0.7.14", - "parking_lot", + "mio", + "parking_lot 0.12.0", "signal-hook", "signal-hook-mio", "winapi", ] -[[package]] -name = "crossterm" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c" -dependencies = [ - "bitflags", - "crossterm_winapi 0.9.0", - "libc", - "mio 0.7.14", - "parking_lot", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507" -dependencies = [ - "winapi", -] - [[package]] name = "crossterm_winapi" version = "0.9.0" @@ -1275,19 +1250,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "mio" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" -dependencies = [ - "libc", - "log", - "miow", - "ntapi", - "winapi", -] - [[package]] name = "mio" version = "0.8.0" @@ -1580,7 +1542,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.3", ] [[package]] @@ -1597,6 +1569,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.2.11", + "smallvec", + "windows-sys", +] + [[package]] name = "path-slash" version = "0.1.4" @@ -2148,7 +2133,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" dependencies = [ "lazy_static", - "parking_lot", + "parking_lot 0.11.2", "serial_test_derive", ] @@ -2188,12 +2173,12 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" dependencies = [ "libc", - "mio 0.7.14", + "mio", "signal-hook", ] @@ -2254,7 +2239,7 @@ dependencies = [ "bitflags", "libc", "libssh2-sys", - "parking_lot", + "parking_lot 0.11.2", ] [[package]] @@ -2372,7 +2357,7 @@ dependencies = [ [[package]] name = "termscp" -version = "0.8.2" +version = "0.9.0" dependencies = [ "argh", "bitflags", @@ -2488,7 +2473,7 @@ dependencies = [ "bytes", "libc", "memchr", - "mio 0.8.0", + "mio", "num_cpus", "pin-project-lite", "socket2", @@ -2573,13 +2558,13 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "tui" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ed0a32c88b039b73f1b6c5acbd0554bfa5b6be94467375fd947c4de3a02271" +checksum = "96fe69244ec2af261bced1d9046a6fee6c8c2a6b0228e59e5ba39bc8ba4ed729" dependencies = [ "bitflags", "cassowary", - "crossterm 0.22.1", + "crossterm", "unicode-segmentation", "unicode-width", ] @@ -2597,12 +2582,12 @@ dependencies = [ [[package]] name = "tuirealm" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1d5d2db30d1e003c37b9f7aba038c3b28015a8cc0c1c364f049b211d649cfb" +checksum = "ef13e401c3c7d1f1b51d6193409d231c316057ad088431467a4e6882b4e25447" dependencies = [ "bitflags", - "crossterm 0.20.0", + "crossterm", "lazy_static", "regex", "thiserror", @@ -2892,36 +2877,79 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9f39345ae0c8ab072c0ac7fe8a8b411636aa34f89be19ddd0d9226544f13944" dependencies = [ - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_i686_gnu 0.24.0", + "windows_i686_msvc 0.24.0", + "windows_x86_64_gnu 0.24.0", + "windows_x86_64_msvc 0.24.0", ] +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + [[package]] name = "windows_i686_gnu" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0866510a3eca9aed73a077490bbbf03e5eaac4e1fd70849d89539e5830501fd" +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + [[package]] name = "windows_i686_msvc" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf0ffed56b7e9369a29078d2ab3aaeceea48eb58999d2cff3aa2494a275b95c6" +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + [[package]] name = "windows_x86_64_gnu" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384a173630588044205a2993b6864a2f56e5a8c1e7668c07b93ec18cf4888dc4" +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + [[package]] name = "windows_x86_64_msvc" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bd8f062d8ca5446358159d79a90be12c543b3a965c847c8f3eedf14b321d399" +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + [[package]] name = "winreg" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index 3fb8d81..33f3328 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ tempfile = "3.2.0" thiserror = "^1.0.0" toml = "0.5.8" tui-realm-stdlib = "1.1.6" -tuirealm = "1.5.0" +tuirealm = "1.6.0" unicode-width = "0.1.8" whoami = "1.2.1" wildmatch = "2.1.0" diff --git a/README.md b/README.md index c7b5477..ce8f69a 100644 --- a/README.md +++ b/README.md @@ -223,10 +223,6 @@ The user manual can be found on the [termscp's website](https://veeso.github.io/ For **2022** there will be two major updates during the year. -Planned for *Summer update 2022*: - -- **File system watcher 🔭**: The feature consists in the possibility to track some files in order to automatically sync them with remote host. For the implementation [notify](https://github.com/notify-rs/notify) will be used. - Planned for *future updates ⏲️*: - **Translations 🌐**: The feature consists in the possibility for the user to install the language pack for the language he prefers in order to replace the default English interface. The following language will be provided along to English: diff --git a/docs/de/man.md b/docs/de/man.md index cb025a8..5785941 100644 --- a/docs/de/man.md +++ b/docs/de/man.md @@ -37,7 +37,12 @@ termscp can be started with the following options: `termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]` +OR + +`termscp [options]... -b [bookmark-name] [local-wrkdir]` + - `-P, --password ` if address is provided, password will be this argument +- `-b, --address-as-bookmark` resolve address argument as a bookmark name - `-c, --config` Open termscp starting from the configuration page - `-q, --quiet` Disable logging - `-t, --theme ` Import specified theme @@ -45,11 +50,11 @@ termscp can be started with the following options: - `-v, --version` Print version info - `-h, --help` Print help page -termscp can be started in two different mode, if no extra arguments is provided, termscp will show the authentication form, where the user will be able to provide the parameters required to connect to the remote peer. +termscp can be started in three different modes, if no extra arguments is provided, termscp will show the authentication form, where the user will be able to provide the parameters required to connect to the remote peer. Alternatively, the user can provide an address as argument to skip the authentication form and starting directly the connection to the remote server. -If address argument is provided you can also provide the start working directory for local host +If address argument or bookmark name is provided you can also provide the start working directory for local host ### Address argument 🌎 diff --git a/docs/es/man.md b/docs/es/man.md index 794b2fc..fb85aed 100644 --- a/docs/es/man.md +++ b/docs/es/man.md @@ -37,7 +37,12 @@ termscp se puede iniciar con las siguientes opciones: `termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]` +OR + +`termscp [options]... -b [bookmark-name] [local-wrkdir]` + - `-P, --password ` si se proporciona la dirección, la contraseña será este argumento +- `-b, --address-as-bookmark` resuelve el argumento de la dirección como un nombre de marcador - `-c, --config` Abrir termscp comenzando desde la página de configuración - `-q, --quiet` Deshabilitar el registro - `-t, --theme ` Importar tema especificado diff --git a/docs/fr/man.md b/docs/fr/man.md index 26389f2..ecce376 100644 --- a/docs/fr/man.md +++ b/docs/fr/man.md @@ -35,7 +35,12 @@ termscp peut être démarré avec les options suivantes : `termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]` +ou + +`termscp [options]... -b [bookmark-name] [local-wrkdir]` + - `-P, --password ` si l'adresse est fournie, le mot de passe sera cet argument +- `-b, --address-as-bookmark` résoudre l'argument d'adresse en tant que nom de signet - `-c, --config` Ouvrir termscp à partir de la page de configuration - `-q, --quiet` Désactiver la journalisation - `-t, --theme ` Importer le thème spécifié diff --git a/docs/it/man.md b/docs/it/man.md index a25338d..8a45121 100644 --- a/docs/it/man.md +++ b/docs/it/man.md @@ -35,7 +35,12 @@ termscp può essere lanciato con questi argomenti: `termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]` +O + +`termscp [options]... -b [bookmark-name] [local-wrkdir]` + - `-P, --password ` Se viene fornito l'argomento indirizzo, questa sarà la password utilizzata per autenticarsi +- `-b, --address-as-bookmark` risolve l'argomento indirizzo come nome di un segnalibro - `-c, --config` Apri la configurazione di termscp - `-q, --quiet` Disabilita i log - `-t, --theme ` Importa il tema al percorso fornito diff --git a/docs/man.md b/docs/man.md index 5c4cc18..131cac8 100644 --- a/docs/man.md +++ b/docs/man.md @@ -35,7 +35,12 @@ termscp can be started with the following options: `termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]` +OR + +`termscp [options]... -b [bookmark-name] [local-wrkdir]` + - `-P, --password ` if address is provided, password will be this argument +- `-b, --address-as-bookmark` resolve address argument as a bookmark name - `-c, --config` Open termscp starting from the configuration page - `-q, --quiet` Disable logging - `-t, --theme ` Import specified theme @@ -43,11 +48,11 @@ termscp can be started with the following options: - `-v, --version` Print version info - `-h, --help` Print help page -termscp can be started in two different mode, if no extra arguments is provided, termscp will show the authentication form, where the user will be able to provide the parameters required to connect to the remote peer. +termscp can be started in three different modes, if no extra arguments is provided, termscp will show the authentication form, where the user will be able to provide the parameters required to connect to the remote peer. Alternatively, the user can provide an address as argument to skip the authentication form and starting directly the connection to the remote server. -If address argument is provided you can also provide the start working directory for local host +If address argument or bookmark name is provided you can also provide the start working directory for local host ### Address argument 🌎 diff --git a/docs/zh-CN/man.md b/docs/zh-CN/man.md index 6f89221..b11a0c6 100644 --- a/docs/zh-CN/man.md +++ b/docs/zh-CN/man.md @@ -35,7 +35,12 @@ termscp启动时可以使用以下选项: `termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]` +或作为 + +`termscp [options]... -b [bookmark-name] [local-wrkdir]` + - `-P, --password ` 登陆密码 +- `-b, --address-as-bookmark` 将地址参数解析为书签名称 - `-c, --config` 打开termscp时打开配置页面 - `-q, --quiet` 禁用日志 - `-t, --theme ` 导入自定义主题 diff --git a/src/activity_manager.rs b/src/activity_manager.rs index 301d872..4e95e82 100644 --- a/src/activity_manager.rs +++ b/src/activity_manager.rs @@ -28,6 +28,7 @@ // Deps use crate::filetransfer::FileTransferParams; use crate::host::{HostError, Localhost}; +use crate::system::bookmarks_client::BookmarksClient; use crate::system::config_client::ConfigClient; use crate::system::environment; use crate::system::theme_provider::ThemeProvider; @@ -36,6 +37,8 @@ use crate::ui::activities::{ ExitReason, }; use crate::ui::context::Context; +use crate::utils::fmt; +use crate::utils::tty; // Namespaces use std::path::{Path, PathBuf}; @@ -64,7 +67,7 @@ impl ActivityManager { pub fn new(local_dir: &Path, ticks: Duration) -> Result { // Prepare Context // Initialize configuration client - let (config_client, error): (ConfigClient, Option) = + let (config_client, error_config): (ConfigClient, Option) = match Self::init_config_client() { Ok(cli) => (cli, None), Err(err) => { @@ -72,8 +75,13 @@ impl ActivityManager { (ConfigClient::degraded(), Some(err)) } }; + let (bookmarks_client, error_bookmark) = match Self::init_bookmarks_client() { + Ok(cli) => (cli, None), + Err(err) => (None, Some(err)), + }; + let error = error_config.or(error_bookmark); let theme_provider: ThemeProvider = Self::init_theme_provider(); - let ctx: Context = Context::new(config_client, theme_provider, error); + let ctx: Context = Context::new(bookmarks_client, config_client, theme_provider, error); Ok(ActivityManager { context: Some(ctx), local_dir: local_dir.to_path_buf(), @@ -82,9 +90,54 @@ impl ActivityManager { } /// Set file transfer params - pub fn set_filetransfer_params(&mut self, params: FileTransferParams) { + pub fn set_filetransfer_params( + &mut self, + mut params: FileTransferParams, + password: Option<&str>, + ) -> Result<(), String> { + // Set password if provided + if params.password_missing() { + if let Some(password) = password { + params.set_default_secret(password.to_string()); + } else { + match tty::read_secret_from_tty("Password: ") { + Err(err) => return Err(format!("Could not read password: {}", err)), + Ok(Some(secret)) => { + debug!( + "Read password from tty: {}", + fmt::shadow_password(secret.as_str()) + ); + params.set_default_secret(secret); + } + Ok(None) => {} + } + } + } // Put params into the context self.context.as_mut().unwrap().set_ftparams(params); + Ok(()) + } + + /// Resolve provided bookmark name and set it as file transfer params. + /// Returns error if bookmark is not found + pub fn resolve_bookmark_name( + &mut self, + bookmark_name: &str, + password: Option<&str>, + ) -> Result<(), String> { + if let Some(bookmarks_client) = self.context.as_mut().unwrap().bookmarks_client_mut() { + match bookmarks_client.get_bookmark(bookmark_name) { + None => Err(format!( + r#"Could not resolve bookmark name: "{}" no such bookmark"#, + bookmark_name + )), + Some(params) => self.set_filetransfer_params(params, password), + } + } else { + Err(String::from( + "Could not resolve bookmark name: bookmarks client not initialized", + )) + } } /// @@ -256,6 +309,33 @@ impl ActivityManager { // -- misc + fn init_bookmarks_client() -> Result, String> { + // Get config dir + match environment::init_config_dir() { + Ok(path) => { + // If some configure client, otherwise do nothing; don't bother users telling them that bookmarks are not supported on their system. + if let Some(config_dir_path) = path { + let bookmarks_file: PathBuf = + environment::get_bookmarks_paths(config_dir_path.as_path()); + // Initialize client + BookmarksClient::new(bookmarks_file.as_path(), config_dir_path.as_path(), 16) + .map(Option::Some) + .map_err(|e| { + format!( + "Could not initialize bookmarks (at \"{}\", \"{}\"): {}", + bookmarks_file.display(), + config_dir_path.display(), + e + ) + }) + } else { + Ok(None) + } + } + Err(err) => Err(err), + } + } + /// Initialize configuration client fn init_config_client() -> Result { // Get config dir diff --git a/src/cli_opts.rs b/src/cli_opts.rs new file mode 100644 index 0000000..89db088 --- /dev/null +++ b/src/cli_opts.rs @@ -0,0 +1,148 @@ +//! ## CLI opts +//! +//! defines the types for main.rs types + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use argh::FromArgs; + +use crate::activity_manager::NextActivity; +use crate::filetransfer::FileTransferParams; +use crate::system::logging::LogLevel; + +use std::path::PathBuf; +use std::time::Duration; + +pub enum Task { + Activity(NextActivity), + ImportTheme(PathBuf), + InstallUpdate, +} + +#[derive(FromArgs)] +#[argh(description = " +where positional can be: + - [address] [local-wrkdir] + OR + - [bookmark-Name] [local-wrkdir] + +Address syntax can be: + + - `protocol://user@address:port:wrkdir` for protocols such as Sftp, Scp, Ftp + - `s3://bucket-name@region:profile:/wrkdir` for Aws S3 protocol + +Please, report issues to +Please, consider supporting the author ")] +pub struct Args { + #[argh( + switch, + short = 'b', + description = "resolve address argument as a bookmark name" + )] + pub address_as_bookmark: bool, + #[argh(switch, short = 'c', description = "open termscp configuration")] + pub config: bool, + #[argh(switch, short = 'D', description = "enable TRACE log level")] + pub debug: bool, + #[argh(option, short = 'P', description = "provide password from CLI")] + pub password: Option, + #[argh(switch, short = 'q', description = "disable logging")] + pub quiet: bool, + #[argh(option, short = 't', description = "import specified theme")] + pub theme: Option, + #[argh( + switch, + short = 'u', + description = "update termscp to the latest version" + )] + pub update: bool, + #[argh( + option, + short = 'T', + default = "10", + description = "set UI ticks; default 10ms" + )] + pub ticks: u64, + #[argh(switch, short = 'v', description = "print version")] + pub version: bool, + // -- positional + #[argh( + positional, + description = "protocol://user@address:port:wrkdir local-wrkdir" + )] + pub positional: Vec, +} + +pub struct RunOpts { + pub remote: Remote, + pub ticks: Duration, + pub log_level: LogLevel, + pub task: Task, +} + +impl Default for RunOpts { + fn default() -> Self { + Self { + remote: Remote::None, + ticks: Duration::from_millis(10), + log_level: LogLevel::Info, + task: Task::Activity(NextActivity::Authentication), + } + } +} + +#[allow(clippy::large_enum_variant)] +pub enum Remote { + Bookmark(BookmarkParams), + Host(HostParams), + None, +} + +pub struct BookmarkParams { + pub name: String, + pub password: Option, +} + +pub struct HostParams { + pub params: FileTransferParams, + pub password: Option, +} + +impl BookmarkParams { + pub fn new>(name: S, password: Option) -> Self { + Self { + name: name.as_ref().to_string(), + password: password.map(|x| x.as_ref().to_string()), + } + } +} + +impl HostParams { + pub fn new>(params: FileTransferParams, password: Option) -> Self { + Self { + params, + password: password.map(|x| x.as_ref().to_string()), + } + } +} diff --git a/src/filetransfer/params.rs b/src/filetransfer/params.rs index 3838f53..65674a8 100644 --- a/src/filetransfer/params.rs +++ b/src/filetransfer/params.rs @@ -84,6 +84,23 @@ impl FileTransferParams { self.entry_directory = dir.map(|x| x.as_ref().to_path_buf()); self } + + /// Returns whether a password is supposed to be required for this protocol params. + /// The result true is returned ONLY if the supposed secret is MISSING!!! + pub fn password_missing(&self) -> bool { + match &self.params { + ProtocolParams::AwsS3(params) => params.password_missing(), + ProtocolParams::Generic(params) => params.password_missing(), + } + } + + /// Set the secret to ft params for the default secret field for this protocol + pub fn set_default_secret(&mut self, secret: String) { + match &mut self.params { + ProtocolParams::AwsS3(params) => params.set_default_secret(secret), + ProtocolParams::Generic(params) => params.set_default_secret(secret), + } + } } impl Default for FileTransferParams { @@ -108,6 +125,7 @@ impl ProtocolParams { } } + #[cfg(test)] /// Get a mutable reference to the inner generic protocol params pub fn mut_generic_params(&mut self) -> Option<&mut GenericProtocolParams> { match self { @@ -163,6 +181,17 @@ impl GenericProtocolParams { self.password = password.map(|x| x.as_ref().to_string()); self } + + /// Returns whether a password is supposed to be required for this protocol params. + /// The result true is returned ONLY if the supposed secret is MISSING!!! + pub fn password_missing(&self) -> bool { + self.password.is_none() + } + + /// Set password + pub fn set_default_secret(&mut self, secret: String) { + self.password = Some(secret); + } } // -- S3 params @@ -218,6 +247,17 @@ impl AwsS3Params { self.new_path_style = new_path_style; self } + + /// Returns whether a password is supposed to be required for this protocol params. + /// The result true is returned ONLY if the supposed secret is MISSING!!! + pub fn password_missing(&self) -> bool { + self.secret_access_key.is_none() && self.security_token.is_none() + } + + /// Set password + pub fn set_default_secret(&mut self, secret: String) { + self.secret_access_key = Some(secret); + } } #[cfg(test)] @@ -300,4 +340,83 @@ mod test { assert!(params.generic_params().is_some()); assert!(params.mut_generic_params().is_some()); } + + #[test] + fn password_missing() { + assert!(FileTransferParams::new( + FileTransferProtocol::Scp, + ProtocolParams::AwsS3(AwsS3Params::new("omar", Some("eu-west-1"), Some("test"))) + ) + .password_missing()); + assert_eq!( + FileTransferParams::new( + FileTransferProtocol::Scp, + ProtocolParams::AwsS3( + AwsS3Params::new("omar", Some("eu-west-1"), Some("test")) + .secret_access_key(Some("test")) + ) + ) + .password_missing(), + false + ); + assert_eq!( + FileTransferParams::new( + FileTransferProtocol::Scp, + ProtocolParams::AwsS3( + AwsS3Params::new("omar", Some("eu-west-1"), Some("test")) + .security_token(Some("test")) + ) + ) + .password_missing(), + false + ); + assert!( + FileTransferParams::new(FileTransferProtocol::Scp, ProtocolParams::default()) + .password_missing() + ); + assert_eq!( + FileTransferParams::new( + FileTransferProtocol::Scp, + ProtocolParams::Generic(GenericProtocolParams::default().password(Some("Hello"))) + ) + .password_missing(), + false + ); + } + + #[test] + fn set_default_secret_aws_s3() { + let mut params = FileTransferParams::new( + FileTransferProtocol::Scp, + ProtocolParams::AwsS3(AwsS3Params::new("omar", Some("eu-west-1"), Some("test"))), + ); + params.set_default_secret(String::from("secret")); + assert_eq!( + params + .params + .s3_params() + .unwrap() + .secret_access_key + .as_deref() + .unwrap(), + "secret" + ); + } + + #[test] + fn set_default_secret_generic() { + let mut params = + FileTransferParams::new(FileTransferProtocol::Scp, ProtocolParams::default()); + params.set_default_secret(String::from("secret")); + assert_eq!( + params + .params + .generic_params() + .unwrap() + .password + .as_deref() + .unwrap(), + "secret" + ); + } } diff --git a/src/main.rs b/src/main.rs index 2abe467..0070917 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,6 @@ const TERMSCP_VERSION: &str = env!("CARGO_PKG_VERSION"); const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); // Crates -extern crate argh; #[macro_use] extern crate bitflags; #[macro_use] @@ -37,13 +36,13 @@ extern crate log; extern crate magic_crypt; // External libs -use argh::FromArgs; use std::env; use std::path::PathBuf; use std::time::Duration; // Include mod activity_manager; +mod cli_opts; mod config; mod explorer; mod filetransfer; @@ -55,82 +54,14 @@ mod utils; // namespaces use activity_manager::{ActivityManager, NextActivity}; +use cli_opts::{Args, BookmarkParams, HostParams, Remote, RunOpts, Task}; use filetransfer::FileTransferParams; use system::logging::{self, LogLevel}; -enum Task { - Activity(NextActivity), - ImportTheme(PathBuf), - InstallUpdate, -} - -#[derive(FromArgs)] -#[argh(description = " -where positional can be: [address] [local-wrkdir] - -Address syntax can be: - - - `protocol://user@address:port:wrkdir` for protocols such as Sftp, Scp, Ftp - - `s3://bucket-name@region:profile:/wrkdir` for Aws S3 protocol - -Please, report issues to -Please, consider supporting the author ")] -struct Args { - #[argh(switch, short = 'c', description = "open termscp configuration")] - config: bool, - #[argh(switch, short = 'D', description = "enable TRACE log level")] - debug: bool, - #[argh(option, short = 'P', description = "provide password from CLI")] - password: Option, - #[argh(switch, short = 'q', description = "disable logging")] - quiet: bool, - #[argh(option, short = 't', description = "import specified theme")] - theme: Option, - #[argh( - switch, - short = 'u', - description = "update termscp to the latest version" - )] - update: bool, - #[argh( - option, - short = 'T', - default = "10", - description = "set UI ticks; default 10ms" - )] - ticks: u64, - #[argh(switch, short = 'v', description = "print version")] - version: bool, - // -- positional - #[argh( - positional, - description = "protocol://user@address:port:wrkdir local-wrkdir" - )] - positional: Vec, -} - -struct RunOpts { - remote: Option, - ticks: Duration, - log_level: LogLevel, - task: Task, -} - -impl Default for RunOpts { - fn default() -> Self { - Self { - remote: None, - ticks: Duration::from_millis(10), - log_level: LogLevel::Info, - task: Task::Activity(NextActivity::Authentication), - } - } -} - fn main() { let args: Args = argh::from_env(); // Parse args - let mut run_opts: RunOpts = match parse_args(args) { + let run_opts: RunOpts = match parse_args(args) { Ok(opts) => opts, Err(err) => { eprintln!("{}", err); @@ -141,22 +72,15 @@ fn main() { if let Err(err) = logging::init(run_opts.log_level) { eprintln!("Failed to initialize logging: {}", err); } - // Read password from remote - if let Err(err) = read_password(&mut run_opts) { - eprintln!("{}", err); - std::process::exit(255); - } info!("termscp {} started!", TERMSCP_VERSION); // Run info!("Starting activity manager..."); - let rc: i32 = run(run_opts); + let rc = run(run_opts); info!("termscp terminated with exitcode {}", rc); // Then return std::process::exit(rc); } -/// ### parse_args -/// /// Parse arguments /// In case of success returns `RunOpts` /// in case something is wrong returns the error message @@ -182,7 +106,7 @@ fn parse_args(args: Args) -> Result { // Match ticks run_opts.ticks = Duration::from_millis(args.ticks); // @! extra modes - if let Some(theme) = args.theme { + if let Some(theme) = args.theme.as_deref() { run_opts.task = Task::ImportTheme(PathBuf::from(theme)); } if args.update { @@ -190,26 +114,17 @@ fn parse_args(args: Args) -> Result { } // @! Ordinary mode // Remote argument - if let Some(remote) = args.positional.get(0) { - // Parse address - match utils::parser::parse_remote_opt(remote.as_str()) { - Ok(mut remote) => { - // If password is provided, set password - if let Some(passwd) = args.password { - if let Some(mut params) = remote.params.mut_generic_params() { - params.password = Some(passwd); - } - } - // Set params - run_opts.remote = Some(remote); - // In this case the first activity will be FileTransfer - run_opts.task = Task::Activity(NextActivity::FileTransfer); - } - Err(err) => { - return Err(format!("Bad address option: {}", err)); - } + match parse_address_arg(&args) { + Err(err) => return Err(err), + Ok(Remote::None) => {} + Ok(remote) => { + // Set params + run_opts.remote = remote; + // In this case the first activity will be FileTransfer + run_opts.task = Task::Activity(NextActivity::FileTransfer); } } + // Local directory if let Some(localdir) = args.positional.get(1) { // Change working directory if local dir is set @@ -221,43 +136,33 @@ fn parse_args(args: Args) -> Result { Ok(run_opts) } -/// ### read_password -/// -/// Read password from tty if address is specified -fn read_password(run_opts: &mut RunOpts) -> Result<(), String> { - // Initialize client if necessary - if let Some(remote) = run_opts.remote.as_mut() { - // Ask password for generic params - if let Some(mut params) = remote.params.mut_generic_params() { - // Ask password only if generic protocol params - if params.password.is_none() { - // Ask password if unspecified - params.password = match rpassword::read_password_from_tty(Some("Password: ")) { - Ok(p) => { - if p.is_empty() { - None - } else { - debug!( - "Read password from tty: {}", - utils::fmt::shadow_password(p.as_str()) - ); - Some(p) - } - } - Err(_) => { - return Err("Could not read password from prompt".to_string()); - } - }; - } +/// Parse address argument from cli args +fn parse_address_arg(args: &Args) -> Result { + if let Some(remote) = args.positional.get(0) { + if args.address_as_bookmark { + Ok(Remote::Bookmark(BookmarkParams::new( + remote, + args.password.as_ref(), + ))) + } else { + // Parse address + parse_remote_address(remote.as_str()) + .map(|x| Remote::Host(HostParams::new(x, args.password.as_deref()))) } + } else { + Ok(Remote::None) } - Ok(()) +} + +/// Parse remote address +fn parse_remote_address(remote: &str) -> Result { + utils::parser::parse_remote_opt(remote).map_err(|e| format!("Bad address option: {}", e)) } /// ### run /// /// Run task and return rc -fn run(mut run_opts: RunOpts) -> i32 { +fn run(run_opts: RunOpts) -> i32 { match run_opts.task { Task::ImportTheme(theme) => match support::import_theme(theme.as_path()) { Ok(_) => { @@ -295,8 +200,20 @@ fn run(mut run_opts: RunOpts) -> i32 { } }; // Set file transfer params if set - if let Some(remote) = run_opts.remote.take() { - manager.set_filetransfer_params(remote); + match run_opts.remote { + Remote::Bookmark(BookmarkParams { name, password }) => { + if let Err(err) = manager.resolve_bookmark_name(&name, password.as_deref()) { + eprintln!("{}", err); + return 1; + } + } + Remote::Host(HostParams { params, password }) => { + if let Err(err) = manager.set_filetransfer_params(params, password.as_deref()) { + eprintln!("{}", err); + return 1; + } + } + Remote::None => {} } manager.run(activity); 0 diff --git a/src/ui/activities/auth/bookmarks.rs b/src/ui/activities/auth/bookmarks.rs index 9b575c1..032c272 100644 --- a/src/ui/activities/auth/bookmarks.rs +++ b/src/ui/activities/auth/bookmarks.rs @@ -28,20 +28,15 @@ // Locals use super::{AuthActivity, FileTransferParams}; use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams}; -use crate::system::bookmarks_client::BookmarksClient; -use crate::system::environment; - -// Ext -use std::path::PathBuf; impl AuthActivity { /// Delete bookmark pub(super) fn del_bookmark(&mut self, idx: usize) { - if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() { + let name = self.bookmarks_list.get(idx).cloned(); + if let Some(bookmarks_cli) = self.bookmarks_client_mut() { // Iterate over kyes - let name: Option<&String> = self.bookmarks_list.get(idx); if let Some(name) = name { - bookmarks_cli.del_bookmark(name); + bookmarks_cli.del_bookmark(&name); // Write bookmarks self.write_bookmarks(); } @@ -52,7 +47,7 @@ impl AuthActivity { /// Load selected bookmark (at index) to input fields pub(super) fn load_bookmark(&mut self, idx: usize) { - if let Some(bookmarks_cli) = self.bookmarks_client.as_ref() { + if let Some(bookmarks_cli) = self.bookmarks_client() { // Iterate over bookmarks if let Some(key) = self.bookmarks_list.get(idx) { if let Some(bookmark) = bookmarks_cli.get_bookmark(key) { @@ -72,7 +67,7 @@ impl AuthActivity { return; } }; - if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() { + if let Some(bookmarks_cli) = self.bookmarks_client_mut() { bookmarks_cli.add_bookmark(name.clone(), params, save_password); // Save bookmarks self.write_bookmarks(); @@ -85,10 +80,10 @@ impl AuthActivity { } /// Delete recent pub(super) fn del_recent(&mut self, idx: usize) { - if let Some(client) = self.bookmarks_client.as_mut() { - let name: Option<&String> = self.recents_list.get(idx); + let name = self.recents_list.get(idx).cloned(); + if let Some(client) = self.bookmarks_client_mut() { if let Some(name) = name { - client.del_recent(name); + client.del_recent(&name); // Write bookmarks self.write_bookmarks(); } @@ -99,7 +94,7 @@ impl AuthActivity { /// Load selected recent (at index) to input fields pub(super) fn load_recent(&mut self, idx: usize) { - if let Some(client) = self.bookmarks_client.as_ref() { + if let Some(client) = self.bookmarks_client() { // Iterate over bookmarks if let Some(key) = self.recents_list.get(idx) { if let Some(bookmark) = client.get_recent(key) { @@ -119,7 +114,7 @@ impl AuthActivity { return; } }; - if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() { + if let Some(bookmarks_cli) = self.bookmarks_client_mut() { bookmarks_cli.add_recent(params); // Save bookmarks self.write_bookmarks(); @@ -128,7 +123,7 @@ impl AuthActivity { /// Write bookmarks to file fn write_bookmarks(&mut self) { - if let Some(bookmarks_cli) = self.bookmarks_client.as_ref() { + if let Some(bookmarks_cli) = self.bookmarks_client() { if let Err(err) = bookmarks_cli.write_bookmarks() { self.mount_error(format!("Could not write bookmarks: {}", err).as_str()); } @@ -137,58 +132,22 @@ impl AuthActivity { /// Initialize bookmarks client pub(super) fn init_bookmarks_client(&mut self) { - // Get config dir - match environment::init_config_dir() { - Ok(path) => { - // If some configure client, otherwise do nothing; don't bother users telling them that bookmarks are not supported on their system. - if let Some(config_dir_path) = path { - let bookmarks_file: PathBuf = - environment::get_bookmarks_paths(config_dir_path.as_path()); - // Initialize client - match BookmarksClient::new( - bookmarks_file.as_path(), - config_dir_path.as_path(), - 16, - ) { - Ok(cli) => { - // Load bookmarks into list - let mut bookmarks_list: Vec = - Vec::with_capacity(cli.iter_bookmarks().count()); - for bookmark in cli.iter_bookmarks() { - bookmarks_list.push(bookmark.clone()); - } - // Load recents into list - let mut recents_list: Vec = - Vec::with_capacity(cli.iter_recents().count()); - for recent in cli.iter_recents() { - recents_list.push(recent.clone()); - } - self.bookmarks_client = Some(cli); - self.bookmarks_list = bookmarks_list; - self.recents_list = recents_list; - // Sort bookmark list - self.sort_bookmarks(); - self.sort_recents(); - } - Err(err) => { - self.mount_error( - format!( - "Could not initialize bookmarks (at \"{}\", \"{}\"): {}", - bookmarks_file.display(), - config_dir_path.display(), - err - ) - .as_str(), - ); - } - } - } + if let Some(cli) = self.bookmarks_client_mut() { + // Load bookmarks into list + let mut bookmarks_list: Vec = Vec::with_capacity(cli.iter_bookmarks().count()); + for bookmark in cli.iter_bookmarks() { + bookmarks_list.push(bookmark.clone()); } - Err(err) => { - self.mount_error( - format!("Could not initialize configuration directory: {}", err).as_str(), - ); + // Load recents into list + let mut recents_list: Vec = Vec::with_capacity(cli.iter_recents().count()); + for recent in cli.iter_recents() { + recents_list.push(recent.clone()); } + self.bookmarks_list = bookmarks_list; + self.recents_list = recents_list; + // Sort bookmark list + self.sort_bookmarks(); + self.sort_recents(); } } diff --git a/src/ui/activities/auth/mod.rs b/src/ui/activities/auth/mod.rs index 490ffce..816b657 100644 --- a/src/ui/activities/auth/mod.rs +++ b/src/ui/activities/auth/mod.rs @@ -172,7 +172,6 @@ const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES"; /// AuthActivity is the data holder for the authentication activity pub struct AuthActivity { app: Application, - bookmarks_client: Option, /// List of bookmarks bookmarks_list: Vec, /// List of recent hosts @@ -196,7 +195,6 @@ impl AuthActivity { .poll_timeout(ticks), ), context: None, - bookmarks_client: None, bookmarks_list: Vec::new(), exit_reason: None, recents_list: Vec::new(), @@ -220,6 +218,14 @@ impl AuthActivity { self.context().config() } + fn bookmarks_client(&self) -> Option<&BookmarksClient> { + self.context().bookmarks_client() + } + + fn bookmarks_client_mut(&mut self) -> Option<&mut BookmarksClient> { + self.context_mut().bookmarks_client_mut() + } + /// Returns a reference to theme fn theme(&self) -> &Theme { self.context().theme_provider().theme() @@ -259,9 +265,8 @@ impl Activity for AuthActivity { // Initialize view self.init(); // Init bookmarks client - if self.bookmarks_client.is_none() { + if self.bookmarks_client().is_some() { self.init_bookmarks_client(); - // View bookarmsk self.view_bookmarks(); self.view_recent_connections(); } diff --git a/src/ui/activities/auth/view.rs b/src/ui/activities/auth/view.rs index 7ef7ee1..964e393 100644 --- a/src/ui/activities/auth/view.rs +++ b/src/ui/activities/auth/view.rs @@ -299,14 +299,7 @@ impl AuthActivity { .bookmarks_list .iter() .map(|x| { - Self::fmt_bookmark( - x, - self.bookmarks_client - .as_ref() - .unwrap() - .get_bookmark(x) - .unwrap(), - ) + Self::fmt_bookmark(x, self.bookmarks_client().unwrap().get_bookmark(x).unwrap()) }) .collect(); let bookmarks_color = self.theme().auth_bookmarks; @@ -325,15 +318,7 @@ impl AuthActivity { let bookmarks: Vec = self .recents_list .iter() - .map(|x| { - Self::fmt_recent( - self.bookmarks_client - .as_ref() - .unwrap() - .get_recent(x) - .unwrap(), - ) - }) + .map(|x| Self::fmt_recent(self.bookmarks_client().unwrap().get_recent(x).unwrap())) .collect(); let recents_color = self.theme().auth_recents; assert!(self diff --git a/src/ui/context.rs b/src/ui/context.rs index 52fc02a..9192eb0 100644 --- a/src/ui/context.rs +++ b/src/ui/context.rs @@ -28,6 +28,7 @@ // Locals use super::store::Store; use crate::filetransfer::FileTransferParams; +use crate::system::bookmarks_client::BookmarksClient; use crate::system::config_client::ConfigClient; use crate::system::theme_provider::ThemeProvider; @@ -36,6 +37,7 @@ use tuirealm::terminal::TerminalBridge; /// Context holds data structures shared by the activities pub struct Context { ft_params: Option, + bookmarks_client: Option, config_client: ConfigClient, pub(crate) store: Store, pub(crate) terminal: TerminalBridge, @@ -46,13 +48,15 @@ pub struct Context { impl Context { /// Instantiates a new Context pub fn new( + bookmarks_client: Option, config_client: ConfigClient, theme_provider: ThemeProvider, error: Option, ) -> Context { Context { - ft_params: None, + bookmarks_client, config_client, + ft_params: None, store: Store::init(), terminal: TerminalBridge::new().expect("Could not initialize terminal"), theme_provider, @@ -66,6 +70,14 @@ impl Context { self.ft_params.as_ref() } + pub fn bookmarks_client(&self) -> Option<&BookmarksClient> { + self.bookmarks_client.as_ref() + } + + pub fn bookmarks_client_mut(&mut self) -> Option<&mut BookmarksClient> { + self.bookmarks_client.as_mut() + } + pub fn config(&self) -> &ConfigClient { &self.config_client } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 910c5c7..59cf59a 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -33,6 +33,7 @@ pub mod parser; pub mod path; pub mod random; pub mod string; +pub mod tty; pub mod ui; #[cfg(test)] diff --git a/src/utils/tty.rs b/src/utils/tty.rs new file mode 100644 index 0000000..70fc773 --- /dev/null +++ b/src/utils/tty.rs @@ -0,0 +1,36 @@ +//! ## Utils +//! +//! `Utils` implements utilities functions to work with layouts + +/** + * MIT License + * + * termscp - Copyright (c) 2022 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/// Read a secret from tty with customisable prompt +pub fn read_secret_from_tty(prompt: &str) -> std::io::Result> { + match rpassword::read_password_from_tty(Some(prompt)) { + Ok(p) if p.is_empty() => Ok(None), + Ok(p) => Ok(Some(p)), + Err(err) => Err(err), + } +}