termscp/src/system/config_client.rs

648 lines
23 KiB
Rust

//! ## ConfigClient
//!
//! `config_client` is the module which provides an API between the Config module and the system
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Deps
extern crate rand;
// Locals
use crate::config::serializer::ConfigSerializer;
use crate::config::{SerializerError, SerializerErrorKind, UserConfig};
use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs;
// Ext
use std::fs::{create_dir, remove_file, File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;
// Types
pub type SshHost = (String, String, PathBuf); // 0: host, 1: username, 2: RSA key path
/// ## ConfigClient
///
/// ConfigClient provides a high level API to communicate with the termscp configuration
pub struct ConfigClient {
config: UserConfig, // Configuration loaded
config_path: PathBuf, // Configuration TOML Path
ssh_key_dir: PathBuf, // SSH Key storage directory
}
impl ConfigClient {
/// ### new
///
/// Instantiate a new `ConfigClient` with provided path
pub fn new(config_path: &Path, ssh_key_dir: &Path) -> Result<ConfigClient, SerializerError> {
// Initialize a default configuration
let default_config: UserConfig = UserConfig::default();
info!(
"Setting up config client with config path {} and SSH key directory {}",
config_path.display(),
ssh_key_dir.display()
);
// Create client
let mut client: ConfigClient = ConfigClient {
config: default_config,
config_path: PathBuf::from(config_path),
ssh_key_dir: PathBuf::from(ssh_key_dir),
};
// If ssh key directory doesn't exist, create it
if !ssh_key_dir.exists() {
if let Err(err) = create_dir(ssh_key_dir) {
error!("Failed to create SSH key dir: {}", err);
return Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
format!(
"Could not create SSH key directory \"{}\": {}",
ssh_key_dir.display(),
err
),
));
}
debug!("Created SSH key directory");
}
// If Config file doesn't exist, create it
if !config_path.exists() {
if let Err(err) = client.write_config() {
error!("Couldn't create configuration file: {}", err);
return Err(err);
}
debug!("Config file didn't exist; created file");
} else {
// otherwise Load configuration from file
if let Err(err) = client.read_config() {
error!("Couldn't read configuration file: {}", err);
return Err(err);
}
debug!("Read configuration file");
}
Ok(client)
}
// Text editor
/// ### get_text_editor
///
/// Get text editor from configuration
pub fn get_text_editor(&self) -> PathBuf {
self.config.user_interface.text_editor.clone()
}
/// ### set_text_editor
///
/// Set text editor path
pub fn set_text_editor(&mut self, path: PathBuf) {
self.config.user_interface.text_editor = path;
}
// Default protocol
/// ### get_default_protocol
///
/// Get default protocol from configuration
pub fn get_default_protocol(&self) -> FileTransferProtocol {
match FileTransferProtocol::from_str(self.config.user_interface.default_protocol.as_str()) {
Ok(p) => p,
Err(_) => FileTransferProtocol::Sftp,
}
}
/// ### set_default_protocol
///
/// Set default protocol to configuration
pub fn set_default_protocol(&mut self, proto: FileTransferProtocol) {
self.config.user_interface.default_protocol = proto.to_string();
}
/// ### get_show_hidden_files
///
/// Get value of `show_hidden_files`
pub fn get_show_hidden_files(&self) -> bool {
self.config.user_interface.show_hidden_files
}
/// ### set_show_hidden_files
///
/// Set new value for `show_hidden_files`
pub fn set_show_hidden_files(&mut self, value: bool) {
self.config.user_interface.show_hidden_files = value;
}
/// ### get_check_for_updates
///
/// Get value of `check_for_updates`
pub fn get_check_for_updates(&self) -> bool {
self.config.user_interface.check_for_updates.unwrap_or(true)
}
/// ### set_check_for_updates
///
/// Set new value for `check_for_updates`
pub fn set_check_for_updates(&mut self, value: bool) {
self.config.user_interface.check_for_updates = Some(value);
}
/// ### get_group_dirs
///
/// Get GroupDirs value from configuration (will be converted from string)
pub fn get_group_dirs(&self) -> Option<GroupDirs> {
// Convert string to `GroupDirs`
match &self.config.user_interface.group_dirs {
None => None,
Some(val) => match GroupDirs::from_str(val.as_str()) {
Ok(val) => Some(val),
Err(_) => None,
},
}
}
/// ### set_group_dirs
///
/// Set value for group_dir in configuration.
/// Provided value, if `Some` will be converted to `GroupDirs`
pub fn set_group_dirs(&mut self, val: Option<GroupDirs>) {
self.config.user_interface.group_dirs = val.map(|val| val.to_string());
}
/// ### get_local_file_fmt
///
/// Get current file fmt for local host
pub fn get_local_file_fmt(&self) -> Option<String> {
self.config.user_interface.file_fmt.clone()
}
/// ### set_local_file_fmt
///
/// Set file fmt parameter for local host
pub fn set_local_file_fmt(&mut self, s: String) {
self.config.user_interface.file_fmt = match s.is_empty() {
true => None,
false => Some(s),
};
}
/// ### get_remote_file_fmt
///
/// Get current file fmt for remote host
pub fn get_remote_file_fmt(&self) -> Option<String> {
self.config.user_interface.remote_file_fmt.clone()
}
/// ### set_remote_file_fmt
///
/// Set file fmt parameter for remote host
pub fn set_remote_file_fmt(&mut self, s: String) {
self.config.user_interface.remote_file_fmt = match s.is_empty() {
true => None,
false => Some(s),
};
}
// SSH Keys
/// ### save_ssh_key
///
/// Save a SSH key into configuration.
/// This operation also creates the key file in `ssh_key_dir`
/// and also commits changes to configuration, to prevent incoerent data
pub fn add_ssh_key(
&mut self,
host: &str,
username: &str,
ssh_key: &str,
) -> Result<(), SerializerError> {
let host_name: String = Self::make_ssh_host_key(host, username);
// Get key path
let ssh_key_path: PathBuf = {
let mut p: PathBuf = self.ssh_key_dir.clone();
p.push(format!("{}.key", host_name));
p
};
info!(
"Writing SSH file to {} for host {}",
ssh_key_path.display(),
host_name
);
// Write key to file
let mut f: File = match File::create(ssh_key_path.as_path()) {
Ok(f) => f,
Err(err) => return Self::make_io_err(err),
};
if let Err(err) = f.write_all(ssh_key.as_bytes()) {
error!("Failed to write SSH key to file: {}", err);
return Self::make_io_err(err);
}
// Add host to keys
self.config.remote.ssh_keys.insert(host_name, ssh_key_path);
// Write config
self.write_config()
}
/// ### del_ssh_key
///
/// Delete a ssh key from configuration, using host as key.
/// This operation also unlinks the key file in `ssh_key_dir`
/// and also commits changes to configuration, to prevent incoerent data
pub fn del_ssh_key(&mut self, host: &str, username: &str) -> Result<(), SerializerError> {
// Remove key from configuration and get key path
info!("Removing key for {}@{}", host, username);
let key_path: PathBuf = match self
.config
.remote
.ssh_keys
.remove(&Self::make_ssh_host_key(host, username))
{
Some(p) => p,
None => return Ok(()), // Return ok if host doesn't exist
};
// Remove file
if let Err(err) = remove_file(key_path.as_path()) {
error!("Failed to remove key file {}: {}", key_path.display(), err);
return Self::make_io_err(err);
}
// Commit changes to configuration
self.write_config()
}
/// ### get_ssh_key
///
/// Get ssh key from host.
/// None is returned if key doesn't exist
/// `std::io::Error` is returned in case it was not possible to read the key file
pub fn get_ssh_key(&self, mkey: &str) -> std::io::Result<Option<SshHost>> {
// Check if Key exists
match self.config.remote.ssh_keys.get(mkey) {
None => Ok(None),
Some(key_path) => {
// Get host and username
let (host, username): (String, String) = Self::get_ssh_tokens(mkey);
// Return key
Ok(Some((host, username, PathBuf::from(key_path))))
}
}
}
/// ### iter_ssh_keys
///
/// Get an iterator through hosts in the ssh key storage
pub fn iter_ssh_keys(&self) -> impl Iterator<Item = &String> + '_ {
Box::new(self.config.remote.ssh_keys.keys())
}
// I/O
/// ### write_config
///
/// Write configuration to file
pub fn write_config(&self) -> Result<(), SerializerError> {
// Open file
match OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(self.config_path.as_path())
{
Ok(writer) => {
let serializer: ConfigSerializer = ConfigSerializer {};
serializer.serialize(Box::new(writer), &self.config)
}
Err(err) => {
error!("Failed to write configuration file: {}", err);
Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
))
}
}
}
/// ### read_config
///
/// Read configuration from file (or reload it if already read)
pub fn read_config(&mut self) -> Result<(), SerializerError> {
// Open bookmarks file for read
match OpenOptions::new()
.read(true)
.open(self.config_path.as_path())
{
Ok(reader) => {
// Deserialize
let deserializer: ConfigSerializer = ConfigSerializer {};
match deserializer.deserialize(Box::new(reader)) {
Ok(config) => {
self.config = config;
Ok(())
}
Err(err) => Err(err),
}
}
Err(err) => {
error!("Failed to read configuration: {}", err);
Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
))
}
}
}
/// ### make_ssh_host_key
///
/// Hosts are saved as `username@host` into configuration.
/// This method creates the key name, starting from host and username
fn make_ssh_host_key(host: &str, username: &str) -> String {
format!("{}@{}", username, host)
}
/// ### get_ssh_tokens
///
/// Get ssh tokens starting from ssh host key
/// Panics if key has invalid syntax
/// Returns: (host, username)
fn get_ssh_tokens(host_key: &str) -> (String, String) {
let tokens: Vec<&str> = host_key.split('@').collect();
assert_eq!(tokens.len(), 2);
(String::from(tokens[1]), String::from(tokens[0]))
}
/// ### make_io_err
///
/// Make serializer error from `std::io::Error`
fn make_io_err(err: std::io::Error) -> Result<(), SerializerError> {
Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::UserConfig;
use crate::utils::random::random_alphanumeric_with_len;
use pretty_assertions::assert_eq;
use std::io::Read;
use tempfile::TempDir;
#[test]
fn test_system_config_new() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, ssh_keys_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let client: ConfigClient = ConfigClient::new(cfg_path.as_path(), ssh_keys_path.as_path())
.ok()
.unwrap();
// Verify parameters
let default_config: UserConfig = UserConfig::default();
assert_eq!(client.config.remote.ssh_keys.len(), 0);
assert_eq!(
client.config.user_interface.default_protocol,
default_config.user_interface.default_protocol
);
assert_eq!(
client.config.user_interface.text_editor,
default_config.user_interface.text_editor
);
assert_eq!(client.config_path, cfg_path);
assert_eq!(client.ssh_key_dir, ssh_keys_path);
}
#[test]
fn test_system_config_new_err() {
assert!(
ConfigClient::new(Path::new("/tmp/oifoif/omar"), Path::new("/tmp/efnnu/omar"),)
.is_err()
);
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, _): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
assert!(ConfigClient::new(cfg_path.as_path(), Path::new("/tmp/efnnu/omar")).is_err());
}
#[test]
fn test_system_config_from_existing() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
.unwrap();
// Change some stuff
client.set_text_editor(PathBuf::from("/usr/bin/vim"));
client.set_default_protocol(FileTransferProtocol::Scp);
assert!(client
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
.is_ok());
assert!(client.write_config().is_ok());
// Istantiate a new client
let client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
.unwrap();
// Verify client has updated parameters
assert_eq!(client.get_default_protocol(), FileTransferProtocol::Scp);
assert_eq!(client.get_text_editor(), PathBuf::from("/usr/bin/vim"));
let mut expected_key_path: PathBuf = key_path.clone();
expected_key_path.push("pi@192.168.1.31.key");
assert_eq!(
client.get_ssh_key("pi@192.168.1.31").unwrap().unwrap(),
(
String::from("192.168.1.31"),
String::from("pi"),
expected_key_path,
)
);
}
#[test]
fn test_system_config_text_editor() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
.unwrap();
client.set_text_editor(PathBuf::from("mcedit"));
assert_eq!(client.get_text_editor(), PathBuf::from("mcedit"));
}
#[test]
fn test_system_config_default_protocol() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
.unwrap();
client.set_default_protocol(FileTransferProtocol::Ftp(true));
assert_eq!(
client.get_default_protocol(),
FileTransferProtocol::Ftp(true)
);
}
#[test]
fn test_system_config_show_hidden_files() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
.unwrap();
client.set_show_hidden_files(true);
assert_eq!(client.get_show_hidden_files(), true);
}
#[test]
fn test_system_config_check_for_updates() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
.unwrap();
assert_eq!(client.get_check_for_updates(), true); // Null ?
client.set_check_for_updates(true);
assert_eq!(client.get_check_for_updates(), true);
client.set_check_for_updates(false);
assert_eq!(client.get_check_for_updates(), false);
}
#[test]
fn test_system_config_group_dirs() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
.unwrap();
client.set_group_dirs(Some(GroupDirs::First));
assert_eq!(client.get_group_dirs(), Some(GroupDirs::First),);
client.set_group_dirs(None);
assert_eq!(client.get_group_dirs(), None,);
}
#[test]
fn test_system_config_local_file_fmt() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
.unwrap();
assert_eq!(client.get_local_file_fmt(), None);
client.set_local_file_fmt(String::from("{NAME}"));
assert_eq!(client.get_local_file_fmt().unwrap(), String::from("{NAME}"));
// Delete
client.set_local_file_fmt(String::from(""));
assert_eq!(client.get_local_file_fmt(), None);
}
#[test]
fn test_system_config_remote_file_fmt() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
.unwrap();
assert_eq!(client.get_remote_file_fmt(), None);
client.set_remote_file_fmt(String::from("{NAME}"));
assert_eq!(
client.get_remote_file_fmt().unwrap(),
String::from("{NAME}")
);
// Delete
client.set_remote_file_fmt(String::from(""));
assert_eq!(client.get_remote_file_fmt(), None);
}
#[test]
fn test_system_config_ssh_keys() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
.unwrap();
// Add a new key
let rsa_key: String = get_sample_rsa_key();
assert!(client
.add_ssh_key("192.168.1.31", "pi", rsa_key.as_str())
.is_ok());
// Iterate keys
for key in client.iter_ssh_keys() {
let host: SshHost = client.get_ssh_key(key).ok().unwrap().unwrap();
assert_eq!(host.0, String::from("192.168.1.31"));
assert_eq!(host.1, String::from("pi"));
let mut expected_key_path: PathBuf = key_path.clone();
expected_key_path.push("pi@192.168.1.31.key");
assert_eq!(host.2, expected_key_path);
// Read rsa key
let mut key_file: File = File::open(expected_key_path.as_path()).ok().unwrap();
// Read
let mut key: String = String::new();
assert!(key_file.read_to_string(&mut key).is_ok());
// Verify rsa key
assert_eq!(key, rsa_key);
}
// Unexisting key
assert!(client.get_ssh_key("test").ok().unwrap().is_none());
// Delete key
assert!(client.del_ssh_key("192.168.1.31", "pi").is_ok());
}
#[test]
fn test_system_config_make_key() {
assert_eq!(
ConfigClient::make_ssh_host_key("192.168.1.31", "pi"),
String::from("pi@192.168.1.31")
);
assert_eq!(
ConfigClient::get_ssh_tokens("pi@192.168.1.31"),
(String::from("192.168.1.31"), String::from("pi"))
);
}
#[test]
fn test_system_config_make_io_err() {
let err: SerializerError =
ConfigClient::make_io_err(std::io::Error::from(std::io::ErrorKind::PermissionDenied))
.err()
.unwrap();
assert_eq!(err.to_string(), "IO error (permission denied)");
}
/// ### get_paths
///
/// Get paths for configuration and keys directory
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
let mut k: PathBuf = PathBuf::from(dir);
let mut c: PathBuf = k.clone();
k.push("ssh-keys/");
c.push("config.toml");
(c, k)
}
fn get_sample_rsa_key() -> String {
format!(
"-----BEGIN OPENSSH PRIVATE KEY-----\n{}\n-----END OPENSSH PRIVATE KEY-----",
random_alphanumeric_with_len(2536)
)
}
}