diff --git a/Cargo.lock b/Cargo.lock index 731c519..b35d2c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,15 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "aes-soft" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" +dependencies = [ + "cipher", + "opaque-debug", +] + [[package]] name = "aho-corasick" version = "0.7.15" @@ -50,12 +60,75 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "block-buffer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d70f2a8c3126a2aec089e0aebcd945607e1155bfb5b89682eddf43c3ce386718" +dependencies = [ + "block-padding 0.1.5", + "byte-tools 0.2.0", + "generic-array 0.11.1", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.4", +] + +[[package]] +name = "block-modes" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a0e8073e8baa88212fb5823574c02ebccb395136ba9a164ab89379ec6072f0" +dependencies = [ + "block-padding 0.2.1", + "cipher", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools 0.3.1", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + [[package]] name = "bumpalo" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +[[package]] +name = "byte-tools" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + [[package]] name = "bytesize" version = "1.0.1" @@ -99,6 +172,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array 0.14.4", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -130,6 +212,21 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + +[[package]] +name = "crc-any" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3784befdf9469f4d51c69ef0b774f6a99de6bcc655285f746f16e0dd63d9007" +dependencies = [ + "debug-helper", +] + [[package]] name = "crossbeam-utils" version = "0.8.1" @@ -166,6 +263,41 @@ dependencies = [ "winapi", ] +[[package]] +name = "debug-helper" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a5bb894f24f42c247f19b25928a88e31867c0f84552c05df41a9dd527435e" + +[[package]] +name = "des" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b24e7c748888aa2fa8bce21d8c64a52efc810663285315ac7476f7197a982fae" +dependencies = [ + "byteorder", + "cipher", + "opaque-debug", +] + +[[package]] +name = "digest" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b072242a8cbaf9c145665af9d250c59af3b958f83ed6824e13533cf76d5b90" +dependencies = [ + "generic-array 0.9.0", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.4", +] + [[package]] name = "dirs" version = "3.0.1" @@ -213,6 +345,34 @@ dependencies = [ "regex", ] +[[package]] +name = "generic-array" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8107dafa78c80c848b71b60133954b4a58609a3a1a5f9af037ecc7f67280f369" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getopts" version = "0.2.21" @@ -327,12 +487,41 @@ dependencies = [ "cfg-if 0.1.10", ] +[[package]] +name = "magic-crypt" +version = "3.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a01cf5086c27e3daff2a06886ab2fc44fe4fdec7d2df7a82e5329483011bfd7" +dependencies = [ + "aes-soft", + "base64", + "block-modes", + "crc-any", + "des", + "digest 0.7.6", + "digest 0.9.0", + "md-5", + "sha2", + "tiger-digest", +] + [[package]] name = "match_cfg" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "memchr" version = "2.3.4" @@ -408,6 +597,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl" version = "0.10.30" @@ -686,6 +881,19 @@ dependencies = [ "syn", ] +[[package]] +name = "sha2" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7aab86fe2149bad8c507606bdb3f4ef5e7b2380eb92350f56122cca72a42a8" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpuid-bool", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "signal-hook" version = "0.1.16" @@ -779,6 +987,8 @@ dependencies = [ "getopts", "hostname", "lazy_static", + "magic-crypt", + "rand", "regex", "rpassword", "serde", @@ -811,6 +1021,17 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "tiger-digest" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68067e91b4b9bb2e1ce3dc55077c984bbe2fa2be65308264dab403c165257545" +dependencies = [ + "block-buffer 0.5.1", + "byte-tools 0.2.0", + "digest 0.7.6", +] + [[package]] name = "time" version = "0.1.44" @@ -844,6 +1065,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + [[package]] name = "unicode-segmentation" version = "1.7.1" @@ -878,6 +1105,12 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 23a9311..b4c84e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ ftp4 = { version = "^4.0.1", features = ["secure"] } getopts = "0.2.21" hostname = "0.3.1" lazy_static = "1.4.0" +magic-crypt = "3.1.6" +rand = "0.7.3" regex = "1.4.2" rpassword = "5.0.0" serde = { version = "1.0.118", features = ["derive"] } diff --git a/src/activity_manager.rs b/src/activity_manager.rs index 68133c3..de81411 100644 --- a/src/activity_manager.rs +++ b/src/activity_manager.rs @@ -160,7 +160,7 @@ impl ActivityManager { 0 => None, _ => Some(activity.password.clone()), }, - protocol: activity.protocol.clone(), + protocol: activity.protocol, }); break; } diff --git a/src/bookmarks/mod.rs b/src/bookmarks/mod.rs index a3ed49b..2cd0cd0 100644 --- a/src/bookmarks/mod.rs +++ b/src/bookmarks/mod.rs @@ -47,6 +47,7 @@ pub struct Bookmark { pub port: u16, pub protocol: String, pub username: String, + pub password: Option, // Password is optional; base64, aes-128 encrypted password } // Errors diff --git a/src/bookmarks/serializer.rs b/src/bookmarks/serializer.rs index acb5cd5..ded9561 100644 --- a/src/bookmarks/serializer.rs +++ b/src/bookmarks/serializer.rs @@ -110,6 +110,7 @@ mod tests { assert_eq!(host.port, 22); assert_eq!(host.protocol, String::from("SCP")); assert_eq!(host.username, String::from("root")); + assert_eq!(host.password, None); // Verify bookmarks assert_eq!(hosts.bookmarks.len(), 3); let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap(); @@ -117,16 +118,19 @@ mod tests { assert_eq!(host.port, 22); assert_eq!(host.protocol, String::from("SFTP")); assert_eq!(host.username, String::from("root")); + assert_eq!(*host.password.as_ref().unwrap(), String::from("mypassword")); let host: &Bookmark = hosts.bookmarks.get("msi-estrem").unwrap(); assert_eq!(host.address, String::from("192.168.1.30")); assert_eq!(host.port, 22); assert_eq!(host.protocol, String::from("SFTP")); assert_eq!(host.username, String::from("cvisintin")); + assert_eq!(*host.password.as_ref().unwrap(), String::from("mysecret")); let host: &Bookmark = hosts.bookmarks.get("aws-server-prod1").unwrap(); assert_eq!(host.address, String::from("51.23.67.12")); assert_eq!(host.port, 21); assert_eq!(host.protocol, String::from("FTPS")); assert_eq!(host.username, String::from("aws001")); + assert_eq!(host.password, None); } #[test] @@ -150,6 +154,7 @@ mod tests { port: 22, protocol: String::from("SFTP"), username: String::from("root"), + password: None, }, ); bookmarks.insert( @@ -159,6 +164,7 @@ mod tests { port: 4022, protocol: String::from("SFTP"), username: String::from("cvisintin"), + password: Some(String::from("password")), }, ); let mut recents: HashMap = HashMap::with_capacity(1); @@ -169,6 +175,7 @@ mod tests { port: 3022, protocol: String::from("SCP"), username: String::from("omar"), + password: Some(String::from("aaa")), }, ); let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap(); @@ -183,8 +190,8 @@ mod tests { let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap(); let file_content: &str = r#" [bookmarks] - raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root" } - msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP", username = "cvisintin" } + raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root", password = "mypassword" } + msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP", username = "cvisintin", password = "mysecret" } aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" } [recents] diff --git a/src/filetransfer/mod.rs b/src/filetransfer/mod.rs index 82af420..a53f40f 100644 --- a/src/filetransfer/mod.rs +++ b/src/filetransfer/mod.rs @@ -37,7 +37,7 @@ pub mod sftp_transfer; /// /// This enum defines the different transfer protocol available in TermSCP -#[derive(std::cmp::PartialEq, std::fmt::Debug, std::clone::Clone)] +#[derive(std::cmp::PartialEq, std::fmt::Debug, std::clone::Clone, Copy)] pub enum FileTransferProtocol { Sftp, Scp, diff --git a/src/lib.rs b/src/lib.rs index f40a0c2..9353ea5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,12 +19,16 @@ * */ -#[macro_use] extern crate lazy_static; +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate magic_crypt; pub mod activity_manager; pub mod bookmarks; pub mod filetransfer; pub mod fs; pub mod host; +pub mod system; pub mod ui; pub mod utils; diff --git a/src/main.rs b/src/main.rs index f41067d..d339ecd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,8 @@ const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); extern crate getopts; #[macro_use] extern crate lazy_static; +#[macro_use] +extern crate magic_crypt; extern crate rpassword; // External libs @@ -40,6 +42,7 @@ mod bookmarks; mod filetransfer; mod fs; mod host; +mod system; mod ui; mod utils; diff --git a/src/system/bookmarks_client.rs b/src/system/bookmarks_client.rs new file mode 100644 index 0000000..cd3a66b --- /dev/null +++ b/src/system/bookmarks_client.rs @@ -0,0 +1,562 @@ +//! ## BookmarksClient +//! +//! `bookmarks_client` is the module which provides an API between the Bookmarks module and the system + +/* +* +* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com +* +* This file is part of "TermSCP" +* +* TermSCP is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* TermSCP is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with TermSCP. If not, see . +* +*/ + +// Deps +extern crate magic_crypt; +extern crate rand; + +// Local +use crate::bookmarks::serializer::BookmarkSerializer; +use crate::bookmarks::{Bookmark, SerializerError, SerializerErrorKind, UserHosts}; +use crate::filetransfer::FileTransferProtocol; +use crate::utils::time_to_str; +// Ext +use magic_crypt::MagicCryptTrait; +use rand::{distributions::Alphanumeric, Rng}; +use std::fs::{OpenOptions, Permissions}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::time::SystemTime; + +/// ## BookmarksClient +/// +/// BookmarksClient provides a layer between the host system and the bookmarks module +pub struct BookmarksClient { + hosts: UserHosts, + bookmarks_file: PathBuf, + key: String, +} + +impl BookmarksClient { + /// ### BookmarksClient + /// + /// Instantiates a new BookmarksClient + /// Bookmarks file path must be provided + /// Key file must be provided + pub fn new(bookmarks_file: &Path, key_file: &Path) -> Result { + // Create default hosts + let default_hosts: UserHosts = Default::default(); + // If key file doesn't exist, create key, otherwise read it + let key: String = match key_file.exists() { + true => match BookmarksClient::load_key(key_file) { + Ok(key) => key, + Err(err) => return Err(err), + }, + false => match BookmarksClient::generate_key(key_file) { + Ok(key) => key, + Err(err) => return Err(err), + }, + }; + let mut client: BookmarksClient = BookmarksClient { + hosts: default_hosts, + bookmarks_file: PathBuf::from(bookmarks_file), + key, + }; + // If bookmark file doesn't exist, initialize it + if !bookmarks_file.exists() { + if let Err(err) = client.write_bookmarks() { + return Err(err); + } + } else { + // Load bookmarks from file + if let Err(err) = client.read_bookmarks() { + return Err(err); + } + } + // Load key + Ok(client) + } + + /// ### iter_bookmarks + /// + /// Iterate over bookmarks keys + pub fn iter_bookmarks(&self) -> Box + '_> { + Box::new(self.hosts.bookmarks.keys()) + } + + /// ### get_bookmark + /// + /// Get bookmark associated to key + pub fn get_bookmark( + &self, + key: &str, + ) -> Option<(String, u16, FileTransferProtocol, String, Option)> { + let entry: &Bookmark = self.hosts.bookmarks.get(key)?; + Some(( + entry.address.clone(), + entry.port, + match entry.protocol.to_ascii_uppercase().as_str() { + "FTP" => FileTransferProtocol::Ftp(false), + "FTPS" => FileTransferProtocol::Ftp(true), + "SCP" => FileTransferProtocol::Scp, + _ => FileTransferProtocol::Sftp, + }, + entry.username.clone(), + match &entry.password { + // Decrypted password if Some; if decryption fails return None + Some(pwd) => match self.decrypt_str(pwd.as_str()) { + Ok(decrypted_pwd) => Some(decrypted_pwd), + Err(_) => None, + }, + None => None, + }, + )) + } + + /// ### add_recent + /// + /// Add a new recent to bookmarks + pub fn add_bookmark( + &mut self, + name: String, + addr: String, + port: u16, + protocol: FileTransferProtocol, + username: String, + password: Option, + ) { + if name.is_empty() { + panic!("Bookmark name can't be empty"); + } + // Make bookmark + let host: Bookmark = self.make_bookmark(addr, port, protocol, username, password); + self.hosts.bookmarks.insert(name, host); + } + + /// ### del_bookmark + /// + /// Delete entry from bookmarks + pub fn del_bookmark(&mut self, name: &str) { + let _ = self.hosts.bookmarks.remove(name); + } + /// ### iter_recents + /// + /// Iterate over recents keys + pub fn iter_recents(&self) -> Box + '_> { + Box::new(self.hosts.recents.keys()) + } + + /// ### get_recent + /// + /// Get recent associated to key + pub fn get_recent(&self, key: &str) -> Option<(String, u16, FileTransferProtocol, String)> { + // NOTE: password is not decrypted; recents will never have password + let entry: &Bookmark = self.hosts.recents.get(key)?; + Some(( + entry.address.clone(), + entry.port, + match entry.protocol.to_ascii_uppercase().as_str() { + "FTP" => FileTransferProtocol::Ftp(false), + "FTPS" => FileTransferProtocol::Ftp(true), + "SCP" => FileTransferProtocol::Scp, + _ => FileTransferProtocol::Sftp, + }, + entry.username.clone(), + )) + } + + /// ### add_recent + /// + /// Add a new recent to bookmarks + pub fn add_recent( + &mut self, + addr: String, + port: u16, + protocol: FileTransferProtocol, + username: String, + ) { + // Make bookmark + let host: Bookmark = self.make_bookmark(addr, port, protocol, username, None); + // Check if duplicated + for recent_host in self.hosts.recents.values() { + if *recent_host == host { + // Don't save duplicates + return; + } + } + // If hosts size is bigger than 16; pop last + if self.hosts.recents.len() >= 16 { + let mut keys: Vec = Vec::with_capacity(self.hosts.recents.len()); + for key in self.hosts.recents.keys() { + keys.push(key.clone()); + } + // Sort keys; NOTE: most recent is the last element + keys.sort(); + // Delete keys starting from the last one + for key in keys.iter() { + let _ = self.hosts.recents.remove(key); + // If length is < 16; break + if self.hosts.recents.len() < 16 { + break; + } + } + } + let name: String = time_to_str(SystemTime::now(), "ISO%Y%m%dT%H%M%S"); + self.hosts.recents.insert(name, host); + } + + /// ### del_recent + /// + /// Delete entry from recents + pub fn del_recent(&mut self, name: &str) { + let _ = self.hosts.recents.remove(name); + } + + /// ### write_bookmarks + /// + /// Write bookmarks to file + pub fn write_bookmarks(&self) -> Result<(), SerializerError> { + // Open file + match OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(self.bookmarks_file.as_path()) + { + Ok(writer) => { + let serializer: BookmarkSerializer = BookmarkSerializer {}; + serializer.serialize(Box::new(writer), &self.hosts) + } + Err(err) => Err(SerializerError::new_ex( + SerializerErrorKind::IoError, + err.to_string(), + )), + } + } + + /// ### read_bookmarks + /// + /// Read bookmarks from file + fn read_bookmarks(&mut self) -> Result<(), SerializerError> { + // Open bookmarks file for read + match OpenOptions::new() + .read(true) + .open(self.bookmarks_file.as_path()) + { + Ok(reader) => { + // Deserialize + let deserializer: BookmarkSerializer = BookmarkSerializer {}; + match deserializer.deserialize(Box::new(reader)) { + Ok(hosts) => { + self.hosts = hosts; + Ok(()) + } + Err(err) => Err(err), + } + } + Err(err) => Err(SerializerError::new_ex( + SerializerErrorKind::IoError, + err.to_string(), + )), + } + } + + /// ### generate_key + /// + /// Generate a new AES key and write it to key file + fn generate_key(key_file: &Path) -> Result { + // Generate 256 bytes (2048 bits) key + let key: String = rand::thread_rng() + .sample_iter(Alphanumeric) + .take(256) + .collect::(); + // Write file + match OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(key_file) + { + Ok(mut file) => { + // Write key to file + if let Err(err) = file.write_all(key.as_bytes()) { + return Err(SerializerError::new_ex( + SerializerErrorKind::IoError, + err.to_string(), + )); + } + // Set file to readonly + let mut permissions: Permissions = file.metadata().unwrap().permissions(); + permissions.set_readonly(true); + let _ = file.set_permissions(permissions); + Ok(key) + } + Err(err) => Err(SerializerError::new_ex( + SerializerErrorKind::IoError, + err.to_string(), + )), + } + } + + /// ### make_bookmark + /// + /// Make bookmark from credentials + fn make_bookmark( + &self, + addr: String, + port: u16, + protocol: FileTransferProtocol, + username: String, + password: Option, + ) -> Bookmark { + Bookmark { + address: addr, + port, + username, + protocol: match protocol { + FileTransferProtocol::Ftp(secure) => match secure { + true => String::from("FTPS"), + false => String::from("FTP"), + }, + FileTransferProtocol::Scp => String::from("SCP"), + FileTransferProtocol::Sftp => String::from("SFTP"), + }, + password: match password { + Some(p) => Some(self.encrypt_str(p.as_str())), // Encrypt password if provided + None => None, + }, + } + } + + /// ### load_key + /// + /// Load key from key_file + fn load_key(key_file: &Path) -> Result { + match OpenOptions::new().read(true).open(key_file) { + Ok(mut file) => { + let mut key: String = String::with_capacity(256); + match file.read_to_string(&mut key) { + Ok(_) => Ok(key), + Err(err) => Err(SerializerError::new_ex( + SerializerErrorKind::IoError, + err.to_string(), + )), + } + } + Err(err) => Err(SerializerError::new_ex( + SerializerErrorKind::IoError, + err.to_string(), + )), + } + } + + /// ### encrypt_str + /// + /// Encrypt provided string using AES-128. Encrypted buffer is then converted to BASE64 + fn encrypt_str(&self, txt: &str) -> String { + let crypter = new_magic_crypt!(self.key.clone(), 128); + crypter.encrypt_str_to_base64(txt.to_string()) + } + + /// ### decrypt_str + /// + /// Decrypt provided string using AES-128 + fn decrypt_str(&self, secret: &str) -> Result { + let crypter = new_magic_crypt!(self.key.clone(), 128); + match crypter.decrypt_base64_to_string(secret.to_string()) { + Ok(txt) => Ok(txt), + Err(err) => Err(SerializerError::new_ex( + SerializerErrorKind::SyntaxError, + err.to_string(), + )), + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_system_bookmarks_new() { + let tmp_dir: tempfile::TempDir = create_tmp_dir(); + let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + // Initialize a new bookmarks client + let client: BookmarksClient = + BookmarksClient::new(cfg_path.as_path(), key_path.as_path()).unwrap(); + // Verify client + assert_eq!(client.hosts.bookmarks.len(), 0); + assert_eq!(client.hosts.recents.len(), 0); + assert!(client.key.len() > 0); + assert_eq!(client.bookmarks_file, cfg_path); + } + + #[test] + fn test_system_bookmarks_new_err() { + assert!( + BookmarksClient::new(Path::new("/tmp/oifoif/omar"), Path::new("/tmp/efnnu/omar")) + .is_err() + ); + } + + #[test] + fn test_system_bookmarks_new_from_existing() { + let tmp_dir: tempfile::TempDir = create_tmp_dir(); + let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + // Initialize a new bookmarks client + let mut client: BookmarksClient = + BookmarksClient::new(cfg_path.as_path(), key_path.as_path()).unwrap(); + // Add some bookmarks + client.add_bookmark( + String::from("raspberry"), + String::from("192.168.1.31"), + 22, + FileTransferProtocol::Sftp, + String::from("pi"), + Some(String::from("mypassword")), + ); + client.add_recent( + String::from("192.168.1.31"), + 22, + FileTransferProtocol::Sftp, + String::from("pi"), + ); + let recent_key: String = String::from(client.iter_recents().next().unwrap()); + assert!(client.write_bookmarks().is_ok()); + let key: String = client.key.clone(); + // Re-initialize a client + let client: BookmarksClient = + BookmarksClient::new(cfg_path.as_path(), key_path.as_path()).unwrap(); + // Verify it loaded parameters correctly + assert_eq!(client.key, key); + let bookmark: (String, u16, FileTransferProtocol, String, Option) = + client.get_bookmark(&String::from("raspberry")).unwrap(); + assert_eq!(bookmark.0, String::from("192.168.1.31")); + assert_eq!(bookmark.1, 22); + assert_eq!(bookmark.2, FileTransferProtocol::Sftp); + assert_eq!(bookmark.3, String::from("pi")); + assert_eq!(*bookmark.4.as_ref().unwrap(), String::from("mypassword")); + let bookmark: (String, u16, FileTransferProtocol, String) = + client.get_recent(&recent_key).unwrap(); + assert_eq!(bookmark.0, String::from("192.168.1.31")); + assert_eq!(bookmark.1, 22); + assert_eq!(bookmark.2, FileTransferProtocol::Sftp); + assert_eq!(bookmark.3, String::from("pi")); + } + + #[test] + fn test_system_bookmarks_manipulate_bookmarks() { + let tmp_dir: tempfile::TempDir = create_tmp_dir(); + let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + // Initialize a new bookmarks client + let mut client: BookmarksClient = + BookmarksClient::new(cfg_path.as_path(), key_path.as_path()).unwrap(); + // Add bookmark + client.add_bookmark( + String::from("raspberry"), + String::from("192.168.1.31"), + 22, + FileTransferProtocol::Sftp, + String::from("pi"), + Some(String::from("mypassword")), + ); + // Get bookmark + let bookmark: (String, u16, FileTransferProtocol, String, Option) = + client.get_bookmark(&String::from("raspberry")).unwrap(); + assert_eq!(bookmark.0, String::from("192.168.1.31")); + assert_eq!(bookmark.1, 22); + assert_eq!(bookmark.2, FileTransferProtocol::Sftp); + assert_eq!(bookmark.3, String::from("pi")); + assert_eq!(*bookmark.4.as_ref().unwrap(), String::from("mypassword")); + // Write bookmarks + assert!(client.write_bookmarks().is_ok()); + // Delete bookmark + client.del_bookmark(&String::from("raspberry")); + // Get unexisting bookmark + assert!(client.get_bookmark(&String::from("raspberry")).is_none()); + // Write bookmarks + assert!(client.write_bookmarks().is_ok()); + } + + #[test] + fn test_system_bookmarks_manipulate_recents() { + let tmp_dir: tempfile::TempDir = create_tmp_dir(); + let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + // Initialize a new bookmarks client + let mut client: BookmarksClient = + BookmarksClient::new(cfg_path.as_path(), key_path.as_path()).unwrap(); + // Add bookmark + client.add_recent( + String::from("192.168.1.31"), + 22, + FileTransferProtocol::Sftp, + String::from("pi"), + ); + let key: String = String::from(client.iter_recents().next().unwrap()); + // Get bookmark + let bookmark: (String, u16, FileTransferProtocol, String) = + client.get_recent(&key).unwrap(); + assert_eq!(bookmark.0, String::from("192.168.1.31")); + assert_eq!(bookmark.1, 22); + assert_eq!(bookmark.2, FileTransferProtocol::Sftp); + assert_eq!(bookmark.3, String::from("pi")); + // Write bookmarks + assert!(client.write_bookmarks().is_ok()); + // Delete bookmark + client.del_recent(&key); + // Get unexisting bookmark + assert!(client.get_bookmark(&key).is_none()); + // Write bookmarks + assert!(client.write_bookmarks().is_ok()); + } + + #[test] + #[should_panic] + fn test_system_bookmarks_add_bookmark_empty() { + let tmp_dir: tempfile::TempDir = create_tmp_dir(); + let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + // Initialize a new bookmarks client + let mut client: BookmarksClient = + BookmarksClient::new(cfg_path.as_path(), key_path.as_path()).unwrap(); + // Add bookmark + client.add_bookmark( + String::from(""), + String::from("192.168.1.31"), + 22, + FileTransferProtocol::Sftp, + String::from("pi"), + Some(String::from("mypassword")), + ); + } + + /// ### get_paths + /// + /// Get paths for configuration and key for bookmarks + fn get_paths(dir: &Path) -> (PathBuf, PathBuf) { + let mut k: PathBuf = PathBuf::from(dir); + let mut c: PathBuf = k.clone(); + k.push("bookmarks.key"); + c.push("bookmarks.toml"); + (c, k) + } + + /// ### create_tmp_dir + /// + /// Create temporary directory + fn create_tmp_dir() -> tempfile::TempDir { + tempfile::TempDir::new().ok().unwrap() + } +} diff --git a/src/system/environment.rs b/src/system/environment.rs new file mode 100644 index 0000000..c4af3fe --- /dev/null +++ b/src/system/environment.rs @@ -0,0 +1,68 @@ +//! ## Environment +//! +//! `environment` is the module which provides Path and values for the system environment + +/* +* +* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com +* +* This file is part of "TermSCP" +* +* TermSCP is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* TermSCP is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with TermSCP. If not, see . +* +*/ + +// Deps +extern crate dirs; + +// Ext +use std::path::PathBuf; + +/// ### get_config_dir +/// +/// Get termscp configuration directory path. +/// Returns None, if it's not possible to get it +pub fn init_config_dir() -> Result, String> { + // Get file + lazy_static! { + static ref CONF_DIR: Option = dirs::config_dir(); + } + if CONF_DIR.is_some() { + // Get path of bookmarks + let mut p: PathBuf = CONF_DIR.as_ref().unwrap().clone(); + // Append termscp dir + p.push("termscp/"); + // If directory doesn't exist, create it + match p.exists() { + true => Ok(Some(p)), + false => match std::fs::create_dir(p.as_path()) { + Ok(_) => Ok(Some(p)), + Err(err) => Err(err.to_string()), + }, + } + } else { + Ok(None) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_system_environment_get_config_dir() { + assert!(init_config_dir().ok().unwrap().is_some()); + } +} diff --git a/src/system/mod.rs b/src/system/mod.rs new file mode 100644 index 0000000..d0fe5be --- /dev/null +++ b/src/system/mod.rs @@ -0,0 +1,28 @@ +//! ## System +//! +//! `system` is the module which contains functions and data types related to current system + +/* +* +* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com +* +* This file is part of "TermSCP" +* +* TermSCP is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* TermSCP is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with TermSCP. If not, see . +* +*/ + +// modules +pub mod bookmarks_client; +pub mod environment; diff --git a/src/ui/activities/auth_activity/bookmarks.rs b/src/ui/activities/auth_activity/bookmarks.rs index 99211a4..e473c81 100644 --- a/src/ui/activities/auth_activity/bookmarks.rs +++ b/src/ui/activities/auth_activity/bookmarks.rs @@ -27,78 +27,31 @@ extern crate dirs; // Locals -use super::{AuthActivity, Color, FileTransferProtocol, InputMode, PopupType, UserHosts}; -use crate::bookmarks::serializer::BookmarkSerializer; -use crate::bookmarks::Bookmark; -use crate::utils::time_to_str; +use super::{AuthActivity, Color, DialogYesNoOption, InputMode, PopupType}; +use crate::system::bookmarks_client::BookmarksClient; +use crate::system::environment; // Ext use std::path::PathBuf; -use std::time::SystemTime; impl AuthActivity { - /// ### read_bookmarks - /// - /// Read bookmarks from data file; Show popup if necessary - pub(super) fn read_bookmarks(&mut self) { - // Init bookmarks - if let Some(bookmark_file) = self.init_bookmarks() { - // Read - if self.context.is_some() { - match self - .context - .as_ref() - .unwrap() - .local - .open_file_read(bookmark_file.as_path()) - { - Ok(reader) => { - // Read bookmarks - let deserializer: BookmarkSerializer = BookmarkSerializer {}; - match deserializer.deserialize(Box::new(reader)) { - Ok(bookmarks) => self.bookmarks = Some(bookmarks), - Err(err) => { - self.input_mode = InputMode::Popup(PopupType::Alert( - Color::Yellow, - format!( - "Could not read bookmarks from \"{}\": {}", - bookmark_file.display(), - err - ), - )) - } - } - } - Err(err) => { - self.input_mode = InputMode::Popup(PopupType::Alert( - Color::Yellow, - format!( - "Could not read bookmarks from \"{}\": {}", - bookmark_file.display(), - err - ), - )) - } - } - } - } - } - /// ### del_bookmark /// /// Delete bookmark pub(super) fn del_bookmark(&mut self, idx: usize) { - if let Some(hosts) = self.bookmarks.as_mut() { + if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() { // Iterate over kyes let mut name: Option = None; - for (i, key) in hosts.bookmarks.keys().enumerate() { + for (i, key) in bookmarks_cli.iter_bookmarks().enumerate() { if i == idx { name = Some(key.clone()); break; } } if let Some(name) = name { - hosts.bookmarks.remove(name.as_str()); + bookmarks_cli.del_bookmark(&name); + // Write bookmarks + self.write_bookmarks(); } } } @@ -107,20 +60,20 @@ impl AuthActivity { /// /// Load selected bookmark (at index) to input fields pub(super) fn load_bookmark(&mut self, idx: usize) { - if let Some(hosts) = self.bookmarks.as_mut() { + if let Some(bookmarks_cli) = self.bookmarks_client.as_ref() { // Iterate over bookmarks - for (i, bookmark) in hosts.bookmarks.values().enumerate() { + for (i, key) in bookmarks_cli.iter_bookmarks().enumerate() { if i == idx { - // Load parameters - self.address = bookmark.address.clone(); - self.port = bookmark.port.to_string(); - self.protocol = match bookmark.protocol.as_str().to_uppercase().as_str() { - "FTP" => FileTransferProtocol::Ftp(false), - "FTPS" => FileTransferProtocol::Ftp(true), - "SCP" => FileTransferProtocol::Scp, - _ => FileTransferProtocol::Sftp, // Default to SFTP - }; - self.username = bookmark.username.clone(); + if let Some(bookmark) = bookmarks_cli.get_bookmark(&key) { + // Load parameters + self.address = bookmark.0; + self.port = bookmark.1.to_string(); + self.protocol = bookmark.2; + self.username = bookmark.3; + if let Some(password) = bookmark.4 { + self.password = password; + } + } // Break break; } @@ -132,29 +85,61 @@ impl AuthActivity { /// /// Save current input fields as a bookmark pub(super) fn save_bookmark(&mut self, name: String) { - if let Ok(host) = self.make_user_host() { - if let Some(hosts) = self.bookmarks.as_mut() { - hosts.bookmarks.insert(name, host); - // Write bookmarks - self.write_bookmarks(); + // Check port + let port: u16 = match self.port.parse::() { + Ok(val) => { + if val > 65535 { + self.input_mode = InputMode::Popup(PopupType::Alert( + Color::Red, + String::from("Specified port must be in range 0-65535"), + )); + return; + } + val as u16 } + Err(_) => { + self.input_mode = InputMode::Popup(PopupType::Alert( + Color::Red, + String::from("Specified port is not a number"), + )); + return; + } + }; + if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() { + // Check if password must be saved + let password: Option = match self.choice_opt { + DialogYesNoOption::Yes => Some(self.password.clone()), + DialogYesNoOption::No => None, + }; + bookmarks_cli.add_bookmark( + name, + self.address.clone(), + port, + self.protocol, + self.username.clone(), + password, + ); + // Save bookmarks + self.write_bookmarks(); } } /// ### del_recent /// /// Delete recent pub(super) fn del_recent(&mut self, idx: usize) { - if let Some(hosts) = self.bookmarks.as_mut() { + if let Some(client) = self.bookmarks_client.as_mut() { // Iterate over kyes let mut name: Option = None; - for (i, key) in hosts.recents.keys().enumerate() { + for (i, key) in client.iter_recents().enumerate() { if i == idx { name = Some(key.clone()); break; } } if let Some(name) = name { - hosts.recents.remove(name.as_str()); + client.del_recent(&name); + // Save bookmarks + self.write_bookmarks(); } } } @@ -163,22 +148,19 @@ impl AuthActivity { /// /// Load selected recent (at index) to input fields pub(super) fn load_recent(&mut self, idx: usize) { - if let Some(hosts) = self.bookmarks.as_mut() { + if let Some(client) = self.bookmarks_client.as_ref() { // Iterate over bookmarks - for (i, bookmark) in hosts.recents.values().enumerate() { + for (i, key) in client.iter_recents().enumerate() { if i == idx { - // Load parameters - self.address = bookmark.address.clone(); - self.port = bookmark.port.to_string(); - self.protocol = match bookmark.protocol.as_str().to_uppercase().as_str() { - "FTP" => FileTransferProtocol::Ftp(false), - "FTPS" => FileTransferProtocol::Ftp(true), - "SCP" => FileTransferProtocol::Scp, - _ => FileTransferProtocol::Sftp, // Default to SFTP - }; - self.username = bookmark.username.clone(); - // Break - break; + if let Some(bookmark) = client.get_recent(key) { + // Load parameters + self.address = bookmark.0; + self.port = bookmark.1.to_string(); + self.protocol = bookmark.2; + self.username = bookmark.3; + // Break + break; + } } } } @@ -188,46 +170,6 @@ impl AuthActivity { /// /// Save current input fields as a "recent" pub(super) fn save_recent(&mut self) { - if let Ok(host) = self.make_user_host() { - if let Some(hosts) = self.bookmarks.as_mut() { - // Check if duplicated - for recent_host in hosts.recents.values() { - if *recent_host == host { - // Don't save duplicates - return; - } - } - // If hosts size is bigger than 16; pop last - if hosts.recents.len() >= 16 { - let mut keys: Vec = Vec::with_capacity(hosts.recents.len()); - for key in hosts.recents.keys() { - keys.push(key.clone()); - } - // Sort keys; NOTE: most recent is the last element - keys.sort(); - // Delete keys starting from the last one - for key in keys.iter() { - let _ = hosts.recents.remove(key); - // If length is < 16; break - if hosts.recents.len() < 16 { - break; - } - } - } - // Create name - let name: String = time_to_str(SystemTime::now(), "ISO%Y%m%dT%H%M%S"); - // Save host to recents - hosts.recents.insert(name, host); - // Write bookmarks - self.write_bookmarks(); - } - } - } - - /// ### make_user_host - /// - /// Make user host from current input fields - fn make_user_host(&mut self) -> Result { // Check port let port: u16 = match self.port.parse::() { Ok(val) => { @@ -236,7 +178,7 @@ impl AuthActivity { Color::Red, String::from("Specified port must be in range 0-65535"), )); - return Err(()); + return; } val as u16 } @@ -245,156 +187,72 @@ impl AuthActivity { Color::Red, String::from("Specified port is not a number"), )); - return Err(()); + return; } }; - Ok(Bookmark { - address: self.address.clone(), - port, - protocol: match self.protocol { - FileTransferProtocol::Ftp(secure) => match secure { - true => String::from("FTPS"), - false => String::from("FTP"), - }, - FileTransferProtocol::Scp => String::from("SCP"), - FileTransferProtocol::Sftp => String::from("SFTP"), - }, - username: self.username.clone(), - }) + if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() { + bookmarks_cli.add_recent( + self.address.clone(), + port, + self.protocol, + self.username.clone(), + ); + // Save bookmarks + self.write_bookmarks(); + } } /// ### write_bookmarks /// /// Write bookmarks to file fn write_bookmarks(&mut self) { - if self.bookmarks.is_some() && self.context.is_some() { - // Open file for write - if let Some(bookmarks_file) = self.init_bookmarks() { - match self - .context - .as_ref() - .unwrap() - .local - .open_file_write(bookmarks_file.as_path()) - { - Ok(writer) => { - let serializer: BookmarkSerializer = BookmarkSerializer {}; - if let Err(err) = serializer - .serialize(Box::new(writer), &self.bookmarks.as_ref().unwrap()) - { - self.input_mode = InputMode::Popup(PopupType::Alert( - Color::Yellow, - format!( - "Could not write default bookmarks at \"{}\": {}", - bookmarks_file.display(), - err - ), - )); - } - } - Err(err) => { - self.input_mode = InputMode::Popup(PopupType::Alert( - Color::Yellow, - format!( - "Could not write default bookmarks at \"{}\": {}", - bookmarks_file.display(), - err - ), - )) - } - } + if let Some(bookmarks_cli) = self.bookmarks_client.as_ref() { + if let Err(err) = bookmarks_cli.write_bookmarks() { + self.input_mode = InputMode::Popup(PopupType::Alert( + Color::Red, + format!("Could not write bookmarks: {}", err), + )); } } } - /// ### init_bookmarks + /// ### init_bookmarks_client /// - /// Initialize bookmarks directory - /// Returns bookmark path - fn init_bookmarks(&mut self) -> Option { - // Get file - lazy_static! { - static ref CONF_DIR: Option = dirs::config_dir(); - } - if CONF_DIR.is_some() { - // Get path of bookmarks - let mut p: PathBuf = CONF_DIR.as_ref().unwrap().clone(); - // Append termscp dir - p.push("termscp/"); - // Mkdir if doesn't exist - if self.context.is_some() { - if let Err(err) = self - .context - .as_mut() - .unwrap() - .local - .mkdir_ex(p.as_path(), true) - { - // Show popup - self.input_mode = InputMode::Popup(PopupType::Alert( - Color::Yellow, - format!( - "Could not create configuration directory at \"{}\": {}", - p.display(), - err - ), - )); - // Return None - return None; - } - } - // Append bookmarks.toml - p.push("bookmarks.toml"); - // If bookmarks.toml doesn't exist, initializae it - if self.context.is_some() - && !self - .context - .as_ref() - .unwrap() - .local - .file_exists(p.as_path()) - { - // Write file - let default_hosts: UserHosts = Default::default(); - match self - .context - .as_ref() - .unwrap() - .local - .open_file_write(p.as_path()) - { - Ok(writer) => { - let serializer: BookmarkSerializer = BookmarkSerializer {}; - // Serialize and write - if let Err(err) = serializer.serialize(Box::new(writer), &default_hosts) { + /// 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(path) = path { + // Prepare paths + let mut bookmarks_file: PathBuf = path.clone(); + bookmarks_file.push("bookmarks.toml"); + let mut key_file: PathBuf = path; + key_file.push(".bookmarks.key"); // key file is hidden + // Initialize client + match BookmarksClient::new(bookmarks_file.as_path(), key_file.as_path()) { + Ok(cli) => self.bookmarks_client = Some(cli), + Err(err) => { self.input_mode = InputMode::Popup(PopupType::Alert( - Color::Yellow, + Color::Red, format!( - "Could not write default bookmarks at \"{}\": {}", - p.display(), + "Could not initialize bookmarks (at \"{}\", \"{}\"): {}", + bookmarks_file.display(), + key_file.display(), err ), - )); - return None; + )) } } - Err(err) => { - self.input_mode = InputMode::Popup(PopupType::Alert( - Color::Yellow, - format!( - "Could not write default bookmarks at \"{}\": {}", - p.display(), - err - ), - )); - return None; - } } } - // return path - Some(p) - } else { - None + Err(err) => { + self.input_mode = InputMode::Popup(PopupType::Alert( + Color::Red, + format!("Could not initialize configuration directory: {}", err), + )) + } } } } diff --git a/src/ui/activities/auth_activity/callbacks.rs b/src/ui/activities/auth_activity/callbacks.rs index ca859e1..5fe802d 100644 --- a/src/ui/activities/auth_activity/callbacks.rs +++ b/src/ui/activities/auth_activity/callbacks.rs @@ -53,6 +53,8 @@ impl AuthActivity { /// /// Callback used to save bookmark with name pub(super) fn callback_save_bookmark(&mut self, input: String) { - self.save_bookmark(input); + if !input.is_empty() { + self.save_bookmark(input); + } } } diff --git a/src/ui/activities/auth_activity/input.rs b/src/ui/activities/auth_activity/input.rs index 3bcc7c5..64d587c 100644 --- a/src/ui/activities/auth_activity/input.rs +++ b/src/ui/activities/auth_activity/input.rs @@ -25,7 +25,7 @@ use super::{ AuthActivity, DialogCallback, DialogYesNoOption, FileTransferProtocol, InputEvent, InputField, - InputForm, InputMode, OnInputSubmitCallback, PopupType, + InputForm, InputMode, PopupType, }; use crossterm::event::{KeyCode, KeyModifiers}; @@ -160,11 +160,10 @@ impl AuthActivity { self.input_mode = InputMode::Popup(PopupType::Help); } 'S' | 's' => { + // Default choice option to no + self.choice_opt = DialogYesNoOption::No; // Save bookmark as... - self.input_mode = InputMode::Popup(PopupType::Input( - String::from("Save bookmark as..."), - AuthActivity::callback_save_bookmark, - )); + self.input_mode = InputMode::Popup(PopupType::SaveBookmark); } _ => { /* Nothing to do */ } } @@ -234,14 +233,14 @@ impl AuthActivity { // Move bookmarks index up if self.bookmarks_idx > 0 { self.bookmarks_idx -= 1; - } else if let Some(hosts) = &self.bookmarks { + } else if let Some(bookmarks_cli) = &self.bookmarks_client { // Put to last index (wrap) - self.bookmarks_idx = hosts.bookmarks.len() - 1; + self.bookmarks_idx = bookmarks_cli.iter_bookmarks().count() - 1; } } KeyCode::Down => { - if let Some(hosts) = &self.bookmarks { - let size: usize = hosts.bookmarks.len(); + if let Some(bookmarks_cli) = &self.bookmarks_client { + let size: usize = bookmarks_cli.iter_bookmarks().count(); // Check if can move down if self.bookmarks_idx + 1 >= size { // Move bookmarks index down @@ -282,11 +281,10 @@ impl AuthActivity { self.input_mode = InputMode::Popup(PopupType::Help); } 'S' | 's' => { + // Default choice option to no + self.choice_opt = DialogYesNoOption::No; // Save bookmark as... - self.input_mode = InputMode::Popup(PopupType::Input( - String::from("Save bookmark as..."), - AuthActivity::callback_save_bookmark, - )); + self.input_mode = InputMode::Popup(PopupType::SaveBookmark); } _ => { /* Nothing to do */ } }, @@ -315,14 +313,14 @@ impl AuthActivity { // Move bookmarks index up if self.recents_idx > 0 { self.recents_idx -= 1; - } else if let Some(hosts) = &self.bookmarks { + } else if let Some(bookmarks_cli) = &self.bookmarks_client { // Put to last index (wrap) - self.recents_idx = hosts.recents.len() - 1; + self.recents_idx = bookmarks_cli.iter_recents().count() - 1; } } KeyCode::Down => { - if let Some(hosts) = &self.bookmarks { - let size: usize = hosts.recents.len(); + if let Some(bookmarks_cli) = &self.bookmarks_client { + let size: usize = bookmarks_cli.iter_recents().count(); // Check if can move down if self.recents_idx + 1 >= size { // Move bookmarks index down @@ -363,11 +361,10 @@ impl AuthActivity { self.input_mode = InputMode::Popup(PopupType::Help); } 'S' | 's' => { + // Default choice option to no + self.choice_opt = DialogYesNoOption::No; // Save bookmark as... - self.input_mode = InputMode::Popup(PopupType::Input( - String::from("Save bookmark as..."), - AuthActivity::callback_save_bookmark, - )); + self.input_mode = InputMode::Popup(PopupType::SaveBookmark); } _ => { /* Nothing to do */ } }, @@ -383,7 +380,7 @@ impl AuthActivity { match ptype { PopupType::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev), PopupType::Help => self.handle_input_event_mode_popup_help(ev), - PopupType::Input(_, cb) => self.handle_input_event_mode_popup_input(ev, cb), + PopupType::SaveBookmark => self.handle_input_event_mode_popup_save_bookmark(ev), PopupType::YesNo(_, yes_cb, no_cb) => { self.handle_input_event_mode_popup_yesno(ev, yes_cb, no_cb) } @@ -418,14 +415,10 @@ impl AuthActivity { } } - /// ### handle_input_event_mode_popup_input + /// ### handle_input_event_mode_popup_save_bookmark /// - /// Input event handler for input popup - pub(super) fn handle_input_event_mode_popup_input( - &mut self, - ev: &InputEvent, - cb: OnInputSubmitCallback, - ) { + /// Input event handler for SaveBookmark popup + pub(super) fn handle_input_event_mode_popup_save_bookmark(&mut self, ev: &InputEvent) { // If enter, close popup, otherwise push chars to input if let InputEvent::Key(key) = ev { match key.code { @@ -435,6 +428,8 @@ impl AuthActivity { self.input_txt.clear(); // Set mode back to form self.input_mode = InputMode::Form; + // Reset choice option to yes + self.choice_opt = DialogYesNoOption::Yes; } KeyCode::Enter => { // Submit @@ -444,8 +439,12 @@ impl AuthActivity { // Set mode back to form BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh? self.input_mode = InputMode::Form; // Call cb - cb(self, input_text); + self.callback_save_bookmark(input_text); + // Reset choice option to yes + self.choice_opt = DialogYesNoOption::Yes; } + KeyCode::Left => self.choice_opt = DialogYesNoOption::Yes, // Move yes/no with arrows + KeyCode::Right => self.choice_opt = DialogYesNoOption::No, // Move yes/no with arrows KeyCode::Char(ch) => self.input_txt.push(ch), KeyCode::Backspace => { let _ = self.input_txt.pop(); diff --git a/src/ui/activities/auth_activity/layout.rs b/src/ui/activities/auth_activity/layout.rs index 02be048..94e2e43 100644 --- a/src/ui/activities/auth_activity/layout.rs +++ b/src/ui/activities/auth_activity/layout.rs @@ -27,8 +27,6 @@ use super::{ AuthActivity, Context, DialogYesNoOption, FileTransferProtocol, InputField, InputForm, InputMode, PopupType, }; - -use crate::bookmarks::Bookmark; use crate::utils::align_text_center; use tui::{ @@ -128,7 +126,7 @@ impl AuthActivity { let (width, height): (u16, u16) = match popup { PopupType::Alert(_, _) => (50, 10), PopupType::Help => (50, 70), - PopupType::Input(_, _) => (40, 10), + PopupType::SaveBookmark => (20, 20), PopupType::YesNo(_, _, _) => (30, 10), }; let popup_area: Rect = self.draw_popup_area(f.size(), width, height); @@ -139,12 +137,25 @@ impl AuthActivity { popup_area, ), PopupType::Help => f.render_widget(self.draw_popup_help(), popup_area), - PopupType::Input(txt, _) => { - f.render_widget(self.draw_popup_input(txt.clone()), popup_area); + PopupType::SaveBookmark => { + let popup_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(3), // Input form + Constraint::Length(2), // Yes/No + ] + .as_ref(), + ) + .split(popup_area); + let (input, yes_no): (Paragraph, Tabs) = self.draw_popup_save_bookmark(); + // Render parts + f.render_widget(input, popup_chunks[0]); + f.render_widget(yes_no, popup_chunks[1]); // Set cursor f.set_cursor( - popup_area.x + self.input_txt.width() as u16 + 1, - popup_area.y + 1, + popup_chunks[0].x + self.input_txt.width() as u16 + 1, + popup_chunks[0].y + 1, ) } PopupType::YesNo(txt, _, _) => { @@ -273,21 +284,26 @@ impl AuthActivity { /// /// Draw local explorer list pub(super) fn draw_bookmarks_tab(&self) -> Option { - self.bookmarks.as_ref()?; + self.bookmarks_client.as_ref()?; let hosts: Vec = self - .bookmarks + .bookmarks_client .as_ref() .unwrap() - .bookmarks - .iter() - .map(|(key, entry): (&String, &Bookmark)| { + .iter_bookmarks() + .map(|key: &String| { + let entry: (String, u16, FileTransferProtocol, String, _) = self + .bookmarks_client + .as_ref() + .unwrap() + .get_bookmark(key) + .unwrap(); ListItem::new(Span::from(format!( "{} ({}://{}@{}:{})", key, - entry.protocol.to_lowercase(), - entry.username, - entry.address, - entry.port + AuthActivity::protocol_to_str(entry.2), + entry.3, + entry.0, + entry.1 ))) }) .collect(); @@ -316,20 +332,25 @@ impl AuthActivity { /// /// Draw local explorer list pub(super) fn draw_recents_tab(&self) -> Option { - self.bookmarks.as_ref()?; + self.bookmarks_client.as_ref()?; let hosts: Vec = self - .bookmarks + .bookmarks_client .as_ref() .unwrap() - .recents - .values() - .map(|entry: &Bookmark| { + .iter_recents() + .map(|key: &String| { + let entry: (String, u16, FileTransferProtocol, String) = self + .bookmarks_client + .as_ref() + .unwrap() + .get_recent(key) + .unwrap(); ListItem::new(Span::from(format!( "{}://{}@{}:{}", - entry.protocol.to_lowercase(), - entry.username, - entry.address, - entry.port + AuthActivity::protocol_to_str(entry.2), + entry.3, + entry.0, + entry.1 ))) }) .collect(); @@ -406,10 +427,33 @@ impl AuthActivity { /// ### draw_popup_input /// /// Draw input popup - pub(super) fn draw_popup_input(&self, text: String) -> Paragraph { - Paragraph::new(self.input_txt.as_ref()) + pub(super) fn draw_popup_save_bookmark(&self) -> (Paragraph, Tabs) { + let input: Paragraph = Paragraph::new(self.input_txt.as_ref()) .style(Style::default().fg(Color::White)) - .block(Block::default().borders(Borders::ALL).title(text)) + .block( + Block::default() + .borders(Borders::TOP | Borders::RIGHT | Borders::LEFT) + .title("Save bookmark as..."), + ); + let choices: Vec = vec![Spans::from("Yes"), Spans::from("No")]; + let index: usize = match self.choice_opt { + DialogYesNoOption::Yes => 0, + DialogYesNoOption::No => 1, + }; + let tabs: Tabs = Tabs::new(choices) + .block( + Block::default() + .borders(Borders::BOTTOM | Borders::RIGHT | Borders::LEFT) + .title("Save password?"), + ) + .select(index) + .style(Style::default()) + .highlight_style( + Style::default() + .add_modifier(Modifier::BOLD) + .fg(Color::LightRed), + ); + (input, tabs) } /// ### draw_popup_yesno @@ -538,4 +582,18 @@ impl AuthActivity { ) .start_corner(Corner::TopLeft) } + + /// ### protocol_to_str + /// + /// Convert protocol to str for layouts + fn protocol_to_str(proto: FileTransferProtocol) -> &'static str { + match proto { + FileTransferProtocol::Ftp(secure) => match secure { + true => "ftps", + false => "ftp", + }, + FileTransferProtocol::Scp => "scp", + FileTransferProtocol::Sftp => "sftp", + } + } } diff --git a/src/ui/activities/auth_activity/mod.rs b/src/ui/activities/auth_activity/mod.rs index fe66be7..c958799 100644 --- a/src/ui/activities/auth_activity/mod.rs +++ b/src/ui/activities/auth_activity/mod.rs @@ -36,8 +36,8 @@ extern crate unicode_width; // locals use super::{Activity, Context}; -use crate::bookmarks::UserHosts; use crate::filetransfer::FileTransferProtocol; +use crate::system::bookmarks_client::BookmarksClient; // Includes use crossterm::event::Event as InputEvent; @@ -46,7 +46,6 @@ use tui::style::Color; // Types type DialogCallback = fn(&mut AuthActivity); -type OnInputSubmitCallback = fn(&mut AuthActivity, String); /// ### InputField /// @@ -76,7 +75,7 @@ enum DialogYesNoOption { enum PopupType { Alert(Color, String), // Show a message displaying text with the provided color Help, // Help page - Input(String, OnInputSubmitCallback), // Input description; Callback for submit + SaveBookmark, YesNo(String, DialogCallback, DialogCallback), // Yes, no callback } @@ -111,7 +110,7 @@ pub struct AuthActivity { pub submit: bool, // becomes true after user has submitted fields pub quit: bool, // Becomes true if user has pressed esc context: Option, - bookmarks: Option, + bookmarks_client: Option, selected_field: InputField, // Selected field in AuthCredentials Form input_mode: InputMode, input_form: InputForm, @@ -143,7 +142,7 @@ impl AuthActivity { submit: false, quit: false, context: None, - bookmarks: None, + bookmarks_client: None, selected_field: InputField::Address, input_mode: InputMode::Form, input_form: InputForm::AuthCredentials, @@ -171,9 +170,9 @@ impl Activity for AuthActivity { // Put raw mode on enabled let _ = enable_raw_mode(); self.input_mode = InputMode::Form; - // Read bookmarks - if self.bookmarks.is_none() { - self.read_bookmarks(); + // Init bookmarks client + if self.bookmarks_client.is_none() { + self.init_bookmarks_client(); } } diff --git a/src/ui/activities/filetransfer_activity/mod.rs b/src/ui/activities/filetransfer_activity/mod.rs index ce7f5c3..dba2add 100644 --- a/src/ui/activities/filetransfer_activity/mod.rs +++ b/src/ui/activities/filetransfer_activity/mod.rs @@ -269,7 +269,7 @@ impl FileTransferActivity { /// /// Instantiates a new FileTransferActivity pub fn new(params: FileTransferParams) -> FileTransferActivity { - let protocol: FileTransferProtocol = params.protocol.clone(); + let protocol: FileTransferProtocol = params.protocol; FileTransferActivity { disconnected: false, quit: false,