mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
Merge branch '0.3.1' into refactoring/1
This commit is contained in:
@@ -27,7 +27,7 @@ use std::path::PathBuf;
|
||||
|
||||
// Deps
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::host::Localhost;
|
||||
use crate::host::{HostError, Localhost};
|
||||
use crate::ui::activities::{
|
||||
auth_activity::AuthActivity, filetransfer_activity::FileTransferActivity,
|
||||
filetransfer_activity::FileTransferParams, setup_activity::SetupActivity, Activity,
|
||||
@@ -60,11 +60,11 @@ impl ActivityManager {
|
||||
/// ### new
|
||||
///
|
||||
/// Initializes a new Activity Manager
|
||||
pub fn new(local_dir: &PathBuf, interval: Duration) -> Result<ActivityManager, ()> {
|
||||
pub fn new(local_dir: &PathBuf, interval: Duration) -> Result<ActivityManager, HostError> {
|
||||
// Prepare Context
|
||||
let host: Localhost = match Localhost::new(local_dir.clone()) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return Err(()),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let ctx: Context = Context::new(host);
|
||||
Ok(ActivityManager {
|
||||
|
||||
@@ -150,10 +150,12 @@ impl FtpFileTransfer {
|
||||
Err(_) => None,
|
||||
};
|
||||
// Get filesize
|
||||
let filesize: usize = match metadata.get(6).unwrap().as_str().parse::<usize>() {
|
||||
Ok(sz) => sz,
|
||||
Err(_) => 0,
|
||||
};
|
||||
let filesize: usize = metadata
|
||||
.get(6)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.parse::<usize>()
|
||||
.unwrap_or(0);
|
||||
let file_name: String = String::from(metadata.get(8).unwrap().as_str());
|
||||
// Check if file_name is '.' or '..'
|
||||
if file_name.as_str() == "." || file_name.as_str() == ".." {
|
||||
@@ -240,10 +242,7 @@ impl FtpFileTransfer {
|
||||
true => 0, // If is directory, filesize is 0
|
||||
false => match metadata.get(3) {
|
||||
// If is file, parse arg 3
|
||||
Some(val) => match val.as_str().parse::<usize>() {
|
||||
Ok(sz) => sz,
|
||||
Err(_) => 0,
|
||||
},
|
||||
Some(val) => val.as_str().parse::<usize>().unwrap_or(0),
|
||||
None => 0, // Should not happen
|
||||
},
|
||||
};
|
||||
|
||||
@@ -36,13 +36,11 @@ use crate::utils::parser::parse_lstime;
|
||||
// Includes
|
||||
use regex::Regex;
|
||||
use ssh2::{Channel, Session};
|
||||
use std::net::TcpStream;
|
||||
use std::io::{BufReader, BufWriter, Read, Write};
|
||||
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
use std::{
|
||||
io::{BufReader, BufWriter, Read, Write},
|
||||
ops::Range,
|
||||
};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
/// ## ScpFileTransfer
|
||||
///
|
||||
@@ -137,10 +135,7 @@ impl ScpFileTransfer {
|
||||
Err(_) => None,
|
||||
};
|
||||
// Get filesize
|
||||
let filesize: usize = match metadata.get(6).unwrap().as_str().parse::<usize>() {
|
||||
Ok(sz) => sz,
|
||||
Err(_) => 0,
|
||||
};
|
||||
let filesize: usize = metadata.get(6).unwrap().as_str().parse::<usize>().unwrap_or(0);
|
||||
// Get link and name
|
||||
let (file_name, symlink_path): (String, Option<PathBuf>) = match is_symlink {
|
||||
true => self.get_name_and_link(metadata.get(8).unwrap().as_str()),
|
||||
@@ -281,12 +276,34 @@ impl FileTransfer for ScpFileTransfer {
|
||||
password: Option<String>,
|
||||
) -> Result<Option<String>, FileTransferError> {
|
||||
// Setup tcp stream
|
||||
let tcp: TcpStream = match TcpStream::connect(format!("{}:{}", address, port)) {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
let socket_addresses: Vec<SocketAddr> =
|
||||
match format!("{}:{}", address, port).to_socket_addrs() {
|
||||
Ok(s) => s.collect(),
|
||||
Err(err) => {
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::BadAddress,
|
||||
format!("{}", err),
|
||||
))
|
||||
}
|
||||
};
|
||||
let mut tcp: Option<TcpStream> = None;
|
||||
// Try addresses
|
||||
for socket_addr in socket_addresses.iter() {
|
||||
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
|
||||
Ok(stream) => {
|
||||
tcp = Some(stream);
|
||||
break;
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
// If stream is None, return connection timeout
|
||||
let tcp: TcpStream = match tcp {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::BadAddress,
|
||||
format!("{}", err),
|
||||
FileTransferErrorType::ConnectionError,
|
||||
String::from("Connection timeout"),
|
||||
))
|
||||
}
|
||||
};
|
||||
@@ -741,7 +758,13 @@ impl FileTransfer for ScpFileTransfer {
|
||||
};
|
||||
(mtime, atime)
|
||||
};
|
||||
match session.scp_send(file_name, mode, local.size as u64, Some(times)) {
|
||||
// We need to get the size of local; NOTE: don't use the `size` attribute, since might be out of sync
|
||||
let file_size: u64 = match std::fs::metadata(local.abs_path.as_path()) {
|
||||
Ok(metadata) => metadata.len(),
|
||||
Err(_) => local.size as u64, // NOTE: fallback to fsentry size
|
||||
};
|
||||
// Send file
|
||||
match session.scp_send(file_name, mode, file_size, Some(times)) {
|
||||
Ok(channel) => Ok(Box::new(BufWriter::with_capacity(65536, channel))),
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
|
||||
@@ -34,7 +34,7 @@ use crate::system::sshkey_storage::SshKeyStorage;
|
||||
// Includes
|
||||
use ssh2::{FileStat, OpenFlags, OpenType, Session, Sftp};
|
||||
use std::io::{BufReader, BufWriter, Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
@@ -203,12 +203,34 @@ impl FileTransfer for SftpFileTransfer {
|
||||
password: Option<String>,
|
||||
) -> Result<Option<String>, FileTransferError> {
|
||||
// Setup tcp stream
|
||||
let tcp: TcpStream = match TcpStream::connect(format!("{}:{}", address, port)) {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
let socket_addresses: Vec<SocketAddr> =
|
||||
match format!("{}:{}", address, port).to_socket_addrs() {
|
||||
Ok(s) => s.collect(),
|
||||
Err(err) => {
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::BadAddress,
|
||||
format!("{}", err),
|
||||
))
|
||||
}
|
||||
};
|
||||
let mut tcp: Option<TcpStream> = None;
|
||||
// Try addresses
|
||||
for socket_addr in socket_addresses.iter() {
|
||||
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
|
||||
Ok(stream) => {
|
||||
tcp = Some(stream);
|
||||
break;
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
// If stream is None, return connection timeout
|
||||
let tcp: TcpStream = match tcp {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::BadAddress,
|
||||
format!("{}", err),
|
||||
FileTransferErrorType::ConnectionError,
|
||||
String::from("Connection timeout"),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ extern crate bitflags;
|
||||
// Locals
|
||||
use super::FsEntry;
|
||||
// Ext
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
@@ -137,7 +138,7 @@ impl FileExplorer {
|
||||
///
|
||||
/// Iterate over files
|
||||
/// Filters are applied based on current options (e.g. hidden files not returned)
|
||||
pub fn iter_files(&self) -> Box<dyn Iterator<Item = &FsEntry> + '_> {
|
||||
pub fn iter_files(&self) -> impl Iterator<Item = &FsEntry> + '_ {
|
||||
// Filter
|
||||
let opts: ExplorerOpts = self.opts;
|
||||
Box::new(self.files.iter().filter(move |x| {
|
||||
@@ -154,7 +155,7 @@ impl FileExplorer {
|
||||
/// ### iter_files_all
|
||||
///
|
||||
/// Iterate all files; doesn't care about options
|
||||
pub fn iter_files_all(&self) -> Box<dyn Iterator<Item = &FsEntry> + '_> {
|
||||
pub fn iter_files_all(&self) -> impl Iterator<Item = &FsEntry> + '_ {
|
||||
Box::new(self.files.iter())
|
||||
}
|
||||
|
||||
@@ -238,16 +239,14 @@ impl FileExplorer {
|
||||
///
|
||||
/// Sort files by creation time; the newest comes first
|
||||
fn sort_files_by_creation_time(&mut self) {
|
||||
self.files
|
||||
.sort_by(|a: &FsEntry, b: &FsEntry| b.get_creation_time().cmp(&a.get_creation_time()));
|
||||
self.files.sort_by_key(|b: &FsEntry| Reverse(b.get_creation_time()));
|
||||
}
|
||||
|
||||
/// ### sort_files_by_size
|
||||
///
|
||||
/// Sort files by size
|
||||
fn sort_files_by_size(&mut self) {
|
||||
self.files
|
||||
.sort_by(|a: &FsEntry, b: &FsEntry| b.get_size().cmp(&a.get_size()));
|
||||
self.files.sort_by_key(|b: &FsEntry| Reverse(b.get_size()));
|
||||
}
|
||||
|
||||
/// ### sort_files_directories_first
|
||||
@@ -432,6 +431,19 @@ impl FileExplorer {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### set_abs_index
|
||||
///
|
||||
/// Set absolute index
|
||||
pub fn set_abs_index(&mut self, idx: usize) {
|
||||
self.index = match idx >= self.files.len() {
|
||||
true => match self.files.len() {
|
||||
0 => 0,
|
||||
_ => self.files.len() - 1,
|
||||
},
|
||||
false => idx,
|
||||
};
|
||||
}
|
||||
|
||||
/// ### toggle_hidden_files
|
||||
///
|
||||
/// Enable/disable hidden files
|
||||
@@ -726,6 +738,33 @@ mod tests {
|
||||
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_set_abs_index() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
explorer.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES);
|
||||
// Create files (files are then sorted by name DEFAULT)
|
||||
explorer.set_files(vec![
|
||||
make_fs_entry("README.md", false),
|
||||
make_fs_entry("src/", true),
|
||||
make_fs_entry(".git/", true),
|
||||
make_fs_entry("CONTRIBUTING.md", false),
|
||||
make_fs_entry("CODE_OF_CONDUCT.md", false),
|
||||
make_fs_entry("CHANGELOG.md", false),
|
||||
make_fs_entry("LICENSE", false),
|
||||
make_fs_entry("Cargo.toml", false),
|
||||
make_fs_entry("Cargo.lock", false),
|
||||
make_fs_entry("codecov.yml", false),
|
||||
make_fs_entry(".gitignore", false),
|
||||
]);
|
||||
explorer.set_abs_index(3);
|
||||
assert_eq!(explorer.get_index(), 3);
|
||||
explorer.set_abs_index(12);
|
||||
assert_eq!(explorer.get_index(), 10);
|
||||
explorer.set_files(vec![]);
|
||||
explorer.set_abs_index(12);
|
||||
assert_eq!(explorer.get_index(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_sort_by_creation_time() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
|
||||
@@ -167,8 +167,8 @@ fn main() {
|
||||
// Create activity manager (and context too)
|
||||
let mut manager: ActivityManager = match ActivityManager::new(&wrkdir, ticks) {
|
||||
Ok(m) => m,
|
||||
Err(_) => {
|
||||
eprintln!("Invalid directory '{}'", wrkdir.display());
|
||||
Err(err) => {
|
||||
eprintln!("Could not start activity manager: {}", err);
|
||||
std::process::exit(255);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate whoami;
|
||||
// Crate
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
use super::keys::keyringstorage::KeyringStorage;
|
||||
use super::keys::{filestorage::FileStorage, KeyStorage, KeyStorageError};
|
||||
// Local
|
||||
use crate::bookmarks::serializer::BookmarkSerializer;
|
||||
use crate::bookmarks::{Bookmark, SerializerError, SerializerErrorKind, UserHosts};
|
||||
@@ -31,8 +37,7 @@ use crate::utils::crypto;
|
||||
use crate::utils::fmt::fmt_time;
|
||||
use crate::utils::random::random_alphanumeric_with_len;
|
||||
// Ext
|
||||
use std::fs::{OpenOptions, Permissions};
|
||||
use std::io::{Read, Write};
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
@@ -53,23 +58,60 @@ impl BookmarksClient {
|
||||
///
|
||||
/// Instantiates a new BookmarksClient
|
||||
/// Bookmarks file path must be provided
|
||||
/// Key file must be provided
|
||||
/// Storage path for file provider must be provided
|
||||
pub fn new(
|
||||
bookmarks_file: &Path,
|
||||
key_file: &Path,
|
||||
storage_path: &Path,
|
||||
recents_size: usize,
|
||||
) -> 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),
|
||||
// Make a key storage (windows / macos)
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
||||
let username: String = whoami::username();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
// Check if keyring storage is supported
|
||||
#[cfg(not(test))]
|
||||
let app_name: &str = "termscp";
|
||||
#[cfg(test)] // NOTE: when running test, add -test
|
||||
let app_name: &str = "termscp-test";
|
||||
match storage.is_supported() {
|
||||
true => (Box::new(storage), app_name),
|
||||
false => (Box::new(FileStorage::new(storage_path)), "bookmarks"),
|
||||
}
|
||||
};
|
||||
// Make a key storage (linux / unix)
|
||||
#[cfg(any(target_os = "linux", target_os = "unix"))]
|
||||
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
||||
#[cfg(not(test))]
|
||||
let app_name: &str = "bookmarks";
|
||||
#[cfg(test)] // NOTE: when running test, add -test
|
||||
let app_name: &str = "bookmarks-test";
|
||||
(Box::new(FileStorage::new(storage_path)), app_name)
|
||||
};
|
||||
// Load key
|
||||
let key: String = match key_storage.get_key(service_id) {
|
||||
Ok(k) => k,
|
||||
Err(e) => match e {
|
||||
KeyStorageError::NoSuchKey => {
|
||||
// If no such key, generate key and set it into the storage
|
||||
let key: String = Self::generate_key();
|
||||
if let Err(e) = key_storage.set_key(service_id, key.as_str()) {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
format!("Could not write key to storage: {}", e),
|
||||
));
|
||||
}
|
||||
// Return key
|
||||
key
|
||||
}
|
||||
_ => {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
format!("Could not get key from storage: {}", e),
|
||||
))
|
||||
}
|
||||
},
|
||||
};
|
||||
let mut client: BookmarksClient = BookmarksClient {
|
||||
@@ -96,7 +138,7 @@ impl BookmarksClient {
|
||||
/// ### iter_bookmarks
|
||||
///
|
||||
/// Iterate over bookmarks keys
|
||||
pub fn iter_bookmarks(&self) -> Box<dyn Iterator<Item = &String> + '_> {
|
||||
pub fn iter_bookmarks(&self) -> impl Iterator<Item = &String> + '_ {
|
||||
Box::new(self.hosts.bookmarks.keys())
|
||||
}
|
||||
|
||||
@@ -156,7 +198,7 @@ impl BookmarksClient {
|
||||
/// ### iter_recents
|
||||
///
|
||||
/// Iterate over recents keys
|
||||
pub fn iter_recents(&self) -> Box<dyn Iterator<Item = &String> + '_> {
|
||||
pub fn iter_recents(&self) -> impl Iterator<Item = &String> + '_ {
|
||||
Box::new(self.hosts.recents.keys())
|
||||
}
|
||||
|
||||
@@ -276,36 +318,10 @@ impl BookmarksClient {
|
||||
|
||||
/// ### generate_key
|
||||
///
|
||||
/// Generate a new AES key and write it to key file
|
||||
fn generate_key(key_file: &Path) -> Result<String, SerializerError> {
|
||||
/// Generate a new AES key
|
||||
fn generate_key() -> String {
|
||||
// Generate 256 bytes (2048 bits) key
|
||||
let key: String = random_alphanumeric_with_len(256);
|
||||
// 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(),
|
||||
)),
|
||||
}
|
||||
random_alphanumeric_with_len(256)
|
||||
}
|
||||
|
||||
/// ### make_bookmark
|
||||
@@ -331,28 +347,6 @@ impl BookmarksClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### 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
|
||||
@@ -375,6 +369,7 @@ impl BookmarksClient {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(not(target_os = "macos"))] // CI/CD blocks
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
@@ -382,6 +377,7 @@ mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
#[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());
|
||||
@@ -397,6 +393,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(target_os = "unix", target_os = "linux"))]
|
||||
fn test_system_bookmarks_new_err() {
|
||||
assert!(BookmarksClient::new(
|
||||
Path::new("/tmp/oifoif/omar"),
|
||||
@@ -413,6 +410,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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());
|
||||
@@ -458,6 +456,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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());
|
||||
@@ -503,6 +502,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
|
||||
fn test_system_bookmarks_bad_bookmark_name() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
@@ -521,6 +521,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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());
|
||||
@@ -555,6 +556,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
fn test_system_bookmarks_dup_recent() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
@@ -579,6 +581,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
fn test_system_bookmarks_recents_more_than_limit() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
@@ -626,6 +629,7 @@ mod tests {
|
||||
|
||||
#[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());
|
||||
@@ -646,10 +650,10 @@ mod tests {
|
||||
/// ### 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 k: PathBuf = PathBuf::from(dir);
|
||||
let mut c: PathBuf = k.clone();
|
||||
k.push("bookmarks.key");
|
||||
c.push("bookmarks.toml");
|
||||
(c, k)
|
||||
}
|
||||
@@ -657,6 +661,7 @@ mod tests {
|
||||
/// ### create_tmp_dir
|
||||
///
|
||||
/// Create temporary directory
|
||||
|
||||
fn create_tmp_dir() -> tempfile::TempDir {
|
||||
tempfile::TempDir::new().ok().unwrap()
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ impl ConfigClient {
|
||||
/// ### iter_ssh_keys
|
||||
///
|
||||
/// Get an iterator through hosts in the ssh key storage
|
||||
pub fn iter_ssh_keys(&self) -> Box<dyn Iterator<Item = &String> + '_> {
|
||||
pub fn iter_ssh_keys(&self) -> impl Iterator<Item = &String> + '_ {
|
||||
Box::new(self.config.remote.ssh_keys.keys())
|
||||
}
|
||||
|
||||
|
||||
@@ -59,14 +59,12 @@ pub fn init_config_dir() -> Result<Option<PathBuf>, String> {
|
||||
/// ### get_bookmarks_paths
|
||||
///
|
||||
/// Get paths for bookmarks client
|
||||
/// Returns: path of bookmarks.toml and path of key
|
||||
pub fn get_bookmarks_paths(config_dir: &Path) -> (PathBuf, PathBuf) {
|
||||
/// Returns: path of bookmarks.toml
|
||||
pub fn get_bookmarks_paths(config_dir: &Path) -> PathBuf {
|
||||
// Prepare paths
|
||||
let mut bookmarks_file: PathBuf = PathBuf::from(config_dir);
|
||||
bookmarks_file.push("bookmarks.toml");
|
||||
let mut key_file: PathBuf = PathBuf::from(config_dir);
|
||||
key_file.push(".bookmarks.key"); // key file is hidden
|
||||
(bookmarks_file, key_file)
|
||||
bookmarks_file
|
||||
}
|
||||
|
||||
/// ### get_config_paths
|
||||
@@ -123,10 +121,7 @@ mod tests {
|
||||
fn test_system_environment_get_bookmarks_paths() {
|
||||
assert_eq!(
|
||||
get_bookmarks_paths(&Path::new("/home/omar/.config/termscp/")),
|
||||
(
|
||||
PathBuf::from("/home/omar/.config/termscp/bookmarks.toml"),
|
||||
PathBuf::from("/home/omar/.config/termscp/.bookmarks.key")
|
||||
)
|
||||
PathBuf::from("/home/omar/.config/termscp/bookmarks.toml"),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
163
src/system/keys/filestorage.rs
Normal file
163
src/system/keys/filestorage.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
//! ## FileStorage
|
||||
//!
|
||||
//! `filestorage` provides an implementation of the `KeyStorage` trait using a file
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Local
|
||||
use super::{KeyStorage, KeyStorageError};
|
||||
// Ext
|
||||
use std::fs::{OpenOptions, Permissions};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// ## FileStorage
|
||||
///
|
||||
/// File storage is an implementation o the `KeyStorage` which uses a file to store the key
|
||||
pub struct FileStorage {
|
||||
dir_path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileStorage {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new `FileStorage`
|
||||
pub fn new(dir_path: &Path) -> Self {
|
||||
FileStorage {
|
||||
dir_path: PathBuf::from(dir_path),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### make_file_path
|
||||
///
|
||||
/// Make file path for key file from `dir_path` and the application id
|
||||
fn make_file_path(&self, storage_id: &str) -> PathBuf {
|
||||
let mut p: PathBuf = self.dir_path.clone();
|
||||
let file_name = format!(".{}.key", storage_id);
|
||||
p.push(file_name);
|
||||
p
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyStorage for FileStorage {
|
||||
/// ### get_key
|
||||
///
|
||||
/// Retrieve key from the key storage.
|
||||
/// The key might be acccess through an identifier, which identifies
|
||||
/// the key in the storage
|
||||
fn get_key(&self, storage_id: &str) -> Result<String, KeyStorageError> {
|
||||
let key_file: PathBuf = self.make_file_path(storage_id);
|
||||
// Check if file exists
|
||||
if !key_file.exists() {
|
||||
return Err(KeyStorageError::NoSuchKey);
|
||||
}
|
||||
// Read key from file
|
||||
match OpenOptions::new().read(true).open(key_file.as_path()) {
|
||||
Ok(mut file) => {
|
||||
let mut key: String = String::new();
|
||||
match file.read_to_string(&mut key) {
|
||||
Ok(_) => Ok(key),
|
||||
Err(_) => Err(KeyStorageError::ProviderError),
|
||||
}
|
||||
}
|
||||
Err(_) => Err(KeyStorageError::ProviderError),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### set_key
|
||||
///
|
||||
/// Set the key into the key storage
|
||||
fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError> {
|
||||
let key_file: PathBuf = self.make_file_path(storage_id);
|
||||
// Write key
|
||||
match OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(key_file.as_path())
|
||||
{
|
||||
Ok(mut file) => {
|
||||
// Write key to file
|
||||
if file.write_all(key.as_bytes()).is_err() {
|
||||
return Err(KeyStorageError::ProviderError);
|
||||
}
|
||||
// Set file to readonly
|
||||
let mut permissions: Permissions = file.metadata().unwrap().permissions();
|
||||
permissions.set_readonly(true);
|
||||
let _ = file.set_permissions(permissions);
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => Err(KeyStorageError::ProviderError),
|
||||
}
|
||||
}
|
||||
|
||||
/// is_supported
|
||||
///
|
||||
/// Returns whether the key storage is supported on the host system
|
||||
fn is_supported(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_system_keys_filestorage_make_dir() {
|
||||
let storage: FileStorage = FileStorage::new(&Path::new("/tmp/"));
|
||||
assert_eq!(
|
||||
storage.make_file_path("bookmarks").as_path(),
|
||||
Path::new("/tmp/.bookmarks.key")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_keys_filestorage_ok() {
|
||||
let key_dir: tempfile::TempDir =
|
||||
tempfile::TempDir::new().expect("Could not create tempdir");
|
||||
let storage: FileStorage = FileStorage::new(key_dir.path());
|
||||
// Supported
|
||||
assert!(storage.is_supported());
|
||||
let app_name: &str = "termscp";
|
||||
let secret: &str = "Th15-15/My-Супер-Секрет";
|
||||
// Secret should not exist
|
||||
assert_eq!(
|
||||
storage.get_key(app_name).err().unwrap(),
|
||||
KeyStorageError::NoSuchKey
|
||||
);
|
||||
// Write secret
|
||||
assert!(storage.set_key(app_name, secret).is_ok());
|
||||
// Get secret
|
||||
assert_eq!(storage.get_key(app_name).ok().unwrap().as_str(), secret);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_keys_filestorage_err() {
|
||||
let bad_dir: &Path = Path::new("/piro/poro/pero/");
|
||||
let storage: FileStorage = FileStorage::new(bad_dir);
|
||||
let app_name: &str = "termscp";
|
||||
let secret: &str = "Th15-15/My-Супер-Секрет";
|
||||
assert!(storage.set_key(app_name, secret).is_err());
|
||||
}
|
||||
}
|
||||
129
src/system/keys/keyringstorage.rs
Normal file
129
src/system/keys/keyringstorage.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
//! ## KeyringStorage
|
||||
//!
|
||||
//! `keyringstorage` provides an implementation of the `KeyStorage` trait using the OS keyring
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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 keyring;
|
||||
// Local
|
||||
use super::{KeyStorage, KeyStorageError};
|
||||
// Ext
|
||||
use keyring::{Keyring, KeyringError};
|
||||
|
||||
/// ## KeyringStorage
|
||||
///
|
||||
/// provides a `KeyStorage` implementation using the keyring crate
|
||||
pub struct KeyringStorage {
|
||||
username: String,
|
||||
}
|
||||
|
||||
impl KeyringStorage {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new KeyringStorage
|
||||
pub fn new(username: &str) -> Self {
|
||||
KeyringStorage {
|
||||
username: username.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyStorage for KeyringStorage {
|
||||
/// ### get_key
|
||||
///
|
||||
/// Retrieve key from the key storage.
|
||||
/// The key might be acccess through an identifier, which identifies
|
||||
/// the key in the storage
|
||||
fn get_key(&self, storage_id: &str) -> Result<String, KeyStorageError> {
|
||||
let storage: Keyring = Keyring::new(storage_id, self.username.as_str());
|
||||
match storage.get_password() {
|
||||
Ok(s) => Ok(s),
|
||||
Err(e) => match e {
|
||||
KeyringError::NoPasswordFound => Err(KeyStorageError::NoSuchKey),
|
||||
#[cfg(target_os = "windows")]
|
||||
KeyringError::WindowsVaultError => Err(KeyStorageError::NoSuchKey),
|
||||
#[cfg(target_os = "macos")]
|
||||
KeyringError::MacOsKeychainError(_) => Err(KeyStorageError::NoSuchKey),
|
||||
_ => panic!("{}", e),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// ### set_key
|
||||
///
|
||||
/// Set the key into the key storage
|
||||
fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError> {
|
||||
let storage: Keyring = Keyring::new(storage_id, self.username.as_str());
|
||||
match storage.set_password(key) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(KeyStorageError::ProviderError),
|
||||
}
|
||||
}
|
||||
|
||||
/// is_supported
|
||||
///
|
||||
/// Returns whether the key storage is supported on the host system
|
||||
fn is_supported(&self) -> bool {
|
||||
let dummy: String = String::from("dummy-service");
|
||||
let storage: Keyring = Keyring::new(dummy.as_str(), self.username.as_str());
|
||||
// Check what kind of error is returned
|
||||
match storage.get_password() {
|
||||
Ok(_) => true,
|
||||
Err(err) => !matches!(err, KeyringError::NoBackendFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
extern crate whoami;
|
||||
use super::*;
|
||||
|
||||
use whoami::username;
|
||||
|
||||
#[test]
|
||||
fn test_system_keys_keyringstorage() {
|
||||
let username: String = username();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
assert!(storage.is_supported());
|
||||
let app_name: &str = "termscp-test2";
|
||||
let secret: &str = "Th15-15/My-Супер-Секрет";
|
||||
let kring: Keyring = Keyring::new(app_name, username.as_str());
|
||||
let _ = kring.delete_password();
|
||||
drop(kring);
|
||||
// Secret should not exist
|
||||
assert_eq!(
|
||||
storage.get_key(app_name).err().unwrap(),
|
||||
KeyStorageError::NoSuchKey
|
||||
);
|
||||
// Write secret
|
||||
assert!(storage.set_key(app_name, secret).is_ok());
|
||||
// Get secret
|
||||
assert_eq!(storage.get_key(app_name).ok().unwrap().as_str(), secret);
|
||||
|
||||
// Delete the key manually...
|
||||
let kring: Keyring = Keyring::new(app_name, username.as_str());
|
||||
assert!(kring.delete_password().is_ok());
|
||||
}
|
||||
}
|
||||
90
src/system/keys/mod.rs
Normal file
90
src/system/keys/mod.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
//! ## KeyStorage
|
||||
//!
|
||||
//! `keystorage` provides the trait to manipulate to a KeyStorage
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Storages
|
||||
pub mod filestorage;
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
pub mod keyringstorage;
|
||||
|
||||
/// ## KeyStorageError
|
||||
///
|
||||
/// defines the error type for the `KeyStorage`
|
||||
#[derive(PartialEq, std::fmt::Debug)]
|
||||
pub enum KeyStorageError {
|
||||
//BadKey,
|
||||
ProviderError,
|
||||
NoSuchKey,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KeyStorageError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let err: String = String::from(match &self {
|
||||
//KeyStorageError::BadKey => "Bad key syntax",
|
||||
KeyStorageError::ProviderError => "Provider service error",
|
||||
KeyStorageError::NoSuchKey => "No such key",
|
||||
});
|
||||
write!(f, "{}", err)
|
||||
}
|
||||
}
|
||||
|
||||
/// ## KeyStorage
|
||||
///
|
||||
/// this traits provides the methods to communicate and interact with the key storage.
|
||||
pub trait KeyStorage {
|
||||
/// ### get_key
|
||||
///
|
||||
/// Retrieve key from the key storage.
|
||||
/// The key might be acccess through an identifier, which identifies
|
||||
/// the key in the storage
|
||||
fn get_key(&self, storage_id: &str) -> Result<String, KeyStorageError>;
|
||||
|
||||
/// ### set_key
|
||||
///
|
||||
/// Set the key into the key storage
|
||||
fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError>;
|
||||
|
||||
/// is_supported
|
||||
///
|
||||
/// Returns whether the key storage is supported on the host system
|
||||
fn is_supported(&self) -> bool;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_system_keys_mod_errors() {
|
||||
assert_eq!(
|
||||
format!("{}", KeyStorageError::ProviderError),
|
||||
String::from("Provider service error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", KeyStorageError::NoSuchKey),
|
||||
String::from("No such key")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -27,4 +27,5 @@
|
||||
pub mod bookmarks_client;
|
||||
pub mod config_client;
|
||||
pub mod environment;
|
||||
pub(crate) mod keys;
|
||||
pub mod sshkey_storage;
|
||||
|
||||
@@ -224,11 +224,15 @@ impl AuthActivity {
|
||||
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 {
|
||||
let (bookmarks_file, key_file): (PathBuf, PathBuf) =
|
||||
environment::get_bookmarks_paths(path.as_path());
|
||||
if let Some(config_dir_path) = path {
|
||||
let bookmarks_file: PathBuf =
|
||||
environment::get_bookmarks_paths(config_dir_path.as_path());
|
||||
// Initialize client
|
||||
match BookmarksClient::new(bookmarks_file.as_path(), key_file.as_path(), 16) {
|
||||
match BookmarksClient::new(
|
||||
bookmarks_file.as_path(),
|
||||
config_dir_path.as_path(),
|
||||
16,
|
||||
) {
|
||||
Ok(cli) => self.bookmarks_client = Some(cli),
|
||||
Err(err) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
@@ -236,7 +240,7 @@ impl AuthActivity {
|
||||
format!(
|
||||
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
|
||||
bookmarks_file.display(),
|
||||
key_file.display(),
|
||||
config_dir_path.display(),
|
||||
err
|
||||
),
|
||||
))
|
||||
|
||||
@@ -600,9 +600,12 @@ impl FileTransferActivity {
|
||||
match self.context.as_ref().unwrap().local.scan_dir(path) {
|
||||
Ok(files) => {
|
||||
// Set files and sort (sorting is implicit)
|
||||
let prev_index: usize = self.local.get_index();
|
||||
self.local.set_files(files);
|
||||
// Restore index
|
||||
self.local.set_abs_index(prev_index);
|
||||
// Set index; keep if possible, otherwise set to last item
|
||||
self.local.set_index(match self.local.get_current_file() {
|
||||
self.local.set_abs_index(match self.local.get_current_file() {
|
||||
Some(_) => self.local.get_index(),
|
||||
None => match self.local.count() {
|
||||
0 => 0,
|
||||
@@ -626,9 +629,12 @@ impl FileTransferActivity {
|
||||
match self.client.list_dir(path) {
|
||||
Ok(files) => {
|
||||
// Set files and sort (sorting is implicit)
|
||||
let prev_index: usize = self.remote.get_index();
|
||||
self.remote.set_files(files);
|
||||
// Restore index
|
||||
self.remote.set_abs_index(prev_index);
|
||||
// Set index; keep if possible, otherwise set to last item
|
||||
self.remote.set_index(match self.remote.get_current_file() {
|
||||
self.remote.set_abs_index(match self.remote.get_current_file() {
|
||||
Some(_) => self.remote.get_index(),
|
||||
None => match self.remote.count() {
|
||||
0 => 0,
|
||||
|
||||
Reference in New Issue
Block a user