From 5d4b255e264b65ceccaef194a204aa44bc847a8c Mon Sep 17 00:00:00 2001 From: ChristianVisintin Date: Tue, 1 Dec 2020 17:40:51 +0100 Subject: [PATCH] Working on ftp transfer --- src/filetransfer/ftp_transfer.rs | 317 +++++++++++++++++++++++++++++++ src/filetransfer/mod.rs | 6 +- 2 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 src/filetransfer/ftp_transfer.rs diff --git a/src/filetransfer/ftp_transfer.rs b/src/filetransfer/ftp_transfer.rs new file mode 100644 index 0000000..6ac4f74 --- /dev/null +++ b/src/filetransfer/ftp_transfer.rs @@ -0,0 +1,317 @@ +//! ## Ftp_transfer +//! +//! `ftp_transfer` is the module which provides the implementation for the FTP/FTPS 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 ftp; +extern crate regex; + +use super::{FileTransfer, FileTransferError, FileTransferErrorType}; +use crate::fs::{FsDirectory, FsEntry, FsFile}; + +// Includes +use ftp::{FtpStream, FtpError}; +use ftp::openssl::ssl::{SslContext, SslMethod}; +use regex::Regex; +use std::io::{Read, Seek, Write}; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime}; + +/// ## FtpFileTransfer +/// +/// Ftp file transfer struct +pub struct FtpFileTransfer { + stream: Option, + ftps: bool, +} + +impl FtpFileTransfer { + + /// ### new + /// + /// Instantiates a new `FtpFileTransfer` + pub fn new(ftps: bool) -> FtpFileTransfer { + FtpFileTransfer { + stream: None, + ftps: ftps, + } + } + +} + +impl FileTransfer for FtpFileTransfer { + + /// ### connect + /// + /// Connect to the remote server + + fn connect( + &mut self, + address: String, + port: u16, + username: Option, + password: Option, + ) -> Result<(), FileTransferError> { + // Get stream + let mut stream: FtpStream = match FtpStream::connect(format!("{}:{}", address, port)) { + Ok(stream) => stream, + Err(err) => return Err(FileTransferError::new_ex(FileTransferErrorType::ConnectionError, format!("{}", err))), + }; + // If SSL, open secure session + if self.ftps { + let mut ctx = SslContext::builder(SslMethod::tls()).unwrap(); + let ctx = ctx.build(); + if let Err(err) = stream.into_secure(ctx) { + return Err(FileTransferError::new_ex(FileTransferErrorType::SslError, format!("{}", err))) + } + } + // If username / password... + if let Some(username) = username { + if let Err(err) = stream.login(username.as_str(), match password { + Some(pwd) => pwd.as_ref(), + None => "" + }) { + return Err(FileTransferError::new_ex(FileTransferErrorType::AuthenticationFailed, format!("{}", err))) + } + } + // Set stream + self.stream = Some(stream); + // Return OK + Ok(()) + } + + /// ### disconnect + /// + /// Disconnect from the remote server + + fn disconnect(&mut self) -> Result<(), FileTransferError> { + match self.stream { + Some(stream) => match stream.quit() { + Ok(_) => Ok(()), + Err(err) => Err(FileTransferError::new_ex(FileTransferErrorType::ConnectionError, format!("{}", err))) + }, + None => Err(FileTransferError::new(FileTransferErrorType::UninitializedSession)) + } + } + + /// ### is_connected + /// + /// Indicates whether the client is connected to remote + fn is_connected(&self) -> bool { + match self.stream { + Some(_) => true, + None => false, + } + } + + /// ### pwd + /// + /// Print working directory + + fn pwd(&self) -> Result { + match self.stream { + Some(stream) => match stream.pwd() { + Ok(path) => Ok(PathBuf::from(path.as_str())), + Err(err) => Err(FileTransferError::new_ex(FileTransferErrorType::ConnectionError, format!("{}", err))) + }, + None => Err(FileTransferError::new(FileTransferErrorType::UninitializedSession)) + } + } + + /// ### change_dir + /// + /// Change working directory + + fn change_dir(&mut self, dir: &Path) -> Result { + match self.stream { + Some(stream) => match stream.cwd(&dir.as_os_str().to_string_lossy()) { + Ok(_) => Ok(PathBuf::from(dir)), + Err(err) => Err(FileTransferError::new_ex(FileTransferErrorType::ConnectionError, format!("{}", err))), + } + None => Err(FileTransferError::new(FileTransferErrorType::UninitializedSession)) + } + } + + /// ### list_dir + /// + /// List directory entries + + fn list_dir(&self, path: &Path) -> Result, FileTransferError> { + // Prepare list regex + // NOTE: about this damn regex + 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(); + } + match self.stream { + Some(stream) => match stream.list(Some(&path.as_os_str().to_string_lossy())) { + Ok(entries) => { + // Prepare result + let mut result: Vec = Vec::with_capacity(entries.len()); + // Iterate over entries + for entry in entries.iter() { + // Apply regex to result + if let Some(metadata) = LS_RE.captures(entry) { // String matches regex + // NOTE: metadata fmt: (regex, file_type, permissions, link_count, uid, gid, filesize, mtime, filename) + // Expected 7 + 1 (8) values: + 1 cause regex is repeated at 0 + if metadata.len() < 8 { + continue + } + // Collect metadata + // Get if is directory and if is symlink + let (is_dir, is_symlink): (bool, bool) = match metadata.get(1).unwrap().as_str() { + "-" => (false, false), + "l" => (false, true), + "d" => (true, false), + _ => continue, // Ignore special files + }; + // Check string length (unix pex) + if metadata.get(2).unwrap().as_str().len() < 9 { + continue; + } + // Get unix pex + let unix_pex: (u8, u8, u8) = { + let owner_pex: u8 = { + let mut count: u8 = 0; + for (i, c) in metadata.get(2).unwrap().as_str()[0..3].chars().enumerate() { + match c { + '-' => {}, + _ => count = count + match i { + 0 => 4, + 1 => 2, + 2 => 1, + _ => 0, + } + } + } + count + }; + let group_pex: u8 = { + let mut count: u8 = 0; + for (i, c) in metadata.get(2).unwrap().as_str()[3..6].chars().enumerate() { + match c { + '-' => {}, + _ => count = count + match i { + 0 => 4, + 1 => 2, + 2 => 1, + _ => 0, + } + } + } + count + }; + let others_pex: u8 = { + let mut count: u8 = 0; + for (i, c) in metadata.get(2).unwrap().as_str()[6..9].chars().enumerate() { + match c { + '-' => {}, + _ => count = count + match i { + 0 => 4, + 1 => 2, + 2 => 1, + _ => 0, + } + } + } + count + }; + (owner_pex, group_pex, others_pex) + }; + // Parse mtime and convert to SystemTime + // Return + } + } + Ok(result) + } + Err(err) => Err(FileTransferError::new_ex(FileTransferErrorType::DirStatFailed, format!("{}", err))), + } + None => Err(FileTransferError::new(FileTransferErrorType::UninitializedSession)) + } + } + + /// ### mkdir + /// + /// Make directory + fn mkdir(&self, dir: &Path) -> Result<(), FileTransferError> { + match self.stream { + Some(stream) => {}, + None => Err(FileTransferError::new(FileTransferErrorType::UninitializedSession)) + } + } + + /// ### remove + /// + /// Remove a file or a directory + fn remove(&self, file: &FsEntry) -> Result<(), FileTransferError> { + match self.stream { + Some(stream) => {}, + None => Err(FileTransferError::new(FileTransferErrorType::UninitializedSession)) + } + } + + /// ### rename + /// + /// Rename file or a directory + fn rename(&self, file: &FsEntry, dst: &Path) -> Result<(), FileTransferError> { + match self.stream { + Some(stream) => {}, + None => Err(FileTransferError::new(FileTransferErrorType::UninitializedSession)) + } + } + + /// ### stat + /// + /// Stat file and return FsEntry + fn stat(&self, path: &Path) -> Result { + match self.stream { + Some(stream) => Err(FileTransferError::new(FileTransferErrorType::UnsupportedFeature)), + None => Err(FileTransferError::new(FileTransferErrorType::UninitializedSession)) + } + } + + /// ### 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 + /// Returns file and its size + fn send_file(&self, file_name: &Path) -> Result, FileTransferError> { + match self.stream { + Some(stream) => {}, + None => Err(FileTransferError::new(FileTransferErrorType::UninitializedSession)) + } + } + + /// ### recv_file + /// + /// Receive file from remote with provided name + /// Returns file and its size + fn recv_file(&self, file_name: &Path) -> Result<(Box, usize), FileTransferError> { + match self.stream { + Some(stream) => {}, + None => Err(FileTransferError::new(FileTransferErrorType::UninitializedSession)) + } + } + +} diff --git a/src/filetransfer/mod.rs b/src/filetransfer/mod.rs index 8265dec..9c42764 100644 --- a/src/filetransfer/mod.rs +++ b/src/filetransfer/mod.rs @@ -29,7 +29,7 @@ use std::path::{Path, PathBuf}; use crate::fs::FsEntry; // Transfers -//pub mod ftp_transfer; +pub mod ftp_transfer; pub mod sftp_transfer; /// ## FileTransferProtocol @@ -59,6 +59,7 @@ pub enum FileTransferErrorType { AuthenticationFailed, BadAddress, ConnectionError, + SslError, DirStatFailed, FileCreateDenied, FileReadonly, @@ -66,6 +67,7 @@ pub enum FileTransferErrorType { NoSuchFileOrDirectory, ProtocolError, UninitializedSession, + UnsupportedFeature, } impl FileTransferError { @@ -105,7 +107,9 @@ impl std::fmt::Display for FileTransferError { String::from("No such file or directory") } FileTransferErrorType::ProtocolError => String::from("Protocol error"), + FileTransferErrorType::SslError => String::from("SSL error"), FileTransferErrorType::UninitializedSession => String::from("Uninitialized session"), + FileTransferErrorType::UnsupportedFeature => String::from("Unsupported feature"), }; match &self.msg { Some(msg) => write!(f, "{} ({})", err, msg),