Added client to rs-tftpd

This commit is contained in:
Jolan Rathelot 2024-06-19 16:18:28 +02:00 committed by Altuğ Bakan
parent 1db6805a51
commit fb3a684f28
10 changed files with 801 additions and 13 deletions

View file

@ -10,4 +10,5 @@ keywords = ["tftp", "server"]
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
[features] [features]
integration = [] integration = []
client = []

View file

@ -17,7 +17,7 @@ Since TFTP servers do not offer any type of login or access control mechanisms,
Documentation for the project can be found in [docs.rs](https://docs.rs/tftpd/latest/tftpd/). Documentation for the project can be found in [docs.rs](https://docs.rs/tftpd/latest/tftpd/).
## Usage ## Usage (Server)
To install the server using Cargo: To install the server using Cargo:
@ -32,6 +32,32 @@ To run the server on the IP address `0.0.0.0`, read-only, on port `1234` in the
tftpd -i 0.0.0.0 -p 1234 -d "/home/user/tftp" -r tftpd -i 0.0.0.0 -p 1234 -d "/home/user/tftp" -r
``` ```
## Usage (Client)
To install the client and server using Cargo:
```bash
cargo install --features client tftpd
tftpd client --help
tftpd server --help
```
To run the server on the IP address `0.0.0.0`, read-only, on port `1234` in the `/home/user/tftp` directory:
```bash
tftpd server -i 0.0.0.0 -p 1234 -d "/home/user/tftp" -r
```
To connect the client to a tftp server running on IP address `127.0.0.1`, read-only, on port `1234` and download a file named `example.file`
```bash
tftpd client example.file -i 0.0.0.0 -p 1234 -d
```
To connect the client to a tftp server running on IP address `127.0.0.1`, read-only, on port `1234` and upload a file named `example.file`
```bash
tftpd client ./example.file -i 0.0.0.0 -p 1234 -u
```
## License ## License
This project is licensed under the [MIT License](https://opensource.org/license/mit/). This project is licensed under the [MIT License](https://opensource.org/license/mit/).

255
src/client.rs Normal file
View file

@ -0,0 +1,255 @@
use crate::packet::{DEFAULT_BLOCKSIZE, DEFAULT_TIMEOUT, DEFAULT_WINDOWSIZE};
use crate::{ClientConfig, OptionType, Packet, Socket, TransferOption, Worker};
use std::cmp::PartialEq;
use std::error::Error;
use std::fs::File;
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket};
use std::path::PathBuf;
use std::time::Duration;
/// Client `struct` is used for client sided TFTP requests.
///
/// This `struct` is meant to be created by [`Client::new()`]. See its
/// documentation for more.
///
/// # Example
///
/// ```rust
/// // Create the TFTP server.
/// use tftpd::{ClientConfig, Client};
///
/// let args = ["test.file", "-u"].iter().map(|s| s.to_string());
/// let config = ClientConfig::new(args).unwrap();
/// let server = Client::new(&config).unwrap();
/// ```
pub struct Client {
remote_address: SocketAddr,
blocksize: usize,
windowsize: u16,
timeout: Duration,
mode: Mode,
filename: PathBuf,
save_path: PathBuf,
}
/// Enum used to set the client either in Download Mode or Upload Mode
#[derive(PartialEq, Clone, Copy, Debug)]
pub enum Mode {
/// Upload Mode
Upload,
/// Download Mode
Download,
}
impl Client {
/// Creates the TFTP Client with the supplied [`ClientConfig`].
pub fn new(config: &ClientConfig) -> Result<Client, Box<dyn Error>> {
Ok(Client {
remote_address: SocketAddr::from((config.remote_ip_address, config.port)),
blocksize: config.blocksize,
windowsize: config.windowsize,
timeout: config.timeout,
mode: config.mode,
filename: config.filename.clone(),
save_path: config.save_directory.clone(),
})
}
/// Starts the Client depending on the [`Mode`] the client is in
pub fn start(&mut self) -> Result<(), Box<dyn Error>> {
match self.mode {
Mode::Upload => self.upload(),
Mode::Download => self.download(),
}
}
fn upload(&mut self) -> Result<(), Box<dyn Error>> {
if self.mode != Mode::Upload {
return Err(Box::from("Client mode is set to Download"));
}
let socket = if self.remote_address.is_ipv4() {
UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?
} else {
UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0))?
};
let file = self.filename.clone();
let size = File::open(self.filename.clone())?.metadata()?.len() as usize;
Socket::send_to(
&socket,
&Packet::Wrq {
filename: file.into_os_string().into_string().unwrap(),
mode: "octet".into(),
options: vec![
TransferOption {
option: OptionType::BlockSize,
value: self.blocksize,
},
TransferOption {
option: OptionType::Windowsize,
value: self.windowsize as usize,
},
TransferOption {
option: OptionType::Timeout,
value: self.timeout.as_secs() as usize,
},
TransferOption {
option: OptionType::TransferSize,
value: size,
}
],
},
&self.remote_address,
)?;
let received = Socket::recv_from(&socket);
if let Ok((packet, from)) = received {
socket.connect(from)?;
match packet {
Packet::Oack(options) => {
self.verify_oack(&options)?;
let worker = self.configure_worker(socket)?;
let join_handle = worker.send(false)?;
let _ = join_handle.join();
}
Packet::Ack(_) => {
self.blocksize = DEFAULT_BLOCKSIZE;
self.windowsize = DEFAULT_WINDOWSIZE;
self.timeout = DEFAULT_TIMEOUT;
let worker = self.configure_worker(socket)?;
let join_handle = worker.send(false)?;
let _ = join_handle.join();
}
Packet::Error { code, msg } => {
return Err(Box::from(format!(
"Client received error from server: {code}: {msg}"
)));
}
_ => {
return Err(Box::from(format!(
"Client received unexpected packet from server: {packet:#?}"
)));
}
}
} else {
return Err(Box::from("Unexpected Error"));
}
Ok(())
}
fn download(&mut self) -> Result<(), Box<dyn Error>> {
if self.mode != Mode::Download {
return Err(Box::from("Client mode is set to Upload"));
}
let socket = if self.remote_address.is_ipv4() {
UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?
} else {
UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0))?
};
let file = self.filename.clone();
Socket::send_to(
&socket,
&Packet::Rrq {
filename: file.into_os_string().into_string().unwrap(),
mode: "octet".into(),
options: vec![
TransferOption {
option: OptionType::BlockSize,
value: self.blocksize,
},
TransferOption {
option: OptionType::Windowsize,
value: self.windowsize as usize,
},
TransferOption {
option: OptionType::Timeout,
value: self.timeout.as_secs() as usize,
},
TransferOption {
option: OptionType::TransferSize,
value: 0,
}
],
},
&self.remote_address,
)?;
let received = Socket::recv_from(&socket);
if let Ok((packet, from)) = received {
socket.connect(from)?;
match packet {
Packet::Oack(options) => {
self.verify_oack(&options)?;
Socket::send_to(&socket, &Packet::Ack(0), &from)?;
let worker = self.configure_worker(socket)?;
let join_handle = worker.receive()?;
let _ = join_handle.join();
}
Packet::Error { code, msg } => {
return Err(Box::from(format!(
"Client received error from server: {code}: {msg}"
)));
}
_ => {
return Err(Box::from(format!(
"Client received unexpected packet from server: {packet:#?}"
)));
}
}
} else {
return Err(Box::from("Unexpected Error"));
}
Ok(())
}
fn verify_oack(&mut self, options: &Vec<TransferOption>) -> Result<(), Box<dyn Error>> {
for option in options {
match option.option {
OptionType::BlockSize {} => self.blocksize = option.value,
OptionType::Windowsize => self.windowsize = option.value as u16,
_ => {}
}
}
Ok(())
}
fn configure_worker(&self, socket: UdpSocket) -> Result<Worker<dyn Socket>, Box<dyn Error>> {
let mut socket: Box<dyn Socket> = Box::new(socket);
socket.set_read_timeout(self.timeout)?;
socket.set_write_timeout(self.timeout)?;
let worker = if self.mode == Mode::Download {
let mut file = self.save_path.clone();
file = file.join(self.filename.clone());
Worker::new(
socket,
file,
self.blocksize,
DEFAULT_TIMEOUT,
self.windowsize,
1,
)
} else {
Worker::new(
socket,
PathBuf::from(self.filename.clone()),
self.blocksize,
DEFAULT_TIMEOUT,
self.windowsize,
1,
)
};
Ok(worker)
}
}

