mirror of
https://github.com/veeso/termscp.git
synced 2026-09-13 23:55:50 -07:00
bc4b096077
Remove production panic and unwrap paths from core modules. Propagate bookmark encryption failures, harden file watcher and temp mapped file handling, and clean up dead code in shared utilities.
533 lines
20 KiB
Rust
533 lines
20 KiB
Rust
//! ## ActivityManager
|
|
//!
|
|
//! `activity_manager` is the module which provides run methods and handling for activities
|
|
|
|
use std::env;
|
|
use std::path::PathBuf;
|
|
use std::time::Duration;
|
|
|
|
use remotefs_ssh::SshKeyStorage as SshKeyStorageTrait;
|
|
|
|
use crate::cli::{Remote, RemoteArgs};
|
|
use crate::filetransfer::{
|
|
FileTransferParams, FileTransferProtocol, HostBridgeParams, ProtocolParams,
|
|
};
|
|
use crate::host::HostError;
|
|
use crate::system::bookmarks_client::BookmarksClient;
|
|
use crate::system::config_client::ConfigClient;
|
|
use crate::system::environment;
|
|
use crate::system::sshkey_storage::SshKeyStorage;
|
|
use crate::system::theme_provider::ThemeProvider;
|
|
use crate::ui::activities::auth::AuthActivity;
|
|
use crate::ui::activities::filetransfer::FileTransferActivity;
|
|
use crate::ui::activities::setup::SetupActivity;
|
|
use crate::ui::activities::{Activity, ExitReason};
|
|
use crate::ui::context::Context;
|
|
use crate::utils::{fmt, tty};
|
|
|
|
/// NextActivity identifies the next identity to run once the current has ended
|
|
pub enum NextActivity {
|
|
Authentication,
|
|
FileTransfer,
|
|
SetupActivity,
|
|
}
|
|
|
|
pub enum Host {
|
|
HostBridge,
|
|
Remote,
|
|
}
|
|
|
|
pub enum HostParams {
|
|
HostBridge(HostBridgeParams),
|
|
Remote(FileTransferParams),
|
|
}
|
|
|
|
/// The activity manager takes care of running activities and handling them until the application has ended
|
|
pub struct ActivityManager {
|
|
context: Option<Context>,
|
|
ticks: Duration,
|
|
}
|
|
|
|
impl ActivityManager {
|
|
/// Initializes a new Activity Manager
|
|
pub fn new(ticks: Duration, keyring: bool) -> Result<ActivityManager, HostError> {
|
|
// Prepare Context
|
|
// Initialize configuration client
|
|
let (config_client, error_config): (ConfigClient, Option<String>) =
|
|
match Self::init_config_client() {
|
|
Ok(cli) => (cli, None),
|
|
Err(err) => {
|
|
error!("Failed to initialize config client: {}", err);
|
|
(ConfigClient::degraded(), Some(err))
|
|
}
|
|
};
|
|
let (bookmarks_client, error_bookmark) = match Self::init_bookmarks_client(keyring) {
|
|
Ok(cli) => (cli, None),
|
|
Err(err) => (None, Some(err)),
|
|
};
|
|
let error = error_config.or(error_bookmark);
|
|
let theme_provider: ThemeProvider = Self::init_theme_provider();
|
|
let ctx: Context = Context::new(bookmarks_client, config_client, theme_provider, error);
|
|
Ok(ActivityManager {
|
|
context: Some(ctx),
|
|
ticks,
|
|
})
|
|
}
|
|
|
|
/// Configure remote args
|
|
pub fn configure_remote_args(&mut self, remote_args: RemoteArgs) -> Result<(), String> {
|
|
// Set for host bridge
|
|
match remote_args.host_bridge {
|
|
Remote::Bookmark(params) => self.resolve_bookmark_name(
|
|
Host::HostBridge,
|
|
¶ms.name,
|
|
params.password.as_deref(),
|
|
),
|
|
Remote::Host(host_params) => self.set_host_params(
|
|
HostParams::HostBridge(HostBridgeParams::Remote(
|
|
host_params.file_transfer_params.protocol,
|
|
host_params.file_transfer_params.params,
|
|
)),
|
|
host_params.password.as_deref(),
|
|
),
|
|
Remote::None => {
|
|
// local dir is remote_args.local_dir if set, otherwise current dir
|
|
let local_dir = remote_args
|
|
.local_dir
|
|
.map(Ok)
|
|
.unwrap_or_else(env::current_dir)
|
|
.map_err(|err| format!("Could not resolve current directory: {err}"))?;
|
|
debug!("host bridge is None, setting local dir to {:?}", local_dir,);
|
|
|
|
self.set_host_params(
|
|
HostParams::HostBridge(HostBridgeParams::Localhost(local_dir)),
|
|
None,
|
|
)
|
|
}
|
|
}?;
|
|
|
|
// set remote
|
|
match remote_args.remote {
|
|
Remote::Bookmark(params) => {
|
|
self.resolve_bookmark_name(Host::Remote, ¶ms.name, params.password.as_deref())
|
|
}
|
|
Remote::Host(host_params) => self.set_host_params(
|
|
HostParams::Remote(host_params.file_transfer_params),
|
|
host_params.password.as_deref(),
|
|
),
|
|
Remote::None => Ok(()),
|
|
}
|
|
}
|
|
|
|
/// Set file transfer params
|
|
pub fn set_host_params(
|
|
&mut self,
|
|
host: HostParams,
|
|
password: Option<&str>,
|
|
) -> Result<(), String> {
|
|
let (remote_local_path, remote_remote_path) = match &host {
|
|
HostParams::Remote(params) => (params.local_path.clone(), params.remote_path.clone()),
|
|
_ => (None, None),
|
|
};
|
|
|
|
let mut remote_params = match &host {
|
|
HostParams::HostBridge(HostBridgeParams::Remote(protocol, protocol_params)) => {
|
|
Some((*protocol, protocol_params.clone()))
|
|
}
|
|
HostParams::HostBridge(HostBridgeParams::Localhost(_)) => None,
|
|
HostParams::Remote(ft_params) => Some((ft_params.protocol, ft_params.params.clone())),
|
|
};
|
|
|
|
// Put params into the context
|
|
if let Some((protocol, params)) = remote_params.as_mut() {
|
|
self.resolve_password_for_protocol_params(*protocol, params, password)?;
|
|
}
|
|
|
|
match host {
|
|
HostParams::HostBridge(HostBridgeParams::Localhost(path)) => {
|
|
self.context_mut()?
|
|
.set_host_bridge_params(HostBridgeParams::Localhost(path));
|
|
}
|
|
HostParams::HostBridge(HostBridgeParams::Remote(_, _)) => {
|
|
let (protocol, params) = remote_params.ok_or_else(|| {
|
|
String::from("Missing remote parameters for host bridge configuration")
|
|
})?;
|
|
self.context_mut()?
|
|
.set_host_bridge_params(HostBridgeParams::Remote(protocol, params));
|
|
}
|
|
HostParams::Remote(_) => {
|
|
let (protocol, params) = remote_params
|
|
.ok_or_else(|| String::from("Missing remote parameters for remote host"))?;
|
|
let params = FileTransferParams {
|
|
local_path: remote_local_path,
|
|
remote_path: remote_remote_path,
|
|
protocol,
|
|
params,
|
|
};
|
|
self.context_mut()?.set_remote_params(params);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn resolve_password_for_protocol_params(
|
|
&mut self,
|
|
protocol: FileTransferProtocol,
|
|
params: &mut ProtocolParams,
|
|
password: Option<&str>,
|
|
) -> Result<(), String> {
|
|
// Set password if provided
|
|
if params.password_missing() {
|
|
if let Some(password) = password {
|
|
params.set_default_secret(password.to_string());
|
|
} else if matches!(
|
|
protocol,
|
|
FileTransferProtocol::Scp | FileTransferProtocol::Sftp,
|
|
) && params.generic_params().is_some()
|
|
{
|
|
// * if protocol is SCP or SFTP check whether a SSH key is registered for this remote, in case not ask password
|
|
let storage = SshKeyStorage::from(self.context_ref()?.config());
|
|
let generic_params = params.generic_params().ok_or_else(|| {
|
|
String::from("Missing generic parameters for SSH password resolution")
|
|
})?;
|
|
let username = generic_params
|
|
.username
|
|
.clone()
|
|
.map(Ok)
|
|
.unwrap_or_else(whoami::username)
|
|
.map_err(|err| format!("Could not get current username: {err}"))?;
|
|
|
|
if storage
|
|
.resolve(&generic_params.address, &username)
|
|
.is_none()
|
|
{
|
|
debug!(
|
|
"storage could not find any suitable key for {}... prompting for password",
|
|
generic_params.address
|
|
);
|
|
self.prompt_password(params)?;
|
|
} else {
|
|
debug!(
|
|
"a key is already set for {}; password is not required",
|
|
generic_params.address
|
|
);
|
|
}
|
|
} else {
|
|
self.prompt_password(params)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Prompt user for password to set into params.
|
|
fn prompt_password(&mut self, params: &mut ProtocolParams) -> Result<(), String> {
|
|
let ctx = self.context_mut()?;
|
|
let prompt = format!("Password for {}: ", params.host_name());
|
|
|
|
match tty::read_secret_from_tty(ctx.terminal(), prompt) {
|
|
Err(err) => Err(format!("Could not read password: {err}")),
|
|
Ok(Some(secret)) => {
|
|
debug!(
|
|
"Read password from tty: {}",
|
|
fmt::shadow_password(secret.as_str())
|
|
);
|
|
params.set_default_secret(secret);
|
|
Ok(())
|
|
}
|
|
Ok(None) => Ok(()),
|
|
}
|
|
}
|
|
|
|
/// Resolve provided bookmark name and set it as file transfer params.
|
|
/// Returns error if bookmark is not found
|
|
pub fn resolve_bookmark_name(
|
|
&mut self,
|
|
host: Host,
|
|
bookmark_name: &str,
|
|
password: Option<&str>,
|
|
) -> Result<(), String> {
|
|
if let Some(bookmarks_client) = self.context_mut()?.bookmarks_client_mut() {
|
|
let params = match bookmarks_client.get_bookmark(bookmark_name) {
|
|
None => {
|
|
return Err(format!(
|
|
r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"#
|
|
));
|
|
}
|
|
Some(params) => params,
|
|
};
|
|
|
|
let params = match host {
|
|
Host::Remote => HostParams::Remote(params),
|
|
Host::HostBridge => {
|
|
HostParams::HostBridge(HostBridgeParams::Remote(params.protocol, params.params))
|
|
}
|
|
};
|
|
|
|
self.set_host_params(params, password)
|
|
} else {
|
|
Err(String::from(
|
|
"Could not resolve bookmark name: bookmarks client not initialized",
|
|
))
|
|
}
|
|
}
|
|
|
|
fn context_mut(&mut self) -> Result<&mut Context, String> {
|
|
self.context
|
|
.as_mut()
|
|
.ok_or_else(|| String::from("Activity manager context is not initialized"))
|
|
}
|
|
|
|
fn context_ref(&self) -> Result<&Context, String> {
|
|
self.context
|
|
.as_ref()
|
|
.ok_or_else(|| String::from("Activity manager context is not initialized"))
|
|
}
|
|
|
|
///
|
|
/// Loop for activity manager. You need to provide the activity to start with
|
|
/// Returns the exitcode
|
|
pub fn run(&mut self, launch_activity: NextActivity) {
|
|
let mut current_activity: Option<NextActivity> = Some(launch_activity);
|
|
loop {
|
|
current_activity = match current_activity {
|
|
Some(activity) => match activity {
|
|
NextActivity::Authentication => self.run_authentication(),
|
|
NextActivity::FileTransfer => self.run_filetransfer(),
|
|
NextActivity::SetupActivity => self.run_setup(),
|
|
},
|
|
None => break, // Exit
|
|
}
|
|
}
|
|
// Drop context
|
|
drop(self.context.take());
|
|
}
|
|
|
|
// -- Activity Loops
|
|
|
|
/// Loop for Authentication activity.
|
|
/// Returns when activity terminates.
|
|
/// Returns the next activity to run
|
|
fn run_authentication(&mut self) -> Option<NextActivity> {
|
|
info!("Starting AuthActivity...");
|
|
// Prepare activity
|
|
let mut activity: AuthActivity = AuthActivity::new(self.ticks);
|
|
// Prepare result
|
|
let result: Option<NextActivity>;
|
|
// Get context
|
|
let ctx: Context = match self.context.take() {
|
|
Some(ctx) => ctx,
|
|
None => {
|
|
error!("Failed to start AuthActivity: context is None");
|
|
return None;
|
|
}
|
|
};
|
|
// Create activity
|
|
activity.on_create(ctx);
|
|
loop {
|
|
// Draw activity
|
|
activity.on_draw();
|
|
// Check if has to be terminated
|
|
if let Some(exit_reason) = activity.will_umount() {
|
|
match exit_reason {
|
|
ExitReason::Quit => {
|
|
info!("AuthActivity terminated due to 'Quit'");
|
|
result = None;
|
|
break;
|
|
}
|
|
ExitReason::EnterSetup => {
|
|
// User requested activity
|
|
info!("AuthActivity terminated due to 'EnterSetup'");
|
|
result = Some(NextActivity::SetupActivity);
|
|
break;
|
|
}
|
|
ExitReason::Connect => {
|
|
// User submitted, set next activity
|
|
info!("AuthActivity terminated due to 'Connect'");
|
|
result = Some(NextActivity::FileTransfer);
|
|
break;
|
|
}
|
|
_ => { /* Nothing to do */ }
|
|
}
|
|
}
|
|
}
|
|
// Destroy activity
|
|
self.context = activity.on_destroy();
|
|
info!("AuthActivity destroyed");
|
|
result
|
|
}
|
|
|
|
/// Loop for FileTransfer activity.
|
|
/// Returns when activity terminates.
|
|
/// Returns the next activity to run
|
|
fn run_filetransfer(&mut self) -> Option<NextActivity> {
|
|
info!("Starting FileTransferActivity");
|
|
// Get context
|
|
let mut ctx: Context = match self.context.take() {
|
|
Some(ctx) => ctx,
|
|
None => {
|
|
error!("Failed to start FileTransferActivity: context is None");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let host_bridge_params = match ctx.host_bridge_params() {
|
|
Some(params) => params.clone(),
|
|
None => {
|
|
error!("Failed to start FileTransferActivity: host bridge params is None");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
// If ft params is None, return None
|
|
let remote_params: &FileTransferParams = match ctx.remote_params() {
|
|
Some(ft_params) => ft_params,
|
|
None => {
|
|
error!("Failed to start FileTransferActivity: file transfer params is None");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
// try to setup activity
|
|
let mut activity =
|
|
match FileTransferActivity::new(host_bridge_params, remote_params, self.ticks) {
|
|
Ok(activity) => activity,
|
|
Err(err) => {
|
|
error!("Failed to start FileTransferActivity: {}", err);
|
|
ctx.set_error(err);
|
|
self.context = Some(ctx);
|
|
// Return to authentication
|
|
return Some(NextActivity::Authentication);
|
|
}
|
|
};
|
|
// Prepare result
|
|
let result: Option<NextActivity>;
|
|
// Create activity
|
|
activity.on_create(ctx);
|
|
loop {
|
|
// Draw activity
|
|
activity.on_draw();
|
|
// Check if has to be terminated
|
|
if let Some(exit_reason) = activity.will_umount() {
|
|
match exit_reason {
|
|
ExitReason::Quit => {
|
|
info!("FileTransferActivity terminated due to 'Quit'");
|
|
result = None;
|
|
break;
|
|
}
|
|
ExitReason::Disconnect => {
|
|
// User disconnected, set next activity to authentication
|
|
info!("FileTransferActivity terminated due to 'Authentication'");
|
|
result = Some(NextActivity::Authentication);
|
|
break;
|
|
}
|
|
_ => { /* Nothing to do */ }
|
|
}
|
|
}
|
|
}
|
|
// Destroy activity
|
|
self.context = activity.on_destroy();
|
|
result
|
|
}
|
|
|
|
/// `SetupActivity` run loop.
|
|
/// Returns when activity terminates.
|
|
/// Returns the next activity to run
|
|
fn run_setup(&mut self) -> Option<NextActivity> {
|
|
// Prepare activity
|
|
let mut activity: SetupActivity = SetupActivity::new(self.ticks);
|
|
// Get context
|
|
let ctx: Context = match self.context.take() {
|
|
Some(ctx) => ctx,
|
|
None => {
|
|
error!("Failed to start SetupActivity: context is None");
|
|
return None;
|
|
}
|
|
};
|
|
// Create activity
|
|
activity.on_create(ctx);
|
|
loop {
|
|
// Draw activity
|
|
activity.on_draw();
|
|
// Check if activity has terminated
|
|
if let Some(ExitReason::Quit) = activity.will_umount() {
|
|
info!("SetupActivity terminated due to 'Quit'");
|
|
break;
|
|
}
|
|
}
|
|
// Destroy activity
|
|
self.context = activity.on_destroy();
|
|
// This activity always returns to AuthActivity
|
|
Some(NextActivity::Authentication)
|
|
}
|
|
|
|
// -- misc
|
|
|
|
fn init_bookmarks_client(keyring: bool) -> Result<Option<BookmarksClient>, String> {
|
|
crate::support::bookmarks_client(keyring)
|
|
}
|
|
|
|
/// Initialize configuration client
|
|
fn init_config_client() -> Result<ConfigClient, String> {
|
|
// Get config dir
|
|
match environment::init_config_dir() {
|
|
Ok(config_dir) => {
|
|
match config_dir {
|
|
Some(config_dir) => {
|
|
// Get config client paths
|
|
let (config_path, ssh_dir): (PathBuf, PathBuf) =
|
|
environment::get_config_paths(config_dir.as_path());
|
|
match ConfigClient::new(config_path.as_path(), ssh_dir.as_path()) {
|
|
Ok(cli) => Ok(cli),
|
|
Err(err) => Err(format!("Could not read configuration: {err}")),
|
|
}
|
|
}
|
|
None => Err(String::from(
|
|
"Your system doesn't provide a configuration directory",
|
|
)),
|
|
}
|
|
}
|
|
Err(err) => Err(format!(
|
|
"Could not initialize configuration directory: {err}"
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn init_theme_provider() -> ThemeProvider {
|
|
match environment::init_config_dir() {
|
|
Ok(config_dir) => {
|
|
match config_dir {
|
|
Some(config_dir) => {
|
|
// Get config client paths
|
|
let theme_path: PathBuf = environment::get_theme_path(config_dir.as_path());
|
|
match ThemeProvider::new(theme_path.as_path()) {
|
|
Ok(provider) => provider,
|
|
Err(err) => {
|
|
error!(
|
|
"Could not initialize theme provider with file '{}': {}; using theme provider in degraded mode",
|
|
theme_path.display(),
|
|
err
|
|
);
|
|
ThemeProvider::degraded()
|
|
}
|
|
}
|
|
}
|
|
None => {
|
|
error!(
|
|
"This system doesn't provide a configuration directory; using theme provider in degraded mode"
|
|
);
|
|
ThemeProvider::degraded()
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
error!(
|
|
"Could not initialize configuration directory: {}; using theme provider in degraded mode",
|
|
err
|
|
);
|
|
ThemeProvider::degraded()
|
|
}
|
|
}
|
|
}
|
|
}
|