Merge pull request #33 from veeso/logging

Logging
This commit is contained in:
Christian Visintin 2021-05-18 09:03:50 +02:00 committed by GitHub
commit 6cd9657446
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 858 additions and 122 deletions

View file

@ -28,6 +28,11 @@ A clear and concise description of what you expected to happen.
- Protocol used
- Remote server version and name
## Log
Report the snippet of the log file containing the unexpected behaviour.
If there is any information you consider to be confidential, shadow it.
## Additional information
Add any other context about the problem here.

View file

@ -33,6 +33,9 @@ Released on FIXME: ??
- Select a file with `<M>`, the file when selected will have a `*` prepended to its name
- Select all files in the current directory with `<CTRL+A>`
- Read more on manual: [Work on multiple files](docs/man.md#Work-on-multiple-files-)
- **Logging**:
- termscp now writes a log file, useful to debug and to contribute to fix issues.
- Read more on [manual](docs/man.md)
- **File transfer changes**
- *SFTP*
- Added **COPY** command to SFTP (Please note that Copy command is not supported by SFTP natively, so here it just uses the `cp` shell command as it does in SCP).
@ -46,6 +49,7 @@ Released on FIXME: ??
- when you change the protocol in the authentication form and the current port is standard (`< 1024`), the port will be automatically changed to default value for the selected protocol (e.g. current port: `123`, protocol is changes to `FTP`, port becomes `21`)
- Bugfix:
- Fixed wrong text wrap in log box
- Fixed empty bookmark name causing termscp to crash
- Fixed error message not being shown after an upload failure
- Fixed default protocol not being loaded from config
- [Issue 23](https://github.com/veeso/termscp/issues/23): Remove created file if transfer failed or was abrupted

View file

@ -61,6 +61,7 @@ Don't set other labels to your issue, not even priority.
When you open a bug try to be the most precise as possible in describing your issue. I'm not saying you should always be that precise, since sometimes it's very easy for maintainers to understand what you're talking about. Just try to be reasonable to understand sometimes we might not know what you're talking about or we just don't have the technical knowledge you might think.
Please always provide the environment you're working on and consider that we don't provide any support for older version of termscp, at least for those not classified as LTS (if we'll ever have them).
If you can, provide the log file or the snippet involving your issue. You can find in the [user manual](docs/man.md) the location of the log file.
Last but not least: the template I've written must be used. Full stop.
Maintainers will may add additional labels to your issue:
@ -68,7 +69,7 @@ Maintainers will may add additional labels to your issue:
- **duplicate**: the issue is duplicated; the reference to the related issue will be added to your description. Your issue will be closed.
- **priority**: this must be fixed asap
- **sorcery**: it is not possible to find out what's causing your bug, nor is reproducible on our test environments.
- **wontfix**: your bug has a very high ratio between the probability to encounter it and the difficult to fix it, or it just isn't a bug, but a feature.
- **wontfix**: your bug has a very high ratio between the difficulty to fix it and the probability to encounter it, or it just isn't a bug, but a feature.
### Feature requests

31
Cargo.lock generated
View file

@ -1230,6 +1230,17 @@ dependencies = [
"libc",
]
[[package]]
name = "simplelog"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59d0fe306a0ced1c88a58042dc22fc2ddd000982c26d75f6aa09a394547c41e0"
dependencies = [
"chrono",
"log",
"termcolor",
]
[[package]]
name = "smallvec"
version = "1.6.1"
@ -1291,6 +1302,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]]
name = "termscp"
version = "0.5.0"
@ -1307,6 +1327,7 @@ dependencies = [
"hostname",
"keyring",
"lazy_static",
"log",
"magic-crypt",
"path-slash",
"pretty_assertions",
@ -1314,6 +1335,7 @@ dependencies = [
"regex",
"rpassword",
"serde",
"simplelog",
"ssh2",
"tempfile",
"textwrap",
@ -1664,6 +1686,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"

View file

@ -37,10 +37,12 @@ edit = "0.1.3"
getopts = "0.2.21"
hostname = "0.3.1"
lazy_static = "1.4.0"
log = "0.4.14"
magic-crypt = "3.1.7"
rand = "0.8.3"
regex = "1.5.4"
rpassword = "5.0.1"
simplelog = "0.10.0"
ssh2 = "0.9.0"
tempfile = "3.1.0"
textwrap = "0.13.4"

View file

@ -15,6 +15,7 @@
- [File Explorer Format](#file-explorer-format)
- [Text Editor ✏](#text-editor-)
- [How do I configure the text editor 🦥](#how-do-i-configure-the-text-editor-)
- [Logging 🩺](#logging-)
---
@ -25,6 +26,7 @@ termscp can be started with the following options:
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
- `-P, --password <password>` if address is provided, password will be this argument
- `-q, --quiet` Disable logging
- `-v, --version` Print version info
- `-h, --help` Print help page
@ -266,3 +268,33 @@ Just a reminder: **you can edit only textual file**; binary files are not suppor
### How do I configure the text editor 🦥
Text editor is automatically found using this [awesome crate](https://github.com/milkey-mouse/edit), if you want to change the text editor to use, change it in termscp configuration. [Read more](#configuration-)
---
## Logging 🩺
termscp writes a log file for each session, which is written at
- `$HOME/.config/termscp/termscp.log` on Linux/BSD
- `$HOME/Library/Application Support/termscp/termscp.log` on MacOs
- `FOLDERID_RoamingAppData\termscp\termscp.log` on Windows
the log won't be rotated, but will just be truncated after each launch of termscp, so if you want to report an issue and you want to attach your log file, keep in mind to save the log file in a safe place before using termscp again.
The log file always reports in *trace* level, so it is kinda verbose.
I know you might have some questions regarding log files, so I made a kind of a Q/A:
> Is it possible to reduce verbosity?
No. The reason is quite simple: when an issue happens, you must be able to know what's causing it and the only way to do that, is to have the log file with the maximum verbosity level set.
> If trace level is set for logging, is the file going to reach a huge size?
Probably not, unless you never quit termscp, but I think that's likely to happne. A long session may produce up to 10MB of log files (I said a long session), but I think a normal session won't exceed 2MB.
> I don't want logging, can I turn it off?
Yes, you can. Just start termscp with `-q or --quiet` option. You can alias termscp to make it persistent. Remember that logging is used to diagnose issues, so since behind every open source project, there should always be this kind of mutual help, keeping log files might be your way to support the project 😉. I don't want you to feel guilty, but just to say.
> Is logging safe?
If you're concerned about security, the log file doesn't contain any plain password, so don't worry and exposes the same information the sibling file `bookmarks` reports.

View file

@ -69,7 +69,10 @@ impl ActivityManager {
let (config_client, error): (Option<ConfigClient>, Option<String>) =
match Self::init_config_client() {
Ok(cli) => (Some(cli), None),
Err(err) => (None, Some(err)),
Err(err) => {
error!("Failed to initialize config client: {}", err);
(None, Some(err))
}
};
let ctx: Context = Context::new(config_client, error);
Ok(ActivityManager {
@ -131,6 +134,7 @@ impl ActivityManager {
/// 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::default();
// Prepare result
@ -138,7 +142,10 @@ impl ActivityManager {
// Get context
let ctx: Context = match self.context.take() {
Some(ctx) => ctx,
None => return None,
None => {
error!("Failed to start AuthActivity: context is None");
return None;
}
};
// Create activity
activity.on_create(ctx);
@ -149,16 +156,19 @@ impl ActivityManager {
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;
}
@ -170,6 +180,7 @@ impl ActivityManager {
}
// Destroy activity
self.context = activity.on_destroy();
info!("AuthActivity destroyed");
result
}
@ -179,15 +190,22 @@ impl ActivityManager {
/// 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 => return None,
None => {
error!("Failed to start FileTransferActivity: context is None");
return None;
}
};
// If ft params is None, return None
let ft_params: &FileTransferParams = match ctx.ft_params.as_ref() {
Some(ft_params) => &ft_params,
None => return None,
None => {
error!("Failed to start FileTransferActivity: file transfer params is None");
return None;
}
};
// Prepare activity
let protocol: FileTransferProtocol = ft_params.protocol;
@ -195,6 +213,7 @@ impl ActivityManager {
Ok(host) => host,
Err(err) => {
// Set error in context
error!("Failed to initialize localhost: {}", err);
ctx.set_error(format!("Could not initialize localhost: {}", err));
return None;
}
@ -211,11 +230,13 @@ impl ActivityManager {
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;
}
@ -241,7 +262,10 @@ impl ActivityManager {
// Get context
let ctx: Context = match self.context.take() {
Some(ctx) => ctx,
None => return None,
None => {
error!("Failed to start SetupActivity: context is None");
return None;
}
};
// Create activity
activity.on_create(ctx);
@ -250,6 +274,7 @@ impl ActivityManager {
activity.on_draw();
// Check if activity has terminated
if let Some(ExitReason::Quit) = activity.will_umount() {
info!("SetupActivity terminated due to 'Quit'");
break;
}
// Sleep for ticks

View file

@ -50,6 +50,7 @@ impl BookmarkSerializer {
))
}
};
trace!("Serialized new bookmarks data: {}", data);
// Write file
match writable.write_all(data.as_bytes()) {
Ok(_) => Ok(()),
@ -72,9 +73,13 @@ impl BookmarkSerializer {
err.to_string(),
));
}
trace!("Read bookmarks from file: {}", data);
// Deserialize
match toml::de::from_str(data.as_str()) {
Ok(hosts) => Ok(hosts),
Ok(bookmarks) => {
debug!("Read bookmarks from file {:?}", bookmarks);
Ok(bookmarks)
}
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::SyntaxError,
err.to_string(),

View file

@ -50,6 +50,7 @@ impl ConfigSerializer {
))
}
};
trace!("Serialized new configuration data: {}", data);
// Write file
match writable.write_all(data.as_bytes()) {
Ok(_) => Ok(()),
@ -72,9 +73,13 @@ impl ConfigSerializer {
err.to_string(),
));
}
trace!("Read configuration from file: {}", data);
// Deserialize
match toml::de::from_str(data.as_str()) {
Ok(hosts) => Ok(hosts),
Ok(config) => {
debug!("Read config from file {:?}", config);
Ok(config)
}
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::SyntaxError,
err.to_string(),

View file

@ -34,6 +34,7 @@ extern crate regex;
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
use crate::fs::{FsDirectory, FsEntry, FsFile};
use crate::utils::fmt::{fmt_time, shadow_password};
use crate::utils::parser::{parse_datetime, parse_lstime};
// Includes
@ -105,6 +106,7 @@ impl FtpFileTransfer {
lazy_static! {
static ref LS_RE: Regex = Regex::new(r#"^([\-ld])([\-rwxs]{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+(.+)$"#).unwrap();
}
debug!("Parsing LIST (UNIX) line: '{}'", line);
// Apply regex to result
match LS_RE.captures(line) {
// String matches regex
@ -182,6 +184,7 @@ impl FtpFileTransfer {
};
// Check if file_name is '.' or '..'
if file_name.as_str() == "." || file_name.as_str() == ".." {
debug!("File name is {}; ignoring entry", file_name);
return Err(());
}
// Get symlink
@ -236,6 +239,19 @@ impl FtpFileTransfer {
.extension()
.map(|s| String::from(s.to_string_lossy()));
// Return
debug!("Follows LIST line '{}' attributes", line);
debug!("Is directory? {}", is_dir);
debug!("Is symlink? {}", is_symlink);
debug!("name: {}", file_name);
debug!("abs_path: {}", abs_path.display());
debug!("last_change_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
debug!("last_access_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
debug!("creation_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
debug!("symlink: {:?}", symlink);
debug!("user: {:?}", uid);
debug!("group: {:?}", gid);
debug!("unix_pex: {:?}", unix_pex);
debug!("---------------------------------------");
// Push to entries
Ok(match is_dir {
true => FsEntry::Directory(FsDirectory {
@ -287,6 +303,7 @@ impl FtpFileTransfer {
)
.unwrap();
}
debug!("Parsing LIST (DOS) line: '{}'", line);
// Apply regex to result
match DOS_RE.captures(line) {
// String matches regex
@ -324,6 +341,14 @@ impl FtpFileTransfer {
.as_path()
.extension()
.map(|s| String::from(s.to_string_lossy()));
debug!("Follows LIST line '{}' attributes", line);
debug!("Is directory? {}", is_dir);
debug!("name: {}", file_name);
debug!("abs_path: {}", abs_path.display());
debug!("last_change_time: {}", fmt_time(time, "%Y-%m-%dT%H:%M:%S"));
debug!("last_access_time: {}", fmt_time(time, "%Y-%m-%dT%H:%M:%S"));
debug!("creation_time: {}", fmt_time(time, "%Y-%m-%dT%H:%M:%S"));
debug!("---------------------------------------");
// Return entry
Ok(match is_dir {
true => FsEntry::Directory(FsDirectory {
@ -382,17 +407,20 @@ impl FileTransfer for FtpFileTransfer {
password: Option<String>,
) -> Result<Option<String>, FileTransferError> {
// Get stream
info!("Connecting to {}:{}", address, port);
let mut stream: FtpStream = match FtpStream::connect(format!("{}:{}", address, port)) {
Ok(stream) => stream,
Err(err) => {
error!("Failed to connect: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
err.to_string(),
))
));
}
};
// If SSL, open secure session
if self.ftps {
info!("Setting up TLS stream...");
let ctx = match TlsConnector::builder()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
@ -400,19 +428,21 @@ impl FileTransfer for FtpFileTransfer {
{
Ok(tls) => tls,
Err(err) => {
error!("Failed to setup TLS stream: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::SslError,
err.to_string(),
))
));
}
};
stream = match stream.into_secure(ctx, address.as_str()) {
Ok(s) => s,
Err(err) => {
error!("Failed to setup TLS stream: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::SslError,
err.to_string(),
))
));
}
};
}
@ -425,14 +455,22 @@ impl FileTransfer for FtpFileTransfer {
Some(pwd) => pwd,
None => String::new(),
};
info!(
"Signin in with username: {}, password: {}",
username,
shadow_password(password.as_str())
);
if let Err(err) = stream.login(username.as_str(), password.as_str()) {
error!("Login failed: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
err.to_string(),
));
}
debug!("Setting transfer type to Binary");
// Initialize file type
if let Err(err) = stream.transfer_type(FileType::Binary) {
error!("Failed to set transfer type to binary: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
err.to_string(),
@ -440,6 +478,7 @@ impl FileTransfer for FtpFileTransfer {
}
// Set stream
self.stream = Some(stream);
info!("Connection successfully established");
// Return OK
Ok(self.stream.as_ref().unwrap().get_welcome_msg())
}
@ -449,6 +488,7 @@ impl FileTransfer for FtpFileTransfer {
/// Disconnect from the remote server
fn disconnect(&mut self) -> Result<(), FileTransferError> {
info!("Disconnecting from FTP server...");
match &mut self.stream {
Some(stream) => match stream.quit() {
Ok(_) => Ok(()),
@ -475,6 +515,7 @@ impl FileTransfer for FtpFileTransfer {
/// Print working directory
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
info!("PWD");
match &mut self.stream {
Some(stream) => match stream.pwd() {
Ok(path) => Ok(PathBuf::from(path.as_str())),
@ -495,6 +536,7 @@ impl FileTransfer for FtpFileTransfer {
fn change_dir(&mut self, dir: &Path) -> Result<PathBuf, FileTransferError> {
let dir: PathBuf = Self::resolve(dir);
info!("Changing directory to {}", dir.display());
match &mut self.stream {
Some(stream) => match stream.cwd(&dir.as_path().to_string_lossy()) {
Ok(_) => Ok(dir),
@ -514,6 +556,7 @@ impl FileTransfer for FtpFileTransfer {
/// Copy file to destination
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> Result<(), FileTransferError> {
// FTP doesn't support file copy
debug!("COPY issues (will fail, since unsupported)");
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
@ -525,9 +568,11 @@ impl FileTransfer for FtpFileTransfer {
fn list_dir(&mut self, path: &Path) -> Result<Vec<FsEntry>, FileTransferError> {
let dir: PathBuf = Self::resolve(path);
info!("LIST dir {}", dir.display());
match &mut self.stream {
Some(stream) => match stream.list(Some(&dir.as_path().to_string_lossy())) {
Ok(entries) => {
debug!("Got {} lines in LIST result", entries.len());
// Prepare result
let mut result: Vec<FsEntry> = Vec::with_capacity(entries.len());
// Iterate over entries
@ -536,6 +581,11 @@ impl FileTransfer for FtpFileTransfer {
result.push(file);
}
}
debug!(
"{} out of {} were valid entries",
result.len(),
entries.len()
);
Ok(result)
}
Err(err) => Err(FileTransferError::new_ex(
@ -554,6 +604,7 @@ impl FileTransfer for FtpFileTransfer {
/// Make directory
fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> {
let dir: PathBuf = Self::resolve(dir);
info!("MKDIR {}", dir.display());
match &mut self.stream {
Some(stream) => match stream.mkdir(&dir.as_path().to_string_lossy()) {
Ok(_) => Ok(()),
@ -577,9 +628,11 @@ impl FileTransfer for FtpFileTransfer {
FileTransferErrorType::UninitializedSession,
));
}
info!("Removing entry {}", fsentry.get_abs_path().display());
match fsentry {
// Match fs entry...
FsEntry::File(file) => {
debug!("entry is a file; removing file");
// Remove file directly
match self.stream.as_mut().unwrap().rm(file.name.as_ref()) {
Ok(_) => Ok(()),
@ -591,9 +644,11 @@ impl FileTransfer for FtpFileTransfer {
}
FsEntry::Directory(dir) => {
// Get directory files
debug!("Entry is a directory; iterating directory entries");
match self.list_dir(dir.abs_path.as_path()) {
Ok(files) => {
// Remove recursively files
debug!("Removing {} entries from directory...", files.len());
for file in files.iter() {
if let Err(err) = self.remove(&file) {
return Err(FileTransferError::new_ex(
@ -603,6 +658,7 @@ impl FileTransfer for FtpFileTransfer {
}
}
// Once all files in directory have been deleted, remove directory
debug!("Finally removing directory {}", dir.name);
match self.stream.as_mut().unwrap().rmdir(dir.name.as_str()) {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
@ -625,6 +681,11 @@ impl FileTransfer for FtpFileTransfer {
/// Rename file or a directory
fn rename(&mut self, file: &FsEntry, dst: &Path) -> Result<(), FileTransferError> {
let dst: PathBuf = Self::resolve(dst);
info!(
"Renaming {} to {}",
file.get_abs_path().display(),
dst.display()
);
match &mut self.stream {
Some(stream) => {
// Get name
@ -691,6 +752,7 @@ impl FileTransfer for FtpFileTransfer {
file_name: &Path,
) -> Result<Box<dyn Write>, FileTransferError> {
let file_name: PathBuf = Self::resolve(file_name);
info!("Sending file {}", file_name.display());
match &mut self.stream {
Some(stream) => match stream.put_with_stream(&file_name.as_path().to_string_lossy()) {
Ok(writer) => Ok(Box::new(writer)), // NOTE: don't use BufWriter here, since already returned by the library
@ -710,6 +772,7 @@ impl FileTransfer for FtpFileTransfer {
/// Receive file from remote with provided name
/// Returns file and its size
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
info!("Receiving file {}", file.abs_path.display());
match &mut self.stream {
Some(stream) => match stream.get(&file.abs_path.as_path().to_string_lossy()) {
Ok(reader) => Ok(Box::new(reader)), // NOTE: don't use BufReader here, since already returned by the library
@ -732,6 +795,7 @@ impl FileTransfer for FtpFileTransfer {
/// This is necessary for some protocols such as FTP.
/// You must call this method each time you want to finalize the write of the remote file.
fn on_sent(&mut self, writable: Box<dyn Write>) -> Result<(), FileTransferError> {
info!("Finalizing put stream");
match &mut self.stream {
Some(stream) => match stream.finalize_put_stream(writable) {
Ok(_) => Ok(()),
@ -754,6 +818,7 @@ impl FileTransfer for FtpFileTransfer {
/// This mighe be necessary for some protocols.
/// You must call this method each time you want to finalize the read of the remote file.
fn on_recv(&mut self, readable: Box<dyn Read>) -> Result<(), FileTransferError> {
info!("Finalizing get");
match &mut self.stream {
Some(stream) => match stream.finalize_get(readable) {
Ok(_) => Ok(()),

View file

@ -319,14 +319,14 @@ impl std::string::ToString for FileTransferProtocol {
}
impl std::str::FromStr for FileTransferProtocol {
type Err = ();
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_uppercase().as_str() {
"FTP" => Ok(FileTransferProtocol::Ftp(false)),
"FTPS" => Ok(FileTransferProtocol::Ftp(true)),
"SCP" => Ok(FileTransferProtocol::Scp),
"SFTP" => Ok(FileTransferProtocol::Sftp),
_ => Err(()),
_ => Err(s.to_string()),
}
}
}

View file

@ -35,6 +35,7 @@ extern crate ssh2;
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
use crate::fs::{FsDirectory, FsEntry, FsFile};
use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::fmt::{fmt_time, shadow_password};
use crate::utils::parser::parse_lstime;
// Includes
@ -90,6 +91,7 @@ impl ScpFileTransfer {
lazy_static! {
static ref LS_RE: Regex = Regex::new(r#"^([\-ld])([\-rwxs]{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+(.+)$"#).unwrap();
}
debug!("Parsing LS line: '{}'", line);
// Apply regex to result
match LS_RE.captures(line) {
// String matches regex
@ -167,6 +169,7 @@ impl ScpFileTransfer {
};
// Check if file_name is '.' or '..'
if file_name.as_str() == "." || file_name.as_str() == ".." {
debug!("File name is {}; ignoring entry", file_name);
return Err(());
}
// Get symlink; PATH mustn't be equal to filename
@ -200,6 +203,19 @@ impl ScpFileTransfer {
.extension()
.map(|s| String::from(s.to_string_lossy()));
// Return
debug!("Follows LS line '{}' attributes", line);
debug!("Is directory? {}", is_dir);
debug!("Is symlink? {}", is_symlink);
debug!("name: {}", file_name);
debug!("abs_path: {}", abs_path.display());
debug!("last_change_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
debug!("last_access_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
debug!("creation_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
debug!("symlink: {:?}", symlink);
debug!("user: {:?}", uid);
debug!("group: {:?}", gid);
debug!("unix_pex: {:?}", unix_pex);
debug!("---------------------------------------");
// Push to entries
Ok(match is_dir {
true => FsEntry::Directory(FsDirectory {
@ -262,6 +278,7 @@ impl ScpFileTransfer {
fn perform_shell_cmd(&mut self, cmd: &str) -> Result<String, FileTransferError> {
match self.session.as_mut() {
Some(session) => {
debug!("Running command: {}", cmd);
// Create channel
let mut channel: Channel = match session.channel_session() {
Ok(ch) => ch,
@ -285,6 +302,7 @@ impl ScpFileTransfer {
Ok(_) => {
// Wait close
let _ = channel.wait_close();
debug!("Command output: {}", output);
Ok(output)
}
Err(err) => Err(FileTransferError::new_ex(
@ -312,6 +330,7 @@ impl FileTransfer for ScpFileTransfer {
password: Option<String>,
) -> Result<Option<String>, FileTransferError> {
// Setup tcp stream
info!("Connecting to {}:{}", address, port);
let socket_addresses: Vec<SocketAddr> =
match format!("{}:{}", address, port).to_socket_addrs() {
Ok(s) => s.collect(),
@ -325,8 +344,10 @@ impl FileTransfer for ScpFileTransfer {
let mut tcp: Option<TcpStream> = None;
// Try addresses
for socket_addr in socket_addresses.iter() {
debug!("Trying socket address {}", socket_addr);
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
Ok(stream) => {
debug!("{} succeded", socket_addr);
tcp = Some(stream);
break;
}
@ -337,26 +358,30 @@ impl FileTransfer for ScpFileTransfer {
let tcp: TcpStream = match tcp {
Some(t) => t,
None => {
error!("No suitable socket address found; connection timeout");
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
String::from("Connection timeout"),
))
));
}
};
// Create session
let mut session: Session = match Session::new() {
Ok(s) => s,
Err(err) => {
error!("Could not create session: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
err.to_string(),
))
));
}
};
// Set TCP stream
session.set_tcp_stream(tcp);
// Open connection
debug!("Initializing handshake");
if let Err(err) = session.handshake() {
error!("Handshake failed: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
err.to_string(),
@ -372,6 +397,11 @@ impl FileTransfer for ScpFileTransfer {
.resolve(address.as_str(), username.as_str())
{
Some(rsa_key) => {
debug!(
"Authenticating with user {} and RSA key {}",
username,
rsa_key.display()
);
// Authenticate with RSA key
if let Err(err) = session.userauth_pubkey_file(
username.as_str(),
@ -379,6 +409,7 @@ impl FileTransfer for ScpFileTransfer {
rsa_key.as_path(),
password.as_deref(),
) {
error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
err.to_string(),
@ -387,10 +418,16 @@ impl FileTransfer for ScpFileTransfer {
}
None => {
// Proceeed with username/password authentication
debug!(
"Authenticating with username {} and password {}",
username,
shadow_password(password.as_deref().unwrap_or(""))
);
if let Err(err) = session.userauth_password(
username.as_str(),
password.unwrap_or_else(|| String::from("")).as_str(),
) {
error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
err.to_string(),
@ -400,13 +437,22 @@ impl FileTransfer for ScpFileTransfer {
}
// Get banner
let banner: Option<String> = session.banner().map(String::from);
debug!(
"Connection established: {}",
banner.as_deref().unwrap_or("")
);
// Set session
self.session = Some(session);
// Get working directory
debug!("Getting working directory...");
match self.perform_shell_cmd("pwd") {
Ok(output) => self.wrkdir = PathBuf::from(output.as_str().trim()),
Err(err) => return Err(err),
}
info!(
"Connection established; working directory: {}",
self.wrkdir.display()
);
Ok(banner)
}
@ -414,6 +460,7 @@ impl FileTransfer for ScpFileTransfer {
///
/// Disconnect from the remote server
fn disconnect(&mut self) -> Result<(), FileTransferError> {
info!("Disconnecting from remote...");
match self.session.as_ref() {
Some(session) => {
// Disconnect (greet server with 'Mandi' as they do in Friuli)
@ -447,6 +494,7 @@ impl FileTransfer for ScpFileTransfer {
/// Print working directory
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
info!("PWD: {}", self.wrkdir.display());
match self.is_connected() {
true => Ok(self.wrkdir.clone()),
false => Err(FileTransferError::new(
@ -471,6 +519,7 @@ impl FileTransfer for ScpFileTransfer {
Self::resolve(p.as_path())
}
};
info!("Changing working directory to {}", remote_path.display());
// Change directory
match self.perform_shell_cmd_with_path(
p.as_path(),
@ -484,6 +533,7 @@ impl FileTransfer for ScpFileTransfer {
true => {
// Set working directory
self.wrkdir = PathBuf::from(&output.as_str()[1..].trim());
info!("Changed working directory to {}", self.wrkdir.display());
Ok(self.wrkdir.clone())
}
false => Err(FileTransferError::new_ex(
@ -512,6 +562,11 @@ impl FileTransfer for ScpFileTransfer {
match self.is_connected() {
true => {
let dst: PathBuf = Self::resolve(dst);
info!(
"Copying {} to {}",
src.get_abs_path().display(),
dst.display()
);
// Run `cp -rf`
let p: PathBuf = self.wrkdir.clone();
match self.perform_shell_cmd_with_path(
@ -555,6 +610,7 @@ impl FileTransfer for ScpFileTransfer {
match self.is_connected() {
true => {
// Send ls -l to path
info!("Getting file entries in {}", path.display());
let path: PathBuf = Self::resolve(path);
let p: PathBuf = self.wrkdir.clone();
match self.perform_shell_cmd_with_path(
@ -572,6 +628,11 @@ impl FileTransfer for ScpFileTransfer {
entries.push(entry);
}
}
info!(
"Found {} out of {} valid file entries",
entries.len(),
lines.len()
);
Ok(entries)
}
Err(err) => Err(FileTransferError::new_ex(
@ -594,6 +655,7 @@ impl FileTransfer for ScpFileTransfer {
match self.is_connected() {
true => {
let dir: PathBuf = Self::resolve(dir);
info!("Making directory {}", dir.display());
let p: PathBuf = self.wrkdir.clone();
// Mkdir dir && echo 0
match self.perform_shell_cmd_with_path(
@ -632,6 +694,7 @@ impl FileTransfer for ScpFileTransfer {
true => {
// Get path
let path: PathBuf = file.get_abs_path();
info!("Removing file {}", path.display());
let p: PathBuf = self.wrkdir.clone();
match self.perform_shell_cmd_with_path(
p.as_path(),
@ -669,6 +732,7 @@ impl FileTransfer for ScpFileTransfer {
// Get path
let dst: PathBuf = Self::resolve(dst);
let path: PathBuf = file.get_abs_path();
info!("Renaming {} to {}", path.display(), dst.display());
let p: PathBuf = self.wrkdir.clone();
match self.perform_shell_cmd_with_path(
p.as_path(),
@ -717,6 +781,7 @@ impl FileTransfer for ScpFileTransfer {
match self.is_connected() {
true => {
let p: PathBuf = self.wrkdir.clone();
info!("Stat {}", path.display());
// make command; Directories require `-d` option
let cmd: String = match path.to_string_lossy().ends_with('/') {
true => format!("ls -ld \"{}\"", path.display()),
@ -760,6 +825,7 @@ impl FileTransfer for ScpFileTransfer {
match self.is_connected() {
true => {
let p: PathBuf = self.wrkdir.clone();
info!("Executing command {}", cmd);
match self.perform_shell_cmd_with_path(p.as_path(), cmd) {
Ok(output) => Ok(output),
Err(err) => Err(FileTransferError::new_ex(
@ -788,7 +854,13 @@ impl FileTransfer for ScpFileTransfer {
match self.session.as_ref() {
Some(session) => {
let file_name: PathBuf = Self::resolve(file_name);
info!(
"Sending file {} to {}",
local.abs_path.display(),
file_name.display()
);
// Set blocking to true
debug!("blocking channel...");
session.set_blocking(true);
// Calculate file mode
let mode: i32 = match local.unix_pex {
@ -818,6 +890,10 @@ impl FileTransfer for ScpFileTransfer {
Ok(metadata) => metadata.len(),
Err(_) => local.size as u64, // NOTE: fallback to fsentry size
};
debug!(
"File mode {:?}; mtime: {}, atime: {}; file size: {}",
mode, times.0, times.1, file_size
);
// Send file
match session.scp_send(file_name.as_path(), mode, file_size, Some(times)) {
Ok(channel) => Ok(Box::new(BufWriter::with_capacity(65536, channel))),
@ -840,7 +916,9 @@ impl FileTransfer for ScpFileTransfer {
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
match self.session.as_ref() {
Some(session) => {
info!("Receiving file {}", file.abs_path.display());
// Set blocking to true
debug!("Set blocking...");
session.set_blocking(true);
match session.scp_recv(file.abs_path.as_path()) {
Ok(reader) => Ok(Box::new(BufReader::with_capacity(65536, reader.0))),

View file

@ -32,6 +32,7 @@ extern crate ssh2;
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
use crate::fs::{FsDirectory, FsEntry, FsFile};
use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::fmt::{fmt_time, shadow_password};
// Includes
use ssh2::{Channel, FileStat, OpenFlags, OpenType, Session, Sftp};
@ -159,6 +160,19 @@ impl SftpFileTransfer {
}
false => None,
};
debug!("Follows {} attributes", path.display());
debug!("Is directory? {}", metadata.is_dir());
debug!("Is symlink? {}", is_symlink);
debug!("name: {}", file_name);
debug!("abs_path: {}", path.display());
debug!("last_change_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
debug!("last_access_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
debug!("creation_time: {}", fmt_time(mtime, "%Y-%m-%dT%H:%M:%S"));
debug!("symlink: {:?}", symlink);
debug!("user: {:?}", uid);
debug!("group: {:?}", gid);
debug!("unix_pex: {:?}", pex);
debug!("---------------------------------------");
// Is a directory?
match metadata.is_dir() {
true => FsEntry::Directory(FsDirectory {
@ -205,6 +219,7 @@ impl SftpFileTransfer {
match self.session.as_mut() {
Some(session) => {
// Create channel
debug!("Running command: {}", cmd);
let mut channel: Channel = match session.channel_session() {
Ok(ch) => ch,
Err(err) => {
@ -227,6 +242,7 @@ impl SftpFileTransfer {
Ok(_) => {
// Wait close
let _ = channel.wait_close();
debug!("Command output: {}", output);
Ok(output)
}
Err(err) => Err(FileTransferError::new_ex(
@ -254,6 +270,7 @@ impl FileTransfer for SftpFileTransfer {
password: Option<String>,
) -> Result<Option<String>, FileTransferError> {
// Setup tcp stream
info!("Connecting to {}:{}", address, port);
let socket_addresses: Vec<SocketAddr> =
match format!("{}:{}", address, port).to_socket_addrs() {
Ok(s) => s.collect(),
@ -267,6 +284,7 @@ impl FileTransfer for SftpFileTransfer {
let mut tcp: Option<TcpStream> = None;
// Try addresses
for socket_addr in socket_addresses.iter() {
debug!("Trying socket address {}", socket_addr);
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
Ok(stream) => {
tcp = Some(stream);
@ -279,26 +297,30 @@ impl FileTransfer for SftpFileTransfer {
let tcp: TcpStream = match tcp {
Some(t) => t,
None => {
error!("No suitable socket address found; connection timeout");
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
String::from("Connection timeout"),
))
));
}
};
// Create session
let mut session: Session = match Session::new() {
Ok(s) => s,
Err(err) => {
error!("Could not create session: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
err.to_string(),
))
));
}
};
// Set TCP stream
session.set_tcp_stream(tcp);
// Open connection
debug!("Initializing handshake");
if let Err(err) = session.handshake() {
error!("Handshake failed: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
err.to_string(),
@ -314,6 +336,11 @@ impl FileTransfer for SftpFileTransfer {
.resolve(address.as_str(), username.as_str())
{
Some(rsa_key) => {
debug!(
"Authenticating with user {} and RSA key {}",
username,
rsa_key.display()
);
// Authenticate with RSA key
if let Err(err) = session.userauth_pubkey_file(
username.as_str(),
@ -321,6 +348,7 @@ impl FileTransfer for SftpFileTransfer {
rsa_key.as_path(),
password.as_deref(),
) {
error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
err.to_string(),
@ -329,10 +357,16 @@ impl FileTransfer for SftpFileTransfer {
}
None => {
// Proceeed with username/password authentication
debug!(
"Authenticating with username {} and password {}",
username,
shadow_password(password.as_deref().unwrap_or(""))
);
if let Err(err) = session.userauth_password(
username.as_str(),
password.unwrap_or_else(|| String::from("")).as_str(),
) {
error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
err.to_string(),
@ -343,16 +377,19 @@ impl FileTransfer for SftpFileTransfer {
// Set blocking to true
session.set_blocking(true);
// Get Sftp client
debug!("Getting SFTP client...");
let sftp: Sftp = match session.sftp() {
Ok(s) => s,
Err(err) => {
error!("Could not get sftp client: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
err.to_string(),
))
));
}
};
// Get working directory
debug!("Getting working directory...");
self.wrkdir = match sftp.realpath(PathBuf::from(".").as_path()) {
Ok(p) => p,
Err(err) => {
@ -367,6 +404,11 @@ impl FileTransfer for SftpFileTransfer {
self.session = Some(session);
// Set sftp
self.sftp = Some(sftp);
info!(
"Connection established: {}; working directory {}",
banner.as_deref().unwrap_or(""),
self.wrkdir.display()
);
Ok(banner)
}
@ -374,6 +416,7 @@ impl FileTransfer for SftpFileTransfer {
///
/// Disconnect from the remote server
fn disconnect(&mut self) -> Result<(), FileTransferError> {
info!("Disconnecting from remote...");
match self.session.as_ref() {
Some(session) => {
// Disconnect (greet server with 'Mandi' as they do in Friuli)
@ -407,6 +450,7 @@ impl FileTransfer for SftpFileTransfer {
///
/// Print working directory
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
info!("PWD: {}", self.wrkdir.display());
match self.sftp {
Some(_) => Ok(self.wrkdir.clone()),
None => Err(FileTransferError::new(
@ -426,6 +470,7 @@ impl FileTransfer for SftpFileTransfer {
Ok(p) => p,
Err(err) => return Err(err),
};
info!("Changed working directory to {}", self.wrkdir.display());
Ok(self.wrkdir.clone())
}
None => Err(FileTransferError::new(
@ -442,6 +487,11 @@ impl FileTransfer for SftpFileTransfer {
match self.is_connected() {
true => {
let dst: PathBuf = self.get_abs_path(dst);
info!(
"Copying {} to {}",
src.get_abs_path().display(),
dst.display()
);
// Run `cp -rf`
match self.perform_shell_cmd_with_path(
format!(
@ -486,6 +536,7 @@ impl FileTransfer for SftpFileTransfer {
Ok(p) => p,
Err(err) => return Err(err),
};
info!("Getting file entries in {}", path.display());
// Get files
match sftp.readdir(dir.as_path()) {
Err(err) => Err(FileTransferError::new_ex(
@ -517,6 +568,7 @@ impl FileTransfer for SftpFileTransfer {
Some(sftp) => {
// Make directory
let path: PathBuf = self.get_abs_path(PathBuf::from(dir).as_path());
info!("Making directory {}", path.display());
match sftp.mkdir(path.as_path(), 0o775) {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
@ -541,6 +593,7 @@ impl FileTransfer for SftpFileTransfer {
));
}
// Match if file is a file or a directory
info!("Removing file {}", file.get_abs_path().display());
match file {
FsEntry::File(f) => {
// Remove file
@ -554,6 +607,7 @@ impl FileTransfer for SftpFileTransfer {
}
FsEntry::Directory(d) => {
// Remove recursively
debug!("{} is a directory; removing all directory entries", d.name);
// Get directory files
let directory_content: Vec<FsEntry> = match self.list_dir(d.abs_path.as_path()) {
Ok(entries) => entries,
@ -585,6 +639,11 @@ impl FileTransfer for SftpFileTransfer {
FileTransferErrorType::UninitializedSession,
)),
Some(sftp) => {
info!(
"Moving {} to {}",
file.get_abs_path().display(),
dst.display()
);
// Resolve destination path
let abs_dst: PathBuf = self.get_abs_path(dst);
// Get abs path of entry
@ -611,6 +670,7 @@ impl FileTransfer for SftpFileTransfer {
Ok(p) => p,
Err(err) => return Err(err),
};
info!("Stat file {}", dir.display());
// Get file
match sftp.stat(dir.as_path()) {
Ok(metadata) => Ok(self.make_fsentry(dir.as_path(), &metadata)),
@ -630,6 +690,7 @@ impl FileTransfer for SftpFileTransfer {
///
/// Execute a command on remote host
fn exec(&mut self, cmd: &str) -> Result<String, FileTransferError> {
info!("Executing command {}", cmd);
match self.is_connected() {
true => match self.perform_shell_cmd_with_path(cmd) {
Ok(output) => Ok(output),
@ -660,14 +721,20 @@ impl FileTransfer for SftpFileTransfer {
)),
Some(sftp) => {
let remote_path: PathBuf = self.get_abs_path(file_name);
info!(
"Sending file {} to {}",
local.abs_path.display(),
remote_path.display()
);
// Calculate file mode
let mode: i32 = match local.unix_pex {
None => 0o644,
Some((u, g, o)) => ((u as i32) << 6) + ((g as i32) << 3) + (o as i32),
};
debug!("File mode {:?}", mode);
match sftp.open_mode(
remote_path.as_path(),
OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::APPEND | OpenFlags::TRUNCATE,
OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::TRUNCATE,
mode,
OpenType::File,
) {
@ -695,6 +762,7 @@ impl FileTransfer for SftpFileTransfer {
Ok(p) => p,
Err(err) => return Err(err),
};
info!("Receiving file {}", remote_path.display());
// Open remote file
match sftp.open(remote_path.as_path()) {
Ok(file) => Ok(Box::new(BufReader::with_capacity(65536, file))),

View file

@ -125,12 +125,17 @@ impl Localhost {
///
/// Instantiates a new Localhost struct
pub fn new(wrkdir: PathBuf) -> Result<Localhost, HostError> {
debug!("Initializing localhost at {}", wrkdir.display());
let mut host: Localhost = Localhost {
wrkdir,
files: Vec::new(),
};
// Check if dir exists
if !host.file_exists(host.wrkdir.as_path()) {
error!(
"Failed to initialize localhost: {} doesn't exist",
host.wrkdir.display()
);
return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory,
None,
@ -140,8 +145,15 @@ impl Localhost {
// Retrieve files for provided path
host.files = match host.scan_dir(host.wrkdir.as_path()) {
Ok(files) => files,
Err(err) => return Err(err),
Err(err) => {
error!(
"Failed to initialize localhost: could not scan wrkdir: {}",
err
);
return Err(err);
}
};
info!("Localhost initialized with success");
Ok(host)
}
@ -165,8 +177,10 @@ impl Localhost {
/// Change working directory with the new provided directory
pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> {
let new_dir: PathBuf = self.to_abs_path(new_dir);
info!("Changing localhost directory to {}...", new_dir.display());
// Check whether directory exists
if !self.file_exists(new_dir.as_path()) {
error!("Could not change directory: No such file or directory");
return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory,
None,
@ -174,10 +188,11 @@ impl Localhost {
));
}
// Change directory
if std::env::set_current_dir(new_dir.as_path()).is_err() {
if let Err(err) = std::env::set_current_dir(new_dir.as_path()) {
error!("Could not enter directory: {}", err);
return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory,
None,
Some(err),
new_dir.as_path(),
));
}
@ -189,11 +204,13 @@ impl Localhost {
self.files = match self.scan_dir(self.wrkdir.as_path()) {
Ok(files) => files,
Err(err) => {
error!("Could not scan new directory: {}", err);
// Restore directory
self.wrkdir = prev_dir;
return Err(err);
}
};
debug!("Changed directory to {}", self.wrkdir.display());
Ok(self.wrkdir.clone())
}
@ -210,6 +227,7 @@ impl Localhost {
/// ignex: don't report error if directory already exists
pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> {
let dir_path: PathBuf = self.to_abs_path(dir_name);
info!("Making directory {}", dir_path.display());
// If dir already exists, return Error
if dir_path.exists() {
match ignex {
@ -229,13 +247,17 @@ impl Localhost {
if dir_name.is_relative() {
self.files = self.scan_dir(self.wrkdir.as_path())?;
}
info!("Created directory {}", dir_path.display());
Ok(())
}
Err(err) => Err(HostError::new(
HostErrorType::CouldNotCreateFile,
Some(err),
dir_path.as_path(),
)),
Err(err) => {
error!("Could not make directory: {}", err);
Err(HostError::new(
HostErrorType::CouldNotCreateFile,
Some(err),
dir_path.as_path(),
))
}
}
}
@ -246,7 +268,9 @@ impl Localhost {
match entry {
FsEntry::Directory(dir) => {
// If file doesn't exist; return error
debug!("Removing directory {}", dir.abs_path.display());
if !dir.abs_path.as_path().exists() {
error!("Directory doesn't exist");
return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory,
None,
@ -258,18 +282,24 @@ impl Localhost {
Ok(_) => {
// Update dir
self.files = self.scan_dir(self.wrkdir.as_path())?;
info!("Removed directory {}", dir.abs_path.display());
Ok(())
}
Err(err) => Err(HostError::new(
HostErrorType::DeleteFailed,
Some(err),
dir.abs_path.as_path(),
)),
Err(err) => {
error!("Could not remove directory: {}", err);
Err(HostError::new(
HostErrorType::DeleteFailed,
Some(err),
dir.abs_path.as_path(),
))
}
}
}
FsEntry::File(file) => {
// If file doesn't exist; return error
debug!("Removing file {}", file.abs_path.display());
if !file.abs_path.as_path().exists() {
error!("File doesn't exist");
return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory,
None,
@ -281,13 +311,17 @@ impl Localhost {
Ok(_) => {
// Update dir
self.files = self.scan_dir(self.wrkdir.as_path())?;
info!("Removed file {}", file.abs_path.display());
Ok(())
}
Err(err) => Err(HostError::new(
HostErrorType::DeleteFailed,
Some(err),
file.abs_path.as_path(),
)),
Err(err) => {
error!("Could not remove file: {}", err);
Err(HostError::new(
HostErrorType::DeleteFailed,
Some(err),
file.abs_path.as_path(),
))
}
}
}
}
@ -302,13 +336,26 @@ impl Localhost {
Ok(_) => {
// Scan dir
self.files = self.scan_dir(self.wrkdir.as_path())?;
debug!(
"Moved file {} to {}",
entry.get_abs_path().display(),
dst_path.display()
);
Ok(())
}
Err(err) => Err(HostError::new(
HostErrorType::CouldNotCreateFile,
Some(err),
abs_path.as_path(),
)),
Err(err) => {
error!(
"Failed to move {} to {}: {}",
entry.get_abs_path().display(),
dst_path.display(),
err
);
Err(HostError::new(
HostErrorType::CouldNotCreateFile,
Some(err),
abs_path.as_path(),
))
}
}
}
@ -318,6 +365,11 @@ impl Localhost {
pub fn copy(&mut self, entry: &FsEntry, dst: &Path) -> Result<(), HostError> {
// Get absolute path of dest
let dst: PathBuf = self.to_abs_path(dst);
info!(
"Copying file {} to {}",
entry.get_abs_path().display(),
dst.display()
);
// Match entry
match entry {
FsEntry::File(file) => {
@ -333,16 +385,19 @@ impl Localhost {
};
// Copy entry path to dst path
if let Err(err) = std::fs::copy(file.abs_path.as_path(), dst.as_path()) {
error!("Failed to copy file: {}", err);
return Err(HostError::new(
HostErrorType::CouldNotCreateFile,
Some(err),
file.abs_path.as_path(),
));
}
info!("File copied");
}
FsEntry::Directory(dir) => {
// If destination path doesn't exist, create destination
if !dst.exists() {
debug!("Directory {} doesn't exist; creating it", dst.display());
self.mkdir(dst.as_path())?;
}
// Scan dir
@ -386,15 +441,17 @@ impl Localhost {
/// Stat file and create a FsEntry
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
info!("Stating file {}", path.display());
let path: PathBuf = self.to_abs_path(path);
let attr: Metadata = match fs::metadata(path.as_path()) {
Ok(metadata) => metadata,
Err(err) => {
error!("Could not read file metadata: {}", err);
return Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
path.as_path(),
))
));
}
};
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
@ -454,14 +511,16 @@ impl Localhost {
#[cfg(not(tarpaulin_include))]
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
let path: PathBuf = self.to_abs_path(path);
info!("Stating file {}", path.display());
let attr: Metadata = match fs::metadata(path.as_path()) {
Ok(metadata) => metadata,
Err(err) => {
error!("Could not read file metadata: {}", err);
return Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
path.as_path(),
))
));
}
};
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
@ -523,16 +582,23 @@ impl Localhost {
let args: Vec<&str> = cmd.split(' ').collect();
let cmd: &str = args.first().unwrap();
let argv: &[&str] = &args[1..];
info!("Executing command: {} {:?}", cmd, argv);
match std::process::Command::new(cmd).args(argv).output() {
Ok(output) => match std::str::from_utf8(&output.stdout) {
Ok(s) => Ok(s.to_string()),
Ok(s) => {
info!("Command output: {}", s);
Ok(s.to_string())
}
Err(_) => Ok(String::new()),
},
Err(err) => Err(HostError::new(
HostErrorType::ExecutionFailed,
Some(err),
self.wrkdir.as_path(),
)),
Err(err) => {
error!("Failed to run command: {}", err);
Err(HostError::new(
HostErrorType::ExecutionFailed,
Some(err),
self.wrkdir.as_path(),
))
}
}
}
@ -548,19 +614,32 @@ impl Localhost {
let mut mpex = metadata.permissions();
mpex.set_mode(self.mode_to_u32(pex));
match set_permissions(path.as_path(), mpex) {
Ok(_) => Ok(()),
Err(err) => Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
path.as_path(),
)),
Ok(_) => {
info!("Changed mode for {} to {:?}", path.display(), pex);
Ok(())
}
Err(err) => {
error!("Could not change mode for file {}: {}", path.display(), err);
Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
path.as_path(),
))
}
}
}
Err(err) => Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
path.as_path(),
)),
Err(err) => {
error!(
"Chmod failed; could not read metadata for file {}: {}",
path.display(),
err
);
Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
path.as_path(),
))
}
}
}
@ -569,7 +648,9 @@ impl Localhost {
/// Open file for read
pub fn open_file_read(&self, file: &Path) -> Result<File, HostError> {
let file: PathBuf = self.to_abs_path(file);
info!("Opening file {} for read", file.display());
if !self.file_exists(file.as_path()) {
error!("File doesn't exist!");
return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory,
None,
@ -583,11 +664,14 @@ impl Localhost {
.open(file.as_path())
{
Ok(f) => Ok(f),
Err(err) => Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
file.as_path(),
)),
Err(err) => {
error!("Could not open file for read: {}", err);
Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
file.as_path(),
))
}
}
}
@ -596,6 +680,7 @@ impl Localhost {
/// Open file for write
pub fn open_file_write(&self, file: &Path) -> Result<File, HostError> {
let file: PathBuf = self.to_abs_path(file);
info!("Opening file {} for write", file.display());
match OpenOptions::new()
.create(true)
.write(true)
@ -603,18 +688,21 @@ impl Localhost {
.open(file.as_path())
{
Ok(f) => Ok(f),
Err(err) => match self.file_exists(file.as_path()) {
true => Err(HostError::new(
HostErrorType::ReadonlyFile,
Some(err),
file.as_path(),
)),
false => Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
file.as_path(),
)),
},
Err(err) => {
error!("Failed to open file: {}", err);
match self.file_exists(file.as_path()) {
true => Err(HostError::new(
HostErrorType::ReadonlyFile,
Some(err),
file.as_path(),
)),
false => Err(HostError::new(
HostErrorType::FileNotAccessible,
Some(err),
file.as_path(),
)),
}
}
}
}
@ -629,13 +717,15 @@ impl Localhost {
///
/// Get content of the current directory as a list of fs entry
pub fn scan_dir(&self, dir: &Path) -> Result<Vec<FsEntry>, HostError> {
info!("Reading directory {}", dir.display());
match std::fs::read_dir(dir) {
Ok(e) => {
let mut fs_entries: Vec<FsEntry> = Vec::new();
for entry in e.flatten() {
// NOTE: 0.4.1, don't fail if stat for one file fails
if let Ok(entry) = self.stat(entry.path().as_path()) {
fs_entries.push(entry);
match self.stat(entry.path().as_path()) {
Ok(entry) => fs_entries.push(entry),
Err(e) => error!("Failed to stat {}: {}", entry.path().display(), e),
}
}
Ok(fs_entries)

View file

@ -35,6 +35,8 @@ extern crate bitflags;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
#[macro_use]
extern crate magic_crypt;
pub mod activity_manager;

View file

@ -32,6 +32,8 @@ extern crate bitflags;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
#[macro_use]
extern crate magic_crypt;
extern crate rpassword;
@ -55,6 +57,7 @@ mod utils;
// namespaces
use activity_manager::{ActivityManager, NextActivity};
use filetransfer::FileTransferProtocol;
use system::logging;
/// ### print_usage
///
@ -79,10 +82,12 @@ fn main() {
let mut remote_wrkdir: Option<PathBuf> = None;
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp; // Default protocol
let mut ticks: Duration = Duration::from_millis(10);
let mut log_enabled: bool = true;
//Process options
let mut opts = Options::new();
opts.optopt("P", "password", "Provide password from CLI", "<password>");
opts.optopt("T", "ticks", "Set UI ticks; default 10ms", "<ms>");
opts.optflag("q", "quiet", "Disable logging");
opts.optflag("v", "version", "");
opts.optflag("h", "help", "Print this menu");
let matches = match opts.parse(&args[1..]) {
@ -105,6 +110,10 @@ fn main() {
);
std::process::exit(255);
}
// Logging
if matches.opt_present("q") {
log_enabled = false;
}
// Match password
if let Some(passwd) = matches.opt_str("P") {
password = Some(passwd);
@ -155,9 +164,17 @@ fn main() {
Ok(dir) => dir,
Err(_) => PathBuf::from("/"),
};
// Setup logging
if log_enabled {
if let Err(err) = logging::init() {
eprintln!("Failed to initialize logging: {}", err);
}
}
info!("termscp {} started!", TERMSCP_VERSION);
// Initialize client if necessary
let mut start_activity: NextActivity = NextActivity::Authentication;
if address.is_some() {
debug!("User has specified remote options: address: {:?}, port: {:?}, protocol: {:?}, user: {:?}, password: {}", address, port, protocol, username, utils::fmt::shadow_password(password.as_deref().unwrap_or("")));
if password.is_none() {
// Ask password if unspecified
password = match rpassword::read_password_from_tty(Some("Password: ")) {
@ -173,6 +190,10 @@ fn main() {
std::process::exit(255);
}
};
debug!(
"Read password from tty: {}",
utils::fmt::shadow_password(password.as_deref().unwrap_or(""))
);
}
// In this case the first activity will be FileTransfer
start_activity = NextActivity::FileTransfer;
@ -190,7 +211,9 @@ fn main() {
manager.set_filetransfer_params(address, port, protocol, username, password, remote_wrkdir);
}
// Run
info!("Starting activity manager...");
manager.run(start_activity);
info!("termscp terminated");
// Then return
std::process::exit(0);
}

View file

@ -68,9 +68,11 @@ impl BookmarksClient {
) -> Result<BookmarksClient, SerializerError> {
// Create default hosts
let default_hosts: UserHosts = Default::default();
debug!("Setting up bookmarks client...");
// Make a key storage (windows / macos)
#[cfg(any(target_os = "windows", target_os = "macos"))]
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
debug!("Setting up KeyStorage");
let username: String = whoami::username();
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
// Check if keyring storage is supported
@ -79,8 +81,14 @@ impl BookmarksClient {
#[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"),
true => {
debug!("Using KeyringStorage");
(Box::new(storage), app_name)
}
false => {
warn!("KeyringStorage is not supported; using FileStorage");
(Box::new(FileStorage::new(storage_path)), "bookmarks")
}
}
};
// Make a key storage (linux / unix)
@ -90,16 +98,22 @@ impl BookmarksClient {
let app_name: &str = "bookmarks";
#[cfg(test)] // NOTE: when running test, add -test
let app_name: &str = "bookmarks-test";
debug!("Using FileStorage");
(Box::new(FileStorage::new(storage_path)), app_name)
};
// Load key
let key: String = match key_storage.get_key(service_id) {
Ok(k) => k,
Ok(k) => {
debug!("Key loaded with success");
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();
debug!("Key doesn't exist yet or could not be loaded; generated a new key");
if let Err(e) = key_storage.set_key(service_id, key.as_str()) {
error!("Failed to set new key into storage: {}", e);
return Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
format!("Could not write key to storage: {}", e),
@ -109,10 +123,11 @@ impl BookmarksClient {
key
}
_ => {
error!("Failed to get key from storage: {}", e);
return Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
format!("Could not get key from storage: {}", e),
))
));
}
},
};
@ -124,15 +139,19 @@ impl BookmarksClient {
};
// If bookmark file doesn't exist, initialize it
if !bookmarks_file.exists() {
info!("Bookmarks file doesn't exist yet; creating it...");
if let Err(err) = client.write_bookmarks() {
error!("Failed to create bookmarks file: {}", err);
return Err(err);
}
} else {
// Load bookmarks from file
if let Err(err) = client.read_bookmarks() {
error!("Failed to load bookmarks: {}", err);
return Err(err);
}
}
info!("Bookmarks client initialized");
// Load key
Ok(client)
}
@ -152,19 +171,29 @@ impl BookmarksClient {
key: &str,
) -> Option<(String, u16, FileTransferProtocol, String, Option<String>)> {
let entry: &Bookmark = self.hosts.bookmarks.get(key)?;
debug!("Getting bookmark {}", key);
Some((
entry.address.clone(),
entry.port,
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
Ok(proto) => proto,
Err(_) => FileTransferProtocol::Sftp, // Default
Err(err) => {
error!(
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
err
);
FileTransferProtocol::Sftp // Default
}
},
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,
Err(err) => {
error!("Failed to decrypt password for bookmark: {}", err);
None
}
},
None => None,
},
@ -184,9 +213,11 @@ impl BookmarksClient {
password: Option<String>,
) {
if name.is_empty() {
error!("Fatal error; bookmark name is empty");
panic!("Bookmark name can't be empty");
}
// Make bookmark
info!("Added bookmark {} with address {}", name, addr);
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, password);
self.hosts.bookmarks.insert(name, host);
}
@ -196,6 +227,7 @@ impl BookmarksClient {
/// Delete entry from bookmarks
pub fn del_bookmark(&mut self, name: &str) {
let _ = self.hosts.bookmarks.remove(name);
info!("Removed bookmark {}", name);
}
/// ### iter_recents
///
@ -209,13 +241,20 @@ impl BookmarksClient {
/// 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
info!("Getting bookmark {}", key);
let entry: &Bookmark = self.hosts.recents.get(key)?;
Some((
entry.address.clone(),
entry.port,
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
Ok(proto) => proto,
Err(_) => FileTransferProtocol::Sftp, // Default
Err(err) => {
error!(
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
err
);
FileTransferProtocol::Sftp // Default
}
},
entry.username.clone(),
))
@ -236,6 +275,7 @@ impl BookmarksClient {
// Check if duplicated
for recent_host in self.hosts.recents.values() {
if *recent_host == host {
debug!("Discarding recent since duplicated ({})", host.address);
// Don't save duplicates
return;
}
@ -252,6 +292,7 @@ impl BookmarksClient {
// Delete keys starting from the last one
for key in keys.iter() {
let _ = self.hosts.recents.remove(key);
debug!("Removed recent bookmark {}", key);
// If length is < self.recents_size; break
if self.hosts.recents.len() < self.recents_size {
break;
@ -259,6 +300,7 @@ impl BookmarksClient {
}
}
let name: String = fmt_time(SystemTime::now(), "ISO%Y%m%dT%H%M%S");
info!("Saved recent host {} ({})", name, host.address);
self.hosts.recents.insert(name, host);
}
@ -267,6 +309,7 @@ impl BookmarksClient {
/// Delete entry from recents
pub fn del_recent(&mut self, name: &str) {
let _ = self.hosts.recents.remove(name);
info!("Removed recent host {}", name);
}
/// ### write_bookmarks
@ -274,6 +317,7 @@ impl BookmarksClient {
/// Write bookmarks to file
pub fn write_bookmarks(&self) -> Result<(), SerializerError> {
// Open file
debug!("Writing bookmarks");
match OpenOptions::new()
.create(true)
.write(true)
@ -284,10 +328,13 @@ impl BookmarksClient {
let serializer: BookmarkSerializer = BookmarkSerializer {};
serializer.serialize(Box::new(writer), &self.hosts)
}
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
)),
Err(err) => {
error!("Failed to write bookmarks: {}", err);
Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
))
}
}
}
@ -296,6 +343,7 @@ impl BookmarksClient {
/// Read bookmarks from file
fn read_bookmarks(&mut self) -> Result<(), SerializerError> {
// Open bookmarks file for read
debug!("Reading bookmarks");
match OpenOptions::new()
.read(true)
.open(self.bookmarks_file.as_path())
@ -311,10 +359,13 @@ impl BookmarksClient {
Err(err) => Err(err),
}
}
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
)),
Err(err) => {
error!("Failed to read bookmarks: {}", err);
Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
))
}
}
}

View file

@ -58,6 +58,11 @@ impl ConfigClient {
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,
@ -67,6 +72,7 @@ impl ConfigClient {
// 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!(
@ -76,17 +82,22 @@ impl ConfigClient {
),
));
}
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)
}
@ -230,12 +241,18 @@ impl ConfigClient {
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
@ -251,6 +268,7 @@ impl ConfigClient {
/// 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
@ -262,6 +280,7 @@ impl ConfigClient {
};
// 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
@ -310,10 +329,13 @@ impl ConfigClient {
let serializer: ConfigSerializer = ConfigSerializer {};
serializer.serialize(Box::new(writer), &self.config)
}
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
)),
Err(err) => {
error!("Failed to write configuration file: {}", err);
Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
))
}
}
}
@ -337,10 +359,13 @@ impl ConfigClient {
Err(err) => Err(err),
}
}
Err(err) => Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
)),
Err(err) => {
error!("Failed to read configuration: {}", err);
Err(SerializerError::new_ex(
SerializerErrorKind::IoError,
err.to_string(),
))
}
}
}

View file

@ -87,6 +87,15 @@ pub fn get_config_paths(config_dir: &Path) -> (PathBuf, PathBuf) {
(bookmarks_file, keys_dir)
}
/// ### get_log_paths
///
/// Returns the path for the supposed log file
pub fn get_log_paths(config_dir: &Path) -> PathBuf {
let mut log_file: PathBuf = PathBuf::from(config_dir);
log_file.push("termscp.log");
log_file
}
#[cfg(test)]
mod tests {
@ -143,4 +152,12 @@ mod tests {
)
);
}
#[test]
fn test_system_environment_get_log_paths() {
assert_eq!(
get_log_paths(&Path::new("/home/omar/.config/termscp/")),
PathBuf::from("/home/omar/.config/termscp/termscp.log"),
);
}
}

72
src/system/logging.rs Normal file
View file

@ -0,0 +1,72 @@
//! ## Logging
//!
//! `logging` is the module which initializes the logging system for termscp
/**
* 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.
*/
// locals
use crate::system::environment::{get_log_paths, init_config_dir};
use crate::utils::file::open_file;
// ext
use simplelog::{ConfigBuilder, LevelFilter, WriteLogger};
use std::fs::File;
use std::path::PathBuf;
/// ### init
///
/// Initialize logger
pub fn init() -> Result<(), String> {
// Init config dir
let config_dir: PathBuf = match init_config_dir() {
Ok(Some(p)) => p,
Ok(None) => {
return Err(String::from(
"This system doesn't seem to support CONFIG_DIR",
))
}
Err(err) => return Err(err),
};
let log_file_path: PathBuf = get_log_paths(config_dir.as_path());
// Open log file
let file: File = open_file(log_file_path.as_path(), true, true, false)
.map_err(|e| format!("Failed to open file {}: {}", log_file_path.display(), e))?;
// Prepare log config
let config = ConfigBuilder::new()
.set_time_format_str("%Y-%m-%dT%H:%M:%S%z")
.build();
// Make logger
WriteLogger::init(LevelFilter::Trace, config, file)
.map_err(|e| format!("Failed to initialize logger: {}", e))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_system_logging_setup() {
assert!(init().is_ok());
}
}

View file

@ -30,4 +30,5 @@ pub mod bookmarks_client;
pub mod config_client;
pub mod environment;
pub(crate) mod keys;
pub mod logging;
pub mod sshkey_storage;

View file

@ -42,6 +42,7 @@ impl SshKeyStorage {
pub fn storage_from_config(cfg_client: &ConfigClient) -> Self {
let mut hosts: HashMap<String, PathBuf> =
HashMap::with_capacity(cfg_client.iter_ssh_keys().count());
debug!("Setting up SSH key storage");
// Iterate over keys
for key in cfg_client.iter_ssh_keys() {
match cfg_client.get_ssh_key(key) {
@ -52,8 +53,12 @@ impl SshKeyStorage {
}
None => continue,
},
Err(_) => continue,
Err(err) => {
error!("Failed to get SSH key for {}: {}", key, err);
continue;
}
}
info!("Got SSH key for {}", key);
}
// Return storage
SshKeyStorage { hosts }

View file

@ -108,17 +108,24 @@ impl AuthActivity {
///
/// If enabled in configuration, check for updates from Github
fn check_for_updates(&mut self) {
debug!("Check for updates...");
// Check version only if unset in the store
let ctx: &Context = self.context.as_ref().unwrap();
if !ctx.store.isset(STORE_KEY_LATEST_VERSION) {
debug!("Version is not set in storage");
let mut new_version: Option<String> = match ctx.config_client.as_ref() {
Some(client) => {
if client.get_check_for_updates() {
debug!("Check for updates is enabled");
// Send request
match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
Ok(version) => version,
Ok(version) => {
info!("Latest version is: {:?}", version);
version
}
Err(err) => {
// Report error
error!("Failed to get latest version: {}", err);
self.mount_error(
format!("Could not check for new updates: {}", err).as_str(),
);
@ -127,6 +134,7 @@ impl AuthActivity {
}
}
} else {
info!("Check for updates is disabled");
None
}
}
@ -149,6 +157,7 @@ impl Activity for AuthActivity {
/// `on_create` must initialize all the data structures used by the activity
/// Context is taken from activity manager and will be released only when activity is destroyed
fn on_create(&mut self, mut context: Context) {
debug!("Initializing activity");
// Initialize file transfer params
context.ft_params = Some(FileTransferParams::default());
// Set context
@ -156,7 +165,9 @@ impl Activity for AuthActivity {
// Clear terminal
self.context.as_mut().unwrap().clear_screen();
// Put raw mode on enabled
let _ = enable_raw_mode();
if let Err(err) = enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// Init bookmarks client
if self.bookmarks_client.is_none() {
self.init_bookmarks_client();
@ -169,6 +180,7 @@ impl Activity for AuthActivity {
self.check_for_updates();
// Initialize view
self.init();
info!("Activity initialized");
}
/// ### on_draw
@ -213,7 +225,9 @@ impl Activity for AuthActivity {
/// This function finally releases the context
fn on_destroy(&mut self) -> Option<Context> {
// Disable raw mode
let _ = disable_raw_mode();
if let Err(err) = disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
self.context.as_ref()?;
// Clear terminal and return
match self.context.take() {

View file

@ -271,7 +271,9 @@ impl AuthActivity {
Some(Payload::One(Value::Usize(0)))
);
// Save bookmark
self.save_bookmark(bookmark_name, save_pwd);
if !bookmark_name.is_empty() {
self.save_bookmark(bookmark_name, save_pwd);
}
// Umount popup
self.umount_bookmark_save_dialog();
// Reload bookmarks

View file

@ -36,6 +36,12 @@ impl FileTransferActivity {
///
/// Add message to log events
pub(super) fn log(&mut self, level: LogLevel, msg: String) {
// Log to file
match level {
LogLevel::Error => error!("{}", msg),
LogLevel::Info => info!("{}", msg),
LogLevel::Warn => warn!("{}", msg),
}
// Create log record
let record: LogRecord = LogRecord::new(level, msg);
//Check if history overflows the size

View file

@ -273,25 +273,33 @@ impl Activity for FileTransferActivity {
/// `on_create` is the function which must be called to initialize the activity.
/// `on_create` must initialize all the data structures used by the activity
fn on_create(&mut self, context: Context) {
debug!("Initializing activity...");
// Set context
self.context = Some(context);
// Clear terminal
self.context.as_mut().unwrap().clear_screen();
// Put raw mode on enabled
let _ = enable_raw_mode();
if let Err(err) = enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// Set working directory
let pwd: PathBuf = self.host.pwd();
// Get files at current wd
self.local_scan(pwd.as_path());
self.local_mut().wrkdir = pwd;
debug!("Read working directory");
// Configure text editor
self.setup_text_editor();
debug!("Setup text editor");
// init view
self.init();
debug!("Initialized view");
// Verify error state from context
if let Some(err) = self.context.as_mut().unwrap().get_error() {
error!("Fatal error on create: {}", err);
self.mount_fatal(&err);
}
info!("Created FileTransferActivity");
}
/// ### on_draw
@ -308,6 +316,10 @@ impl Activity for FileTransferActivity {
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() {
let params = self.context.as_ref().unwrap().ft_params.as_ref().unwrap();
info!(
"Client is not connected to remote; connecting to {}:{}",
params.address, params.port
);
let msg: String = format!("Connecting to {}:{}...", params.address, params.port);
// Set init state to connecting popup
self.mount_wait(msg.as_str());
@ -341,7 +353,9 @@ impl Activity for FileTransferActivity {
/// This function must be called once before terminating the activity.
fn on_destroy(&mut self) -> Option<Context> {
// Disable raw mode
let _ = disable_raw_mode();
if let Err(err) = disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
// Disconnect client
if self.client.is_connected() {
let _ = self.client.disconnect();

View file

@ -783,7 +783,9 @@ impl FileTransferActivity {
}
}
// Put input mode back to normal
let _ = disable_raw_mode();
if let Err(err) = disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
// Leave alternate mode
if let Some(ctx) = self.context.as_mut() {
ctx.leave_alternate_screen();

View file

@ -111,13 +111,17 @@ impl SetupActivity {
env::set_var("EDITOR", cli.get_text_editor());
let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host);
// Put input mode back to normal
let _ = disable_raw_mode();
if let Err(err) = disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
// Leave alternate mode
if let Some(ctx) = self.context.as_mut() {
ctx.leave_alternate_screen();
}
// Re-enable raw mode
let _ = enable_raw_mode();
if let Err(err) = enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// Write key to file
match edit::edit(placeholder.as_bytes()) {
Ok(rsa_key) => {

View file

@ -88,7 +88,9 @@ impl SetupActivity {
env::set_var("EDITOR", config_cli.get_text_editor());
}
// Prepare terminal
let _ = disable_raw_mode();
if let Err(err) = disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
// Leave alternate mode
ctx.leave_alternate_screen();
// Get result
@ -121,7 +123,9 @@ impl SetupActivity {
// Enter alternate mode
ctx.enter_alternate_screen();
// Re-enable raw mode
let _ = enable_raw_mode();
if let Err(err) = enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// Return result
result
}

View file

@ -110,7 +110,9 @@ impl Activity for SetupActivity {
// Clear terminal
self.context.as_mut().unwrap().clear_screen();
// Put raw mode on enabled
let _ = enable_raw_mode();
if let Err(err) = enable_raw_mode() {
error!("Failed to enter raw mode: {}", err);
}
// Init view
self.init_setup();
// Verify error state from context
@ -161,7 +163,9 @@ impl Activity for SetupActivity {
/// This function finally releases the context
fn on_destroy(&mut self) -> Option<Context> {
// Disable raw mode
let _ = disable_raw_mode();
if let Err(err) = disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
self.context.as_ref()?;
// Clear terminal and return
match self.context.take() {

View file

@ -104,29 +104,38 @@ impl Context {
///
/// Enter alternate screen (gui window)
pub fn enter_alternate_screen(&mut self) {
let _ = execute!(
match execute!(
self.terminal.backend_mut(),
EnterAlternateScreen,
DisableMouseCapture
);
) {
Err(err) => error!("Failed to enter alternate screen: {}", err),
Ok(_) => info!("Entered alternate screen"),
}
}
/// ### leave_alternate_screen
///
/// Go back to normal screen (gui window)
pub fn leave_alternate_screen(&mut self) {
let _ = execute!(
match execute!(
self.terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
);
) {
Err(err) => error!("Failed to leave alternate screen: {}", err),
Ok(_) => info!("Left alternate screen"),
}
}
/// ### clear_screen
///
/// Clear terminal screen
pub fn clear_screen(&mut self) {
let _ = self.terminal.clear();
match self.terminal.clear() {
Err(err) => error!("Failed to clear screen: {}", err),
Ok(_) => info!("Cleared screen"),
}
}
}

57
src/utils/file.rs Normal file
View file

@ -0,0 +1,57 @@
//! ## File
//!
//! `file` is the module which exposes file related utilities
/**
* 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.
*/
use std::fs::File;
use std::fs::OpenOptions;
use std::io;
use std::path::Path;
/// ### open_file
///
/// Open file provided as parameter
pub fn open_file<P>(filename: P, create: bool, write: bool, append: bool) -> io::Result<File>
where
P: AsRef<Path>,
{
OpenOptions::new()
.create(create)
.write(write)
.append(append)
.truncate(!append)
.open(filename)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_utils_file_open() {
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
assert!(open_file(tmpfile.path(), true, true, true).is_ok());
}
}

View file

@ -152,6 +152,13 @@ pub fn fmt_path_elide(p: &Path, width: usize) -> String {
}
}
/// ### shadow_password
///
/// Return a string with the same length of input string, but each character is replaced by '*'
pub fn shadow_password(s: &str) -> String {
(0..s.len()).map(|_| '*').collect()
}
#[cfg(test)]
mod tests {
@ -219,4 +226,9 @@ mod tests {
let p: &Path = &Path::new("/develop/pippo/foo/bar");
assert_eq!(fmt_path_elide(p, 16), String::from("/develop/.../foo/bar"));
}
#[test]
fn test_utils_fmt_shadow_password() {
assert_eq!(shadow_password("foobar"), String::from("******"));
}
}

View file

@ -27,6 +27,7 @@
*/
// modules
pub mod crypto;
pub mod file;
pub mod fmt;
pub mod git;
pub mod parser;