216
src/client_config.rs Normal file
View file

@ -0,0 +1,216 @@
use crate::client::Mode;
use crate::packet::{DEFAULT_BLOCKSIZE, DEFAULT_TIMEOUT, DEFAULT_WINDOWSIZE};
use std::error::Error;
use std::net::{IpAddr, Ipv4Addr};
use std::path::{Path, PathBuf};
use std::process;
use std::time::Duration;
/// Configuration `struct` used for parsing TFTP Client options from user
/// input.
///
/// This `struct` is meant to be created by [`ClientConfig::new()`]. See its
/// documentation for more.
///
/// # Example
///
/// ```rust
/// // Create TFTP configuration from user arguments.
/// use std::env;
/// use tftpd::ClientConfig;
///
/// let client_config = ClientConfig::new(env::args());
/// ```
#[derive(Debug)]
pub struct ClientConfig {
/// Local IP address of the TFTP Server. (default: 127.0.0.1)
pub remote_ip_address: IpAddr,
/// Local Port number of the TFTP Server. (default: 69)
pub port: u16,
/// Blocksize to use during transfer. (default: 512)
pub blocksize: usize,
/// Windowsize to use during transfer. (default: 1)
pub windowsize: u16,
/// Timeout to use during transfer. (default: 5s)
pub timeout: Duration,
/// Upload or Download a file. (default: Download)
pub mode: Mode,
/// Directory where to save downloaded files. (default: Current Working Directory)
pub save_directory: PathBuf,
/// File to Upload or Download.
pub filename: PathBuf,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
remote_ip_address: IpAddr::V4(Ipv4Addr::LOCALHOST),
port: 69,
blocksize: DEFAULT_BLOCKSIZE,
windowsize: DEFAULT_WINDOWSIZE,
timeout: DEFAULT_TIMEOUT,
mode: Mode::Download,
save_directory: Default::default(),
filename: Default::default(),
}
}
}
impl ClientConfig {
/// Creates a new configuration by parsing the supplied arguments. It is
/// intended for use with [`env::args()`].
pub fn new<T: Iterator<Item = String>>(mut args: T) -> Result<ClientConfig, Box<dyn Error>> {
let mut config = ClientConfig::default();
args.next();
if let Some(file_name) = args.next() {
config.filename = PathBuf::from(file_name);
} else {
return Err("Missing file to upload or download".into());
}
while let Some(arg) = args.next() {
match arg.as_str() {
"-i" | "--ip-address" => {
if let Some(ip_str) = args.next() {
let ip_addr: IpAddr = ip_str.parse()?;
config.remote_ip_address = ip_addr;
} else {
return Err("Missing ip address after flag".into());
}
}
"-p" | "--port" => {
if let Some(port_str) = args.next() {
config.port = port_str.parse::<u16>()?;
} else {
return Err("Missing port number after flag".into());
}
}
"-b" | "--blocksize" => {
if let Some(blocksize_str) = args.next() {
config.blocksize = blocksize_str.parse::<usize>()?;
} else {
return Err("Missing blocksize after flag".into());
}
}
"-w" | "--windowsize" => {
if let Some(windowsize_str) = args.next() {
config.windowsize = windowsize_str.parse::<u16>()?;
} else {
return Err("Missing windowsize after flag".into());
}
}
"-t" | "--timeout" => {
if let Some(timeout_str) = args.next() {
config.timeout = Duration::from_secs(timeout_str.parse::<u64>()?);
} else {
return Err("Missing timeout after flag".into());
}
}
"-sd" | "--save-directory" => {
if let Some(dir_str) = args.next() {
if !Path::new(&dir_str).exists() {
return Err(format!("{dir_str} does not exist").into());
}
config.save_directory = dir_str.into();
} else {
return Err("Missing save directory after flag".into());
}
}
"-u" | "--upload" => {
config.mode = Mode::Upload;
}
"-d" | "--download" => {
config.mode = Mode::Download;
}
"-h" | "--help" => {
println!("TFTP Client\n");
println!("Usage: tftpd client <File> [OPTIONS]\n");
println!("Options:");
println!(" -i, --ip-address <IP ADDRESS>\tIp address of the server (default: 127.0.0.1)");
println!(" -p, --port <PORT>\t\tPort of the server (default: 69)");
println!(" -b, --blocksize <number>\tSets the blocksize (default: 512)");
println!(" -w, --windowsize <number>\tSets the windowsize (default: 1)");
println!(" -t, --timeout <seconds>\tSets the timeout in seconds (default: 5)");
println!(" -u, --upload\t\t\tSets the client to upload mode, Ignores all previous download flags");
println!(" -d, --download\t\tSet the client to download mode, Invalidates all previous upload flags");
println!(" -sd, --save-directory <DIRECTORY>\tSet the directory to save files when in Download Mode (default: the directory setting)");
println!(" -h, --help\t\t\tPrint help information");
process::exit(0);
}
invalid => return Err(format!("Invalid flag: {invalid}").into()),
}
}
Ok(config)
}
}
#[cfg(test)]
mod tests {
use ClientConfig;
use super::*;
#[test]
fn parses_full_config() {
let config = ClientConfig::new(
[
"client",
"test.file",
"-i",
"0.0.0.0",
"-p",
"1234",
"-sd",
"/",
"-d",
"-u",
"-b",
"1024",
"-w",
"2",
"-t",
"4"
]
.iter()
.map(|s| s.to_string()),
)
.unwrap();
assert_eq!(config.remote_ip_address, Ipv4Addr::new(0, 0, 0, 0));
assert_eq!(config.port, 1234);
assert_eq!(config.save_directory, PathBuf::from("/"));
assert_eq!(config.filename, PathBuf::from("test.file"));
assert_eq!(config.windowsize, 2);
assert_eq!(config.blocksize, 1024);
assert_eq!(config.mode, Mode::Upload);
assert_eq!(config.timeout, Duration::from_secs(4));
}
#[test]
fn parses_partial_config() {
let config = ClientConfig::new(
[
"client",
"test.file",
"-d",
"-b",
"2048",
"-p",
"2000",
]
.iter()
.map(|s| s.to_string()),
)
.unwrap();
assert_eq!(config.port, 2000);
assert_eq!(config.filename, PathBuf::from("test.file"));
assert_eq!(config.blocksize, 2048);
assert_eq!(config.mode, Mode::Download);
}
}

