Merge branch 'bookmarks' into 0.2.0

This commit is contained in:
ChristianVisintin
2020-12-16 20:42:47 +01:00
17 changed files with 1156 additions and 332 deletions

233
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"] }

View File

@@ -160,7 +160,7 @@ impl ActivityManager {
0 => None,
_ => Some(activity.password.clone()),
},
protocol: activity.protocol.clone(),
protocol: activity.protocol,
});
break;
}

View File

@@ -47,6 +47,7 @@ pub struct Bookmark {
pub port: u16,
pub protocol: String,
pub username: String,
pub password: Option<String>, // Password is optional; base64, aes-128 encrypted password
}
// Errors

View File

@@ -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<String, Bookmark> = 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]

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
// 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<BookmarksClient, SerializerError> {
// 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<dyn Iterator<Item = &String> + '_> {
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<String>)> {
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<String>,
) {
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<dyn Iterator<Item = &String> + '_> {
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<String> = 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<String, SerializerError> {
// Generate 256 bytes (2048 bits) key
let key: String = rand::thread_rng()
.sample_iter(Alphanumeric)
.take(256)
.collect::<String>();
// 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<String>,
) -> 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<String, SerializerError> {
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<String, SerializerError> {
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<String>) =
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<String>) =
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()
}
}

68
src/system/environment.rs Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
// 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<Option<PathBuf>, String> {
// Get file
lazy_static! {
static ref CONF_DIR: Option<PathBuf> = 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());
}
}

28
src/system/mod.rs Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
// modules
pub mod bookmarks_client;
pub mod environment;

View File

@@ -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<String> = 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::<usize>() {
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<String> = 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<String> = 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<String> = 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<Bookmark, ()> {
// Check port
let port: u16 = match self.port.parse::<usize>() {
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<PathBuf> {
// Get file
lazy_static! {
static ref CONF_DIR: Option<PathBuf> = 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),
))
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();

View File

@@ -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<List> {
self.bookmarks.as_ref()?;
self.bookmarks_client.as_ref()?;
let hosts: Vec<ListItem> = 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<List> {
self.bookmarks.as_ref()?;
self.bookmarks_client.as_ref()?;
let hosts: Vec<ListItem> = 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<Spans> = 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",
}
}
}

View File

@@ -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<Context>,
bookmarks: Option<UserHosts>,
bookmarks_client: Option<BookmarksClient>,
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();
}
}

View File

@@ -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,