diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 65ac3a9..578c27f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f0d5c7..3e69da8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,9 @@ Released on FIXME: ?? - Select a file with ``, the file when selected will have a `*` prepended to its name - Select all files in the current directory with `` - 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2eea9e..5db7290 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index ae1a15a..b351f1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index e9d44be..efe5e2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/docs/man.md b/docs/man.md index d348ffa..7599ab5 100644 --- a/docs/man.md +++ b/docs/man.md @@ -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 ` 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. diff --git a/src/activity_manager.rs b/src/activity_manager.rs index 7e8f797..98585c1 100644 --- a/src/activity_manager.rs +++ b/src/activity_manager.rs @@ -69,7 +69,10 @@ impl ActivityManager { let (config_client, error): (Option, Option) = 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 { + 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 { + 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 diff --git a/src/bookmarks/serializer.rs b/src/bookmarks/serializer.rs index 6a5b0be..6d7d597 100644 --- a/src/bookmarks/serializer.rs +++ b/src/bookmarks/serializer.rs @@ -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(), diff --git a/src/config/serializer.rs b/src/config/serializer.rs index dd5670d..2375243 100644 --- a/src/config/serializer.rs +++ b/src/config/serializer.rs @@ -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(), diff --git a/src/filetransfer/ftp_transfer.rs b/src/filetransfer/ftp_transfer.rs index 1d7d6e7..4b69880 100644 --- a/src/filetransfer/ftp_transfer.rs +++ b/src/filetransfer/ftp_transfer.rs @@ -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, ) -> Result, 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 { + 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 { 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, 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 = 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, 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, 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) -> 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) -> Result<(), FileTransferError> { + info!("Finalizing get"); match &mut self.stream { Some(stream) => match stream.finalize_get(readable) { Ok(_) => Ok(()), diff --git a/src/filetransfer/mod.rs b/src/filetransfer/mod.rs index 028e2ff..fff23c4 100644 --- a/src/filetransfer/mod.rs +++ b/src/filetransfer/mod.rs @@ -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 { 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()), } } } diff --git a/src/filetransfer/scp_transfer.rs b/src/filetransfer/scp_transfer.rs index 8a7011f..df17b47 100644 --- a/src/filetransfer/scp_transfer.rs +++ b/src/filetransfer/scp_transfer.rs @@ -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 { 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, ) -> Result, FileTransferError> { // Setup tcp stream + info!("Connecting to {}:{}", address, port); let socket_addresses: Vec = match format!("{}:{}", address, port).to_socket_addrs() { Ok(s) => s.collect(), @@ -325,8 +344,10 @@ impl FileTransfer for ScpFileTransfer { let mut tcp: Option = 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 = 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 { + 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, 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))), diff --git a/src/filetransfer/sftp_transfer.rs b/src/filetransfer/sftp_transfer.rs index 7e2b62f..d63f1c4 100644 --- a/src/filetransfer/sftp_transfer.rs +++ b/src/filetransfer/sftp_transfer.rs @@ -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, ) -> Result, FileTransferError> { // Setup tcp stream + info!("Connecting to {}:{}", address, port); let socket_addresses: Vec = match format!("{}:{}", address, port).to_socket_addrs() { Ok(s) => s.collect(), @@ -267,6 +284,7 @@ impl FileTransfer for SftpFileTransfer { let mut tcp: Option = 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 { + 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 = 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 { + 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))), diff --git a/src/host/mod.rs b/src/host/mod.rs index dcb6162..f3215bc 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -125,12 +125,17 @@ impl Localhost { /// /// Instantiates a new Localhost struct pub fn new(wrkdir: PathBuf) -> Result { + 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 { 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 { + 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 { 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 { 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 { 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, HostError> { + info!("Reading directory {}", dir.display()); match std::fs::read_dir(dir) { Ok(e) => { let mut fs_entries: Vec = 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) diff --git a/src/lib.rs b/src/lib.rs index de77513..3847ecd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/main.rs b/src/main.rs index e24f4fe..83e44d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 = 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", ""); opts.optopt("T", "ticks", "Set UI ticks; default 10ms", ""); + 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); } diff --git a/src/system/bookmarks_client.rs b/src/system/bookmarks_client.rs index 530aea8..fdff0c9 100644 --- a/src/system/bookmarks_client.rs +++ b/src/system/bookmarks_client.rs @@ -68,9 +68,11 @@ impl BookmarksClient { ) -> Result { // 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, &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)> { 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, ) { 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(), + )) + } } } diff --git a/src/system/config_client.rs b/src/system/config_client.rs index d85decf..b0919b8 100644 --- a/src/system/config_client.rs +++ b/src/system/config_client.rs @@ -58,6 +58,11 @@ impl ConfigClient { pub fn new(config_path: &Path, ssh_key_dir: &Path) -> Result { // 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(), + )) + } } } diff --git a/src/system/environment.rs b/src/system/environment.rs index 619c679..c17cc99 100644 --- a/src/system/environment.rs +++ b/src/system/environment.rs @@ -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"), + ); + } } diff --git a/src/system/logging.rs b/src/system/logging.rs new file mode 100644 index 0000000..bea7d7d --- /dev/null +++ b/src/system/logging.rs @@ -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()); + } +} diff --git a/src/system/mod.rs b/src/system/mod.rs index f48b603..7ff9694 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -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; diff --git a/src/system/sshkey_storage.rs b/src/system/sshkey_storage.rs index fe5b4a1..ea6cfb4 100644 --- a/src/system/sshkey_storage.rs +++ b/src/system/sshkey_storage.rs @@ -42,6 +42,7 @@ impl SshKeyStorage { pub fn storage_from_config(cfg_client: &ConfigClient) -> Self { let mut hosts: HashMap = 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 } diff --git a/src/ui/activities/auth/mod.rs b/src/ui/activities/auth/mod.rs index 790cd1f..3b48d5e 100644 --- a/src/ui/activities/auth/mod.rs +++ b/src/ui/activities/auth/mod.rs @@ -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 = 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 { // 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() { diff --git a/src/ui/activities/auth/update.rs b/src/ui/activities/auth/update.rs index 4afe7de..cc8f17d 100644 --- a/src/ui/activities/auth/update.rs +++ b/src/ui/activities/auth/update.rs @@ -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 diff --git a/src/ui/activities/filetransfer/misc.rs b/src/ui/activities/filetransfer/misc.rs index ec174ac..b13e47e 100644 --- a/src/ui/activities/filetransfer/misc.rs +++ b/src/ui/activities/filetransfer/misc.rs @@ -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 diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 9b324e0..5327ba7 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -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 { // 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(); diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index ad733db..5257531 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -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(); diff --git a/src/ui/activities/setup/actions.rs b/src/ui/activities/setup/actions.rs index 34af461..d965d42 100644 --- a/src/ui/activities/setup/actions.rs +++ b/src/ui/activities/setup/actions.rs @@ -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) => { diff --git a/src/ui/activities/setup/config.rs b/src/ui/activities/setup/config.rs index 5a235df..25c1b0d 100644 --- a/src/ui/activities/setup/config.rs +++ b/src/ui/activities/setup/config.rs @@ -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 } diff --git a/src/ui/activities/setup/mod.rs b/src/ui/activities/setup/mod.rs index 5eed9f8..15966fa 100644 --- a/src/ui/activities/setup/mod.rs +++ b/src/ui/activities/setup/mod.rs @@ -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 { // 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() { diff --git a/src/ui/context.rs b/src/ui/context.rs index a0fcfa7..e3d44ef 100644 --- a/src/ui/context.rs +++ b/src/ui/context.rs @@ -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"), + } } } diff --git a/src/utils/file.rs b/src/utils/file.rs new file mode 100644 index 0000000..46c3018 --- /dev/null +++ b/src/utils/file.rs @@ -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

(filename: P, create: bool, write: bool, append: bool) -> io::Result +where + P: AsRef, +{ + 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()); + } +} diff --git a/src/utils/fmt.rs b/src/utils/fmt.rs index 202cdf3..8f3a210 100644 --- a/src/utils/fmt.rs +++ b/src/utils/fmt.rs @@ -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("******")); + } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index d3028c6..e11bd67 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -27,6 +27,7 @@ */ // modules pub mod crypto; +pub mod file; pub mod fmt; pub mod git; pub mod parser;