View file

@ -118,6 +118,9 @@ impl Config {
} }
"-h" | "--help" => { "-h" | "--help" => {
println!("TFTP Server Daemon\n"); println!("TFTP Server Daemon\n");
#[cfg(feature = "client")]
println!("Usage: tftpd server [OPTIONS]\n");
#[cfg(not(feature = "client"))]
println!("Usage: tftpd [OPTIONS]\n"); println!("Usage: tftpd [OPTIONS]\n");
println!("Options:"); println!("Options:");
println!(" -i, --ip-address <IP ADDRESS>\tSet the ip address of the server (default: 127.0.0.1)"); println!(" -i, --ip-address <IP ADDRESS>\tSet the ip address of the server (default: 127.0.0.1)");

View file

@ -15,6 +15,11 @@
//! Since TFTP servers do not offer any type of login or access control mechanisms, this server only allows //! Since TFTP servers do not offer any type of login or access control mechanisms, this server only allows
//! transfer and receiving inside a chosen folder, and disallows external file access. //! transfer and receiving inside a chosen folder, and disallows external file access.
#[cfg(feature = "client")]
mod client;
#[cfg(feature = "client")]
mod client_config;
mod config; mod config;
mod convert; mod convert;
mod packet; mod packet;
@ -23,6 +28,12 @@ mod socket;
mod window; mod window;
mod worker; mod worker;
#[cfg(feature = "client")]
pub use client::Client;
#[cfg(feature = "client")]
pub use client::Mode;
#[cfg(feature = "client")]
pub use client_config::ClientConfig;
pub use config::Config; pub use config::Config;
pub use convert::Convert; pub use convert::Convert;
pub use packet::ErrorCode; pub use packet::ErrorCode;

View file

@ -1,8 +1,67 @@
#[cfg(feature = "client")]
use std::error::Error;
use std::{env, net::SocketAddr, process}; use std::{env, net::SocketAddr, process};
#[cfg(not(feature = "client"))]
use tftpd::{Config, Server}; use tftpd::{Config, Server};
#[cfg(feature = "client")]
use tftpd::{Client, ClientConfig, Config, Mode, Server};
#[cfg(feature = "client")]
fn main() { fn main() {
let config = Config::new(env::args()).unwrap_or_else(|err| { let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("{}: incorrect usage", args[0]);
eprintln!("{} <client | server> [args]", args[0]);
} else if args[1] == "client" {
client(args[1..].iter().map(|s| s.to_string())).unwrap_or_else(|err| {
eprintln!("{err}");
})
} else if args[1] == "server" {
server(args[1..].iter().map(|s| s.to_string()));
} else {
eprintln!("{}: incorrect usage", args[0]);
eprintln!("{} (client | server) [args]", args[0]);
}
}
#[cfg(not(feature = "client"))]
fn main() {
let args: Vec<String> = env::args().collect();
server(args[0..].iter().map(|s| s.to_string()));
}
#[cfg(feature = "client")]
fn client<T: Iterator<Item = String>>(args: T) -> Result<(), Box<dyn Error>> {
let config = ClientConfig::new(args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1)
});
let mut server = Client::new(&config).unwrap_or_else(|err| {
eprintln!("Problem creating client: {err}");
process::exit(1)
});
if config.mode == Mode::Upload {
println!(
"Starting TFTP Client, uploading {} to {}",
config.filename.display(),
SocketAddr::new(config.remote_ip_address, config.port),
);
} else {
println!(
"Starting TFTP Client, downloading {} to {}",
config.filename.display(),
SocketAddr::new(config.remote_ip_address, config.port),
);
}
server.start()
}
fn server<T: Iterator<Item = String>>(args: T) {
let config = Config::new(args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}"); eprintln!("Problem parsing arguments: {err}");
process::exit(1) process::exit(1)
}); });

