diff --git a/Cargo.toml b/Cargo.toml index e7d323c23..4f427e28f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ftp" -version = "2.0.0" -authors = ["Matt McCoy "] +version = "2.0.1" +authors = ["Matt McCoy ", "Zack Mullaly "] documentation = "http://mattnenterprise.github.io/rust-ftp" repository = "https://github.com/mattnenterprise/rust-ftp" description = "FTP client for Rust" @@ -27,5 +27,5 @@ regex = "0.1" chrono = "0.2" [dependencies.openssl] -version = "0.7" +version = "^0.9" optional = true diff --git a/examples/encrypted.rs b/examples/encrypted.rs new file mode 100644 index 000000000..14f6a4e5c --- /dev/null +++ b/examples/encrypted.rs @@ -0,0 +1,33 @@ +#[cfg(feature = "secure")] + +extern crate ftp; +extern crate openssl; + +use ftp::FtpStream; +use openssl::ssl::{ + SslContext, + SslMethod, + SSL_OP_NO_SSLV2, + SSL_OP_NO_SSLV3, + SSL_OP_NO_COMPRESSION, +}; + +fn main() { + let mut builder = SslContext::builder(SslMethod::tls()).unwrap(); + builder.set_certificate_file("./tests/test.crt", openssl::x509::X509_FILETYPE_PEM).unwrap(); + builder.set_options(SSL_OP_NO_SSLV2 | SSL_OP_NO_SSLV3 | SSL_OP_NO_COMPRESSION); + builder.set_cipher_list("ALL!EXPORT!EXPORT40!EXPORT56!aNULL!LOW!RC4@STRENGTH").unwrap(); + let ctx = builder.build(); + let result = FtpStream::connect("127.0.0.1:21") + .and_then(|mut client| client.login("anonymous", "").map(|_| client)) + .and_then(|client| client.into_secure(ctx)) + .and_then(|mut client| client.list(None)); + match result { + Ok(dir) => { + for file in dir.iter() { + println!("{}", file); + } + }, + Err(err) => println!("Error: {:?}", err) + } +} diff --git a/src/ftp.rs b/src/ftp.rs index 530d7a063..7dca100b7 100644 --- a/src/ftp.rs +++ b/src/ftp.rs @@ -1,6 +1,4 @@ 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; @@ -9,7 +7,7 @@ use regex::Regex; use chrono::{DateTime, UTC}; use chrono::offset::TimeZone; #[cfg(feature = "secure")] -use openssl::ssl::{Ssl, SslStream, IntoSsl}; +use openssl::ssl::{Ssl, SslContext, SslStream}; use super::data_stream::DataStream; use super::status; use super::types::{FileType, FtpError, Line, Result}; @@ -31,7 +29,7 @@ lazy_static! { pub struct FtpStream { reader: BufReader, #[cfg(feature = "secure")] - ssl_cfg: Option, + ssl_cfg: Option, } impl FtpStream { @@ -84,17 +82,18 @@ impl FtpStream { /// let mut ftp_stream = ftp_stream.into_secure(ctx).unwrap(); /// ``` #[cfg(feature = "secure")] - pub fn into_secure(mut self, ssl: T) -> Result { + pub fn into_secure(mut self, ssl: SslContext) -> Result { // Ask the server to start securing data. let auth_command = String::from("AUTH TLS\r\n"); try!(self.write_str(&auth_command)); try!(self.read_response(status::AUTH_OK)); - let ssl_copy = try!(ssl.clone().into_ssl().map_err(|e| FtpError::SecureError(e.description().to_owned()))); - let stream = try!(SslStream::connect(ssl, self.reader.into_inner().into_tcp_stream()) - .map_err(|e| FtpError::SecureError(e.description().to_owned()))); + //let ssl_copy = ssl.clone(); + let ssl_cfg = try!(Ssl::new(&ssl).map_err(|e| FtpError::SecureError(Box::new(e)))); + let tcp_stream = self.reader.into_inner().into_tcp_stream(); + let stream = try!(ssl_cfg.connect(tcp_stream).map_err(|e| FtpError::SecureError(Box::new(e)))); let mut secured_ftp_tream = FtpStream { reader: BufReader::new(DataStream::Ssl(stream)), - ssl_cfg: Some(ssl_copy) + ssl_cfg: Some(ssl), }; // Set protection buffer size let pbsz_command = format!("PBSZ 0\r\n"); @@ -149,19 +148,16 @@ impl FtpStream { /// Execute command which send data back in a separate stream #[cfg(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(|stream| { - match self.ssl_cfg { - Some(ref ssl) => { - SslStream::connect(ssl.clone(), stream) - .map(|stream| DataStream::Ssl(stream)) - .map_err(|e| FtpError::SecureError(e.description().to_owned())) - }, - None => Ok(DataStream::Tcp(stream)) - } - }) + let addr = try!(self.pasv()); + try!(self.write_str(cmd)); + let stream = try!(TcpStream::connect(addr).map_err(|e| FtpError::ConnectionError(e))); + if let Some(ref ssl) = self.ssl_cfg { + let ssl_cfg = try!(Ssl::new(ssl).map_err(|e| FtpError::SecureError(Box::new(e)))); + let ssl_stream = try!(ssl_cfg.connect(stream).map_err(|e| FtpError::SecureError(Box::new(e)))); + Ok(DataStream::Ssl(ssl_stream)) + } else { + Ok(DataStream::Tcp(stream)) + } } /// Log in to the FTP server. @@ -479,10 +475,10 @@ impl FtpStream { 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 = try!( + 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 diff --git a/src/types.rs b/src/types.rs index 5ab8b81a0..76bdb727d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -12,9 +12,10 @@ pub type Result = ::std::result::Result; #[derive(Debug)] pub enum FtpError { ConnectionError(::std::io::Error), - SecureError(String), InvalidResponse(String), InvalidAddress(::std::net::AddrParseError), + #[cfg(feature = "secure")] + SecureError(Box), } /// Text Format Control used in `TYPE` command @@ -74,9 +75,10 @@ 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), + #[cfg(feature = "secure")] + FtpError::SecureError(ref desc) => write!(f, "FTP SecureError: {}", desc.clone()), } } } @@ -85,18 +87,20 @@ 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(), + #[cfg(feature = "secure")] + FtpError::SecureError(ref sslerr) => sslerr.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::InvalidAddress(ref perr) => Some(perr), + #[cfg(feature = "secure")] + FtpError::SecureError(_) => None, } } } diff --git a/tests/ftp-server.py b/tests/ftp-server.py new file mode 100644 index 000000000..d11d1f8e2 --- /dev/null +++ b/tests/ftp-server.py @@ -0,0 +1,22 @@ +import os + +from pyftpdlib.authorizers import DummyAuthorizer +from pyftpdlib.handlers import TLS_FTPHandler +from pyftpdlib.servers import FTPServer +from pyftpdlib.filesystems import AbstractedFS + +authorizer = DummyAuthorizer() +authorizer.add_anonymous(os.getcwd()) + +handler = TLS_FTPHandler +handler.keyfile = './test.key' +handler.certfile = './test.crt' +handler.authorizer = authorizer +handler.passive_ports = range(60000, 65535) + +# Instantiate FTP server class and listen on 0.0.0.0:21 +address = ('', 21) +server = FTPServer(address, handler) + +# start ftp server +server.serve_forever() diff --git a/tests/test.crt b/tests/test.crt new file mode 100644 index 000000000..5a3aef562 --- /dev/null +++ b/tests/test.crt @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEoTCCA4mgAwIBAgIJAPEIZ11cXg7IMA0GCSqGSIb3DQEBBQUAMIGRMQswCQYD +VQQGEwJWQTERMA8GA1UECBMIVmlyZ2luaWExEDAOBgNVBAcTB0hlcm5kb24xHTAb +BgNVBAoTFFN0cmF0dW0gU2VjdXJpdHkgTExDMQ0wCwYDVQQLEwRYRklMMRIwEAYD +VQQDEwkxMjcuMC4wLjExGzAZBgkqhkiG9w0BCQEWDGRldkB4ZmlsLmNvbTAeFw0x +NjA4MjYxNDIzMzRaFw0xNzA4MjYxNDIzMzRaMIGRMQswCQYDVQQGEwJWQTERMA8G +A1UECBMIVmlyZ2luaWExEDAOBgNVBAcTB0hlcm5kb24xHTAbBgNVBAoTFFN0cmF0 +dW0gU2VjdXJpdHkgTExDMQ0wCwYDVQQLEwRYRklMMRIwEAYDVQQDEwkxMjcuMC4w +LjExGzAZBgkqhkiG9w0BCQEWDGRldkB4ZmlsLmNvbTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMlM1YlC3nyK7ewmX2xJXWXe95QVxUwjEpPhZAB8a/Kf +jjJ6I7LpqITfq/aICMQPgYnmqeQQfX1wmPGSZUzRTmW7A/P0Ba0+XlcbZhJlN3M6 +ZlbnIdv5rVSfcZp1E43upYdZK3Aqj92JBjkNjrQ4Bo5NmrQu75M5qcSvMtADSZQa +ZONkrTbLVbZaXtR9kH+6onYMggWIpYpwur8+cfzi3FbLziJbUicYVCuaz8ySxJ+T +h+MqBWDJjqoF5I5ZtCuivbhjwQjt5d/lq3by4NR8ENmyvIEb06TMfc2TYK33V2Hh +9ZxOEbTp71fz7JLrtIbqVSR6ncpcEeGet7acvdbIa9ECAwEAAaOB+TCB9jAdBgNV +HQ4EFgQU0Tj9SReQeiLeyWm4gMP9mmGzMBYwgcYGA1UdIwSBvjCBu4AU0Tj9SReQ +eiLeyWm4gMP9mmGzMBahgZekgZQwgZExCzAJBgNVBAYTAlZBMREwDwYDVQQIEwhW +aXJnaW5pYTEQMA4GA1UEBxMHSGVybmRvbjEdMBsGA1UEChMUU3RyYXR1bSBTZWN1 +cml0eSBMTEMxDTALBgNVBAsTBFhGSUwxEjAQBgNVBAMTCTEyNy4wLjAuMTEbMBkG +CSqGSIb3DQEJARYMZGV2QHhmaWwuY29tggkA8QhnXVxeDsgwDAYDVR0TBAUwAwEB +/zANBgkqhkiG9w0BAQUFAAOCAQEAbRxSQ7jTCRepM/SgqEkKOgJ0zadPLimW8JRS +y1L86pVFm88crxb79xqfFMmWFPcqHKjNDoQ4qxm69u1Os+MKAYn6uvH4RQYidv0K +y0JbRkaTNUmcySxUlEYvj3QJU6AbbiyZluGbXtT6NjrIvhO7UBhQWSDa5IHOf2K7 +VK9/EHllgm12V5eAyO+qf91Me/8PMRjK3xVRwO6NAx2yo48yZISJvNmNIqx20GKz +F3s19qtYvAz1Y/WvbjcpZqbnihwd9F3EFgGPwD6bhwZjxMqoBDtxDH0whdkEyCnl +Bsk5L0YMTvRyjUi+zhPKGSRxStyaarQti9iqtj486kqh6xhgfA== +-----END CERTIFICATE----- diff --git a/tests/test.key b/tests/test.key new file mode 100644 index 000000000..abf649c65 --- /dev/null +++ b/tests/test.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAyUzViULefIrt7CZfbEldZd73lBXFTCMSk+FkAHxr8p+OMnoj +sumohN+r9ogIxA+Bieap5BB9fXCY8ZJlTNFOZbsD8/QFrT5eVxtmEmU3czpmVuch +2/mtVJ9xmnUTje6lh1krcCqP3YkGOQ2OtDgGjk2atC7vkzmpxK8y0ANJlBpk42St +NstVtlpe1H2Qf7qidgyCBYilinC6vz5x/OLcVsvOIltSJxhUK5rPzJLEn5OH4yoF +YMmOqgXkjlm0K6K9uGPBCO3l3+WrdvLg1HwQ2bK8gRvTpMx9zZNgrfdXYeH1nE4R +tOnvV/Pskuu0hupVJHqdylwR4Z63tpy91shr0QIDAQABAoIBAQClmwxhrB2VoEY0 +bS07zO+Fi3Vq4q46APCbsGWw8KtuI028wTb1Tb1R8yFp5Ggxw//yD03dTqOuux9Y +PfQQynEQyFZsMGkrKZA7YVML9zEzHuxquiPk8PdkEvhG9eJsddTAEN/nm1xYCQ6R +iVHJef4KvFV1vtdh66J7KRdIgivaJpce2joPskK4ddbk41ROxlSj/y4nbA2I020r +CptRMcOnt8zvMslAEBroo1WAvQGRtBrtFvPIo3bv2EoXpiM08tCyESvqYcTb3lLR +Nx6P5QesbC0XAEH58s61OJ2khNXJYLKIk3w2vYQW7oLyWcA9JZfVH/grGNsjdamp +UiGyavcBAoGBAOb58yULdpKhYB3yO/msTBDiWS81RY4c+k4Ap0WAI4nkpxK+I6SL +TwGu0IytBQDyIMh1liBYVTqnWCZEVKEtVrOGreqmDwq50ou6ESxEM/OunNy0kGXo +5r3r+C9Z29Q8+M4q7bOXqcpWRoq8c3UCMErblPksut37Jk6/Yw+F1IDJAoGBAN8b +0v9DG7JzWUHNLtZ/bzx14hKk/JY3pvqp7YCk35YXE7YsXKKsoFddcRnov5vWXn+u ++p74u3ApH4mHO0jD9iD//3/9zavjOZcNCjLK4CHLqvVNa+Cgz3fcjrN1tmKeVkkS +k0OIfje2SJ41+s8u2ld+0VV6eD6I2jwvrAoaud7JAoGBALtgXh3ZVNHTVJQ2pO0B +F5xC47Lmdoy5eV26Lfi14R7Gfbs4wNWFpgxpcwoMepiv1GSK78VBo7K3e01f9X/j +tghh48kN+fnrkaCEy9WrZrHzH5H0cYgbDsVYHrjbHvjolbc7ICanjLh2kTePdeKg +aejwhcQ2w0m9qvALVyOKoD/xAoGAVDgpnugTNXqrb9ZnXtr9/4G0VDtpib76TlcE +63HRYNPXQgZe9Z1abYA9aH1ejxIN2/8OZiIYh09Os1iT/XTTnUNljEgfcko0/BsV +BXVlw/wgzbZrCYFKr8FXMNE3huSkR7M2WeDVXGx33xkbU0gpbavWk4DGkTyRvPR1 +6d6K2VkCgYBkg3b1oeQV+NKc6RMYIJIrfVX6TaP44eRkz4UclGqgyRNLa7LbI68g +J1sQNateIhSfMLPy0y0K62LB25+Ev7Q0FdDH0a+8EHCYojSSmbMxL8KIQMxwcwLI +6uk9ouMt4wr0q007qMLUqGZberhkeDnAbJamlOB3CIeqpWhu8JXT8A== +-----END RSA PRIVATE KEY-----