From eee1a720e6f435ad71b14ffa5e2c240c6eca23dd Mon Sep 17 00:00:00 2001 From: ChristianVisintin Date: Sun, 15 Nov 2020 14:50:46 +0100 Subject: [PATCH] SFTP transfer --- Cargo.lock | 76 +++++ Cargo.toml | 1 + src/filetransfer/mod.rs | 29 +- src/filetransfer/sftp_transfer.rs | 467 ++++++++++++++++++++++++++++++ 4 files changed, 562 insertions(+), 11 deletions(-) create mode 100644 src/filetransfer/sftp_transfer.rs diff --git a/Cargo.lock b/Cargo.lock index 5d0f821..251ee2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,11 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + [[package]] name = "bitflags" version = "1.2.1" @@ -12,6 +18,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +[[package]] +name = "cc" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1770ced377336a88a67c473594ccc14eca6f4559217c34f64aac8f83d641b40" + [[package]] name = "cfg-if" version = "0.1.10" @@ -124,6 +136,32 @@ version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +[[package]] +name = "libssh2-sys" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca46220853ba1c512fc82826d0834d87b06bcd3c2a42241b7de72f3d2fe17056" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.3.4" @@ -183,6 +221,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "openssl-sys" +version = "0.9.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.10.2" @@ -233,6 +284,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + [[package]] name = "ppv-lite86" version = "0.2.10" @@ -339,6 +396,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "ssh2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba56d741dab9a295bcb131ebfbe57f8fea2e1b7ae203e9184f5d7648213e4460" +dependencies = [ + "bitflags", + "libc", + "libssh2-sys", + "parking_lot 0.10.2", +] + [[package]] name = "tempfile" version = "3.1.0" @@ -359,6 +428,7 @@ version = "0.1.0" dependencies = [ "crossterm 0.18.2", "getopts", + "ssh2", "tempfile", "tui", "users", @@ -399,6 +469,12 @@ dependencies = [ "log", ] +[[package]] +name = "vcpkg" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 1bfb083..438a4d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ readme = "README.md" [dependencies] crossterm = "0.18.2" getopts = "0.2.21" +ssh2 = "0.8.2" tui = { version = "0.12.0", features = ["crossterm"], default-features = false } users = "0.11.0" diff --git a/src/filetransfer/mod.rs b/src/filetransfer/mod.rs index 326e962..4d11f07 100644 --- a/src/filetransfer/mod.rs +++ b/src/filetransfer/mod.rs @@ -23,18 +23,23 @@ * */ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::fs::File; use crate::fs::FsEntry; +// Transfers +pub mod sftp_transfer; + +// Types +type ProgressCallback = fn(bytes_written: usize, size: usize); + /// ## FileTransferProtocol /// /// This enum defines the different transfer protocol available in TermSCP #[derive(PartialEq, Clone)] pub enum FileTransferProtocol { - Scp, Sftp, Ftps, } @@ -43,15 +48,17 @@ pub enum FileTransferProtocol { /// /// FileTransferError defines the possible errors available for a file transfer -#[derive(PartialEq, Clone)] pub enum FileTransferError { - ConnectionError, - BadAddress, AuthenticationFailed, - NoSuchFileOrDirectory, + BadAddress, + ConnectionError, DirStatFailed, + FileCreateDenied, FileReadonly, - DownloadError, + IoErr(std::io::Error), + NoSuchFileOrDirectory, + ProtocolError, + UninitializedSession, UnknownError, } @@ -83,13 +90,13 @@ pub trait FileTransfer { /// /// Change working directory - fn change_dir(&mut self, dir: PathBuf) -> Result; + fn change_dir(&mut self, dir: &Path) -> Result; /// ### list_dir /// /// List directory entries - fn list_dir(&self) -> Result, FileTransferError>; + fn list_dir(&self, path: &Path) -> Result, FileTransferError>; /// ### mkdir /// @@ -106,11 +113,11 @@ pub trait FileTransfer { /// Send file to remote /// File name is referred to the name of the file as it will be saved /// Data contains the file data - fn send_file(&self, file_name: PathBuf, file: File) -> Result<(), FileTransferError>; + fn send_file(&self, file_name: &Path, file: &mut File, prog_cb: Option) -> Result<(), FileTransferError>; /// ### recv_file /// /// Receive file from remote with provided name - fn recv_file(&self, file_name: PathBuf) -> Result, FileTransferError>; + fn recv_file(&self, file_name: &Path, dest_file: &mut File, prog_cb: Option) -> Result<(), FileTransferError>; } diff --git a/src/filetransfer/sftp_transfer.rs b/src/filetransfer/sftp_transfer.rs new file mode 100644 index 0000000..febb2a5 --- /dev/null +++ b/src/filetransfer/sftp_transfer.rs @@ -0,0 +1,467 @@ +//! ## SFTP_Transfer +//! +//! `sftp_transfer` is the module which provides the implementation for the SFTP file transfer + +/* +* +* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com +* +* This file is part of "TermSCP" +* +* TermSCP is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* TermSCP is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with TermSCP. If not, see . +* +*/ + +// Dependencies +extern crate ssh2; + +// Locals +use super::{FileTransfer, FileTransferError, ProgressCallback}; +use crate::fs::{FsDirectory, FsEntry, FsFile}; + +// Includes +use ssh2::{Session, Sftp}; +use std::fs::File; +use std::io::{Read, Seek, Write}; +use std::net::TcpStream; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime}; + +/// ## SftpFileTransfer +/// +/// SFTP file transfer structure +pub struct SftpFileTransfer { + session: Option, + sftp: Option, + wrkdir: PathBuf, +} + +impl SftpFileTransfer { + /// ### new + /// + /// Instantiates a new SftpFileTransfer + pub fn new() -> SftpFileTransfer { + SftpFileTransfer { + session: None, + sftp: None, + wrkdir: PathBuf::from("~"), + } + } + + /// ### get_abs_path + /// + /// Get absolute path from path argument and check if it exists + fn get_remote_path(&self, p: &Path) -> Result { + match p.is_relative() { + true => { + let mut root: PathBuf = self.wrkdir.clone(); + root.push(p); + match self.sftp.as_ref().unwrap().realpath(root.as_path()) { + Ok(p) => Ok(p), + Err(_) => Err(FileTransferError::NoSuchFileOrDirectory), + } + } + false => Ok(PathBuf::from(p)), + } + } + + /// ### get_abs_path + /// + /// Returns absolute path on remote, but without errors + fn get_abs_path(&self, p: &Path) -> PathBuf { + match p.is_relative() { + true => { + let mut root: PathBuf = self.wrkdir.clone(); + root.push(p); + match self.sftp.as_ref().unwrap().realpath(root.as_path()) { + Ok(p) => p, + Err(_) => root, + } + } + false => PathBuf::from(p), + } + } +} + +impl FileTransfer for SftpFileTransfer { + /// ### connect + /// + /// Connect to the remote server + fn connect( + &mut self, + address: String, + port: usize, + username: Option, + password: Option, + ) -> Result<(), FileTransferError> { + // Setup tcp stream + let tcp: TcpStream = match TcpStream::connect(format!("{}:{}", address, port)) { + Ok(stream) => stream, + Err(_) => return Err(FileTransferError::BadAddress), + }; + // Create session + let mut session: Session = match Session::new() { + Ok(s) => s, + Err(_) => return Err(FileTransferError::ConnectionError), + }; + // Set TCP stream + session.set_tcp_stream(tcp); + // Open connection + if let Err(_) = session.handshake() { + return Err(FileTransferError::ConnectionError); + } + // Try authentication + if let Err(_) = session.userauth_password( + username.unwrap_or(String::from("")).as_str(), + password.unwrap_or(String::from("")).as_str(), + ) { + return Err(FileTransferError::AuthenticationFailed); + } + // Set blocking to true + session.set_blocking(true); + // Get Sftp client + let sftp: Sftp = match session.sftp() { + Ok(s) => s, + Err(_) => return Err(FileTransferError::ProtocolError), + }; + // Get working directory + self.wrkdir = match sftp.realpath(PathBuf::from(".").as_path()) { + Ok(p) => p, + Err(_) => return Err(FileTransferError::ProtocolError), + }; + // Set session + self.session = Some(session); + // Set sftp + self.sftp = Some(sftp); + Ok(()) + } + + /// ### disconnect + /// + /// Disconnect from the remote server + fn disconnect(&mut self) -> Result<(), FileTransferError> { + match self.session.as_ref() { + Some(session) => { + // Disconnect (greet server with 'Mandi' as they do in Friuli) + match session.disconnect(None, "Mandi!", None) { + Ok(()) => { + // Set session and sftp to none + self.session = None; + self.sftp = None; + Ok(()) + } + Err(_) => Err(FileTransferError::ConnectionError), + } + } + None => Err(FileTransferError::UninitializedSession), + } + } + + /// ### pwd + /// + /// Print working directory + fn pwd(&self) -> Result { + match self.sftp { + Some(_) => Ok(self.wrkdir.clone()), + None => Err(FileTransferError::UninitializedSession), + } + } + + /// ### change_dir + /// + /// Change working directory + fn change_dir(&mut self, dir: &Path) -> Result { + match self.sftp.as_ref() { + Some(_) => { + // Change working directory + self.wrkdir = match self.get_remote_path(dir) { + Ok(p) => p, + Err(err) => return Err(err), + }; + Ok(self.wrkdir.clone()) + } + None => Err(FileTransferError::UninitializedSession), + } + } + + /// ### list_dir + /// + /// List directory entries + fn list_dir(&self, path: &Path) -> Result, FileTransferError> { + match self.sftp.as_ref() { + Some(sftp) => { + // Get path + let dir: PathBuf = match self.get_remote_path(path) { + Ok(p) => p, + Err(err) => return Err(err), + }; + // Get files + match sftp.readdir(dir.as_path()) { + Err(_) => return Err(FileTransferError::DirStatFailed), + Ok(files) => { + // Allocate vector + let mut entries: Vec = Vec::with_capacity(files.len()); + // Iterate over files + for (path, metadata) in files { + // Get common parameters + let file_name: String = + String::from(path.file_name().unwrap().to_str().unwrap_or("")); + let file_type: Option = match path.extension() { + Some(ext) => Some(String::from(ext.to_str().unwrap_or(""))), + None => None, + }; + let uid: Option = metadata.uid; + let gid: Option = metadata.gid; + let pex: Option<(u8, u8, u8)> = match metadata.perm { + Some(perms) => Some(( + ((perms >> 6) & 0x7) as u8, + ((perms >> 3) & 0x7) as u8, + (perms & 0x7) as u8, + )), + None => None, + }; + let size: u64 = metadata.size.unwrap_or(0); + let mut atime: SystemTime = SystemTime::UNIX_EPOCH; + atime = atime + .checked_add(Duration::from_secs(metadata.atime.unwrap_or(0))) + .unwrap_or(SystemTime::UNIX_EPOCH); + let mut mtime: SystemTime = SystemTime::UNIX_EPOCH; + mtime = mtime + .checked_add(Duration::from_secs(metadata.mtime.unwrap_or(0))) + .unwrap_or(SystemTime::UNIX_EPOCH); + // Check if symlink + let is_symlink: bool = metadata.file_type().is_symlink(); + let symlink: Option = match is_symlink { + true => { + // Read symlink + match sftp.readlink(path.as_path()) { + Ok(p) => Some(p), + Err(_) => None, + } + } + false => None, + }; + // Is a directory? + match metadata.is_dir() { + true => { + entries.push(FsEntry::Directory(FsDirectory { + name: file_name, + abs_path: path.clone(), + last_change_time: mtime, + last_access_time: atime, + creation_time: SystemTime::UNIX_EPOCH, + readonly: false, + symlink: symlink, + user: uid, + group: gid, + unix_pex: pex, + })); + } + false => { + entries.push(FsEntry::File(FsFile { + name: file_name, + abs_path: path.clone(), + size: size as usize, + ftype: file_type, + last_change_time: mtime, + last_access_time: atime, + creation_time: SystemTime::UNIX_EPOCH, + readonly: false, + symlink: symlink, + user: uid, + group: gid, + unix_pex: pex, + })); + } + } + } + Ok(entries) + } + } + } + None => Err(FileTransferError::UninitializedSession), + } + } + + /// ### mkdir + /// + /// Make directory + fn mkdir(&self, dir: String) -> Result<(), FileTransferError> { + match self.sftp.as_ref() { + Some(sftp) => { + // Make directory + let path: PathBuf = self.get_abs_path(PathBuf::from(dir).as_path()); + match sftp.mkdir(path.as_path(), 0o755) { + Ok(_) => Ok(()), + Err(_) => Err(FileTransferError::FileCreateDenied), + } + } + None => Err(FileTransferError::UninitializedSession), + } + } + + /// ### remove + /// + /// Remove a file or a directory + fn remove(&self, file: FsEntry) -> Result<(), FileTransferError> { + match self.sftp.as_ref() { + None => Err(FileTransferError::UninitializedSession), + Some(sftp) => { + // Match if file is a file or a directory + match file { + FsEntry::File(f) => { + // Remove file + match sftp.unlink(f.abs_path.as_path()) { + Ok(_) => Ok(()), + Err(_) => Err(FileTransferError::FileReadonly), + } + } + FsEntry::Directory(d) => { + // Remove recursively + // Get directory files + match self.list_dir(d.abs_path.as_path()) { + Ok(entries) => { + // Remove each entry + for entry in entries { + if let Err(err) = self.remove(entry) { + return Err(err); + } + } + // Finally remove directory + match sftp.rmdir(d.abs_path.as_path()) { + Ok(_) => Ok(()), + Err(_) => Err(FileTransferError::FileReadonly), + } + } + Err(err) => return Err(err), + } + } + } + } + } + } + + /// ### send_file + /// + /// Send file to remote + /// File name is referred to the name of the file as it will be saved + /// Data contains the file data + fn send_file( + &self, + file_name: &Path, + file: &mut File, + prog_cb: Option, + ) -> Result<(), FileTransferError> { + match self.sftp.as_ref() { + None => Err(FileTransferError::UninitializedSession), + Some(sftp) => { + let remote_path: PathBuf = self.get_abs_path(file_name); + // Get file size + let file_size: usize = file.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize; + // rewind + if let Err(err) = file.seek(std::io::SeekFrom::Start(0)) { + return Err(FileTransferError::IoErr(err)); + }; + // Open remote file + match sftp.create(remote_path.as_path()) { + Ok(mut rhnd) => { + let mut total_bytes_written: usize = 0; + loop { + // Read till you can + let mut buffer: [u8; 8192] = [0; 8192]; + match file.read(&mut buffer) { + Ok(bytes_read) => { + total_bytes_written += bytes_read; + if bytes_read == 0 { + break; + } else { + // Write bytes + if let Err(err) = rhnd.write(&buffer) { + return Err(FileTransferError::IoErr(err)); + } + // Call callback + if let Some(cb) = prog_cb { + cb(total_bytes_written, file_size); + } + } + } + Err(err) => return Err(FileTransferError::IoErr(err)), + } + } + Ok(()) + } + Err(_) => return Err(FileTransferError::FileCreateDenied), + } + } + } + } + + /// ### recv_file + /// + /// Receive file from remote with provided name + fn recv_file( + &self, + file_name: &Path, + dest_file: &mut File, + prog_cb: Option, + ) -> Result<(), FileTransferError> { + match self.sftp.as_ref() { + None => Err(FileTransferError::UninitializedSession), + Some(sftp) => { + // Get remote file name + let remote_path: PathBuf = match self.get_remote_path(file_name) { + Ok(p) => p, + Err(err) => return Err(err), + }; + // Open remote file + match sftp.open(remote_path.as_path()) { + Ok(mut rhnd) => { + let file_size: usize = + rhnd.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize; + // rewind + if let Err(err) = rhnd.seek(std::io::SeekFrom::Start(0)) { + return Err(FileTransferError::IoErr(err)); + }; + // Write local file + let mut total_bytes_written: usize = 0; + loop { + // Read till you can + let mut buffer: [u8; 8192] = [0; 8192]; + match rhnd.read(&mut buffer) { + Ok(bytes_read) => { + total_bytes_written += bytes_read; + if bytes_read == 0 { + break; + } else { + // Write bytes + if let Err(err) = dest_file.write(&buffer) { + return Err(FileTransferError::IoErr(err)); + } + // Call callback + if let Some(cb) = prog_cb { + cb(total_bytes_written, file_size); + } + } + } + Err(err) => return Err(FileTransferError::IoErr(err)), + } + } + Ok(()) + } + Err(_) => return Err(FileTransferError::NoSuchFileOrDirectory), + } + } + } + } +}