View file

@ -1,4 +1,6 @@
use crate::Convert; use crate::Convert;
#[cfg(feature = "client")]
use std::time::Duration;
use std::{error::Error, fmt, str::FromStr}; use std::{error::Error, fmt, str::FromStr};
/// Packet `enum` represents the valid TFTP packet types. /// Packet `enum` represents the valid TFTP packet types.
@ -64,19 +66,28 @@ impl Packet {
Opcode::Rrq | Opcode::Wrq => parse_rq(buf, opcode), Opcode::Rrq | Opcode::Wrq => parse_rq(buf, opcode),
Opcode::Data => parse_data(buf), Opcode::Data => parse_data(buf),
Opcode::Ack => parse_ack(buf), Opcode::Ack => parse_ack(buf),
Opcode::Oack => parse_oack(buf),
Opcode::Error => parse_error(buf), Opcode::Error => parse_error(buf),
_ => Err("Invalid packet".into()),
} }
} }
/// Serializes a [`Packet`] into a [`Vec<u8>`]. /// Serializes a [`Packet`] into a [`Vec<u8>`].
pub fn serialize(&self) -> Result<Vec<u8>, &'static str> { pub fn serialize(&self) -> Result<Vec<u8>, &'static str> {
match self { match self {
Packet::Rrq {
filename,
mode,
options,
} => Ok(serialize_rrq(filename, mode, options)),
Packet::Wrq {
filename,
mode,
options,
} => Ok(serialize_wrq(filename, mode, options)),
Packet::Data { block_num, data } => Ok(serialize_data(block_num, data)), Packet::Data { block_num, data } => Ok(serialize_data(block_num, data)),
Packet::Ack(block_num) => Ok(serialize_ack(block_num)), Packet::Ack(block_num) => Ok(serialize_ack(block_num)),
Packet::Error { code, msg } => Ok(serialize_error(code, msg)), Packet::Error { code, msg } => Ok(serialize_error(code, msg)),
Packet::Oack(options) => Ok(serialize_oack(options)), Packet::Oack(options) => Ok(serialize_oack(options)),
_ => Err("Invalid packet"),
} }
} }
} }
@ -192,6 +203,18 @@ pub enum OptionType {
Windowsize, Windowsize,
} }
#[cfg(feature = "client")]
/// Default Timeout
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
#[cfg(feature = "client")]
/// Default Blocksize
pub const DEFAULT_BLOCKSIZE: usize = 512;
#[cfg(feature = "client")]
/// Default Windowsize
pub const DEFAULT_WINDOWSIZE: u16 = 1;
impl OptionType { impl OptionType {
/// Converts an [`OptionType`] to a [`str`]. /// Converts an [`OptionType`] to a [`str`].
pub fn as_str(&self) -> &'static str { pub fn as_str(&self) -> &'static str {
@ -339,6 +362,26 @@ fn parse_ack(buf: &[u8]) -> Result<Packet, Box<dyn Error>> {
Ok(Packet::Ack(Convert::to_u16(&buf[2..])?)) Ok(Packet::Ack(Convert::to_u16(&buf[2..])?))
} }
fn parse_oack(buf: &[u8]) -> Result<Packet, Box<dyn Error>> {
let mut options = vec![];
let mut value: String;
let mut option;
let mut zero_index = 1usize;
while zero_index < buf.len() - 1 {
(option, zero_index) = Convert::to_string(buf, zero_index + 1)?;
(value, zero_index) = Convert::to_string(buf, zero_index + 1)?;
if let Ok(option) = OptionType::from_str(option.to_lowercase().as_str()) {
options.push(TransferOption {
option,
value: value.parse()?,
});
}
}
Ok(Packet::Oack(options))
}
fn parse_error(buf: &[u8]) -> Result<Packet, Box<dyn Error>> { fn parse_error(buf: &[u8]) -> Result<Packet, Box<dyn Error>> {
let code = ErrorCode::from_u16(Convert::to_u16(&buf[2..])?)?; let code = ErrorCode::from_u16(Convert::to_u16(&buf[2..])?)?;
if let Ok((msg, _)) = Convert::to_string(buf, 4) { if let Ok((msg, _)) = Convert::to_string(buf, 4) {
@ -351,6 +394,38 @@ fn parse_error(buf: &[u8]) -> Result<Packet, Box<dyn Error>> {
} }
} }
fn serialize_rrq(file: &String, mode: &String, options: &Vec<TransferOption>) -> Vec<u8> {
let mut buf = [
&Opcode::Rrq.as_bytes(),
file.as_bytes(),
&[0x00],
mode.as_bytes(),
&[0x00],
]
.concat();
for option in options {
buf = [buf, option.as_bytes()].concat();
}
buf
}
fn serialize_wrq(file: &String, mode: &String, options: &Vec<TransferOption>) -> Vec<u8> {
let mut buf = [
&Opcode::Wrq.as_bytes(),
file.as_bytes(),
&[0x00],
mode.as_bytes(),
&[0x00],
]
.concat();
for option in options {
buf = [buf, option.as_bytes()].concat();
}
buf
}
fn serialize_data(block_num: &u16, data: &Vec<u8>) -> Vec<u8> { fn serialize_data(block_num: &u16, data: &Vec<u8>) -> Vec<u8> {
[ [
&Opcode::Data.as_bytes(), &Opcode::Data.as_bytes(),
@ -576,6 +651,53 @@ mod tests {
} }
} }
#[test]
fn parses_oack() {
let buf = [
&Opcode::Oack.as_bytes()[..],
(OptionType::TransferSize.as_str().as_bytes()),
&[0x00],
("0".as_bytes()),
&[0x00],
(OptionType::Timeout.as_str().as_bytes()),
&[0x00],
("5".as_bytes()),
&[0x00],
(OptionType::Windowsize.as_str().as_bytes()),
&[0x00],
("4".as_bytes()),
&[0x00],
]
.concat();
if let Ok(Packet::Oack(options)) = parse_oack(&buf) {
assert_eq!(options.len(), 3);
assert_eq!(
options[0],
TransferOption {
option: OptionType::TransferSize,
value: 0
}
);
assert_eq!(
options[1],
TransferOption {
option: OptionType::Timeout,
value: 5
}
);
assert_eq!(
options[2],
TransferOption {
option: OptionType::Windowsize,
value: 4
}
);
} else {
panic!("cannot parse read request with options")
}
}
#[test] #[test]
fn parses_error() { fn parses_error() {
let buf = [ let buf = [
@ -611,6 +733,98 @@ mod tests {
} }
} }
#[test]
fn serializes_rrq() {
let serialized_data = vec![0x00, 0x01, 0x74, 0x65, 0x73, 0x74, 0x00, 0x6f, 0x63, 0x74, 0x65, 0x74, 0x00];
assert_eq!(
serialize_rrq(
&"test".into(),
&"octet".into(),
&vec![]
),
serialized_data
)
}
#[test]
fn serializes_rrq_with_options() {
let serialized_data = vec![
0x00, 0x01, 0x74, 0x65, 0x73, 0x74, 0x00, 0x6f, 0x63, 0x74, 0x65, 0x74, 0x00, 0x62,
0x6c, 0x6b, 0x73, 0x69, 0x7a, 0x65, 0x00, 0x31, 0x34, 0x36, 0x38, 0x00, 0x77, 0x69,
0x6e, 0x64, 0x6f, 0x77, 0x73, 0x69, 0x7a, 0x65, 0x00, 0x31, 0x00, 0x74, 0x69, 0x6d,
0x65, 0x6f, 0x75, 0x74, 0x00, 0x35, 0x00,
];
assert_eq!(
serialize_rrq(
&"test".into(),
&"octet".into(),
&vec![
TransferOption {
option: OptionType::BlockSize,
value: 1468,
},
TransferOption {
option: OptionType::Windowsize,
value: 1,
},
TransferOption {
option: OptionType::Timeout,
value: 5,
}
]
),
serialized_data
)
}
#[test]
fn serializes_wrq() {
let serialized_data = vec![0x00, 0x02, 0x74, 0x65, 0x73, 0x74, 0x00, 0x6f, 0x63, 0x74, 0x65, 0x74, 0x00];
assert_eq!(
serialize_wrq(
&"test".into(),
&"octet".into(),
&vec![]
),
serialized_data
)
}
#[test]
fn serializes_wrq_with_options() {
let serialized_data = vec![
0x00, 0x02, 0x74, 0x65, 0x73, 0x74, 0x00, 0x6f, 0x63, 0x74, 0x65, 0x74, 0x00, 0x62,
0x6c, 0x6b, 0x73, 0x69, 0x7a, 0x65, 0x00, 0x31, 0x34, 0x36, 0x38, 0x00, 0x77, 0x69,
0x6e, 0x64, 0x6f, 0x77, 0x73, 0x69, 0x7a, 0x65, 0x00, 0x31, 0x00, 0x74, 0x69, 0x6d,
0x65, 0x6f, 0x75, 0x74, 0x00, 0x35, 0x00,
];
assert_eq!(
serialize_wrq(
&"test".into(),
&"octet".into(),
&vec![
TransferOption {
option: OptionType::BlockSize,
value: 1468,
},
TransferOption {
option: OptionType::Windowsize,
value: 1,
},
TransferOption {
option: OptionType::Timeout,
value: 5,
}
]
),
serialized_data
)
}
#[test] #[test]
fn serializes_data() { fn serializes_data() {
let serialized_data = vec![0x00, 0x03, 0x00, 0x10, 0x01, 0x02, 0x03, 0x04]; let serialized_data = vec![0x00, 0x03, 0x00, 0x10, 0x01, 0x02, 0x03, 0x04];

View file

@ -184,7 +184,8 @@ impl Server {
worker_options.window_size, worker_options.window_size,
self.duplicate_packets + 1, self.duplicate_packets + 1,
); );
worker.send(!options.is_empty()) worker.send(!options.is_empty())?;
Ok(())
} }
_ => Err("Unexpected error code when checking file".into()), _ => Err("Unexpected error code when checking file".into()),
} }
@ -224,7 +225,8 @@ impl Server {
worker_options.window_size, worker_options.window_size,
self.duplicate_packets + 1, self.duplicate_packets + 1,
); );
worker.receive() worker.receive()?;
Ok(())
}; };
match check_file_exists(file_path, &self.receive_directory) { match check_file_exists(file_path, &self.receive_directory) {

View file

@ -1,4 +1,5 @@
use crate::{ErrorCode, Packet, Socket, Window}; use crate::{ErrorCode, Packet, Socket, Window};
use std::thread::JoinHandle;
use std::{ use std::{
error::Error, error::Error,
fs::{self, File}, fs::{self, File},
@ -70,11 +71,11 @@ impl<T: Socket + ?Sized> Worker<T> {
/// Sends a file to the remote [`SocketAddr`] that has sent a read request using /// Sends a file to the remote [`SocketAddr`] that has sent a read request using
/// a random port, asynchronously. /// a random port, asynchronously.
pub fn send(self, check_response: bool) -> Result<(), Box<dyn Error>> { pub fn send(self, check_response: bool) -> Result<JoinHandle<()>, Box<dyn Error>> {
let file_name = self.file_name.clone(); let file_name = self.file_name.clone();
let remote_addr = self.socket.remote_addr().unwrap(); let remote_addr = self.socket.remote_addr().unwrap();
thread::spawn(move || { let handle = thread::spawn(move || {
let handle_send = || -> Result<(), Box<dyn Error>> { let handle_send = || -> Result<(), Box<dyn Error>> {
self.send_file(File::open(&file_name)?, check_response)?; self.send_file(File::open(&file_name)?, check_response)?;
@ -95,16 +96,16 @@ impl<T: Socket + ?Sized> Worker<T> {
} }
}); });
Ok(()) Ok(handle)
} }
/// Receives a file from the remote [`SocketAddr`] that has sent a write request using /// Receives a file from the remote [`SocketAddr`] that has sent a write request using
/// the supplied socket, asynchronously. /// the supplied socket, asynchronously.
pub fn receive(self) -> Result<(), Box<dyn Error>> { pub fn receive(self) -> Result<JoinHandle<()>, Box<dyn Error>> {
let file_name = self.file_name.clone(); let file_name = self.file_name.clone();
let remote_addr = self.socket.remote_addr().unwrap(); let remote_addr = self.socket.remote_addr().unwrap();
thread::spawn(move || { let handle = thread::spawn(move || {
let handle_receive = || -> Result<(), Box<dyn Error>> { let handle_receive = || -> Result<(), Box<dyn Error>> {
self.receive_file(File::create(&file_name)?)?; self.receive_file(File::create(&file_name)?)?;
@ -128,7 +129,7 @@ impl<T: Socket + ?Sized> Worker<T> {
} }
}); });
Ok(()) Ok(handle)
} }
fn send_file(self, file: File, check_response: bool) -> Result<(), Box<dyn Error>> { fn send_file(self, file: File, check_response: bool) -> Result<(), Box<dyn Error>> {