diff --git a/.travis.yml b/.travis.yml index 6312c7b9d..a851a6350 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,9 @@ script: | travis-cargo build && travis-cargo test -- --features secure && - travis-cargo --only stable doc + travis-cargo --only stable doc && + rustup component add rustfmt && + cargo fmt --all -- --check after_success: # upload the documentation from the build with stable (automatically only actually diff --git a/Cargo.toml b/Cargo.toml index fda3ca271..6042dd5b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,17 +19,17 @@ path = "src/lib.rs" [features] # Enable support of FTPS which requires openssl -secure = ["openssl"] +secure = ["native-tls"] # Add debug output (to STDOUT) of commands sent to the server # and lines read from the server debug_print = [] [dependencies] -lazy_static = "0.1" -regex = "0.1" -chrono = "0.2" +lazy_static = "1.4.0" +regex = "1.4.2" +chrono = "0.4.19" -[dependencies.openssl] -version = "0.9" +[dependencies.native-tls] +version = "^0.2" optional = true diff --git a/README.md b/README.md index a34e5e28b..15ca14eda 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -rust-ftp -================ +# rust-ftp FTP client for Rust @@ -11,15 +10,26 @@ FTP client for Rust [Documentation](https://docs.rs/ftp/) +--- + +- [rust-ftp](#rust-ftp) + - [Installation](#installation) + - [Usage](#usage) + - [License](#license) + - [Contribution](#contribution) + - [Development environment](#development-environment) + ## Installation -FTPS support is disabled by default. To enable it `secure` should be activated in `Cargo.toml`. +FTPS support is achieved through [rust-native-tls](https://github.com/sfackler/rust-native-tls) and is disabled by default. To enable it `secure` should be activated in `Cargo.toml`. + ```toml [dependencies] ftp = { version = "", features = ["secure"] } ``` ## Usage + ```rust extern crate ftp; @@ -34,7 +44,7 @@ fn main() { // Get the current directory that the client will be reading from and writing to. println!("Current directory: {}", ftp_stream.pwd().unwrap()); - + // Change into a new directory, relative to the one we are currently in. let _ = ftp_stream.cwd("test_data").unwrap(); @@ -57,8 +67,8 @@ fn main() { Licensed under either of - * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) - * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) +- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) at your option. @@ -96,6 +106,7 @@ cargo test ``` The following commands can be useful: + ```bash # List running containers of ftp-server image # (to include stopped containers use -a option) diff --git a/examples/connecting.rs b/examples/connecting.rs index 345b174b3..5da70f6e1 100644 --- a/examples/connecting.rs +++ b/examples/connecting.rs @@ -1,8 +1,8 @@ extern crate ftp; -use std::str; +use ftp::{FtpError, FtpStream}; use std::io::Cursor; -use ftp::{FtpStream, FtpError}; +use std::str; fn test_ftp(addr: &str, user: &str, pass: &str) -> Result<(), FtpError> { let mut ftp_stream = FtpStream::connect((addr, 21)).unwrap(); diff --git a/src/data_stream.rs b/src/data_stream.rs index 10eb732f6..b78af4adf 100644 --- a/src/data_stream.rs +++ b/src/data_stream.rs @@ -1,15 +1,14 @@ -use std::io::{Read, Write, Result}; -use std::net::TcpStream; #[cfg(feature = "secure")] -use openssl::ssl::SslStream; - +use native_tls::TlsStream; +use std::io::{Read, Result, Write}; +use std::net::TcpStream; /// Data Stream used for communications #[derive(Debug)] pub enum DataStream { Tcp(TcpStream), #[cfg(feature = "secure")] - Ssl(SslStream), + Ssl(TlsStream), } #[cfg(feature = "secure")] @@ -26,7 +25,7 @@ impl DataStream { pub fn is_ssl(&self) -> bool { match self { &DataStream::Ssl(_) => true, - _ => false + _ => false, } } } @@ -52,7 +51,6 @@ impl Read for DataStream { } } - impl Write for DataStream { fn write(&mut self, buf: &[u8]) -> Result { match self { diff --git a/src/ftp.rs b/src/ftp.rs index 6f87e3347..170db72c5 100644 --- a/src/ftp.rs +++ b/src/ftp.rs @@ -1,21 +1,19 @@ //! FTP module. -use std::borrow::Cow; -use std::io::{Read, BufRead, BufReader, BufWriter, Cursor, Write, copy}; -#[cfg(feature = "secure")] -use std::error::Error; -use std::net::{TcpStream, SocketAddr}; -use std::string::String; -use std::str::FromStr; -use std::net::ToSocketAddrs; -use regex::Regex; -use chrono::{DateTime, UTC}; -use chrono::offset::TimeZone; -#[cfg(feature = "secure")] -use openssl::ssl::{ SslContext, Ssl }; use super::data_stream::DataStream; use super::status; use super::types::{FileType, FtpError, Line, Result}; +use chrono::offset::TimeZone; +use chrono::{DateTime, Utc}; +#[cfg(feature = "secure")] +use native_tls::TlsConnector; +use regex::Regex; +use std::borrow::Cow; +use std::io::{copy, BufRead, BufReader, BufWriter, Cursor, Read, Write}; +use std::net::ToSocketAddrs; +use std::net::{SocketAddr, TcpStream}; +use std::str::FromStr; +use std::string::String; lazy_static! { // This regex extracts IP and Port details from PASV command response. @@ -33,8 +31,11 @@ lazy_static! { #[derive(Debug)] pub struct FtpStream { reader: BufReader, + welcome_msg: Option, + #[cfg(feature = "secure")] + tls_ctx: Option, #[cfg(feature = "secure")] - ssl_cfg: Option, + domain: Option, } impl FtpStream { @@ -46,12 +47,18 @@ impl FtpStream { .and_then(|stream| { let mut ftp_stream = FtpStream { reader: BufReader::new(DataStream::Tcp(stream)), + welcome_msg: None, }; - ftp_stream.read_response(status::READY) - .map(|_| ftp_stream) + match ftp_stream.read_response(status::READY) { + Ok(line) => { + ftp_stream.welcome_msg = Some(line.1); + Ok(ftp_stream) + } + Err(err) => Err(err), + } }) } - + /// Creates an FTP Stream. #[cfg(feature = "secure")] pub fn connect(addr: A) -> Result { @@ -60,13 +67,20 @@ impl FtpStream { .and_then(|stream| { let mut ftp_stream = FtpStream { reader: BufReader::new(DataStream::Tcp(stream)), - ssl_cfg: None, + welcome_msg: None, + tls_ctx: None, + domain: None, }; - ftp_stream.read_response(status::READY) - .map(|_| ftp_stream) + match ftp_stream.read_response(status::READY) { + Ok(line) => { + ftp_stream.welcome_msg = Some(line.1); + Ok(ftp_stream) + } + Err(err) => Err(err), + } }) } - + /// Switch to a secure mode if possible, using a provided SSL configuration. /// This method does nothing if the connect is already secured. /// @@ -79,36 +93,37 @@ impl FtpStream { /// ```rust,no_run /// use std::path::Path; /// use ftp::FtpStream; - /// use ftp::openssl::ssl::{ SslContext, SslMethod }; + /// use ftp::native_tls::{TlsConnector, TlsStream}; /// - /// // Create an SslContext with a custom cert. - /// let mut ctx = SslContext::builder(SslMethod::tls()).unwrap(); - /// let _ = ctx.set_ca_file(Path::new("/path/to/a/cert.pem")).unwrap(); - /// let ctx = ctx.build(); + /// // Create a TlsConnector + /// // NOTE: For custom options see + /// let mut ctx = TlsConnector::new().unwrap(); /// let mut ftp_stream = FtpStream::connect("127.0.0.1:21").unwrap(); - /// let mut ftp_stream = ftp_stream.into_secure(ctx).unwrap(); + /// let mut ftp_stream = ftp_stream.into_secure(ctx, "localhost").unwrap(); /// ``` #[cfg(feature = "secure")] - pub fn into_secure(mut self, ssl_context: SslContext) -> Result { + pub fn into_secure(mut self, tls_connector: TlsConnector, domain: &str) -> Result { // Ask the server to start securing data. - try!(self.write_str("AUTH TLS\r\n")); - try!(self.read_response(status::AUTH_OK)); - let ssl_cfg = try!(Ssl::new(&ssl_context).map_err(|e| FtpError::SecureError(e.description().to_owned()))); - let stream = try!(ssl_cfg.connect(self.reader.into_inner().into_tcp_stream()).map_err(|e| FtpError::SecureError(e.description().to_owned()))); - + self.write_str("AUTH TLS\r\n")?; + self.read_response(status::AUTH_OK)?; + let stream = tls_connector + .connect(domain, self.reader.into_inner().into_tcp_stream()) + .map_err(|e| FtpError::SecureError(format!("{}", e)))?; let mut secured_ftp_tream = FtpStream { reader: BufReader::new(DataStream::Ssl(stream)), - ssl_cfg: Some(ssl_context) + tls_ctx: Some(tls_connector), + domain: Some(String::from(domain)), + welcome_msg: self.welcome_msg.clone(), }; // Set protection buffer size - try!(secured_ftp_tream.write_str("PBSZ 0\r\n")); - try!(secured_ftp_tream.read_response(status::COMMAND_OK)); + secured_ftp_tream.write_str("PBSZ 0\r\n")?; + secured_ftp_tream.read_response(status::COMMAND_OK)?; // Change the level of data protectio to Private - try!(secured_ftp_tream.write_str("PROT P\r\n")); - try!(secured_ftp_tream.read_response(status::COMMAND_OK)); + secured_ftp_tream.write_str("PROT P\r\n")?; + secured_ftp_tream.read_response(status::COMMAND_OK)?; Ok(secured_ftp_tream) } - + /// Switch to insecure mode. If the connection is already /// insecure does nothing. /// @@ -118,14 +133,12 @@ impl FtpStream { /// use std::path::Path; /// use ftp::FtpStream; /// - /// use ftp::openssl::ssl::{ SslContext, SslMethod }; + /// use ftp::native_tls::{TlsConnector, TlsStream}; /// - /// // Create an SslContext with a custom cert. - /// let mut ctx = SslContext::builder(SslMethod::tls()).unwrap(); - /// let _ = ctx.set_ca_file(Path::new("/path/to/a/cert.pem")).unwrap(); - /// let ctx = ctx.build(); + /// // Create an TlsConnector + /// let mut ctx = TlsConnector::new().unwrap(); /// let mut ftp_stream = FtpStream::connect("127.0.0.1:21").unwrap(); - /// let mut ftp_stream = ftp_stream.into_secure(ctx).unwrap(); + /// let mut ftp_stream = ftp_stream.into_secure(ctx, "localhost").unwrap(); /// // Do all secret things /// // Switch back to the insecure mode /// let mut ftp_stream = ftp_stream.into_insecure().unwrap(); @@ -135,22 +148,30 @@ impl FtpStream { #[cfg(feature = "secure")] pub fn into_insecure(mut self) -> Result { // Ask the server to stop securing data - try!(self.write_str("CCC\r\n")); - try!(self.read_response(status::COMMAND_OK)); + self.write_str("CCC\r\n")?; + self.read_response(status::COMMAND_OK)?; let plain_ftp_stream = FtpStream { reader: BufReader::new(DataStream::Tcp(self.reader.into_inner().into_tcp_stream())), - ssl_cfg: None, + tls_ctx: None, + domain: None, + welcome_msg: self.welcome_msg.clone(), }; Ok(plain_ftp_stream) } - + + /// ### get_welcome_msg + /// + /// Returns welcome message retrieved from server (if available) + pub fn get_welcome_msg(&self) -> Option { + self.welcome_msg.clone() + } + /// Execute command which send data back in a separate stream #[cfg(not(feature = "secure"))] fn data_command(&mut self, cmd: &str) -> Result { self.pasv() .and_then(|addr| self.write_str(cmd).map(|_| addr)) - .and_then(|addr| TcpStream::connect(addr) - .map_err(|e| FtpError::ConnectionError(e))) + .and_then(|addr| TcpStream::connect(addr).map_err(|e| FtpError::ConnectionError(e))) .map(|stream| DataStream::Tcp(stream)) } @@ -160,15 +181,12 @@ impl FtpStream { self.pasv() .and_then(|addr| self.write_str(cmd).map(|_| addr)) .and_then(|addr| TcpStream::connect(addr).map_err(|e| FtpError::ConnectionError(e))) - .and_then(|stream| { - match self.ssl_cfg { - Some(ref ssl) => { - Ssl::new(ssl).unwrap().connect(stream) - .map(|stream| DataStream::Ssl(stream)) - .map_err(|e| FtpError::SecureError(e.description().to_owned())) - }, - None => Ok(DataStream::Tcp(stream)) - } + .and_then(|stream| match self.tls_ctx { + Some(ref tls_ctx) => tls_ctx + .connect(self.domain.as_ref().unwrap(), stream) + .map(|stream| DataStream::Ssl(stream)) + .map_err(|e| FtpError::SecureError(format!("{}", e))), + None => Ok(DataStream::Tcp(stream)), }) } @@ -177,10 +195,12 @@ impl FtpStream { /// Example: /// ```no_run /// use std::net::TcpStream; + /// use ftp::FtpStream; + /// use std::time::Duration; /// /// let stream = FtpStream::connect("127.0.0.1:21") /// .expect("Couldn't connect to the server..."); - /// stream.get_ref().set_read_timeout(Duration::from_secs(10)) + /// stream.get_ref().set_read_timeout(Some(Duration::from_secs(10))) /// .expect("set_read_timeout call failed"); /// ``` pub fn get_ref(&self) -> &TcpStream { @@ -189,12 +209,12 @@ impl FtpStream { /// Log in to the FTP server. pub fn login(&mut self, user: &str, password: &str) -> Result<()> { - try!(self.write_str(format!("USER {}\r\n", user))); + self.write_str(format!("USER {}\r\n", user))?; self.read_response_in(&[status::LOGGED_IN, status::NEED_PASSWORD]) .and_then(|Line(code, _)| { if code == status::NEED_PASSWORD { - try!(self.write_str(format!("PASS {}\r\n", password))); - try!(self.read_response(status::LOGGED_IN)); + self.write_str(format!("PASS {}\r\n", password))?; + self.read_response(status::LOGGED_IN)?; } Ok(()) }) @@ -202,68 +222,73 @@ impl FtpStream { /// Change the current directory to the path specified. pub fn cwd(&mut self, path: &str) -> Result<()> { - try!(self.write_str(format!("CWD {}\r\n", path))); - self.read_response(status::REQUESTED_FILE_ACTION_OK).map(|_| ()) + self.write_str(format!("CWD {}\r\n", path))?; + self.read_response(status::REQUESTED_FILE_ACTION_OK) + .map(|_| ()) } /// Move the current directory to the parent directory. pub fn cdup(&mut self) -> Result<()> { - try!(self.write_str("CDUP\r\n")); - self.read_response_in(&[status::COMMAND_OK, status::REQUESTED_FILE_ACTION_OK]).map(|_| ()) + self.write_str("CDUP\r\n")?; + self.read_response_in(&[status::COMMAND_OK, status::REQUESTED_FILE_ACTION_OK]) + .map(|_| ()) } /// Gets the current directory pub fn pwd(&mut self) -> Result { - try!(self.write_str("PWD\r\n")); + self.write_str("PWD\r\n")?; self.read_response(status::PATH_CREATED) - .and_then(|Line(_, content)| { - match (content.find('"'), content.rfind('"')) { + .and_then( + |Line(_, content)| match (content.find('"'), content.rfind('"')) { (Some(begin), Some(end)) if begin < end => { - Ok(content[begin + 1 .. end].to_string()) - }, + Ok(content[begin + 1..end].to_string()) + } _ => { let cause = format!("Invalid PWD Response: {}", content); Err(FtpError::InvalidResponse(cause)) } - } - }) + }, + ) } /// This does nothing. This is usually just used to keep the connection open. pub fn noop(&mut self) -> Result<()> { - try!(self.write_str("NOOP\r\n")); + self.write_str("NOOP\r\n")?; self.read_response(status::COMMAND_OK).map(|_| ()) } /// This creates a new directory on the server. pub fn mkdir(&mut self, pathname: &str) -> Result<()> { - try!(self.write_str(format!("MKD {}\r\n", pathname))); + self.write_str(format!("MKD {}\r\n", pathname))?; self.read_response(status::PATH_CREATED).map(|_| ()) } /// Runs the PASV command. fn pasv(&mut self) -> Result { - try!(self.write_str("PASV\r\n")); + self.write_str("PASV\r\n")?; // PASV response format : 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). - let Line(_, line) = try!(self.read_response(status::PASSIVE_MODE)); - PORT_RE.captures(&line) - .ok_or(FtpError::InvalidResponse(format!("Invalid PASV response: {}", line))) + let Line(_, line) = self.read_response(status::PASSIVE_MODE)?; + PORT_RE + .captures(&line) + .ok_or(FtpError::InvalidResponse(format!( + "Invalid PASV response: {}", + line + ))) .and_then(|caps| { // If the regex matches we can be sure groups contains numbers let (oct1, oct2, oct3, oct4) = ( caps[1].parse::().unwrap(), caps[2].parse::().unwrap(), caps[3].parse::().unwrap(), - caps[4].parse::().unwrap() + caps[4].parse::().unwrap(), ); let (msb, lsb) = ( caps[5].parse::().unwrap(), - caps[6].parse::().unwrap() + caps[6].parse::().unwrap(), ); let port = ((msb as u16) << 8) + lsb as u16; let addr = format!("{}.{}.{}.{}:{}", oct1, oct2, oct3, oct4, port); - SocketAddr::from_str(&addr) - .map_err(|parse_err| FtpError::InvalidAddress(parse_err)) + SocketAddr::from_str(&addr).map_err(|parse_err| FtpError::InvalidAddress(parse_err)) }) } @@ -271,13 +296,13 @@ impl FtpStream { /// of `TYPE` command. pub fn transfer_type(&mut self, file_type: FileType) -> Result<()> { let type_command = format!("TYPE {}\r\n", file_type.to_string()); - try!(self.write_str(&type_command)); + self.write_str(&type_command)?; self.read_response(status::COMMAND_OK).map(|_| ()) } /// Quits the current FTP session. pub fn quit(&mut self) -> Result<()> { - try!(self.write_str("QUIT\r\n")); + self.write_str("QUIT\r\n")?; self.read_response(status::CLOSING).map(|_| ()) } @@ -287,17 +312,19 @@ impl FtpStream { /// Also you will have to read the response to make sure it has the correct value. pub fn get(&mut self, file_name: &str) -> Result> { let retr_command = format!("RETR {}\r\n", file_name); - let data_stream = BufReader::new(try!(self.data_command(&retr_command))); - self.read_response(status::ABOUT_TO_SEND).map(|_| data_stream) + let data_stream = BufReader::new(self.data_command(&retr_command)?); + self.read_response_in(&[status::ABOUT_TO_SEND, status::ALREADY_OPEN])?; + Ok(data_stream) } /// Renames the file from_name to to_name pub fn rename(&mut self, from_name: &str, to_name: &str) -> Result<()> { - try!(self.write_str(format!("RNFR {}\r\n", from_name))); + self.write_str(format!("RNFR {}\r\n", from_name))?; self.read_response(status::REQUEST_FILE_PENDING) .and_then(|_| { - try!(self.write_str(format!("RNTO {}\r\n", to_name))); - self.read_response(status::REQUESTED_FILE_ACTION_OK).map(|_| ()) + self.write_str(format!("RNTO {}\r\n", to_name))?; + self.read_response(status::REQUESTED_FILE_ACTION_OK) + .map(|_| ()) }) } @@ -322,14 +349,22 @@ impl FtpStream { /// # assert!(conn.rm("retr.txt").is_ok()); /// ``` pub fn retr(&mut self, filename: &str, reader: F) -> Result - where F: Fn(&mut Read) -> Result { + where + F: Fn(&mut dyn Read) -> Result, + { let retr_command = format!("RETR {}\r\n", filename); { - let mut data_stream = BufReader::new(try!(self.data_command(&retr_command))); + let mut data_stream = BufReader::new(self.data_command(&retr_command)?); self.read_response_in(&[status::ABOUT_TO_SEND, status::ALREADY_OPEN]) .and_then(|_| reader(&mut data_stream)) - }.and_then(|res| - self.read_response_in(&[status::CLOSING_DATA_CONNECTION,status::REQUESTED_FILE_ACTION_OK]).map(|_| res)) + } + .and_then(|res| { + self.read_response_in(&[ + status::CLOSING_DATA_CONNECTION, + status::REQUESTED_FILE_ACTION_OK, + ]) + .map(|_| res) + }) } /// Simple way to retr a file from the server. This stores the file in memory. @@ -350,26 +385,32 @@ impl FtpStream { pub fn simple_retr(&mut self, file_name: &str) -> Result>> { self.retr(file_name, |reader| { let mut buffer = Vec::new(); - reader.read_to_end(&mut buffer).map(|_| buffer).map_err(|read_err| FtpError::ConnectionError(read_err)) - }).map(|buffer| Cursor::new(buffer)) + reader + .read_to_end(&mut buffer) + .map(|_| buffer) + .map_err(|read_err| FtpError::ConnectionError(read_err)) + }) + .map(|buffer| Cursor::new(buffer)) } /// Removes the remote pathname from the server. pub fn rmdir(&mut self, pathname: &str) -> Result<()> { - try!(self.write_str(format!("RMD {}\r\n", pathname))); - self.read_response(status::REQUESTED_FILE_ACTION_OK).map(|_| ()) + self.write_str(format!("RMD {}\r\n", pathname))?; + self.read_response(status::REQUESTED_FILE_ACTION_OK) + .map(|_| ()) } /// Remove the remote file from the server. pub fn rm(&mut self, filename: &str) -> Result<()> { - try!(self.write_str(format!("DELE {}\r\n", filename))); - self.read_response(status::REQUESTED_FILE_ACTION_OK).map(|_| ()) + self.write_str(format!("DELE {}\r\n", filename))?; + self.read_response(status::REQUESTED_FILE_ACTION_OK) + .map(|_| ()) } fn put_file(&mut self, filename: &str, r: &mut R) -> Result<()> { let stor_command = format!("STOR {}\r\n", filename); - let mut data_stream = BufWriter::new(try!(self.data_command(&stor_command))); - try!(self.read_response_in(&[status::ALREADY_OPEN, status::ABOUT_TO_SEND])); + let mut data_stream = BufWriter::new(self.data_command(&stor_command)?); + self.read_response_in(&[status::ALREADY_OPEN, status::ABOUT_TO_SEND])?; copy(r, &mut data_stream) .map_err(|read_err| FtpError::ConnectionError(read_err)) .map(|_| ()) @@ -377,82 +418,124 @@ impl FtpStream { /// This stores a file on the server. pub fn put(&mut self, filename: &str, r: &mut R) -> Result<()> { - try!(self.put_file(filename, r)); - self.read_response_in(&[status::CLOSING_DATA_CONNECTION,status::REQUESTED_FILE_ACTION_OK]) - .map(|_| ()) + self.put_file(filename, r)?; + self.read_response_in(&[ + status::CLOSING_DATA_CONNECTION, + status::REQUESTED_FILE_ACTION_OK, + ]) + .map(|_| ()) } /// Execute a command which returns list of strings in a separate stream - fn list_command(&mut self, cmd: Cow<'static, str>, open_code: u32, close_code: &[u32]) -> Result> { + fn list_command( + &mut self, + cmd: Cow<'static, str>, + open_code: u32, + close_code: &[u32], + ) -> Result> { + let data_stream = BufReader::new(self.data_command(&cmd)?); + self.read_response_in(&[open_code, status::ALREADY_OPEN])?; + let lines = Self::get_lines_from_stream(data_stream); + self.read_response_in(close_code)?; + lines + } + + fn get_lines_from_stream(data_stream: BufReader) -> Result> { let mut lines: Vec = Vec::new(); - { - let mut data_stream = BufReader::new(try!(self.data_command(&cmd))); - try!(self.read_response_in(&[open_code, status::ALREADY_OPEN])); - - let mut line = String::new(); - loop { - match data_stream.read_to_string(&mut line) { - Ok(0) => break, - Ok(_) => lines.extend(line.split("\r\n").into_iter().map(|s| String::from(s)).filter(|s| s.len() > 0)), - Err(err) => return Err(FtpError::ConnectionError(err)), - }; + + let mut lines_stream = data_stream.lines(); + loop { + let line = lines_stream.next(); + match line { + Some(line) => match line { + Ok(l) => { + if l.is_empty() { + continue; + } + lines.push(l); + } + Err(_) => { + return Err(FtpError::InvalidResponse(String::from( + "Invalid lines in response", + ))) + } + }, + None => break Ok(lines), } } - - self.read_response_in(close_code).map(|_| lines) } /// Execute `LIST` command which returns the detailed file listing in human readable format. /// If `pathname` is omited then the list of files in the current directory will be /// returned otherwise it will the list of files on `pathname`. pub fn list(&mut self, pathname: Option<&str>) -> Result> { - let command = pathname.map_or("LIST\r\n".into(), |path| format!("LIST {}\r\n", path).into()); + let command = pathname.map_or("LIST\r\n".into(), |path| { + format!("LIST {}\r\n", path).into() + }); - self.list_command(command, status::ABOUT_TO_SEND, &[status::CLOSING_DATA_CONNECTION,status::REQUESTED_FILE_ACTION_OK]) + self.list_command( + command, + status::ABOUT_TO_SEND, + &[ + status::CLOSING_DATA_CONNECTION, + status::REQUESTED_FILE_ACTION_OK, + ], + ) } /// Execute `NLST` command which returns the list of file names only. /// If `pathname` is omited then the list of files in the current directory will be /// returned otherwise it will the list of files on `pathname`. pub fn nlst(&mut self, pathname: Option<&str>) -> Result> { - let command = pathname.map_or("NLST\r\n".into(), |path| format!("NLST {}\r\n", path).into()); + let command = pathname.map_or("NLST\r\n".into(), |path| { + format!("NLST {}\r\n", path).into() + }); - self.list_command(command, status::ABOUT_TO_SEND, &[status::CLOSING_DATA_CONNECTION,status::REQUESTED_FILE_ACTION_OK]) + self.list_command( + command, + status::ABOUT_TO_SEND, + &[ + status::CLOSING_DATA_CONNECTION, + status::REQUESTED_FILE_ACTION_OK, + ], + ) } /// Retrieves the modification time of the file at `pathname` if it exists. /// In case the file does not exist `None` is returned. - pub fn mdtm(&mut self, pathname: &str) -> Result>> { - try!(self.write_str(format!("MDTM {}\r\n", pathname))); - let Line(_, content) = try!(self.read_response(status::FILE)); + pub fn mdtm(&mut self, pathname: &str) -> Result>> { + self.write_str(format!("MDTM {}\r\n", pathname))?; + let Line(_, content) = self.read_response(status::FILE)?; match MDTM_RE.captures(&content) { Some(caps) => { let (year, month, day) = ( caps[1].parse::().unwrap(), caps[2].parse::().unwrap(), - caps[3].parse::().unwrap() + caps[3].parse::().unwrap(), ); let (hour, minute, second) = ( caps[4].parse::().unwrap(), caps[5].parse::().unwrap(), - caps[6].parse::().unwrap() + caps[6].parse::().unwrap(), ); - Ok(Some(UTC.ymd(year, month, day).and_hms(hour, minute, second))) - }, - None => Ok(None) + Ok(Some( + Utc.ymd(year, month, day).and_hms(hour, minute, second), + )) + } + None => Ok(None), } } /// Retrieves the size of the file in bytes at `pathname` if it exists. /// In case the file does not exist `None` is returned. pub fn size(&mut self, pathname: &str) -> Result> { - try!(self.write_str(format!("SIZE {}\r\n", pathname))); - let Line(_, content) = try!(self.read_response(status::FILE)); + self.write_str(format!("SIZE {}\r\n", pathname))?; + let Line(_, content) = self.read_response(status::FILE)?; match SIZE_RE.captures(&content) { Some(caps) => Ok(Some(caps[1].parse().unwrap())), - None => Ok(None) + None => Ok(None), } } @@ -462,7 +545,8 @@ impl FtpStream { } let stream = self.reader.get_mut(); - stream.write_all(command.as_ref().as_bytes()) + stream + .write_all(command.as_ref().as_bytes()) .map_err(|send_err| FtpError::ConnectionError(send_err)) } @@ -473,21 +557,23 @@ impl FtpStream { /// Retrieve single line response pub fn read_response_in(&mut self, expected_code: &[u32]) -> Result { let mut line = String::new(); - try!(self.reader.read_line(&mut line) - .map_err(|read_err| FtpError::ConnectionError(read_err))); + self.reader + .read_line(&mut line) + .map_err(|read_err| FtpError::ConnectionError(read_err))?; if cfg!(feature = "debug_print") { print!("FTP {}", line); } if line.len() < 5 { - return Err(FtpError::InvalidResponse("error: could not read reply code".to_owned())); + return Err(FtpError::InvalidResponse( + "error: could not read reply code".to_owned(), + )); } - let code: u32 = try!(line[0..3].parse() - .map_err(|err| { - FtpError::InvalidResponse(format!("error: could not parse reply code: {}", err)) - })); + let code: u32 = line[0..3].parse().map_err(|err| { + FtpError::InvalidResponse(format!("error: could not parse reply code: {}", err)) + })?; // multiple line reply // loop while the line does not begin with the code and a space @@ -503,10 +589,15 @@ impl FtpStream { } } + line = String::from(line.trim()); + if expected_code.into_iter().any(|ec| code == *ec) { Ok(Line(code, line)) } else { - Err(FtpError::InvalidResponse(format!("Expected code {:?}, got response: {}", expected_code, line))) + Err(FtpError::InvalidResponse(format!( + "Expected code {:?}, got response: {}", + expected_code, line + ))) } } } diff --git a/src/lib.rs b/src/lib.rs index b99dbb4b9..f54802cb5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,12 +30,12 @@ //! //! ```rust,no_run //! use ftp::FtpStream; -//! use ftp::openssl::ssl::{ SslContext, SslMethod }; +//! use ftp::native_tls::{TlsConnector, TlsStream}; //! //! let ftp_stream = FtpStream::connect("127.0.0.1:21").unwrap(); -//! let ctx = SslContext::builder(SslMethod::tls()).unwrap().build(); +//! let mut ctx = TlsConnector::new().unwrap(); //! // Switch to the secure mode -//! let mut ftp_stream = ftp_stream.into_secure(ctx).unwrap(); +//! let mut ftp_stream = ftp_stream.into_secure(ctx, "localhost").unwrap(); //! ftp_stream.login("anonymous", "anonymous").unwrap(); //! // Do other secret stuff //! // Switch back to the insecure mode (if required) @@ -45,18 +45,18 @@ //! ``` //! - -#[macro_use] extern crate lazy_static; -extern crate regex; +#[macro_use] +extern crate lazy_static; extern crate chrono; +extern crate regex; #[cfg(feature = "secure")] -pub extern crate openssl; +pub extern crate native_tls; -mod ftp; mod data_stream; -pub mod types; +mod ftp; pub mod status; +pub mod types; pub use self::ftp::FtpStream; pub use self::types::FtpError; diff --git a/src/path.rs b/src/path.rs deleted file mode 100644 index c17754c42..000000000 --- a/src/path.rs +++ /dev/null @@ -1,12 +0,0 @@ - -Enum FtpPathType { - directory, - file, - linc, -} - -Struct FtpPath { - home: String, - absolute_path: String, - path_type: FtpPathType, -} diff --git a/src/status.rs b/src/status.rs index fc5cd79c4..039e01e3d 100644 --- a/src/status.rs +++ b/src/status.rs @@ -1,56 +1,56 @@ // 1xx: Positive Preliminary Reply -pub const INITIATING: u32 = 100; -pub const RESTART_MARKER: u32 = 110; -pub const READY_MINUTE: u32 = 120; -pub const ALREADY_OPEN: u32 = 125; -pub const ABOUT_TO_SEND: u32 = 150; +pub const INITIATING: u32 = 100; +pub const RESTART_MARKER: u32 = 110; +pub const READY_MINUTE: u32 = 120; +pub const ALREADY_OPEN: u32 = 125; +pub const ABOUT_TO_SEND: u32 = 150; // 2xx: Positive Completion Reply -pub const COMMAND_OK: u32 = 200; -pub const COMMAND_NOT_IMPLEMENTED: u32 = 202; -pub const SYSTEM: u32 = 211; -pub const DIRECTORY: u32 = 212; -pub const FILE: u32 = 213; -pub const HELP: u32 = 214; -pub const NAME: u32 = 215; -pub const READY: u32 = 220; -pub const CLOSING: u32 = 221; -pub const DATA_CONNECTION_OPEN: u32 = 225; -pub const CLOSING_DATA_CONNECTION: u32 = 226; -pub const PASSIVE_MODE: u32 = 227; -pub const LONG_PASSIVE_MODE: u32 = 228; -pub const EXTENDED_PASSIVE_MODE: u32 = 229; -pub const LOGGED_IN: u32 = 230; -pub const LOGGED_OUT: u32 = 231; -pub const LOGOUT_ACK: u32 = 232; -pub const AUTH_OK: u32 = 234; -pub const REQUESTED_FILE_ACTION_OK: u32 = 250; -pub const PATH_CREATED: u32 = 257; +pub const COMMAND_OK: u32 = 200; +pub const COMMAND_NOT_IMPLEMENTED: u32 = 202; +pub const SYSTEM: u32 = 211; +pub const DIRECTORY: u32 = 212; +pub const FILE: u32 = 213; +pub const HELP: u32 = 214; +pub const NAME: u32 = 215; +pub const READY: u32 = 220; +pub const CLOSING: u32 = 221; +pub const DATA_CONNECTION_OPEN: u32 = 225; +pub const CLOSING_DATA_CONNECTION: u32 = 226; +pub const PASSIVE_MODE: u32 = 227; +pub const LONG_PASSIVE_MODE: u32 = 228; +pub const EXTENDED_PASSIVE_MODE: u32 = 229; +pub const LOGGED_IN: u32 = 230; +pub const LOGGED_OUT: u32 = 231; +pub const LOGOUT_ACK: u32 = 232; +pub const AUTH_OK: u32 = 234; +pub const REQUESTED_FILE_ACTION_OK: u32 = 250; +pub const PATH_CREATED: u32 = 257; // 3xx: Positive intermediate Reply -pub const NEED_PASSWORD: u32 = 331; -pub const LOGIN_NEED_ACCOUNT: u32 = 332; -pub const REQUEST_FILE_PENDING: u32 = 350; +pub const NEED_PASSWORD: u32 = 331; +pub const LOGIN_NEED_ACCOUNT: u32 = 332; +pub const REQUEST_FILE_PENDING: u32 = 350; // 4xx: Transient Negative Completion Reply -pub const NOT_AVAILABLE: u32 = 421; +pub const NOT_AVAILABLE: u32 = 421; pub const CANNOT_OPEN_DATA_CONNECTION: u32 = 425; -pub const TRANSER_ABORTED: u32 = 426; -pub const INVALID_CREDENTIALS: u32 = 430; -pub const HOST_UNAVAILABLE: u32 = 434; +pub const TRANSER_ABORTED: u32 = 426; +pub const INVALID_CREDENTIALS: u32 = 430; +pub const HOST_UNAVAILABLE: u32 = 434; pub const REQUEST_FILE_ACTION_IGNORED: u32 = 450; -pub const ACTION_ABORTED: u32 = 451; -pub const REQUESTED_ACTION_NOT_TAKEN: u32 = 452; +pub const ACTION_ABORTED: u32 = 451; +pub const REQUESTED_ACTION_NOT_TAKEN: u32 = 452; // 5xx: Permanent Negative Completion Reply -pub const BAD_COMMAND: u32 = 500; -pub const BAD_ARGUMENTS: u32 = 501; -pub const NOT_IMPLEMENTED: u32 = 502; -pub const BAD_SEQUENCE: u32 = 503; -pub const NOT_IMPLEMENTED_PARAMETER: u32 = 504; -pub const NOT_LOGGED_IN: u32 = 530; -pub const STORING_NEED_ACCOUNT: u32 = 532; -pub const FILE_UNAVAILABLE: u32 = 550; -pub const PAGE_TYPE_UNKNOWN: u32 = 551; -pub const EXCEEDED_STORAGE: u32 = 552; -pub const BAD_FILENAME: u32 = 553; +pub const BAD_COMMAND: u32 = 500; +pub const BAD_ARGUMENTS: u32 = 501; +pub const NOT_IMPLEMENTED: u32 = 502; +pub const BAD_SEQUENCE: u32 = 503; +pub const NOT_IMPLEMENTED_PARAMETER: u32 = 504; +pub const NOT_LOGGED_IN: u32 = 530; +pub const STORING_NEED_ACCOUNT: u32 = 532; +pub const FILE_UNAVAILABLE: u32 = 550; +pub const PAGE_TYPE_UNKNOWN: u32 = 551; +pub const EXCEEDED_STORAGE: u32 = 552; +pub const BAD_FILENAME: u32 = 553; diff --git a/src/types.rs b/src/types.rs index 5ab8b81a0..17b940fb9 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,7 +1,6 @@ //! The set of valid values for FTP commands use std::convert::From; -use std::error::Error; use std::fmt; /// A shorthand for a Result whose error type is always an FtpError. @@ -30,7 +29,6 @@ pub enum FormatControl { Asa, } - /// File Type used in `TYPE` command #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum FileType { @@ -74,29 +72,11 @@ impl fmt::Display for FtpError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { FtpError::ConnectionError(ref ioerr) => write!(f, "FTP ConnectionError: {}", ioerr), - FtpError::SecureError(ref desc) => write!(f, "FTP SecureError: {}", desc.clone()), - FtpError::InvalidResponse(ref desc) => write!(f, "FTP InvalidResponse: {}", desc.clone()), - FtpError::InvalidAddress(ref perr) => write!(f, "FTP InvalidAddress: {}", perr), - } - } -} - -impl Error for FtpError { - fn description(&self) -> &str { - match *self { - FtpError::ConnectionError(ref ioerr) => ioerr.description(), - FtpError::SecureError(ref desc) => desc.as_str(), - FtpError::InvalidResponse(ref desc) => desc.as_str(), - FtpError::InvalidAddress(ref perr) => perr.description(), - } - } - - fn cause(&self) -> Option<&Error> { - match *self { - FtpError::ConnectionError(ref ioerr) => Some(ioerr), - FtpError::SecureError(_) => None, - FtpError::InvalidResponse(_) => None, - FtpError::InvalidAddress(ref perr) => Some(perr) + FtpError::SecureError(ref desc) => write!(f, "FTP SecureError: {}", desc.clone()), + FtpError::InvalidResponse(ref desc) => { + write!(f, "FTP InvalidResponse: {}", desc.clone()) + } + FtpError::InvalidAddress(ref perr) => write!(f, "FTP InvalidAddress: {}", perr), } } } diff --git a/tests/lib.rs b/tests/lib.rs index da844abf3..5f6dbd321 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,13 +1,13 @@ #[cfg(test)] - extern crate ftp; -use std::io::Cursor; use ftp::FtpStream; +use std::io::Cursor; #[test] fn test_ftp() { let mut ftp_stream = FtpStream::connect("127.0.0.1:21").unwrap(); + println!("Welcome message: {:?}", ftp_stream.get_welcome_msg()); let _ = ftp_stream.login("Doe", "mumble").unwrap(); ftp_stream.mkdir("test_dir").unwrap(); @@ -20,14 +20,18 @@ fn test_ftp() { assert!(ftp_stream.put("test_file.txt", &mut reader).is_ok()); // retrieve file - assert!(ftp_stream.simple_retr("test_file.txt").map(|bytes| - assert_eq!(bytes.into_inner(), file_data.as_bytes())).is_ok()); + assert!(ftp_stream + .simple_retr("test_file.txt") + .map(|bytes| assert_eq!(bytes.into_inner(), file_data.as_bytes())) + .is_ok()); // remove file assert!(ftp_stream.rm("test_file.txt").is_ok()); // cleanup: go up, remove folder, and quit - assert!(ftp_stream.cdup().and_then(|_| - ftp_stream.rmdir("test_dir")).and_then(|_| - ftp_stream.quit()).is_ok()); + assert!(ftp_stream + .cdup() + .and_then(|_| ftp_stream.rmdir("test_dir")) + .and_then(|_| ftp_stream.quit()) + .is_ok()); }