diff --git a/suppaftp/src/async_ftp/mod.rs b/suppaftp/src/async_ftp/mod.rs index 540077b..56f676a 100644 --- a/suppaftp/src/async_ftp/mod.rs +++ b/suppaftp/src/async_ftp/mod.rs @@ -66,8 +66,9 @@ impl FtpStream { match ftp_stream.read_response(Status::Ready).await { Ok(response) => { - debug!("Server READY; response: {}", response.body); - ftp_stream.welcome_msg = Some(response.body); + let welcome_msg = response.as_string().ok(); + debug!("Server READY; response: {:?}", welcome_msg); + ftp_stream.welcome_msg = welcome_msg; Ok(ftp_stream) } Err(err) => Err(err), @@ -92,8 +93,9 @@ impl FtpStream { debug!("Reading server response..."); match ftp_stream.read_response(Status::Ready).await { Ok(response) => { - debug!("Server READY; response: {}", response.body); - ftp_stream.welcome_msg = Some(response.body); + let welcome_msg = response.as_string().ok(); + debug!("Server READY; response: {:?}", welcome_msg); + ftp_stream.welcome_msg = welcome_msg; Ok(ftp_stream) } Err(err) => Err(err), @@ -209,8 +211,9 @@ impl FtpStream { debug!("Reading server response..."); match stream.read_response(Status::Ready).await { Ok(response) => { - debug!("Server READY; response: {}", response.body); - stream.welcome_msg = Some(response.body); + let welcome_msg = response.as_string().ok(); + debug!("Server READY; response: {:?}", welcome_msg); + stream.welcome_msg = welcome_msg; } Err(err) => return Err(err), } @@ -300,14 +303,16 @@ impl FtpStream { pub async fn pwd(&mut self) -> FtpResult { debug!("Getting working directory"); self.perform(Command::Pwd).await?; - self.read_response(Status::PathCreated) - .await - .and_then( - |Response { status, body }| match (body.find('"'), body.rfind('"')) { - (Some(begin), Some(end)) if begin < end => Ok(body[begin + 1..end].to_string()), - _ => Err(FtpError::UnexpectedResponse(Response::new(status, body))), - }, - ) + let response = self.read_response(Status::PathCreated).await?; + let body = response.as_string().map_err(|_| FtpError::BadResponse)?; + let status = response.status; + match (body.find('"'), body.rfind('"')) { + (Some(begin), Some(end)) if begin < end => Ok(body[begin + 1..end].to_string()), + _ => Err(FtpError::UnexpectedResponse(Response::new( + status, + response.body, + ))), + } } /// This does nothing. This is usually just used to keep the connection open. @@ -562,8 +567,9 @@ impl FtpStream { self.perform(Command::Mdtm(pathname.as_ref().to_string())) .await?; let response: Response = self.read_response(Status::File).await?; + let body = response.as_string().map_err(|_| FtpError::BadResponse)?; - match MDTM_RE.captures(&response.body) { + match MDTM_RE.captures(&body) { Some(caps) => { let (year, month, day) = ( caps[1].parse::().unwrap(), @@ -575,7 +581,10 @@ impl FtpStream { caps[5].parse::().unwrap(), caps[6].parse::().unwrap(), ); - Ok(Utc.ymd(year, month, day).and_hms(hour, minute, second)) + Utc.with_ymd_and_hms(year, month, day, hour, minute, second) + .single() + .map(Ok) + .unwrap_or(Err(FtpError::BadResponse)) } None => Err(FtpError::BadResponse), } @@ -587,8 +596,9 @@ impl FtpStream { self.perform(Command::Size(pathname.as_ref().to_string())) .await?; let response: Response = self.read_response(Status::File).await?; + let body = response.as_string().map_err(|_| FtpError::BadResponse)?; - match SIZE_RE.captures(&response.body) { + match SIZE_RE.captures(&body) { Some(caps) => Ok(caps[1].parse().unwrap()), None => Err(FtpError::BadResponse), } @@ -639,8 +649,9 @@ impl FtpStream { self.perform(Command::Pasv).await?; // PASV response format : 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). let response: Response = self.read_response(Status::PassiveMode).await?; + let response_str = response.as_string().map_err(|_| FtpError::BadResponse)?; let caps = PORT_RE - .captures(&response.body) + .captures(&response_str) .ok_or_else(|| FtpError::UnexpectedResponse(response.clone()))?; // If the regex matches we can be sure groups contains numbers let (oct1, oct2, oct3, oct4) = ( @@ -749,34 +760,27 @@ impl FtpStream { /// Retrieve single line response pub async fn read_response_in(&mut self, expected_code: &[Status]) -> FtpResult { - let mut line = String::new(); - self.reader - .read_line(&mut line) - .await - .map_err(FtpError::ConnectionError)?; + let mut line = Vec::new(); + self.read_line(&mut line).await?; - trace!("CC IN: {}", line.trim_end()); + trace!("CC IN: {:?}", line); if line.len() < 5 { return Err(FtpError::BadResponse); } - let code: u32 = line[0..3].parse().map_err(|_| FtpError::BadResponse)?; - let code = Status::from(code); + let code_word: u32 = self.code_from_buffer(&line, 3)?; + let code = Status::from(code_word); // multiple line reply // loop while the line does not begin with the code and a space - let expected = format!("{} ", &line[0..3]); + let expected = [line[0], line[1], line[2], 0x20]; while line.len() < 5 || line[0..4] != expected { line.clear(); - if let Err(e) = self.reader.read_line(&mut line).await { - return Err(FtpError::ConnectionError(e)); - } - - trace!("CC IN: {}", line.trim_end()); + self.read_line(&mut line).await?; + trace!("CC IN: {:?}", line); } - line = String::from(line.trim()); let response: Response = Response::new(code, line); // Return Ok or error with response if expected_code.iter().any(|ec| code == *ec) { @@ -786,6 +790,24 @@ impl FtpStream { } } + async fn read_line(&mut self, line: &mut Vec) -> FtpResult { + self.reader + .read_until(0x0A, line.as_mut()) + .await + .map_err(FtpError::ConnectionError)?; + Ok(line.len()) + } + + /// Get code from buffer + fn code_from_buffer(&self, buf: &[u8], len: usize) -> Result { + if buf.len() < len { + return Err(FtpError::BadResponse); + } + let buffer = buf[0..len].to_vec(); + let as_string = String::from_utf8(buffer).map_err(|_| FtpError::BadResponse)?; + as_string.parse::().map_err(|_| FtpError::BadResponse) + } + /// Execute a command which returns list of strings in a separate stream async fn stream_lines(&mut self, cmd: Command, open_code: Status) -> FtpResult> { let mut data_stream = BufReader::new(self.data_command(cmd).await?); diff --git a/suppaftp/src/list.rs b/suppaftp/src/list.rs index b39c76f..a65dcf4 100644 --- a/suppaftp/src/list.rs +++ b/suppaftp/src/list.rs @@ -364,7 +364,7 @@ impl File { Ok(date) => { // Case 2. // Return NaiveDateTime from NaiveDate with time 00:00:00 - date.and_hms(0, 0, 0) + date.and_hms_opt(0, 0, 0).unwrap() } Err(_) => { // Might be case 1. diff --git a/suppaftp/src/sync_ftp/mod.rs b/suppaftp/src/sync_ftp/mod.rs index 8bb20cc..69388de 100644 --- a/suppaftp/src/sync_ftp/mod.rs +++ b/suppaftp/src/sync_ftp/mod.rs @@ -20,7 +20,6 @@ use chrono::{DateTime, Utc}; use lazy_regex::{Lazy, Regex}; use std::io::{copy, BufRead, BufReader, Cursor, Read, Write}; use std::net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream, ToSocketAddrs}; -use std::string::String; #[cfg(feature = "secure")] pub use tls::TlsConnector; @@ -66,8 +65,9 @@ impl FtpStream { debug!("Reading server response..."); match ftp_stream.read_response(Status::Ready) { Ok(response) => { - debug!("Server READY; response: {}", response.body); - ftp_stream.welcome_msg = Some(response.body); + let welcome_msg = response.as_string().ok(); + debug!("Server READY; response: {:?}", welcome_msg); + ftp_stream.welcome_msg = welcome_msg; Ok(ftp_stream) } Err(err) => Err(err), @@ -94,8 +94,9 @@ impl FtpStream { debug!("Reading server response..."); match ftp_stream.read_response(Status::Ready) { Ok(response) => { - debug!("Server READY; response: {}", response.body); - ftp_stream.welcome_msg = Some(response.body); + let welcome_msg = response.as_string().ok(); + debug!("Server READY; response: {:?}", welcome_msg); + ftp_stream.welcome_msg = welcome_msg; Ok(ftp_stream) } Err(err) => Err(err), @@ -218,8 +219,9 @@ impl FtpStream { debug!("Reading server response..."); match stream.read_response(Status::Ready) { Ok(response) => { - debug!("Server READY; response: {}", response.body); - stream.welcome_msg = Some(response.body); + let welcome_msg = response.as_string().ok(); + debug!("Server READY; response: {:?}", welcome_msg); + stream.welcome_msg = welcome_msg; } Err(err) => return Err(err), } @@ -300,12 +302,17 @@ impl FtpStream { debug!("Getting working directory"); self.perform(Command::Pwd)?; self.read_response(Status::PathCreated) - .and_then( - |Response { status, body }| match (body.find('"'), body.rfind('"')) { + .and_then(|response| { + let body = response.as_string().map_err(|_| FtpError::BadResponse)?; + let status = response.status; + match (body.find('"'), body.rfind('"')) { (Some(begin), Some(end)) if begin < end => Ok(body[begin + 1..end].to_string()), - _ => Err(FtpError::UnexpectedResponse(Response::new(status, body))), - }, - ) + _ => Err(FtpError::UnexpectedResponse(Response::new( + status, + response.body, + ))), + } + }) } /// This does nothing. This is usually just used to keep the connection open. @@ -580,8 +587,9 @@ impl FtpStream { debug!("Getting modification time for {}", pathname.as_ref()); self.perform(Command::Mdtm(pathname.as_ref().to_string()))?; let response: Response = self.read_response(Status::File)?; + let body = response.as_string().map_err(|_| FtpError::BadResponse)?; - match MDTM_RE.captures(&response.body) { + match MDTM_RE.captures(&body) { Some(caps) => { let (year, month, day) = ( caps[1].parse::().unwrap(), @@ -593,7 +601,11 @@ impl FtpStream { caps[5].parse::().unwrap(), caps[6].parse::().unwrap(), ); - Ok(Utc.ymd(year, month, day).and_hms(hour, minute, second)) + + Utc.with_ymd_and_hms(year, month, day, hour, minute, second) + .single() + .map(Ok) + .unwrap_or(Err(FtpError::BadResponse)) } None => Err(FtpError::BadResponse), } @@ -604,8 +616,9 @@ impl FtpStream { debug!("Getting file size for {}", pathname.as_ref()); self.perform(Command::Size(pathname.as_ref().to_string()))?; let response: Response = self.read_response(Status::File)?; + let body = response.as_string().map_err(|_| FtpError::BadResponse)?; - match SIZE_RE.captures(&response.body) { + match SIZE_RE.captures(&body) { Some(caps) => Ok(caps[1].parse().unwrap()), None => Err(FtpError::BadResponse), } @@ -648,33 +661,29 @@ impl FtpStream { /// Retrieve single line response fn read_response_in(&mut self, expected_code: &[Status]) -> FtpResult { - let mut line = String::new(); - self.reader - .read_line(&mut line) - .map_err(FtpError::ConnectionError)?; + let mut line = Vec::new(); + self.read_line(&mut line)?; - trace!("CC IN: {}", line.trim_end()); + trace!("CC IN: {:?}", line); if line.len() < 5 { return Err(FtpError::BadResponse); } - let code: u32 = line[0..3].parse().map_err(|_| FtpError::BadResponse)?; - let code = Status::from(code); + let code_word: u32 = self.code_from_buffer(&line, 3)?; + let code = Status::from(code_word); + + trace!("Code parsed from response: {} ({})", code, code_word); // multiple line reply // loop while the line does not begin with the code and a space - let expected = format!("{} ", &line[0..3]); + let expected = [line[0], line[1], line[2], 0x20]; while line.len() < 5 || line[0..4] != expected { line.clear(); - if let Err(e) = self.reader.read_line(&mut line) { - return Err(FtpError::ConnectionError(e)); - } - - trace!("CC IN: {}", line.trim_end()); + self.read_line(&mut line)?; + trace!("CC IN: {:?}", line); } - line = String::from(line.trim()); let response: Response = Response::new(code, line); // Return Ok or error with response if expected_code.iter().any(|ec| code == *ec) { @@ -684,6 +693,24 @@ impl FtpStream { } } + /// Read bytes from reader until 0x0A or EOF is found + fn read_line(&mut self, line: &mut Vec) -> FtpResult { + self.reader + .read_until(0x0A, line.as_mut()) + .map_err(FtpError::ConnectionError)?; + Ok(line.len()) + } + + /// Get code from buffer + fn code_from_buffer(&self, buf: &[u8], len: usize) -> Result { + if buf.len() < len { + return Err(FtpError::BadResponse); + } + let buffer = buf[0..len].to_vec(); + let as_string = String::from_utf8(buffer).map_err(|_| FtpError::BadResponse)?; + as_string.parse::().map_err(|_| FtpError::BadResponse) + } + /// Write data to stream with command to perform fn perform(&mut self, command: Command) -> FtpResult<()> { let command = command.to_string(); @@ -758,8 +785,9 @@ impl FtpStream { self.perform(Command::Pasv)?; // PASV response format : 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). let response: Response = self.read_response(Status::PassiveMode)?; + let response_str = response.as_string().map_err(|_| FtpError::BadResponse)?; let caps = PORT_RE - .captures(&response.body) + .captures(&response_str) .ok_or_else(|| FtpError::UnexpectedResponse(response.clone()))?; // If the regex matches we can be sure groups contains numbers let (oct1, oct2, oct3, oct4) = ( diff --git a/suppaftp/src/types.rs b/suppaftp/src/types.rs index a6048c0..f421446 100644 --- a/suppaftp/src/types.rs +++ b/suppaftp/src/types.rs @@ -5,6 +5,7 @@ use super::Status; use std::convert::From; use std::fmt; +use std::string::FromUtf8Error; use thiserror::Error; /// A shorthand for a Result whose error type is always an FtpError. @@ -38,7 +39,7 @@ pub enum FtpError { #[derive(Clone, Debug, Error)] pub struct Response { pub status: Status, - pub body: String, + pub body: Vec, } /// Text Format Control used in `TYPE` command @@ -78,17 +79,24 @@ pub enum Mode { impl fmt::Display for Response { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "[{}] {}", self.status.code(), self.body) + write!( + f, + "[{}] {}", + self.status.code(), + self.as_string().ok().unwrap_or_default() + ) } } impl Response { /// Instantiates a new `Response` - pub fn new>(status: Status, body: S) -> Self { - Self { - status, - body: body.as_ref().to_string(), - } + pub fn new(status: Status, body: Vec) -> Self { + Self { status, body } + } + + /// Get response as string + pub fn as_string(&self) -> Result { + String::from_utf8(self.body.clone()).map(|x| x.trim_end().to_string()) } } @@ -136,9 +144,12 @@ mod test { "Secure error: omar" ); assert_eq!( - FtpError::UnexpectedResponse(Response::new(Status::ExceededStorage, "error")) - .to_string() - .as_str(), + FtpError::UnexpectedResponse(Response::new( + Status::ExceededStorage, + "error".as_bytes().to_vec() + )) + .to_string() + .as_str(), "Invalid response: [552] error" ); assert_eq!( @@ -149,16 +160,16 @@ mod test { #[test] fn response() { - let response: Response = Response::new(Status::AboutToSend, "error"); + let response: Response = Response::new(Status::AboutToSend, "error".as_bytes().to_vec()); assert_eq!(response.status, Status::AboutToSend); - assert_eq!(response.body.as_str(), "error"); + assert_eq!(response.as_string().unwrap(), "error"); } #[test] fn fmt_response() { let response: Response = Response::new( Status::FileUnavailable, - "Can't create directory: File exists", + "Can't create directory: File exists".as_bytes().to_vec(), ); assert_eq!( response.to_string().as_str(),