File rs-tftpd-0.5.0.obscpio of Package rs-tftpd
07070100000000000041ED00000000000000000000000268DE72A500000000000000000000000000000000000000000000001700000000rs-tftpd-0.5.0/.github07070100000001000041ED00000000000000000000000268DE72A500000000000000000000000000000000000000000000002100000000rs-tftpd-0.5.0/.github/workflows07070100000002000081A400000000000000000000000168DE72A50000021F000000000000000000000000000000000000003100000000rs-tftpd-0.5.0/.github/workflows/integration.ymlname: Integration Tests
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install atftp and curl
run: |
sudo apt-get update
sudo apt-get install atftp curl
- name: Build
run: cargo build --features client --verbose
- name: Run tests
run: cargo test --test integration_test --features integration --verbose -- --test-threads 1
07070100000003000081A400000000000000000000000168DE72A5000001E0000000000000000000000000000000000000002A00000000rs-tftpd-0.5.0/.github/workflows/unit.ymlname: Unit Tests
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --verbose
- name: Build client
run: cargo build --features client --verbose
- name: Test
run: cargo test --verbose
- name: Test client
run: cargo test --features client --verbose
07070100000004000081A400000000000000000000000168DE72A500000014000000000000000000000000000000000000001A00000000rs-tftpd-0.5.0/.gitignore.vscode
/target
/tmp07070100000005000081A400000000000000000000000168DE72A500000095000000000000000000000000000000000000001A00000000rs-tftpd-0.5.0/Cargo.lock# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "tftpd"
version = "0.5.0"
07070100000006000081A400000000000000000000000168DE72A5000001F1000000000000000000000000000000000000001A00000000rs-tftpd-0.5.0/Cargo.toml[package]
name = "tftpd"
version = "0.5.0"
authors = ["Altuğ Bakan <mail@alt.ug>"]
edition = "2021"
description = "Multithreaded TFTP server daemon"
repository = "https://github.com/altugbakan/rs-tftpd"
license = "MIT"
keywords = ["tftp", "server"]
categories = ["command-line-utilities"]
[[bin]]
name = "tftpc"
path = "src/client_main.rs"
required-features = ["client"]
[[bin]]
name = "tftpd"
path = "src/main.rs"
[features]
client = []
integration = ["debug_drop", "client"]
debug_drop = []
07070100000007000081A400000000000000000000000168DE72A500000424000000000000000000000000000000000000001A00000000rs-tftpd-0.5.0/LICENSE.mdCopyright 2023 Altuğ Bakan
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
07070100000008000081A400000000000000000000000168DE72A50000076D000000000000000000000000000000000000001900000000rs-tftpd-0.5.0/README.md# TFTP Server Daemon
Pure [Rust](https://www.rust-lang.org/) implementation of a Trivial File Transfer Protocol server daemon.
This server implements [RFC 1350](https://www.rfc-editor.org/rfc/rfc1350), The TFTP Protocol (Revision 2). It also supports the following [RFC 2347](https://www.rfc-editor.org/rfc/rfc2347) TFTP Option Extensions:
- [RFC 2348](https://www.rfc-editor.org/rfc/rfc2348) Blocksize Option
- [RFC 2349](https://www.rfc-editor.org/rfc/rfc2349) Timeout Interval Option
- [RFC 2349](https://www.rfc-editor.org/rfc/rfc2349) Transfer Size Option
- [RFC 7440](https://www.rfc-editor.org/rfc/rfc7440) Windowsize Option
## Security
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.
## Documentation
Documentation for the project can be found in [docs.rs](https://docs.rs/tftpd/latest/tftpd/).
## Usage (Server)
To install the server using Cargo:
```bash
cargo install tftpd
tftpd --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 -i 0.0.0.0 -p 1234 -d "/home/user/tftp" -r
```
## Usage (Client)
Client code is protected by a feature flag names `client`.
To install the client and server using Cargo:
```bash
cargo install --features client tftpd
tftpc --help
```
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
tftpc 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
tftpc example.file -i 0.0.0.0 -p 1234 -u
```
## License
This project is licensed under the [MIT License](https://opensource.org/license/mit/).
07070100000009000081A400000000000000000000000168DE72A5000005A2000000000000000000000000000000000000001B00000000rs-tftpd-0.5.0/SECURITY.md# Security Policy
The TFTP Server Daemon project takes security bugs in this repository seriously. Your efforts to responsibly disclose your findings is appreciated, and we will make every effort to acknowledge your contributions.
## Reporting a Vulnerability
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/altugbakan/rs-tftpd/security/advisories/new) tab on our GitHub repository.
Please **DO NOT** use public channels (e.g., GitHub issues) for initial reporting of bona fide security vulnerabilities.
Once you report a security issue, a reviewer will respond with the next steps. After the initial reply, you will be kept informed of the progress towards a fix and any forthcoming announcements. The reviewer may ask for additional information or guidance during this process. If we determine that your report does not constitute a genuine security vulnerability, you will be informed and the report will be closed. Your report may be turned into an issue for further tracking.
## Security Recommendations
Since TFTP lacks login or access control mechanisms, the server limits file transfers to a designated folder. It is highly recommended to run the server in a secure and isolated environment to prevent unauthorized file access and to only enable read-only mode if file uploads are not required.
Thank you for helping us keep the TFTP Server Daemon project secure!
0707010000000A000041ED00000000000000000000000268DE72A500000000000000000000000000000000000000000000001300000000rs-tftpd-0.5.0/src0707010000000B000081A400000000000000000000000168DE72A500001CC8000000000000000000000000000000000000001D00000000rs-tftpd-0.5.0/src/client.rsuse std::cmp::PartialEq;
use std::error::Error;
use std::fs;
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket};
use std::path::PathBuf;
use std::time::Duration;
use crate::{ClientConfig, Packet, Socket, Worker, log::*};
use crate::options::{OptionsProtocol, OptionsPrivate, OptionFmt};
/// 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,
timeout_req: Duration,
mode: Mode,
file_path: PathBuf,
receive_directory: PathBuf,
opt_local: OptionsPrivate,
opt_common: OptionsProtocol,
}
/// 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)),
timeout_req: config.timeout_req,
mode: config.mode,
file_path: config.file_path.clone(),
receive_directory: config.receive_directory.clone(),
opt_local: config.opt_local.clone(),
opt_common: config.opt_common.clone(),
})
}
/// Run the Client depending on the [`Mode`] the client is in
pub fn run(&mut self) -> Result<bool, Box<dyn Error>> {
let socket = if self.remote_address.is_ipv4() {
UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?
} else {
UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0))?
};
socket.set_read_timeout(Some(self.timeout_req))?;
match self.mode {
Mode::Upload => self.upload(socket),
Mode::Download => self.download(socket),
}
}
fn upload(&mut self, socket : UdpSocket) -> Result<bool, Box<dyn Error>> {
if self.mode != Mode::Upload {
return Err(Box::from("Client mode is set to Download"));
}
let filename = self
.file_path
.file_name()
.ok_or("Invalid filename")?
.to_str()
.ok_or("Filename is not valid UTF-8")?
.to_owned();
self.opt_common.transfer_size = Some(fs::metadata(self.file_path.clone())?.len());
log_dbg!(" Sending Write request");
Socket::send_to(
&socket,
&Packet::Wrq {
filename,
mode: "octet".into(),
options : self.opt_common.prepare(),
},
&self.remote_address,
)?;
match Socket::recv_from(&socket) {
Ok((packet, from)) => {
socket.connect(from)?;
match packet {
Packet::Oack(options) => {
self.opt_common.apply(&options)?;
log_dbg!(" Accepted options: {}", OptionFmt(&options));
let worker = self.configure_worker(socket)?;
let join_handle = worker.send(false)?;
Ok(join_handle.join().unwrap())
}
Packet::Ack(_) => {
self.opt_common = Default::default();
let worker = self.configure_worker(socket)?;
let join_handle = worker.send(false)?;
Ok(join_handle.join().unwrap())
}
Packet::Error { code, msg } => Err(Box::from(format!(
"Client received error from server: {code}: {msg}"))),
_ => Err(Box::from(format!(
"Client received unexpected packet from server: {packet:#?}"))),
}
}
Err(err) => Err(Box::from(format!("Unexpected Error: {err}")))
}
}
fn download(&mut self, socket : UdpSocket) -> Result<bool, Box<dyn Error>> {
if self.mode != Mode::Download {
return Err(Box::from("Client mode is set to Upload"));
}
let filename = self
.file_path
.clone()
.into_os_string()
.into_string()
.unwrap_or_else(|_| "Invalid filename".to_string());
log_dbg!(" Sending Read request");
Socket::send_to(
&socket,
&Packet::Rrq {
filename,
mode: "octet".into(),
options : self.opt_common.prepare(),
},
&self.remote_address,
)?;
match Socket::recv_from(&socket) {
Ok((packet, from)) => {
socket.connect(from)?;
match packet {
Packet::Oack(options) => {
self.opt_common.apply(&options)?;
log_dbg!(" Accepted options: {}", OptionFmt(&options));
Socket::send_to(&socket, &Packet::Ack(0), &from)?;
let worker = self.configure_worker(socket)?;
let join_handle = worker.receive()?;
Ok(join_handle.join().unwrap())
}
// We could implement this by forwarding Option<packet::Data> to worker.receive()
Packet::Data { .. } => Err(
"Client received data instead of o-ack. This implementation \
does not support servers without options (RFC 2347)".into()),
Packet::Error { code, msg } => Err(Box::from(format!(
"Client received error from server: {code}: {msg}"))),
_ => Err(Box::from(format!(
"Client received unexpected packet from server: {packet:#?}"))),
}
}
Err(err) => Err(Box::from(format!("Unexpected Error: {err}")))
}
}
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.opt_common.timeout)?;
socket.set_write_timeout(self.opt_common.timeout)?;
let worker = if self.mode == Mode::Download {
let mut file = self.receive_directory.clone();
file = file.join(
self.file_path
.clone()
.file_name()
.ok_or("Invalid filename")?,
);
Worker::new(
socket,
file,
self.opt_local.clone(),
self.opt_common.clone(),
)
} else {
Worker::new(
socket,
self.file_path.clone(),
self.opt_local.clone(),
self.opt_common.clone(),
)
};
Ok(worker)
}
}
0707010000000C000081A400000000000000000000000168DE72A500002DDD000000000000000000000000000000000000002400000000rs-tftpd-0.5.0/src/client_config.rsuse std::error::Error;
use std::net::{IpAddr, Ipv4Addr};
use std::path::{Path, PathBuf, MAIN_SEPARATOR};
use std::process;
use std::time::Duration;
use crate::client::Mode;
use crate::config;
use crate::options::{DEFAULT_TIMEOUT, OptionsProtocol, OptionsPrivate};
use crate::log::*;
#[cfg(feature = "debug_drop")]
use crate::drop::drop_set;
/// 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 Client. (default: 127.0.0.1)
pub remote_ip_address: IpAddr,
/// Local Port number of the TFTP Client. (default: 69)
pub port: u16,
/// Timeout to use after request. (default: 5s)
pub timeout_req: Duration,
/// Upload or Download a file. (default: Download)
pub mode: Mode,
/// Download directory of the TFTP Client. (default: current working directory)
pub receive_directory: PathBuf,
/// File to Upload or Download.
pub file_path: PathBuf,
/// Local options for client
pub opt_local: OptionsPrivate,
/// Common options for client
pub opt_common: OptionsProtocol,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
remote_ip_address: IpAddr::V4(Ipv4Addr::LOCALHOST),
port: 69,
timeout_req: DEFAULT_TIMEOUT,
mode: Mode::Download,
receive_directory: Default::default(),
file_path: Default::default(),
opt_local: Default::default(),
opt_common: Default::default(),
}
}
}
fn parse_duration<T : Iterator<Item = String>>(args : &mut T) -> Result<Duration, Box<dyn Error>> {
if let Some(dur_str) = args.next() {
let dur = Duration::from_secs_f32(dur_str.parse::<f32>()?);
if dur < Duration::from_secs_f32(0.001) {
Err("duration cannot be shorter than 1 ms".into())
} else if dur > Duration::from_secs(255) {
Err("duration cannot be greater than 255 s".into())
} else {
Ok(dur)
}
} else {
Err("Missing duration after flag".into())
}
}
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();
let mut verbosity : isize = 1;
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.opt_common.block_size = blocksize_str.parse::<u16>()?;
} else {
return Err("Missing blocksize after flag".into());
}
}
"-w" | "--windowsize" => {
if let Some(windowsize_str) = args.next() {
config.opt_common.window_size = windowsize_str.parse::<u16>()?;
} else {
return Err("Missing windowsize after flag".into());
}
}
"-W" | "--windowwait" => {
config.opt_common.window_wait = parse_duration(&mut args)?;
}
"-t" | "--timeout" => {
config.opt_common.timeout = parse_duration(&mut args)?;
}
"-T" | "--timeout-req" => {
config.timeout_req = parse_duration(&mut args)?;
}
"-rd" | "--receive-directory" => {
if let Some(dir_str) = args.next() {
if !Path::new(&dir_str).exists() {
return Err(format!("{dir_str} does not exist").into());
}
config.receive_directory = dir_str.into();
} else {
return Err("Missing receive 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>\t\tIP address of the server (default: 127.0.0.1)");
println!(" -p, --port <PORT>\t\t\tUDP port of the server (default: 69)");
println!(" -b, --blocksize <number>\t\tset the blocksize (default: 512)");
println!(" -w, --windowsize <number>\t\tset the windowsize (default: 1)");
println!(" -W, --windowwait <seconds>\t\t inter-packet wait time in seconds for windows (default: 0.01)");
println!(" -t, --timeout <seconds>\t\tset the timeout for data in seconds (default: 5, can be float)");
println!(" -T, --timeout-req <seconds>\t\tset the timeout after request in seconds (default: 5, can be float)");
println!(" -u, --upload\t\t\t\tselect upload mode, ignores previous flags");
println!(" -d, --download\t\t\tselect download mode, ignores previous flags");
println!(" -rd, --receive-directory <DIR>\tdirectory to receive files when in Download mode (default: current)");
config::print_opt_local_help();
println!(" -h, --help\t\t\t\tprint help information");
process::exit(0);
}
"-q" | "--quiet" => verbosity -= 1,
"-v" | "--verbose" => verbosity += 1,
#[cfg(feature = "debug_drop")]
"-D" => drop_set(args.next())?,
"--" => {
for arg in args.by_ref() {
if !config.file_path.as_os_str().is_empty() {
return Err("too many arguments".into());
}
config.file_path = convert_file_path_abs(arg.as_str());
}
}
arg => if !config::parse_local_args(arg, &mut args, &mut config.opt_local)? {
if !config.file_path.as_os_str().is_empty() {
return Err("too many arguments".into());
}
if arg.starts_with('-') {
return Err(format!("unkwon flag {arg} (or use '--' to force into filename)").into());
}
config.file_path = convert_file_path_abs(arg);
}
}
}
if config.file_path.as_os_str().is_empty() {
return Err("missing filename".into());
}
verbosity_set(verbosity);
Ok(config)
}
}
pub fn convert_file_path_abs(filename: &str) -> PathBuf {
let normalized_filename = if MAIN_SEPARATOR == '\\' {
filename.replace('/', "\\")
} else {
filename.replace('\\', "/")
};
PathBuf::from(normalized_filename)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_full_config() {
let config = ClientConfig::new(
[
"test.file",
"-i",
"0.0.0.0",
"-p",
"1234",
"-rd",
"/",
"-d",
"-u",
"-b",
"1024",
"-w",
"2",
"-W",
"0.02",
"-t",
"4",
"--keep-on-error",
]
.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.receive_directory, PathBuf::from("/"));
assert_eq!(config.file_path, PathBuf::from("test.file"));
assert_eq!(config.opt_common.window_size, 2);
assert_eq!(config.opt_common.window_wait, Duration::from_millis(20));
assert_eq!(config.opt_common.block_size, 1024);
assert_eq!(config.mode, Mode::Upload);
assert_eq!(config.opt_common.timeout, Duration::from_secs(4));
assert!(!config.opt_local.clean_on_error);
}
#[test]
fn parses_partial_config() {
let config = ClientConfig::new(
["test.file", "-d", "-b", "2048", "-p", "2000"]
.iter()
.map(|s| s.to_string()),
)
.unwrap();
assert_eq!(config.port, 2000);
assert_eq!(config.file_path, PathBuf::from("test.file"));
assert_eq!(config.opt_common.block_size, 2048);
assert_eq!(config.mode, Mode::Download);
}
#[test]
fn parses_file_paths() {
let config =
ClientConfig::new(["test/test.file"].iter().map(|s| s.to_string())).unwrap();
let mut path = PathBuf::new();
path.push("test");
path.push("test.file");
assert_eq!(config.file_path, path);
let config = ClientConfig::new(
["test\\test\\test.file"]
.iter()
.map(|s| s.to_string()),
)
.unwrap();
let mut path = PathBuf::new();
path.push("test");
path.push("test");
path.push("test.file");
assert_eq!(config.file_path, path);
}
#[test]
fn converts_file_path_abs() {
let path = convert_file_path_abs("test.file");
let mut correct_path = PathBuf::new();
correct_path.push("test.file");
assert_eq!(path, correct_path);
let path = convert_file_path_abs("\\test.file");
let mut correct_path = PathBuf::new();
correct_path.push(std::path::MAIN_SEPARATOR_STR);
correct_path.push("test.file");
assert_eq!(path, correct_path);
let path = convert_file_path_abs("/test.file");
let mut correct_path = PathBuf::new();
correct_path.push("/test.file");
assert_eq!(path, correct_path);
#[cfg(target_os = "windows")]
{
let path = convert_file_path_abs("C:\\test.file");
let mut correct_path = PathBuf::new();
correct_path.push("C:");
correct_path.push(std::path::MAIN_SEPARATOR_STR);
correct_path.push("test.file");
assert_eq!(path, correct_path);
}
}
}
0707010000000D000081A400000000000000000000000168DE72A500000507000000000000000000000000000000000000002200000000rs-tftpd-0.5.0/src/client_main.rsuse std::{error::Error, env, net::SocketAddr, process, process::ExitCode};
use tftpd::{Client, ClientConfig, Mode, log_err, log_info};
fn main() -> ExitCode{
match client(env::args()) {
Ok(true) => ExitCode::SUCCESS,
Ok(false) => ExitCode::FAILURE,
Err(err) => {
log_err!("{err}");
ExitCode::FAILURE
}
}
}
fn client<T: Iterator<Item = String>>(args: T) -> Result<bool, Box<dyn Error>> {
// Parse arguments, skipping first one (exec name)
let config = ClientConfig::new(args.skip(1)).unwrap_or_else(|err| {
log_err!("Problem parsing arguments: {err}");
process::exit(1)
});
let mut client = Client::new(&config).unwrap_or_else(|err| {
log_err!("Problem creating client: {err}");
process::exit(1)
});
if config.mode == Mode::Upload {
log_info!(
"Starting TFTP Client, uploading {} to {}",
config.file_path.display(),
SocketAddr::new(config.remote_ip_address, config.port),
);
} else {
log_info!(
"Starting TFTP Client, downloading {} from {}",
config.file_path.display(),
SocketAddr::new(config.remote_ip_address, config.port),
);
}
client.run()
}
0707010000000E000081A400000000000000000000000168DE72A50000332D000000000000000000000000000000000000001D00000000rs-tftpd-0.5.0/src/config.rsuse std::error::Error;
use std::net::{IpAddr, Ipv4Addr};
use std::path::{Path, PathBuf};
use std::{env, process};
use crate::options::{Rollover, OptionsPrivate};
use crate::log::*;
#[cfg(feature = "debug_drop")]
use crate::drop::drop_set;
/// Configuration `struct` used for parsing TFTP options from user
/// input.
///
/// This `struct` is meant to be created by [`Config::new()`]. See its
/// documentation for more.
///
/// # Example
///
/// ```rust
/// // Create TFTP configuration from user arguments.
/// use std::env;
/// use tftpd::Config;
///
/// let config = Config::new(env::args()).unwrap();
/// ```
pub struct Config {
/// Local IP address of the TFTP Server. (default: 127.0.0.1)
pub ip_address: IpAddr,
/// Local Port number of the TFTP Server. (default: 69)
pub port: u16,
/// Default directory of the TFTP Server. (default: current working directory)
pub directory: PathBuf,
/// Upload directory of the TFTP Server. (default: directory)
pub receive_directory: PathBuf,
/// Download directory of the TFTP Server. (default: directory)
pub send_directory: PathBuf,
/// Use a single port for both sending and receiving. (default: false)
pub single_port: bool,
/// Refuse all write requests, making the server read-only. (default: false)
pub read_only: bool,
/// Overwrite existing files. (default: false)
pub overwrite: bool,
/// Local options for server
pub opt_local: OptionsPrivate,
}
impl Default for Config {
fn default() -> Self {
Self {
ip_address: IpAddr::V4(Ipv4Addr::LOCALHOST),
port: 69,
directory: env::current_dir().unwrap_or_else(|_| env::temp_dir()),
receive_directory: Default::default(),
send_directory: Default::default(),
single_port: Default::default(),
read_only: Default::default(),
overwrite: Default::default(),
opt_local: Default::default(),
}
}
}
pub fn parse_local_args<T: Iterator<Item = String>>(arg: &str, args: &mut T, opt_local: &mut OptionsPrivate) -> Result<bool, Box<dyn Error>> {
match arg {
"--duplicate-packets" => {
if let Some(duplicate_packets_str) = args.next() {
let duplicate_packets = duplicate_packets_str.parse::<u8>()?;
if duplicate_packets == u8::MAX {
return Err(format!("Duplicate packets should be less than {}", u8::MAX).into());
}
opt_local.repeat_count = duplicate_packets + 1;
} else {
return Err("Missing duplicate packets after flag".into());
}
}
"--keep-on-error" => {
opt_local.clean_on_error = false;
}
"-m" | "--maxretries" => {
if let Some(retries_str) = args.next() {
opt_local.max_retries = retries_str.parse::<usize>()?;
} else {
return Err("Missing max retries after flag".into());
}
}
"-R" | "--rollover" => {
if let Some(arg_str) = args.next() {
opt_local.rollover = match arg_str.as_str() {
"n" => Rollover::None,
"0" => Rollover::Enforce0,
"1" => Rollover::Enforce1,
"x" => Rollover::DontCare,
_ => return Err("Invalid rollover policy value: use n, 0, 1, x".into()),
}
} else {
return Err("Rollover policy value missing: use n, 0, 1, x".into())
}
}
_ => return Ok(false),
}
Ok(true)
}
pub fn print_opt_local_help() {
println!(" -m, --maxretries <cnt>\t\tSets the max retries count (default: 6)");
println!(" -R, --rollover <policy>\t\tsets the rollover policy: 0, 1, n (forbidden), x (dont care) (default: 0)");
println!(" --duplicate-packets <NUM>\t\tDuplicate all packets sent from the server (default: 0)");
println!(" --keep-on-error\t\t\tPrevent daemon from deleting files after receiving errors");
}
impl Config {
/// 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<Config, Box<dyn Error>> {
let mut config = Config::default();
let mut verbosity : isize = 1;
// Skip arg 0 (executable name)
args.next();
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.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());
}
}
"-d" | "--directory" => {
if let Some(dir_str) = args.next() {
if !Path::new(&dir_str).exists() {
return Err(format!("{dir_str} does not exist").into());
}
config.directory = dir_str.into();
} else {
return Err("Missing directory after flag".into());
}
}
"-rd" | "--receive-directory" => {
if let Some(dir_str) = args.next() {
if !Path::new(&dir_str).exists() {
return Err(format!("{dir_str} does not exist").into());
}
config.receive_directory = dir_str.into();
} else {
return Err("Missing receive directory after flag".into());
}
}
"-sd" | "--send-directory" => {
if let Some(dir_str) = args.next() {
if !Path::new(&dir_str).exists() {
return Err(format!("{dir_str} does not exist").into());
}
config.send_directory = dir_str.into();
} else {
return Err("Missing send directory after flag".into());
}
}
"-s" | "--single-port" => {
config.single_port = true;
}
"-r" | "--read-only" => {
config.read_only = true;
}
"-h" | "--help" => {
println!("TFTP Server Daemon\n");
println!("Usage: tftpd [OPTIONS]\n");
println!("Options:");
println!(" -i, --ip-address <IP ADDRESS>\t\tSet the ip address of the server (default: 127.0.0.1)");
println!(" -p, --port <PORT>\t\t\tSet the listening port of the server (default: 69)");
println!(" -d, --directory <DIRECTORY>\t\tSet the serving directory (default: current working directory)");
println!(" -rd, --receive-directory <DIRECTORY>\tSet the directory to receive files to (default: the directory setting)");
println!(" -sd, --send-directory <DIRECTORY>\tSet the directory to send files from (default: the directory setting)");
println!(" -s, --single-port\t\t\tUse a single port for both sending and receiving (default: false)");
println!(" -r, --read-only\t\t\tRefuse all write requests, making the server read-only (default: false)");
println!(" --overwrite\t\t\t\tOverwrite existing files (default: false)");
print_opt_local_help();
println!(" -h, --help\t\t\t\tPrint help information");
process::exit(0);
}
"--overwrite" => {
config.overwrite = true;
}
"-q" | "--quiet" => verbosity -= 1,
"-v" | "--verbose" => verbosity += 1,
#[cfg(feature = "debug_drop")]
"-D" => drop_set(args.next())?,
arg => if !parse_local_args(arg, &mut args, &mut config.opt_local)? {
return Err(format!("Invalid flag: {arg}").into());
}
}
}
if config.receive_directory.as_os_str().is_empty() {
config.receive_directory.clone_from(&config.directory);
}
if config.send_directory.as_os_str().is_empty() {
config.send_directory.clone_from(&config.directory);
}
verbosity_set(verbosity);
Ok(config)
}
}
#[cfg(test)]
mod tests {
use std::net::Ipv6Addr;
use super::*;
#[test]
fn parses_full_config() {
let config = Config::new(
[
"/",
"-i",
"0.0.0.0",
"-p",
"1234",
"-d",
"/",
"-rd",
"/",
"-sd",
"/",
"-s",
"-r",
"--keep-on-error",
]
.iter()
.map(|s| s.to_string()),
)
.unwrap();
assert_eq!(config.ip_address, Ipv4Addr::new(0, 0, 0, 0));
assert_eq!(config.port, 1234);
assert_eq!(config.directory, PathBuf::from("/"));
assert_eq!(config.receive_directory, PathBuf::from("/"));
assert_eq!(config.send_directory, PathBuf::from("/"));
assert!(!config.opt_local.clean_on_error);
assert!(config.single_port);
assert!(config.read_only);
}
#[test]
fn parses_config_with_ipv6() {
let config = Config::new(
["/", "-i", "0:0:0:0:0:0:0:0", "-p", "1234"]
.iter()
.map(|s| s.to_string()),
)
.unwrap();
assert_eq!(config.ip_address, Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0));
assert_eq!(config.port, 1234);
}
#[test]
fn parses_some_config() {
let config = Config::new(
["/", "-i", "0.0.0.0", "-d", "/"]
.iter()
.map(|s| s.to_string()),
)
.unwrap();
assert_eq!(config.ip_address, Ipv4Addr::new(0, 0, 0, 0));
assert_eq!(config.port, 69);
assert_eq!(config.directory, PathBuf::from("/"));
}
#[test]
fn sets_receive_directory_to_directory() {
let config = Config::new(["/", "-d", "/"].iter().map(|s| s.to_string())).unwrap();
assert_eq!(config.receive_directory, PathBuf::from("/"));
}
#[test]
fn sets_send_directory_to_directory() {
let config = Config::new(["/", "-d", "/"].iter().map(|s| s.to_string())).unwrap();
assert_eq!(config.send_directory, PathBuf::from("/"));
}
#[test]
fn returns_error_on_invalid_ip() {
assert!(Config::new(
["/", "-i", "1234.5678.9012.3456"]
.iter()
.map(|s| s.to_string()),
)
.is_err());
}
#[test]
fn returns_error_on_invalid_port() {
assert!(Config::new(["/", "-p", "1234567"].iter().map(|s| s.to_string()),).is_err());
}
#[test]
fn returns_error_on_invalid_directory() {
assert!(Config::new(
["/", "-d", "/this/does/not/exist"]
.iter()
.map(|s| s.to_string()),
)
.is_err());
}
#[test]
fn returns_error_on_invalid_up_directory() {
assert!(Config::new(
["/", "-ud", "/this/does/not/exist"]
.iter()
.map(|s| s.to_string()),
)
.is_err());
}
#[test]
fn returns_error_on_invalid_down_directory() {
assert!(Config::new(
["/", "-dd", "/this/does/not/exist"]
.iter()
.map(|s| s.to_string()),
)
.is_err());
}
#[test]
fn returns_error_on_invalid_duplicate_packets() {
assert!(Config::new(
["/", "--duplicate-packets", "-1"]
.iter()
.map(|s| s.to_string()),
)
.is_err());
}
#[test]
fn returns_error_on_max_duplicate_packets() {
assert!(Config::new(
["/", "--duplicate-packets", format!("{}", u8::MAX).as_str()]
.iter()
.map(|s| s.to_string()),
)
.is_err());
}
#[test]
fn initializes_duplicate_packets_as_zero() {
let config = Config::new(["/"].iter().map(|s| s.to_string())).unwrap();
assert_eq!(config.opt_local.repeat_count, 1);
}
}
0707010000000F000081A400000000000000000000000168DE72A500000A5C000000000000000000000000000000000000001E00000000rs-tftpd-0.5.0/src/convert.rsuse std::error::Error;
/// Allows conversions between byte arrays and other types.
///
/// # Example
///
/// ```rust
/// use tftpd::Convert;
///
/// assert_eq!(Convert::to_u16(&[0x01, 0x02]).unwrap(), 0x0102);
///
/// let (result, index) = Convert::to_string(b"hello world\0", 0).unwrap();
/// assert_eq!(result, "hello world");
/// assert_eq!(index, 11);
/// ```
pub struct Convert;
impl Convert {
/// Converts a [`u8`] slice to a [`u16`].
pub fn to_u16(buf: &[u8]) -> Result<u16, &'static str> {
if buf.len() < 2 {
Err("Error when converting to u16")
} else {
Ok(((buf[0] as u16) << 8) + buf[1] as u16)
}
}
/// Converts a zero-terminated [`u8`] slice to a [`String`], and returns the
/// size of the [`String`]. Useful for TFTP packet conversions.
pub fn to_string(buf: &[u8], start: usize) -> Result<(String, usize), Box<dyn Error>> {
match buf[start..].iter().position(|&b| b == 0x00) {
Some(index) => Ok((
String::from_utf8(buf[start..start + index].to_vec())?,
index + start,
)),
None => Err("Invalid string".into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converts_to_u16() {
assert_eq!(Convert::to_u16(&[0x01, 0x02]).unwrap(), 0x0102);
assert_eq!(Convert::to_u16(&[0x00, 0x02]).unwrap(), 0x0002);
assert_eq!(Convert::to_u16(&[0xfe, 0xdc, 0xba]).unwrap(), 0xfedc);
}
#[test]
fn returns_error_on_short_array() {
assert!(Convert::to_u16(&[0x01]).is_err());
assert!(Convert::to_u16(&[]).is_err());
}
#[test]
fn converts_to_string() {
let (result, index) = Convert::to_string(b"hello world\0", 0).unwrap();
assert_eq!(result, "hello world");
assert_eq!(index, 11);
let (result, index) = Convert::to_string(b"hello\0world", 0).unwrap();
assert_eq!(result, "hello");
assert_eq!(index, 5);
let (result, index) = Convert::to_string(b"\0hello world", 0).unwrap();
assert_eq!(result, "");
assert_eq!(index, 0);
}
#[test]
fn converts_to_string_with_index() {
let (result, index) = Convert::to_string(b"hello\0world\0", 0).unwrap();
assert_eq!(result, "hello");
assert_eq!(index, 5);
let (result, index) = Convert::to_string(b"hello\0world\0", 5).unwrap();
assert_eq!(result, "");
assert_eq!(index, 5);
let (result, index) = Convert::to_string(b"hello\0world\0", 6).unwrap();
assert_eq!(result, "world");
assert_eq!(index, 11);
}
}
07070100000010000081A400000000000000000000000168DE72A500000390000000000000000000000000000000000000001B00000000rs-tftpd-0.5.0/src/drop.rsuse std::sync::Mutex;
use std::error::Error;
use crate::Packet;
static TX_DROP: Mutex<Vec<i32>> = Mutex::new(Vec::new());
pub fn drop_set(opt : Option<String>) -> Result<(), Box<dyn Error>> {
if let Some(arg) = opt {
let mut tx_drop = TX_DROP.lock().unwrap();
for val in arg.split(',') {
let val_num = val.parse::<i32>()?;
tx_drop.push(val_num);
}
Ok(())
} else {
Err("Missing argument".into())
}
}
fn check_seq_num(num: u16) -> bool
{
let mut tx_drop = TX_DROP.lock().unwrap();
if !tx_drop.is_empty() && tx_drop[0] == num as i32 {
tx_drop.remove(0);
return true;
}
false
}
pub fn drop_check(packet: &Packet) -> bool
{
match packet {
Packet::Data{block_num, data: _ } => check_seq_num(*block_num),
Packet::Ack(block_num) => check_seq_num(*block_num),
_ => false,
}
}07070100000011000081A400000000000000000000000168DE72A500000625000000000000000000000000000000000000001A00000000rs-tftpd-0.5.0/src/lib.rs#![warn(missing_docs)]
//! Multithreaded TFTP daemon implemented in pure Rust.
//!
//! This server implements [RFC 1350](https://www.rfc-editor.org/rfc/rfc1350), The TFTP Protocol (Revision 2).
//! It also supports the following [RFC 2347](https://www.rfc-editor.org/rfc/rfc2347) TFTP Option Extensions:
//!
//! - [RFC 2348](https://www.rfc-editor.org/rfc/rfc2348) Blocksize Option
//! - [RFC 2349](https://www.rfc-editor.org/rfc/rfc2349) Timeout Interval Option
//! - [RFC 2349](https://www.rfc-editor.org/rfc/rfc2349) Transfer Size Option
//! - [RFC 7440](https://www.rfc-editor.org/rfc/rfc7440) Windowsize Option
//!
//! # Security
//!
//! 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.
#[cfg(feature = "client")]
mod client;
#[cfg(feature = "client")]
mod client_config;
mod config;
mod options;
mod convert;
mod packet;
mod server;
mod socket;
mod window;
mod worker;
mod log;
#[cfg(feature = "debug_drop")]
mod drop;
#[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 convert::Convert;
pub use options::TransferOption;
pub use options::OptionType;
pub use packet::ErrorCode;
pub use packet::Opcode;
pub use packet::Packet;
pub use server::Server;
pub use socket::ServerSocket;
pub use socket::Socket;
pub use window::Window;
pub use worker::Worker;
pub use log::verbosity;
07070100000012000081A400000000000000000000000168DE72A5000004B1000000000000000000000000000000000000001A00000000rs-tftpd-0.5.0/src/log.rs#![allow(unused_imports)]
use std::cmp::max;
use std::sync::OnceLock;
static VERBOSITY: OnceLock<usize> = OnceLock::new();
/// Verbosity should be set once at program start.
pub fn verbosity_set(verbosity : isize) {
VERBOSITY.get_or_init(|| max(0, verbosity) as usize);
}
/// Helper function to retrieve verbosity level for following macros
pub fn verbosity() -> usize {
*VERBOSITY.get().unwrap_or(&1)
}
/// Report error logs
#[macro_export]
macro_rules! log_err {
($($x:tt)*) => { eprintln!($($x)*) }
}
/// Report warning logs
#[macro_export]
macro_rules! log_warn {
($($x:tt)*) => { if 0 < $crate::verbosity() { println!($($x)*)} }
}
/// Report info logs
#[macro_export]
macro_rules! log_info {
($($x:tt)*) => { if 1 < $crate::verbosity() { println!($($x)*)} }
}
/// Report debug logs
#[macro_export]
#[cfg(debug_assertions)]
macro_rules! log_dbg {
($($x:tt)*) => { if 2 < $crate::verbosity() { println!($($x)*)} }
}
/// Do not compile debug logs with release target
#[macro_export]
#[cfg(not(debug_assertions))]
macro_rules! log_dbg {
($($x:tt)*) => { () }
}
pub(crate) use log_err;
pub(crate) use log_warn;
pub(crate) use log_info;
pub(crate) use log_dbg;
07070100000013000081A400000000000000000000000168DE72A500000442000000000000000000000000000000000000001B00000000rs-tftpd-0.5.0/src/main.rsuse std::{env, net::SocketAddr, process};
use tftpd::{Config, Server, log_err, log_warn};
fn main() {
server(env::args());
}
fn server<T: Iterator<Item = String>>(args: T) {
let config = Config::new(args).unwrap_or_else(|err| {
log_err!("Problem parsing arguments: {err}");
process::exit(1)
});
let mut server = Server::new(&config).unwrap_or_else(|err| {
log_err!(
"Problem creating server on {}:{}: {err}",
config.ip_address, config.port
);
process::exit(1)
});
if config.receive_directory == config.send_directory {
log_warn!(
"Running TFTP Server on {} in {}",
SocketAddr::new(config.ip_address, config.port),
config.directory.display()
);
} else {
log_warn!(
"Running TFTP Server on {}. Sending from {}, receiving to {}",
SocketAddr::new(config.ip_address, config.port),
config.send_directory.display(),
config.receive_directory.display(),
);
}
server.listen();
}
07070100000014000081A400000000000000000000000168DE72A5000029C3000000000000000000000000000000000000001E00000000rs-tftpd-0.5.0/src/options.rsuse std::error::Error;
use std::time::Duration;
use std::str::FromStr;
use std::fmt;
use crate::{server::RequestType, log::*};
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
pub const DEFAULT_BLOCK_SIZE: u16 = 512;
pub const DEFAULT_WINDOW_SIZE: u16 = 1;
pub const DEFAULT_WINDOW_WAIT: Duration = Duration::from_millis(0);
pub const DEFAULT_MAX_RETRIES: usize = 6;
pub const DEFAULT_ROLLOVER : Rollover = Rollover::Enforce0;
/// Enum used to set the block counter roll-over policy
#[derive(PartialEq, Clone, Copy, Debug)]
pub enum Rollover {
/// Rollover forbidden
None,
/// Enforce 0 in Rx and Tx
Enforce0,
/// Enforce 1 in Rx and Tx
Enforce1,
/// Allow both cases in Rx and use value in Tx
DontCare,
}
/// Local options `struct` used for storing and passing options for client and server
/// set directly from executable arguments. Though present on both sides of the
/// transfer, they can differ and are independent.
#[derive(Clone, Debug)]
pub struct OptionsPrivate {
/// Duplicate all packets sent from the server. (default: 0)
pub repeat_count: u8,
/// Should clean (delete) files after receiving errors. (default: true)
pub clean_on_error: bool,
/// Max count of retires (default: 6)
pub max_retries: usize,
/// Block counter roll-over policy (default: Enforce0)
pub rollover: Rollover,
}
impl Default for OptionsPrivate {
fn default() -> Self {
Self {
repeat_count: 1,
clean_on_error: true,
max_retries: DEFAULT_MAX_RETRIES,
rollover: DEFAULT_ROLLOVER,
}
}
}
/// Common options `struct` used for storing and passing options for client and server
/// negotiated before data exchange. User can set them on client side as executable
/// arguments, server will then validate and send them back, and client will use this
/// definitive version.
/// Some options are defined by RFC and some others are non standard.
#[derive(Clone, Debug, PartialEq)]
pub struct OptionsProtocol {
/// Blocksize to use during transfer. (default: 512)
pub block_size: u16,
/// Windowsize to use during transfer. (default: 1)
pub window_size: u16,
/// Inter packets wait delay in windows (default: 10ms)
pub window_wait: Duration,
/// Timeout to use during transfer. (default: 5s)
pub timeout: Duration,
/// Size of the file to transfer (default: N/A)
pub transfer_size: Option<u64>,
}
impl OptionsProtocol {
pub fn prepare(&self) -> Vec<TransferOption> {
let mut options = vec![
TransferOption {
option: OptionType::BlockSize,
value: self.block_size as u64,
},
TransferOption {
option: OptionType::TransferSize,
value: self.transfer_size.unwrap_or(0),
},
TransferOption {
option: OptionType::WindowSize,
value: self.window_size as u64,
},
];
if self.window_wait.as_millis() != 0 {
options.push(TransferOption {
option: OptionType::WindowWait,
value: self.window_wait.as_millis() as u64,
});
}
options.push(if self.timeout.subsec_millis() == 0 {
TransferOption {
option: OptionType::Timeout,
value: self.timeout.as_secs(),
}
} else {
TransferOption {
option: OptionType::TimeoutMs,
value: self.timeout.as_millis() as u64,
}
});
options
}
pub fn parse(options: &mut [TransferOption], request_type: RequestType) -> Result<OptionsProtocol, &'static str> {
let mut opt_common = OptionsProtocol::default();
for option in options {
let TransferOption {
option: option_type,
value,
} = option;
match option_type {
OptionType::BlockSize => {
if *value == 0 {
// RFC 2348 requests block size to be in range 8-65464
// but we use 1-65464 as 1 is useful to speed up some tests
log_warn!(" Invalid block size 0. Changed to {DEFAULT_BLOCK_SIZE}.");
*value = DEFAULT_BLOCK_SIZE as u64;
} else if 65464 < *value {
log_warn!(" Invalid block size {}. Changed to 65464.", *value);
*value = 65464;
}
opt_common.block_size = *value as u16;
}
OptionType::TransferSize => match request_type {
RequestType::Read(size) => {
*value = size;
opt_common.transfer_size = Some(size);
}
RequestType::Write => opt_common.transfer_size = Some(*value),
},
OptionType::Timeout => {
if *value == 0 {
// RFC 2349 requests timeout to be in range 1-255
log_warn!(" Invalid timeout value 0. Changed to 1.");
*value = 1;
} else if 255 < *value {
log_warn!(" Invalid timeout value {}. Changed to 255.", *value);
*value = 255;
}
opt_common.timeout = Duration::from_secs(*value);
}
OptionType::TimeoutMs => {
if *value == 0 {
log_warn!(" Invalid timeoutms value 0. Changed to 1.");
*value = 1;
}
opt_common.timeout = Duration::from_millis(*value);
}
OptionType::WindowSize => {
if *value == 0 {
// RFC 7440 requests window to be in range 1-65535
log_warn!(" Invalid window size 0. Changed to 1.");
*value = 1;
} else if 65535 < *value {
log_warn!(" Invalid window size {}. Changed to 65535.", *value);
*value = 65535;
}
opt_common.window_size = *value as u16;
}
OptionType::WindowWait => {
opt_common.window_wait = Duration::from_millis(*value);
}
}
}
Ok(opt_common)
}
pub fn apply(&mut self, options: &Vec<TransferOption>) -> Result<(), Box<dyn Error>> {
for option in options {
match option.option {
OptionType::BlockSize => self.block_size = option.value as u16,
OptionType::WindowSize => self.window_size = option.value as u16,
OptionType::WindowWait => self.window_wait = Duration::from_millis(option.value),
OptionType::Timeout => self.timeout = Duration::from_secs(option.value),
OptionType::TimeoutMs => self.timeout = Duration::from_millis(option.value),
OptionType::TransferSize => self.transfer_size = Some(option.value),
}
}
Ok(())
}
}
impl Default for OptionsProtocol {
fn default() -> Self {
Self {
block_size: DEFAULT_BLOCK_SIZE,
window_size: DEFAULT_WINDOW_SIZE,
window_wait: DEFAULT_WINDOW_WAIT,
timeout: DEFAULT_TIMEOUT,
transfer_size: None,
}
}
}
/// TransferOption `struct` represents the TFTP transfer options.
///
/// This `struct` has a function implementation for converting [`TransferOption`]s
/// to [`Vec<u8>`]s.
///
/// # Example
///
/// ```rust
/// use tftpd::{TransferOption, OptionType};
///
/// assert_eq!(TransferOption { option: OptionType::BlockSize, value: 1432 }.as_bytes(), vec![
/// 0x62, 0x6C, 0x6B, 0x73, 0x69, 0x7A, 0x65, 0x00, 0x31, 0x34, 0x33, 0x32,
/// 0x00,
/// ]);
/// ```
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TransferOption {
/// Type of the option
pub option: OptionType,
/// Value of the option
pub value: u64,
}
impl TransferOption {
/// Converts a [`TransferOption`] to a [`Vec<u8>`].
pub fn as_bytes(&self) -> Vec<u8> {
[
self.option.as_str().as_bytes(),
&[0x00],
self.value.to_string().as_bytes(),
&[0x00],
]
.concat()
}
}
/// Wrapper to print TransferOption slices
pub struct OptionFmt<'a>(pub &'a [TransferOption]);
impl fmt::Display for OptionFmt<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for (i, e) in self.0.iter().enumerate() {
if i != 0 { write!(f, ", ")? }
write!(f, "{}:{}", e.option.as_str(), e.value)?;
}
Ok(())
}
}
/// OptionType `enum` represents the TFTP option types
///
/// This `enum` has function implementations for conversion between
/// [`OptionType`]s and [`str`]s.
///
/// # Example
///
/// ```rust
/// use tftpd::OptionType;
///
/// assert_eq!(OptionType::BlockSize, "blksize".parse().unwrap());
/// assert_eq!("tsize", OptionType::TransferSize.as_str());
/// ```
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum OptionType {
/// Block Size option type
BlockSize,
/// Transfer Size option type
TransferSize,
/// Timeout option type
Timeout,
/// Timeout in ms option type
TimeoutMs,
/// Windowsize option type
WindowSize,
/// Windowwait option type
WindowWait,
}
impl OptionType {
/// Converts an [`OptionType`] to a [`str`].
pub fn as_str(&self) -> &'static str {
match self {
OptionType::BlockSize => "blksize",
OptionType::TransferSize => "tsize",
OptionType::Timeout => "timeout",
OptionType::TimeoutMs => "timeoutms",
OptionType::WindowSize => "windowsize",
OptionType::WindowWait => "windowwait",
}
}
}
impl FromStr for OptionType {
type Err = &'static str;
/// Converts a [`str`] to an [`OptionType`].
fn from_str(value: &str) -> Result<Self, &'static str> {
match value {
"blksize" => Ok(OptionType::BlockSize),
"tsize" => Ok(OptionType::TransferSize),
"timeout" => Ok(OptionType::Timeout),
"timeoutms" => Ok(OptionType::TimeoutMs),
"windowsize" => Ok(OptionType::WindowSize),
"windowwait" => Ok(OptionType::WindowWait),
_ => Err("Invalid option type"),
}
}
}
07070100000015000081A400000000000000000000000168DE72A500005583000000000000000000000000000000000000001D00000000rs-tftpd-0.5.0/src/packet.rsuse std::error::Error;
use std::str::FromStr;
use std::fmt;
use crate::{Convert, TransferOption, OptionType};
/// Packet `enum` represents the valid TFTP packet types.
///
/// This `enum` has function implementaions for serializing [`Packet`]s into
/// [`Vec<u8>`]s and deserializing [`u8`] slices to [`Packet`]s.
///
/// # Example
/// ```rust
/// use tftpd::Packet;
///
/// let packet = Packet::Data { block_num: 15, data: vec![0x01, 0x02, 0x03] };
///
/// assert_eq!(packet.serialize().unwrap(), vec![0x00, 0x03, 0x00, 0x0F, 0x01, 0x02, 0x03]);
/// assert_eq!(Packet::deserialize(&[0x00, 0x03, 0x00, 0x0F, 0x01, 0x02, 0x03]).unwrap(), packet);
/// ```
#[derive(Debug, PartialEq)]
pub enum Packet {
/// Read Request `struct`
Rrq {
/// Name of the requested file
filename: String,
/// Transfer mode
mode: String,
/// Transfer options
options: Vec<TransferOption>,
},
/// Write Request `struct`
Wrq {
/// Name of the requested file
filename: String,
/// Transfer mode
mode: String,
/// Transfer options
options: Vec<TransferOption>,
},
/// Data `struct`
Data {
/// Block number
block_num: u16,
/// Data
data: Vec<u8>,
},
/// Acknowledgement `tuple` with block number
Ack(u16),
/// Error `struct`
Error {
/// Error code
code: ErrorCode,
/// Error message
msg: String,
},
/// Option acknowledgement `tuple` with transfer options
Oack(Vec<TransferOption>),
}
impl Packet {
/// Deserializes a [`u8`] slice into a [`Packet`].
pub fn deserialize(buf: &[u8]) -> Result<Packet, Box<dyn Error>> {
if buf.len() < 2 {
return Err("Buffer too short to serialize".into());
}
let opcode = Opcode::from_u16(Convert::to_u16(&buf[0..=1])?)?;
match opcode {
Opcode::Rrq | Opcode::Wrq => parse_rq(buf, opcode),
Opcode::Data => parse_data(buf),
Opcode::Ack => parse_ack(buf),
Opcode::Oack => parse_oack(buf),
Opcode::Error => parse_error(buf),
}
}
/// Serializes a [`Packet`] into a [`Vec<u8>`].
pub fn serialize(&self) -> Result<Vec<u8>, &'static str> {
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::Ack(block_num) => Ok(serialize_ack(block_num)),
Packet::Error { code, msg } => Ok(serialize_error(code, msg)),
Packet::Oack(options) => Ok(serialize_oack(options)),
}
}
}
/// Opcode `enum` represents the opcodes used in the TFTP definition.
///
/// This `enum` has function implementations for converting [`u16`]s to
/// [`Opcode`]s and [`Opcode`]s to [`u8`] arrays.
///
/// # Example
///
/// ```rust
/// use tftpd::Opcode;
///
/// assert_eq!(Opcode::from_u16(3).unwrap(), Opcode::Data);
/// assert_eq!(Opcode::Ack.as_bytes(), [0x00, 0x04]);
/// ```
#[repr(u16)]
#[derive(Debug, PartialEq)]
pub enum Opcode {
/// Read request opcode
Rrq = 0x0001,
/// Write request opcode
Wrq = 0x0002,
/// Data opcode
Data = 0x0003,
/// Acknowledgement opcode
Ack = 0x0004,
/// Error opcode
Error = 0x0005,
/// Option acknowledgement opcode
Oack = 0x0006,
}
impl Opcode {
/// Converts a [`u16`] to an [`Opcode`].
pub fn from_u16(val: u16) -> Result<Opcode, &'static str> {
match val {
0x0001 => Ok(Opcode::Rrq),
0x0002 => Ok(Opcode::Wrq),
0x0003 => Ok(Opcode::Data),
0x0004 => Ok(Opcode::Ack),
0x0005 => Ok(Opcode::Error),
0x0006 => Ok(Opcode::Oack),
_ => Err("Invalid opcode"),
}
}
/// Converts a [`u16`] to a [`u8`] array with 2 elements.
pub const fn as_bytes(self) -> [u8; 2] {
(self as u16).to_be_bytes()
}
}
/// ErrorCode `enum` represents the error codes used in the TFTP definition.
///
/// This `enum` has function implementations for converting [`u16`]s to
/// [`ErrorCode`]s and [`ErrorCode`]s to [`u8`] arrays.
///
/// # Example
///
/// ```rust
/// use tftpd::ErrorCode;
///
/// assert_eq!(ErrorCode::from_u16(3).unwrap(), ErrorCode::DiskFull);
/// assert_eq!(ErrorCode::FileExists.as_bytes(), [0x00, 0x06]);
/// ```
#[repr(u16)]
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum ErrorCode {
/// Not Defined error code
NotDefined = 0,
/// File not found error code
FileNotFound = 1,
/// Access violation error code
AccessViolation = 2,
/// Disk full error code
DiskFull = 3,
/// Illegal operation error code
IllegalOperation = 4,
/// Unknown ID error code
UnknownId = 5,
/// File exists error code
FileExists = 6,
/// No such user error code
NoSuchUser = 7,
/// Refused option error code
RefusedOption = 8,
}
impl ErrorCode {
/// Converts a [`u16`] to an [`ErrorCode`].
pub fn from_u16(code: u16) -> Result<ErrorCode, &'static str> {
match code {
0 => Ok(ErrorCode::NotDefined),
1 => Ok(ErrorCode::FileNotFound),
2 => Ok(ErrorCode::AccessViolation),
3 => Ok(ErrorCode::DiskFull),
4 => Ok(ErrorCode::IllegalOperation),
5 => Ok(ErrorCode::UnknownId),
6 => Ok(ErrorCode::FileExists),
7 => Ok(ErrorCode::NoSuchUser),
8 => Ok(ErrorCode::RefusedOption),
_ => Err("Invalid error code"),
}
}
/// Converts an [`ErrorCode`] to a [`u8`] array with 2 elements.
pub fn as_bytes(self) -> [u8; 2] {
(self as u16).to_be_bytes()
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorCode::NotDefined => write!(f, "Not Defined"),
ErrorCode::FileNotFound => write!(f, "File Not Found"),
ErrorCode::AccessViolation => write!(f, "Access Violation"),
ErrorCode::DiskFull => write!(f, "Disk Full"),
ErrorCode::IllegalOperation => write!(f, "Illegal Operation"),
ErrorCode::UnknownId => write!(f, "Unknown ID"),
ErrorCode::FileExists => write!(f, "File Exists"),
ErrorCode::NoSuchUser => write!(f, "No Such User"),
ErrorCode::RefusedOption => write!(f, "Refused option"),
}
}
}
fn parse_rq(buf: &[u8], opcode: Opcode) -> Result<Packet, Box<dyn Error>> {
let mut options = vec![];
let filename: String;
let mode: String;
let mut zero_index: usize;
(filename, zero_index) = Convert::to_string(buf, 2)?;
(mode, zero_index) = Convert::to_string(buf, zero_index + 1)?;
let mut value: String;
let mut option;
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()?,
});
}
}
match opcode {
Opcode::Rrq => Ok(Packet::Rrq {
filename,
mode,
options,
}),
Opcode::Wrq => Ok(Packet::Wrq {
filename,
mode,
options,
}),
_ => Err("Non request opcode".into()),
}
}
fn parse_data(buf: &[u8]) -> Result<Packet, Box<dyn Error>> {
Ok(Packet::Data {
block_num: Convert::to_u16(&buf[2..])?,
data: buf[4..].to_vec(),
})
}
fn parse_ack(buf: &[u8]) -> Result<Packet, Box<dyn Error>> {
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>> {
let code = ErrorCode::from_u16(Convert::to_u16(&buf[2..])?)?;
if let Ok((msg, _)) = Convert::to_string(buf, 4) {
Ok(Packet::Error { code, msg })
} else {
Ok(Packet::Error {
code,
msg: "(no message)".to_string(),
})
}
}
fn serialize_rrq(filename: &String, mode: &String, options: &Vec<TransferOption>) -> Vec<u8> {
let mut buf = [
&Opcode::Rrq.as_bytes(),
filename.as_bytes(),
&[0x00],
mode.as_bytes(),
&[0x00],
]
.concat();
for option in options {
buf = [buf, option.as_bytes()].concat();
}
buf
}
fn serialize_wrq(filename: &String, mode: &String, options: &Vec<TransferOption>) -> Vec<u8> {
let mut buf = [
&Opcode::Wrq.as_bytes(),
filename.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> {
[
&Opcode::Data.as_bytes(),
&block_num.to_be_bytes(),
data.as_slice(),
]
.concat()
}
fn serialize_ack(block_num: &u16) -> Vec<u8> {
[Opcode::Ack.as_bytes(), block_num.to_be_bytes()].concat()
}
fn serialize_error(code: &ErrorCode, msg: &String) -> Vec<u8> {
[
&Opcode::Error.as_bytes()[..],
&code.as_bytes()[..],
msg.as_bytes(),
&[0x00],
]
.concat()
}
fn serialize_oack(options: &Vec<TransferOption>) -> Vec<u8> {
let mut buf = Opcode::Oack.as_bytes().to_vec();
for option in options {
buf = [buf, option.as_bytes()].concat();
}
buf
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_read_request() {
let buf = [
&Opcode::Rrq.as_bytes()[..],
("test.png".as_bytes()),
&[0x00],
("octet".as_bytes()),
&[0x00],
]
.concat();
if let Ok(Packet::Rrq {
filename,
mode,
options,
}) = parse_rq(&buf, Opcode::Rrq)
{
assert_eq!(filename, "test.png");
assert_eq!(mode, "octet");
assert_eq!(options.len(), 0);
} else {
panic!("cannot parse read request")
}
}
#[test]
fn parses_read_request_with_options() {
let buf = [
&Opcode::Rrq.as_bytes()[..],
("test.png".as_bytes()),
&[0x00],
("octet".as_bytes()),
&[0x00],
(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::Rrq {
filename,
mode,
options,
}) = parse_rq(&buf, Opcode::Rrq)
{
assert_eq!(filename, "test.png");
assert_eq!(mode, "octet");
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]
fn parses_write_request() {
let buf = [
&Opcode::Wrq.as_bytes()[..],
("test.png".as_bytes()),
&[0x00],
("octet".as_bytes()),
&[0x00],
]
.concat();
if let Ok(Packet::Wrq {
filename,
mode,
options,
}) = parse_rq(&buf, Opcode::Wrq)
{
assert_eq!(filename, "test.png");
assert_eq!(mode, "octet");
assert_eq!(options.len(), 0);
} else {
panic!("cannot parse write request")
}
}
#[test]
fn parses_write_request_with_options() {
let buf = [
&Opcode::Wrq.as_bytes()[..],
("test.png".as_bytes()),
&[0x00],
("octet".as_bytes()),
&[0x00],
(OptionType::TransferSize.as_str().as_bytes()),
&[0x00],
("12341234".as_bytes()),
&[0x00],
(OptionType::BlockSize.as_str().as_bytes()),
&[0x00],
("1024".as_bytes()),
&[0x00],
]
.concat();
if let Ok(Packet::Wrq {
filename,
mode,
options,
}) = parse_rq(&buf, Opcode::Wrq)
{
assert_eq!(filename, "test.png");
assert_eq!(mode, "octet");
assert_eq!(options.len(), 2);
assert_eq!(
options[0],
TransferOption {
option: OptionType::TransferSize,
value: 12341234
}
);
assert_eq!(
options[1],
TransferOption {
option: OptionType::BlockSize,
value: 1024
}
);
} else {
panic!("cannot parse write request with options")
}
}
#[test]
fn parses_data() {
let buf = [
&Opcode::Data.as_bytes()[..],
&5u16.to_be_bytes(),
&[
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
],
]
.concat();
if let Ok(Packet::Data { block_num, data }) = parse_data(&buf) {
assert_eq!(block_num, 5);
assert_eq!(
data,
[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C]
);
} else {
panic!("cannot parse data")
}
}
#[test]
fn parses_ack() {
let buf = [&Opcode::Ack.as_bytes()[..], &12u16.to_be_bytes()].concat();
if let Ok(Packet::Ack(block_num)) = parse_ack(&buf) {
assert_eq!(block_num, 12);
} else {
panic!("cannot parse ack")
}
}
#[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]
fn parses_error() {
let buf = [
&Opcode::Error.as_bytes()[..],
&ErrorCode::FileExists.as_bytes(),
"file already exists".as_bytes(),
&[0x00],
]
.concat();
if let Ok(Packet::Error { code, msg }) = parse_error(&buf) {
assert_eq!(code, ErrorCode::FileExists);
assert_eq!(msg, "file already exists");
} else {
panic!("cannot parse error")
}
}
#[test]
fn parses_error_without_message() {
let buf = [
&Opcode::Error.as_bytes()[..],
&ErrorCode::FileExists.as_bytes(),
&[0x00],
]
.concat();
if let Ok(Packet::Error { code, msg }) = parse_error(&buf) {
assert_eq!(code, ErrorCode::FileExists);
assert_eq!(msg, "");
} else {
panic!("cannot parse error")
}
}
#[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]
fn serializes_data() {
let serialized_data = vec![0x00, 0x03, 0x00, 0x10, 0x01, 0x02, 0x03, 0x04];
assert_eq!(
serialize_data(&16, &vec![0x01, 0x02, 0x03, 0x04]),
serialized_data
);
}
#[test]
fn serializes_ack() {
let serialized_ack = vec![0x00, 0x04, 0x04, 0xD2];
assert_eq!(serialize_ack(&1234), serialized_ack);
}
#[test]
fn serializes_error() {
let serialized_error = vec![
0x00, 0x05, 0x00, 0x04, 0x69, 0x6C, 0x6C, 0x65, 0x67, 0x61, 0x6C, 0x20, 0x6F, 0x70,
0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x00,
];
assert_eq!(
serialize_error(
&ErrorCode::IllegalOperation,
&"illegal operation".to_string()
),
serialized_error
);
}
#[test]
fn serializes_oack() {
let serialized_oack = vec![
0x00, 0x06, 0x62, 0x6C, 0x6B, 0x73, 0x69, 0x7A, 0x65, 0x00, 0x31, 0x34, 0x33, 0x32,
0x00,
];
assert_eq!(
serialize_oack(&vec![TransferOption {
option: OptionType::BlockSize,
value: 1432
}]),
serialized_oack
);
}
}
07070100000016000081A400000000000000000000000168DE72A500003F71000000000000000000000000000000000000001D00000000rs-tftpd-0.5.0/src/server.rsuse std::cmp::max;
use std::collections::HashMap;
use std::error::Error;
use std::net::{SocketAddr, UdpSocket};
use std::path::{Path, PathBuf, MAIN_SEPARATOR};
use std::sync::mpsc::Sender;
use std::time::Duration;
use crate::{Config, ErrorCode, Packet};
use crate::{ServerSocket, Socket, TransferOption, Worker, log::*};
use crate::options::{DEFAULT_BLOCK_SIZE, OptionsPrivate, OptionsProtocol, OptionFmt};
#[cfg(test)]
use crate::OptionType;
/// Server `struct` is used for handling incoming TFTP requests.
///
/// This `struct` is meant to be created by [`Server::new()`]. See its
/// documentation for more.
///
/// # Example
///
/// ```rust
/// // Create the TFTP server.
/// use tftpd::{Config, Server};
///
/// let args = ["/", "-p", "1234"].iter().map(|s| s.to_string());
/// let config = Config::new(args).unwrap();
/// let server = Server::new(&config).unwrap();
/// ```
pub struct Server {
socket: UdpSocket,
receive_directory: PathBuf,
send_directory: PathBuf,
single_port: bool,
read_only: bool,
overwrite: bool,
largest_block_size: u16,
clients: HashMap<SocketAddr, Sender<Packet>>,
opt_local: OptionsPrivate,
}
impl Server {
/// Creates the TFTP Server with the supplied [`Config`].
pub fn new(config: &Config) -> Result<Server, Box<dyn Error>> {
let socket = UdpSocket::bind(SocketAddr::from((config.ip_address, config.port)))?;
let server = Server {
socket,
receive_directory: config.receive_directory.clone(),
send_directory: config.send_directory.clone(),
single_port: config.single_port,
read_only: config.read_only,
overwrite: config.overwrite,
largest_block_size: DEFAULT_BLOCK_SIZE,
clients: HashMap::new(),
opt_local: config.opt_local.clone(),
};
Ok(server)
}
/// Starts listening for connections. Note that this function does not finish running until termination.
pub fn listen(&mut self) {
loop {
let received = if self.single_port {
self.socket.recv_from_with_size(self.largest_block_size as usize)
} else {
Socket::recv_from(&self.socket)
};
if let Ok((packet, from)) = received {
match packet {
Packet::Rrq {
filename,
mut options,
..
} => {
log_info!("Received Read request from {from}: {filename}");
if let Err(err) = self.handle_rrq(filename.clone(), &mut options, &from) {
log_err!("Error while sending file: {err}")
}
}
Packet::Wrq {
filename,
mut options,
..
} => {
if self.read_only {
if Socket::send_to(
&self.socket,
&Packet::Error {
code: ErrorCode::AccessViolation,
msg: "server is read-only".to_string(),
},
&from,
)
.is_err()
{
log_err!("Could not send error packet");
};
log_warn!("Received write request while in read-only mode");
continue;
}
log_info!("Received Write request from {from}: {filename}");
if let Err(err) = self.handle_wrq(filename, &mut options, &from) {
log_err!("Error while receiving file: {err}")
}
}
_ => {
if self.route_packet(packet, &from).is_err() {
if Socket::send_to(
&self.socket,
&Packet::Error {
code: ErrorCode::IllegalOperation,
msg: "invalid request".to_string(),
},
&from,
)
.is_err()
{
log_err!("Could not send error packet");
};
log_warn!("Received invalid request");
}
}
};
}
}
}
fn handle_rrq(
&mut self,
filename: String,
options: &mut [TransferOption],
to: &SocketAddr,
) -> Result<(), Box<dyn Error>> {
let file_path = convert_file_path(&filename);
let file_path = &self.send_directory.join(file_path);
match check_file_exists(file_path, &self.send_directory) {
ErrorCode::FileNotFound => {
log_warn!("Cannot find requested file: {}", file_path.display());
Socket::send_to(
&self.socket,
&Packet::Error {
code: ErrorCode::FileNotFound,
msg: format!("file {} does not exist", file_path.display()),
},
to,
)
}
ErrorCode::AccessViolation => {
log_warn!("Cannot access requested file: {}", file_path.display());
Socket::send_to(
&self.socket,
&Packet::Error {
code: ErrorCode::AccessViolation,
msg: format!("file access violation: {}", file_path.display()),
},
to,
)
}
ErrorCode::FileExists => {
let worker_options = OptionsProtocol::parse(options, RequestType::Read(file_path.metadata()?.len()))?;
let mut socket: Box<dyn Socket>;
if self.single_port {
let single_socket = create_single_socket(&self.socket, to, worker_options.timeout)?;
self.clients.insert(*to, single_socket.sender());
self.largest_block_size =
max(self.largest_block_size, worker_options.block_size);
socket = Box::new(single_socket);
} else {
socket = Box::new(create_multi_socket(&self.socket.local_addr()?, to)?);
}
socket.set_read_timeout(worker_options.timeout)?;
socket.set_write_timeout(worker_options.timeout)?;
log_dbg!(" Accepted options: {}", OptionFmt(options));
accept_request(
&socket,
options,
RequestType::Read(file_path.metadata()?.len()),
)?;
let worker = Worker::new(
socket,
file_path.clone(),
self.opt_local.clone(),
worker_options.clone(),
);
worker.send(!options.is_empty())?;
Ok(())
}
_ => Err("Unexpected error code when checking file".into()),
}
}
fn handle_wrq(
&mut self,
filename: String,
options: &mut [TransferOption],
to: &SocketAddr,
) -> Result<(), Box<dyn Error>> {
let file_path = convert_file_path(&filename);
let file_path = &self.receive_directory.join(file_path);
let initialize_write = &mut || -> Result<(), Box<dyn Error>> {
let worker_options = OptionsProtocol::parse(options, RequestType::Write)?;
let mut socket: Box<dyn Socket>;
if self.single_port {
let single_socket = create_single_socket(&self.socket, to, worker_options.timeout)?;
self.clients.insert(*to, single_socket.sender());
self.largest_block_size = max(self.largest_block_size, worker_options.block_size);
socket = Box::new(single_socket);
} else {
socket = Box::new(create_multi_socket(&self.socket.local_addr()?, to)?);
}
socket.set_read_timeout(worker_options.timeout)?;
socket.set_write_timeout(worker_options.timeout)?;
log_dbg!(" Accepted options: {}", OptionFmt(options));
accept_request(&socket, options, RequestType::Write)?;
let worker = Worker::new(
socket,
file_path.clone(),
self.opt_local.clone(),
worker_options.clone(),
);
worker.receive()?;
Ok(())
};
match check_file_exists(file_path, &self.receive_directory) {
ErrorCode::FileExists => {
if self.overwrite {
initialize_write()
} else {
log_err!("File {} already exists", file_path.display());
Socket::send_to(
&self.socket,
&Packet::Error {
code: ErrorCode::FileExists,
msg: "requested file already exists".to_string(),
},
to,
)
}
}
ErrorCode::AccessViolation => {
log_err!("Access violation detected for file {}", file_path.display());
Socket::send_to(
&self.socket,
&Packet::Error {
code: ErrorCode::AccessViolation,
msg: format!("file access violation: {}", file_path.display()),
},
to,
)
}
ErrorCode::FileNotFound => initialize_write(),
_ => Err("Unexpected error code when checking file".into()),
}
}
fn route_packet(&self, packet: Packet, to: &SocketAddr) -> Result<(), Box<dyn Error>> {
if self.clients.contains_key(to) {
self.clients[to].send(packet)?;
Ok(())
} else {
Err("No client found for packet".into())
}
}
}
#[derive(Debug, PartialEq)]
pub enum RequestType {
Read(u64),
Write,
}
pub fn convert_file_path(filename: &str) -> PathBuf {
let mut chars_filename = filename.chars();
let nodrive_filename = if chars_filename.nth(1) == Some(':') {
//nth() is consumming 2 firsts chars
chars_filename.as_str()
} else {
filename
};
let formatted_filename = nodrive_filename.trim_start_matches(['/', '\\']).to_string();
let normalized_filename = if MAIN_SEPARATOR == '\\' {
formatted_filename.replace('/', "\\")
} else {
formatted_filename.replace('\\', "/")
};
PathBuf::from(normalized_filename)
}
fn create_single_socket(
socket: &UdpSocket,
remote: &SocketAddr,
timeout: Duration,
) -> Result<ServerSocket, Box<dyn Error>> {
let socket = ServerSocket::new(socket.try_clone()?, *remote, timeout);
Ok(socket)
}
fn create_multi_socket(
addr: &SocketAddr,
remote: &SocketAddr,
) -> Result<UdpSocket, Box<dyn Error>> {
let socket = UdpSocket::bind(SocketAddr::from((addr.ip(), 0)))?;
socket.connect(remote)?;
Ok(socket)
}
fn accept_request<T: Socket>(
socket: &T,
options: &[TransferOption],
request_type: RequestType,
) -> Result<(), Box<dyn Error>> {
if !options.is_empty() {
socket.send(&Packet::Oack(options.to_vec()))?;
} else if request_type == RequestType::Write {
socket.send(&Packet::Ack(0))?;
}
Ok(())
}
fn check_file_exists(file: &Path, directory: &PathBuf) -> ErrorCode {
if !validate_file_path(file, directory) {
return ErrorCode::AccessViolation;
}
if !file.exists() {
return ErrorCode::FileNotFound;
}
ErrorCode::FileExists
}
fn validate_file_path(file: &Path, directory: &PathBuf) -> bool {
!file.to_str().unwrap().contains("..") && file.ancestors().any(|a| a == directory)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converts_file_path() {
let path = convert_file_path("test.file");
let mut correct_path = PathBuf::new();
correct_path.push("test.file");
assert_eq!(path, correct_path);
let path = convert_file_path("\\test.file");
let mut correct_path = PathBuf::new();
correct_path.push("test.file");
assert_eq!(path, correct_path);
let path = convert_file_path("/test.file");
let mut correct_path = PathBuf::new();
correct_path.push("test.file");
assert_eq!(path, correct_path);
let path = convert_file_path("C:\\test.file");
let mut correct_path = PathBuf::new();
correct_path.push("test.file");
assert_eq!(path, correct_path);
let path = convert_file_path("test\\test.file");
let mut correct_path = PathBuf::new();
correct_path.push("test");
correct_path.push("test.file");
assert_eq!(path, correct_path);
let path = convert_file_path("test/test/test.file");
let mut correct_path = PathBuf::new();
correct_path.push("test");
correct_path.push("test");
correct_path.push("test.file");
assert_eq!(path, correct_path);
}
#[test]
fn validates_file_path() {
assert!(validate_file_path(
&PathBuf::from("/dir/test/file"),
&PathBuf::from("/dir/test")
));
assert!(!validate_file_path(
&PathBuf::from("/system/data.txt"),
&PathBuf::from("/dir/test")
));
assert!(!validate_file_path(
&PathBuf::from("~/some_data.txt"),
&PathBuf::from("/dir/test")
));
assert!(!validate_file_path(
&PathBuf::from("/dir/test/../file"),
&PathBuf::from("/dir/test")
));
}
#[test]
fn parses_write_options() {
let mut options = vec![
TransferOption {
option: OptionType::BlockSize,
value: 1024,
},
TransferOption {
option: OptionType::TransferSize,
value: 0,
},
TransferOption {
option: OptionType::Timeout,
value: 5,
},
];
let work_type = RequestType::Read(12341234);
let worker_options = OptionsProtocol::parse(&mut options, work_type).unwrap();
assert_eq!(options[0].value, worker_options.block_size as u64);
assert_eq!(options[1].value, worker_options.transfer_size.unwrap());
assert_eq!(options[2].value, worker_options.timeout.as_secs());
}
#[test]
fn parses_read_options() {
let mut options = vec![
TransferOption {
option: OptionType::BlockSize,
value: 1024,
},
TransferOption {
option: OptionType::TransferSize,
value: 44554455,
},
TransferOption {
option: OptionType::Timeout,
value: 5,
},
];
let work_type = RequestType::Write;
let worker_options = OptionsProtocol::parse(&mut options, work_type).unwrap();
assert_eq!(options[0].value, worker_options.block_size as u64);
assert_eq!(options[1].value, worker_options.transfer_size.unwrap());
assert_eq!(options[2].value, worker_options.timeout.as_secs());
}
#[test]
fn parses_default_options() {
assert_eq!(
OptionsProtocol::parse(&mut [], RequestType::Write).unwrap(),
OptionsProtocol::default(),
);
}
}
07070100000017000081A400000000000000000000000168DE72A500002461000000000000000000000000000000000000001D00000000rs-tftpd-0.5.0/src/socket.rsuse crate::Packet;
use std::{
io::{Error as IoError, ErrorKind},
error::Error,
net::{SocketAddr, UdpSocket},
sync::{
mpsc::{self, Receiver, Sender},
Mutex,
},
time::Duration,
};
const MAX_REQUEST_PACKET_SIZE: usize = 512;
/// Socket `trait` is used to allow building custom sockets to be used for
/// TFTP communication.
pub trait Socket: Send + Sync + 'static {
/// Sends a [`Packet`] to the socket's connected remote [`Socket`].
fn send(&self, packet: &Packet) -> Result<(), Box<dyn Error>>;
/// Sends a [`Packet`] to the specified remote [`Socket`].
fn send_to(&self, packet: &Packet, to: &SocketAddr) -> Result<(), Box<dyn Error>>;
/// Receives a [`Packet`] from the socket's connected remote [`Socket`]. This
/// function cannot handle large data packets due to the limited buffer size,
/// so it is intended for only accepting incoming requests. For handling data
/// packets, see [`Socket::recv_with_size()`].
fn recv(&self) -> Result<Packet, Box<dyn Error>> {
self.recv_with_size(MAX_REQUEST_PACKET_SIZE)
}
/// Receives a data packet from the socket's connected remote, and returns the
/// parsed [`Packet`]. The received packet can actually be of any type, however,
/// this function also allows supplying the buffer size for an incoming request.
fn recv_with_size(&self, size: usize) -> Result<Packet, Box<dyn Error>>;
/// Receives a [`Packet`] from any remote [`Socket`] and returns the [`SocketAddr`]
/// of the remote [`Socket`]. This function cannot handle large data packets
/// due to the limited buffer size, so it is intended for only accepting incoming
/// requests. For handling data packets, see [`Socket::recv_from_with_size()`].
fn recv_from(&self) -> Result<(Packet, SocketAddr), Box<dyn Error>> {
self.recv_from_with_size(MAX_REQUEST_PACKET_SIZE)
}
/// Receives a data packet from any incoming remote request, and returns the
/// parsed [`Packet`] and the requesting [`SocketAddr`]. The received packet can
/// actually be of any type, however, this function also allows supplying the
/// buffer size for an incoming request.
fn recv_from_with_size(&self, size: usize) -> Result<(Packet, SocketAddr), Box<dyn Error>>;
/// Returns the remote [`SocketAddr`] if it exists.
fn remote_addr(&self) -> Result<SocketAddr, Box<dyn Error>>;
/// Sets the read timeout for the [`Socket`].
fn set_read_timeout(&mut self, dur: Duration) -> Result<(), Box<dyn Error>>;
/// Sets the write timeout for the [`Socket`].
fn set_write_timeout(&mut self, dur: Duration) -> Result<(), Box<dyn Error>>;
/// Sets the [`Socket`] as blocking or not.
fn set_nonblocking(&mut self, nonblocking: bool) -> Result<(), Box<dyn Error>>;
}
impl Socket for UdpSocket {
fn send(&self, packet: &Packet) -> Result<(), Box<dyn Error>> {
self.send(&packet.serialize()?)?;
Ok(())
}
fn send_to(&self, packet: &Packet, to: &SocketAddr) -> Result<(), Box<dyn Error>> {
self.send_to(&packet.serialize()?, to)?;
Ok(())
}
fn recv_with_size(&self, size: usize) -> Result<Packet, Box<dyn Error>> {
let mut buf = vec![0; size + 4];
let amt = self.recv(&mut buf)?;
let packet = Packet::deserialize(&buf[..amt])?;
Ok(packet)
}
fn recv_from_with_size(&self, size: usize) -> Result<(Packet, SocketAddr), Box<dyn Error>> {
let mut buf = vec![0; size + 4];
let (amt, addr) = self.recv_from(&mut buf)?;
let packet = Packet::deserialize(&buf[..amt])?;
Ok((packet, addr))
}
fn remote_addr(&self) -> Result<SocketAddr, Box<dyn Error>> {
Ok(self.peer_addr()?)
}
fn set_read_timeout(&mut self, dur: Duration) -> Result<(), Box<dyn Error>> {
UdpSocket::set_read_timeout(self, Some(dur))?;
Ok(())
}
fn set_write_timeout(&mut self, dur: Duration) -> Result<(), Box<dyn Error>> {
UdpSocket::set_write_timeout(self, Some(dur))?;
Ok(())
}
fn set_nonblocking(&mut self, nonblocking: bool) -> Result<(), Box<dyn Error>> {
UdpSocket::set_nonblocking(self, nonblocking)?;
Ok(())
}
}
/// ServerSocket `struct` is used as an abstraction layer for a server
/// [`Socket`]. This `struct` is used for abstraction of single socket
/// communication.
///
/// # Example
///
/// ```rust
/// use std::net::{SocketAddr, UdpSocket};
/// use std::str::FromStr;
/// use tftpd::{Socket, ServerSocket, Packet};
/// use std::time::Duration;
///
/// let socket = ServerSocket::new(
/// UdpSocket::bind("127.0.0.1:0").unwrap(),
/// SocketAddr::from_str("127.0.0.1:50000").unwrap(),
/// Duration::from_secs(3)
/// );
/// socket.send(&Packet::Ack(1)).unwrap();
/// ```
pub struct ServerSocket {
socket: UdpSocket,
remote: SocketAddr,
sender: Mutex<Sender<Packet>>,
receiver: Mutex<Receiver<Packet>>,
timeout: Duration,
nonblocking: bool,
}
impl Socket for ServerSocket {
fn send(&self, packet: &Packet) -> Result<(), Box<dyn Error>> {
self.send_to(packet, &self.remote)
}
fn send_to(&self, packet: &Packet, to: &SocketAddr) -> Result<(), Box<dyn Error>> {
self.socket.send_to(&packet.serialize()?, to)?;
Ok(())
}
fn recv_with_size(&self, _size: usize) -> Result<Packet, Box<dyn Error>> {
if let Ok(receiver) = self.receiver.lock() {
if self.nonblocking {
if let Ok(packet) = receiver.try_recv() {
Ok(packet)
} else {
Err(IoError::from(ErrorKind::WouldBlock).into())
}
} else if let Ok(packet) = receiver.recv_timeout(self.timeout) {
Ok(packet)
} else {
Err("Failed to receive".into())
}
} else {
Err("Failed to lock mutex".into())
}
}
fn recv_from_with_size(&self, _size: usize) -> Result<(Packet, SocketAddr), Box<dyn Error>> {
Ok((self.recv()?, self.remote))
}
fn remote_addr(&self) -> Result<SocketAddr, Box<dyn Error>> {
Ok(self.remote)
}
fn set_read_timeout(&mut self, dur: Duration) -> Result<(), Box<dyn Error>> {
self.timeout = dur;
Ok(())
}
fn set_write_timeout(&mut self, dur: Duration) -> Result<(), Box<dyn Error>> {
self.socket.set_write_timeout(Some(dur))?;
Ok(())
}
fn set_nonblocking(&mut self, nonblocking: bool) -> Result<(), Box<dyn Error>> {
self.nonblocking = nonblocking;
self.socket.set_nonblocking(nonblocking)?;
Ok(())
}
}
impl ServerSocket {
/// Creates a new [`ServerSocket`] from a [`UdpSocket`] and a remote [`SocketAddr`].
pub fn new(socket: UdpSocket, remote: SocketAddr, timeout: Duration) -> Self {
let (sender, receiver) = mpsc::channel();
Self {
socket,
remote,
sender: Mutex::new(sender),
receiver: Mutex::new(receiver),
timeout,
nonblocking: false,
}
}
/// Returns a [`Sender`] for sending [`Packet`]s to the remote [`Socket`].
pub fn sender(&self) -> Sender<Packet> {
self.sender.lock().unwrap().clone()
}
}
impl<T: Socket + ?Sized> Socket for Box<T> {
fn send(&self, packet: &Packet) -> Result<(), Box<dyn Error>> {
(**self).send(packet)
}
fn send_to(&self, packet: &Packet, to: &SocketAddr) -> Result<(), Box<dyn Error>> {
(**self).send_to(packet, to)
}
fn recv_with_size(&self, size: usize) -> Result<Packet, Box<dyn Error>> {
(**self).recv_with_size(size)
}
fn recv_from_with_size(&self, size: usize) -> Result<(Packet, SocketAddr), Box<dyn Error>> {
(**self).recv_from_with_size(size)
}
fn remote_addr(&self) -> Result<SocketAddr, Box<dyn Error>> {
(**self).remote_addr()
}
fn set_read_timeout(&mut self, dur: Duration) -> Result<(), Box<dyn Error>> {
(**self).set_read_timeout(dur)
}
fn set_write_timeout(&mut self, dur: Duration) -> Result<(), Box<dyn Error>> {
(**self).set_write_timeout(dur)
}
fn set_nonblocking(&mut self, nonblocking: bool) -> Result<(), Box<dyn Error>> {
(**self).set_nonblocking(nonblocking)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_recv() {
let socket = ServerSocket::new(
UdpSocket::bind("127.0.0.1:0").unwrap(),
SocketAddr::from_str("127.0.0.1:50000").unwrap(),
Duration::from_secs(3)
);
socket.sender.lock().unwrap().send(Packet::Ack(1)).unwrap();
let packet = socket.recv().unwrap();
assert_eq!(packet, Packet::Ack(1));
socket
.sender
.lock()
.unwrap()
.send(Packet::Data {
block_num: 15,
data: vec![0x01, 0x02, 0x03],
})
.unwrap();
let packet = socket.recv().unwrap();
assert_eq!(
packet,
Packet::Data {
block_num: 15,
data: vec![0x01, 0x02, 0x03]
}
);
}
}
07070100000018000081A400000000000000000000000168DE72A50000195A000000000000000000000000000000000000001D00000000rs-tftpd-0.5.0/src/window.rsuse std::{
collections::VecDeque,
error::Error,
fs::File,
io::{Read, Write},
};
/// Window `struct` is used to store chunks of data from a file. It is
/// used to help store the data that is being sent or received for the
/// [RFC 7440](https://www.rfc-editor.org/rfc/rfc7440) Windowsize option.
///
/// # Example
/// ```rust
/// use std::{fs::{self, OpenOptions, File}, io::Write};
/// use tftpd::Window;
///
/// let mut file = File::create("test.txt").unwrap();
/// file.write_all(b"Hello, world!").unwrap();
/// file.flush().unwrap();
///
/// let file = File::open("test.txt").unwrap();
/// let mut window = Window::new(5, 512, file);
/// window.fill().unwrap();
/// fs::remove_file("test.txt").unwrap();
/// ```
pub struct Window {
elements: VecDeque<Vec<u8>>,
size: u16,
chunk_size: u16,
file: File,
}
impl Window {
/// Creates a new `Window` with the supplied size and chunk size.
pub fn new(size: u16, chunk_size: u16, file: File) -> Window {
Window {
elements: VecDeque::new(),
size,
chunk_size,
file,
}
}
/// Fills the `Window` with chunks of data from the file.
/// Returns `true` if the `Window` is full.
pub fn fill(&mut self) -> Result<bool, Box<dyn Error>> {
for _ in self.len()..self.size {
let mut chunk = vec![0; self.chunk_size as usize];
let size = self.file.read(&mut chunk)?;
if size != self.chunk_size as usize {
chunk.truncate(size);
self.elements.push_back(chunk);
return Ok(false);
}
self.elements.push_back(chunk);
}
Ok(true)
}
/// Empties the `Window` by writing the data to the file.
pub fn empty(&mut self) -> Result<(), Box<dyn Error>> {
for data in &self.elements {
self.file.write_all(data)?;
}
self.elements.clear();
Ok(())
}
/// Removes the first `amount` of elements from the `Window`.
pub fn remove(&mut self, amount: u16) -> Result<(), &'static str> {
if amount > self.len() {
return Err("amount cannot be larger than length of window");
}
drop(self.elements.drain(0..amount as usize));
Ok(())
}
/// Adds a data `Vec<u8>` to the `Window`.
pub fn add(&mut self, data: Vec<u8>) -> Result<(), &'static str> {
if self.len() == self.size {
return Err("cannot add to a full window");
}
self.elements.push_back(data);
Ok(())
}
/// Returns a reference to the `VecDeque` containing the elements.
pub fn get_elements(&self) -> &VecDeque<Vec<u8>> {
&self.elements
}
/// Returns the length of the `Window`.
pub fn len(&self) -> u16 {
self.elements.len() as u16
}
/// Returns `true` if the `Window` is empty.
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
/// Returns `true` if the `Window` is full.
pub fn is_full(&self) -> bool {
self.elements.len() as u16 == self.size
}
/// Returns the length of the file
pub fn file_len(&self) -> Result<u64, Box<dyn Error>> {
Ok(self.file.metadata()?.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{
fs::{self, OpenOptions},
io::Write,
};
const DIR_NAME: &str = "target/test";
#[test]
fn fills_and_removes_from_window() {
const FILENAME: &str = "fills_and_removes_from_window.txt";
let mut file = initialize(FILENAME);
file.write_all(b"Hello, world!").unwrap();
file.flush().unwrap();
drop(file);
file = open(FILENAME);
let mut window = Window::new(2, 5, file);
window.fill().unwrap();
assert_eq!(window.elements.len(), 2);
assert_eq!(window.elements[0], b"Hello"[..]);
assert_eq!(window.elements[1], b", wor"[..]);
window.remove(1).unwrap();
assert_eq!(window.elements.len(), 1);
assert_eq!(window.elements[0], b", wor"[..]);
window.fill().unwrap();
assert_eq!(window.elements.len(), 2);
assert_eq!(window.elements[0], b", wor"[..]);
assert_eq!(window.elements[1], b"ld!"[..]);
clean(FILENAME);
}
#[test]
fn adds_to_and_empties_window() {
const FILENAME: &str = "adds_to_and_empties_window.txt";
let file = initialize(FILENAME);
let mut window = Window::new(3, 5, file);
window.add(b"Hello".to_vec()).unwrap();
assert_eq!(window.elements.len(), 1);
assert_eq!(window.elements[0], b"Hello"[..]);
window.add(b", wor".to_vec()).unwrap();
assert_eq!(window.elements.len(), 2);
assert_eq!(window.elements[0], b"Hello"[..]);
assert_eq!(window.elements[1], b", wor"[..]);
window.add(b"ld!".to_vec()).unwrap();
assert_eq!(window.elements.len(), 3);
assert_eq!(window.elements[0], b"Hello"[..]);
assert_eq!(window.elements[1], b", wor"[..]);
assert_eq!(window.elements[2], b"ld!"[..]);
window.empty().unwrap();
assert_eq!(window.elements.len(), 0);
let mut contents = Default::default();
File::read_to_string(
&mut File::open(DIR_NAME.to_string() + "/" + FILENAME).unwrap(),
&mut contents,
)
.unwrap();
assert_eq!(contents, "Hello, world!");
clean(FILENAME);
}
fn initialize(filename: &str) -> File {
let filename = DIR_NAME.to_string() + "/" + filename;
let _ = fs::create_dir_all(DIR_NAME);
if File::open(&filename).is_ok() {
fs::remove_file(&filename).unwrap();
}
OpenOptions::new()
.read(true)
.append(true)
.create(true)
.open(&filename)
.unwrap()
}
fn open(filename: &str) -> File {
let filename = DIR_NAME.to_string() + "/" + filename;
OpenOptions::new()
.read(true)
.append(true)
.create(true)
.open(filename)
.unwrap()
}
fn clean(filename: &str) {
let filename = DIR_NAME.to_string() + "/" + filename;
fs::remove_file(filename).unwrap();
if fs::remove_dir(DIR_NAME).is_err() {
// ignore removing directory, as other tests are
// still running
}
}
}
07070100000019000081A400000000000000000000000168DE72A500003E62000000000000000000000000000000000000001D00000000rs-tftpd-0.5.0/src/worker.rsuse std::{
error::Error,
io::ErrorKind,
fs::{self, File},
path::PathBuf,
thread,
time::{Duration, Instant},
};
use crate::{ErrorCode, Packet, Socket, Window};
use crate::options::{OptionsPrivate, OptionsProtocol, Rollover};
use crate::log::*;
#[cfg(feature = "debug_drop")]
use crate::drop::drop_check;
const DEFAULT_DUPLICATE_DELAY: Duration = Duration::from_millis(1);
/// Worker `struct` is used for multithreaded file sending and receiving.
/// It creates a new socket using the Server's IP and a random port
/// requested from the OS to communicate with the requesting client.
///
/// See [`Worker::send()`] and [`Worker::receive()`] for more details.
///
/// # Example
///
/// ```rust
/// use std::{net::{UdpSocket, SocketAddr}, path::PathBuf, str::FromStr, time::Duration};
/// use tftpd::{Worker};
///
/// // Send a file, responding to a read request.
/// let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
/// socket.connect(SocketAddr::from_str("127.0.0.1:12345").unwrap()).unwrap();
/// let has_options = false;
///
/// let worker = Worker::new(
/// Box::new(socket),
/// PathBuf::from_str("Cargo.toml").unwrap(),
/// Default::default(),
/// Default::default(),
/// );
///
/// worker.send(has_options).unwrap();
/// ```
pub struct Worker<T: Socket + ?Sized> {
socket: Box<T>,
file_path: PathBuf,
opt_local: OptionsPrivate,
opt_common: OptionsProtocol,
}
impl<T: Socket + ?Sized> Worker<T> {
/// Creates a new [`Worker`] with the supplied options.
pub fn new(
socket: Box<T>,
file_path: PathBuf,
opt_local : OptionsPrivate,
opt_common : OptionsProtocol,
) -> Worker<T> {
Worker {
socket,
file_path,
opt_local,
opt_common,
}
}
/// Sends a file to the remote [`SocketAddr`] that has sent a read request using
/// a random port, asynchronously.
pub fn send(self, check_response: bool) -> Result<thread::JoinHandle<bool>, Box<dyn Error>> {
let file_path = self.file_path.clone();
let remote_addr = self.socket.remote_addr().unwrap();
let handle = thread::spawn(move || {
let handle_send = || -> Result<(), Box<dyn Error>> {
self.send_file(File::open(&file_path)?, check_response)
};
match handle_send() {
Ok(_) => {
log_info!(
"Sent {} to {}",
&file_path.file_name().unwrap().to_string_lossy(),
&remote_addr
);
true
}
Err(err) => {
log_err!(
"Error \"{err}\", while sending {} to {}",
&file_path.file_name().unwrap().to_string_lossy(),
&remote_addr
);
false
}
}
});
Ok(handle)
}
/// Receives a file from the remote [`SocketAddr`] (client or server) using
/// the supplied socket, asynchronously.
pub fn receive(self) -> Result<thread::JoinHandle<bool>, Box<dyn Error>> {
let clean_on_error = self.opt_local.clean_on_error;
let file_path = self.file_path.clone();
let remote_addr = self.socket.remote_addr().unwrap();
let opt_tsize = self.opt_common.transfer_size;
let handle = thread::spawn(move || {
let handle_receive = || -> Result<u64, Box<dyn Error>> {
self.receive_file(File::create(&file_path)?)
};
match handle_receive() {
Ok(size) => {
if let Some(tsize) = opt_tsize {
if tsize != size {
log_err!("Size mismatch, negotiated: {tsize}, transferred: {size}");
return false;
}
}
log_info!(
"Received {} ({} bytes) from {}",
&file_path.file_name().unwrap().to_string_lossy(),
size, remote_addr
);
true
}
Err(err) => {
log_err!(
"Error \"{err}\", while receiving {} from {}",
&file_path.file_name().unwrap().to_string_lossy(),
remote_addr
);
if clean_on_error && fs::remove_file(&file_path).is_err() {
log_err!("Error while cleaning {}", &file_path.to_str().unwrap());
}
false
}
}
});
Ok(handle)
}
fn send_file(mut self, file: File, check_response: bool) -> Result<(), Box<dyn Error>> {
let mut block_seq_win : u16 = 0;
let mut win_idx : u16 = 0;
let mut window = Window::new(self.opt_common.window_size, self.opt_common.block_size, file);
let mut more = window.fill()?;
let mut timeout_end = Instant::now();
let mut retry_cnt = 0;
self.socket.set_read_timeout(self.opt_common.timeout)?;
if check_response {
self.check_response()?;
}
self.socket.set_nonblocking(true)?;
loop {
if let Some(frame) = window.get_elements().get(win_idx as usize) {
let mut block_seq_tx = block_seq_win.wrapping_add(win_idx + 1);
if block_seq_tx < block_seq_win {
match self.opt_local.rollover {
Rollover::None => return Err(self.send_rollover_error()),
Rollover::Enforce0 | Rollover::DontCare=> (),
Rollover::Enforce1 => block_seq_tx += 1,
}
}
self.send_packet(&Packet::Data {
block_num: block_seq_tx,
data: frame.to_vec(),
})?;
win_idx += 1;
if win_idx < window.len() {
if !self.opt_common.window_wait.is_zero() {
thread::sleep(self.opt_common.window_wait);
}
} else {
self.socket.set_nonblocking(false)?;
timeout_end = Instant::now() + self.opt_common.timeout;
}
}
let mut last_ack : Option<u16> = None;
loop {
match self.socket.recv() {
Ok(Packet::Ack(block_seq_rx)) => {
if last_ack.is_none() {
self.socket.set_nonblocking(true)?;
}
last_ack = Some(block_seq_rx);
continue;
}
Ok(Packet::Error{code, msg}) => return Err(format!("Received error code {code}: {msg}").into()),
Ok(_) => log_info!(" Received unexpected packet"),
Err(e) => {
if let Some(io_e) = e.downcast_ref::<std::io::Error>() {
match io_e.kind() {
/* On non-blocking sockets, Windows returns WouldBlock and Unix TimedOut */
ErrorKind::WouldBlock |
ErrorKind::TimedOut => {
if let Some(ack) = last_ack {
let mut diff = ack.wrapping_sub(block_seq_win);
if ack < block_seq_win && self.opt_local.rollover == Rollover::Enforce1 {
diff -= 1;
}
if diff == 0 {
break;
} else if diff <= self.opt_common.window_size {
block_seq_win = ack;
window.remove(diff)?;
if !more && window.is_empty() {
return Ok(());
}
more = more && window.fill()?;
win_idx = 0;
break;
} else {
log_dbg!(" Received Ack with unexpected seq {ack} (prev {block_seq_win})");
}
}
if win_idx < window.len() {
break;
}
}
ErrorKind::ConnectionReset => log_info!(" Cnx reset during reception {io_e:?}"),
_ => log_warn!(" IO error during reception {io_e:?}"),
}
} else {
log_warn!(" Unkown error during reception {e:?}");
}
}
}
if timeout_end < Instant::now() {
log_info!(" Ack timeout {}/{}", retry_cnt, self.opt_local.max_retries);
if retry_cnt == self.opt_local.max_retries {
return Err(format!("Transfer timed out after {} tries", self.opt_local.max_retries).into());
}
retry_cnt += 1;
timeout_end = Instant::now() + self.opt_common.timeout;
win_idx = 0;
break;
}
}
}
}
fn send_rollover_error(&self) -> Box<dyn Error> {
self.send_packet(&Packet::Error {
code: ErrorCode::IllegalOperation,
msg: "Block counter rollover error".to_string(),
}).unwrap_or_else(|err| {
log_err!("Error: error '{err:?}' while sending error code");
});
"Block counter rollover error".into()
}
fn receive_file(mut self, file: File) -> Result<u64, Box<dyn Error>> {
let mut block_number: u16 = 0;
let mut window = Window::new(self.opt_common.window_size, self.opt_common.block_size, file);
let mut retry_cnt = 0;
let mut last = false;
let mut listen_all = false;
let mut send_ack = false;
while !last {
while !send_ack {
match self.socket.recv_with_size(self.opt_common.block_size as usize) {
Ok(Packet::Data {
block_num: received_block_number,
data,
}) => {
let mut new_block_number = block_number.wrapping_add(1);
if new_block_number == 0 {
match self.opt_local.rollover {
Rollover::None => return Err(self.send_rollover_error()),
Rollover::Enforce0 => if received_block_number == 1 {
log_warn!(" Warning: data packet 0 missed. Possible rollover policy mismatch.");
},
Rollover::Enforce1 => {
new_block_number = 1;
if received_block_number == 0 {
return Err(self.send_rollover_error());
}
}
Rollover::DontCare => if received_block_number == 1 {
// Possible data loss if previous packet was 0 and lost
log_dbg!(" Data packet 0 missed. Possible data loss.");
new_block_number = 1;
}
}
}
if received_block_number == new_block_number {
block_number = received_block_number;
last = data.len() < self.opt_common.block_size as usize;
window.add(data)?;
send_ack = window.is_full() || last;
} else {
log_dbg!(" Data packet mismatch. Received {received_block_number} instead of {new_block_number}.");
send_ack = true;
}
self.socket.set_nonblocking(true)?;
listen_all = true;
}
Ok(Packet::Error { code, msg }) => {
return Err(format!("Received error '{code}': {msg}").into());
}
Ok(_) => log_info!(" Received unexpected packet"),
Err(e) => {
if let Some(io_e) = e.downcast_ref::<std::io::Error>() {
match io_e.kind() {
ErrorKind::WouldBlock |
ErrorKind::TimedOut => {
if listen_all {
self.socket.set_nonblocking(false)?;
listen_all = false;
} else {
log_dbg!(" Ack timeout {}/{}", retry_cnt, self.opt_local.max_retries);
if retry_cnt == self.opt_local.max_retries {
return Err(format!("Transfer timed out after {} tries", self.opt_local.max_retries).into());
}
retry_cnt += 1;
send_ack = true;
}
}
ErrorKind::ConnectionReset => {
log_info!(" Cnx reset during reception {io_e:?}");
self.socket.set_nonblocking(false)?;
}
_ => log_warn!(" IO error during reception {io_e:?}"),
}
} else {
log_warn!(" Unkown error during reception {e:?}");
}
}
}
}
window.empty()?;
self.send_packet(&Packet::Ack(block_number))?;
send_ack = false;
}
// we should wait and listen a bit more as per RFC 1350 section 6
window.file_len()
}
fn send_packet(&self, packet: &Packet) -> Result<(), Box<dyn Error>> {
#[cfg(feature = "debug_drop")]
if drop_check(packet) { return Ok(()) };
for i in 0..self.opt_local.repeat_count {
if i > 0 {
std::thread::sleep(DEFAULT_DUPLICATE_DELAY);
}
self.socket.send(packet)?;
}
Ok(())
}
fn check_response(&self) -> Result<(), Box<dyn Error>> {
let pkt = self.socket.recv()?;
if let Packet::Ack(received_block_number) = pkt {
if received_block_number == 0 {
return Ok(());
}
}
self.socket.send(&Packet::Error {
code: ErrorCode::IllegalOperation,
msg: "invalid oack response".to_string(),
})?;
Err(format!("Unexpected packet received instead of Ack(0): {pkt:#?}").into())
}
}
0707010000001A000041ED00000000000000000000000268DE72A500000000000000000000000000000000000000000000001600000000rs-tftpd-0.5.0/target0707010000001B000081A400000000000000000000000168DE72A5000003F4000000000000000000000000000000000000002700000000rs-tftpd-0.5.0/target/.rustc_info.json{"rustc_fingerprint":18379971321153410632,"outputs":{"4614504638168534921":{"success":true,"status":"","code":0,"stdout":"rustc 1.83.0 (90b35a623 2024-11-26) (built from a source tarball)\nbinary: rustc\ncommit-hash: 90b35a6239c3d8bdabc530a6a0816f7ff89a0aaf\ncommit-date: 2024-11-26\nhost: x86_64-unknown-linux-gnu\nrelease: 1.83.0\nLLVM version: 19.1.1\n","stderr":""},"15729799797837862367":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/usr\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}}0707010000001C000081A400000000000000000000000168DE72A5000000B1000000000000000000000000000000000000002300000000rs-tftpd-0.5.0/target/CACHEDIR.TAGSignature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/
0707010000001D000041ED00000000000000000000000268DE72A500000000000000000000000000000000000000000000001C00000000rs-tftpd-0.5.0/target/debug0707010000001E000081A400000000000000000000000168DE72A500000000000000000000000000000000000000000000002800000000rs-tftpd-0.5.0/target/debug/.cargo-lock0707010000001F000041ED00000000000000000000000268DE72A500000000000000000000000000000000000000000000002900000000rs-tftpd-0.5.0/target/debug/.fingerprint07070100000020000041ED00000000000000000000000268DE72A500000000000000000000000000000000000000000000004000000000rs-tftpd-0.5.0/target/debug/.fingerprint/tftpd-857593ddc74d542307070100000021000041ED00000000000000000000000268DE72A500000000000000000000000000000000000000000000002200000000rs-tftpd-0.5.0/target/debug/build07070100000022000041ED00000000000000000000000268DE72A500000000000000000000000000000000000000000000002100000000rs-tftpd-0.5.0/target/debug/deps07070100000023000041ED00000000000000000000000268DE72A500000000000000000000000000000000000000000000002500000000rs-tftpd-0.5.0/target/debug/examples07070100000024000041ED00000000000000000000000268DE72A500000000000000000000000000000000000000000000002800000000rs-tftpd-0.5.0/target/debug/incremental07070100000025000041ED00000000000000000000000268DE72A500000000000000000000000000000000000000000000001500000000rs-tftpd-0.5.0/tests07070100000026000081A400000000000000000000000168DE72A50000480E000000000000000000000000000000000000002900000000rs-tftpd-0.5.0/tests/integration_test.rs#![cfg(feature = "integration")]
use std::fs::{self, create_dir_all, remove_dir_all};
use std::process::{Child, Command, ExitStatus, Stdio};
use std::thread;
use std::time::{Duration, Instant};
use std::io::Read;
const SERVER_DIR: &str = "target/integration/server";
const CLIENT_DIR: &str = "target/integration/client";
struct CommandRunner {
process: Child,
}
impl CommandRunner {
fn new(program: &str, args: &[&str]) -> Self {
let command = Command::new(program)
.args(args)
.spawn()
.expect("error starting process");
Self { process: command }
}
fn new_piped(program: &str, args: &[&str]) -> Self {
let command = Command::new(program)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("error starting process");
Self { process: command }
}
fn wait(&mut self) -> ExitStatus {
self.process.wait().expect("error waiting for process")
}
fn kill(&mut self) {
self.process.kill().expect("error killing process");
}
}
impl Drop for CommandRunner {
fn drop(&mut self) {
self.kill()
}
}
#[test]
fn test_send() {
let filename = "send";
let port = "6969";
initialize(format!("{SERVER_DIR}/{filename}").as_str());
let _server = CommandRunner::new("target/debug/tftpd", &["-p", port, "-d", SERVER_DIR]);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new(
"atftp",
&[
"-g",
"-r",
filename,
"-l",
format!("{CLIENT_DIR}/{filename}").as_str(),
"127.0.0.1",
port,
],
);
let status = client.wait();
assert!(status.success());
}
#[test]
fn test_receive() {
let filename = "receive";
let port = "6970";
initialize(format!("{CLIENT_DIR}/{filename}").as_str());
let _server = CommandRunner::new("target/debug/tftpd", &["-p", port, "-d", SERVER_DIR]);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new(
"atftp",
&[
"-p",
"-r",
filename,
"-l",
format!("{CLIENT_DIR}/{filename}").as_str(),
"127.0.0.1",
port,
],
);
let status = client.wait();
assert!(status.success());
}
#[test]
fn test_send_dir() {
let filename = "send_dir";
let port = "6971";
initialize(format!("{SERVER_DIR}/{filename}").as_str());
let _server = CommandRunner::new("target/debug/tftpd", &["-p", port, "-sd", SERVER_DIR]);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new(
"atftp",
&[
"-g",
"-r",
filename,
"-l",
format!("{CLIENT_DIR}/{filename}").as_str(),
"127.0.0.1",
port,
],
);
let status = client.wait();
assert!(status.success());
check_files(filename);
}
#[test]
fn test_receive_dir() {
let filename = "receive_dir";
let port = "6972";
initialize(format!("{CLIENT_DIR}/{filename}").as_str());
let _server = CommandRunner::new("target/debug/tftpd", &["-p", port, "-rd", SERVER_DIR]);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new(
"atftp",
&[
"-p",
"-r",
filename,
"-l",
format!("{CLIENT_DIR}/{filename}").as_str(),
"127.0.0.1",
port,
],
);
let status = client.wait();
assert!(status.success());
check_files(filename);
}
#[test]
fn test_send_ipv6() {
let filename = "send_ipv6";
let port = "6973";
initialize(format!("{SERVER_DIR}/{filename}").as_str());
let _server = CommandRunner::new(
"target/debug/tftpd",
&["-i", "::1", "-p", port, "-d", SERVER_DIR],
);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new(
"atftp",
&[
"-g",
"-r",
filename,
"-l",
format!("{CLIENT_DIR}/{filename}").as_str(),
"::1",
port,
],
);
let status = client.wait();
assert!(status.success());
check_files(filename);
}
#[test]
fn test_receive_ipv6() {
let filename = "receive_ipv6";
let port = "6974";
initialize(format!("{CLIENT_DIR}/{filename}").as_str());
let _server = CommandRunner::new(
"target/debug/tftpd",
&["-i", "::1", "-p", port, "-d", SERVER_DIR],
);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new(
"atftp",
&[
"-p",
"-r",
filename,
"-l",
format!("{CLIENT_DIR}/{filename}").as_str(),
"::1",
port,
],
);
let status = client.wait();
assert!(status.success());
check_files(filename);
}
#[test]
fn test_send_single_port_options() {
let filename = "send_single_port_options";
let port = "6975";
initialize(format!("{SERVER_DIR}/{filename}").as_str());
let _server = CommandRunner::new("target/debug/tftpd", &["-p", port, "-d", SERVER_DIR, "-s"]);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new(
"atftp",
&[
"-g",
"-r",
filename,
"-l",
format!("{CLIENT_DIR}/{filename}").as_str(),
"--option",
"windowsize 10",
"127.0.0.1",
port,
],
);
let status = client.wait();
assert!(status.success());
check_files(filename);
}
#[test]
fn test_client_send() {
let filename = "client_send";
let port = "6980";
initialize(format!("{CLIENT_DIR}/{filename}").as_str());
let _server = CommandRunner::new("target/debug/tftpd", &["-p", port, "-d", SERVER_DIR]);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new(
"target/debug/tftpc",
&[
format!("{CLIENT_DIR}/{filename}").as_str(),
"-p",
port,
"-u",
],
);
let status = client.wait();
assert!(status.success());
check_files(filename);
}
#[test]
fn test_client_receive() {
let filename = "client_receive";
let port = "6981";
initialize(format!("{SERVER_DIR}/{filename}").as_str());
let _server = CommandRunner::new("target/debug/tftpd", &["-p", port, "-d", SERVER_DIR]);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new(
"target/debug/tftpc",
&[filename, "-p", port, "-d", "-rd", CLIENT_DIR],
);
let status = client.wait();
assert!(status.success());
check_files(filename);
}
#[test]
fn test_client_receive_paths() {
let filename = "client_receive_paths";
let port = "6982";
create_dir_all(format!("{SERVER_DIR}/subdir").as_str())
.expect("error creating server directory");
create_file(format!("{SERVER_DIR}/subdir/{filename}").as_str(), 10*1024*1024);
let _server = CommandRunner::new("target/debug/tftpd", &["-p", port, "-d", SERVER_DIR]);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new(
"target/debug/tftpc",
&[
format!("subdir/{filename}").as_str(),
"-p",
port,
"-d",
"-rd",
CLIENT_DIR,
],
);
let status = client.wait();
assert!(status.success());
let server_file = format!("{SERVER_DIR}/subdir/{filename}");
let client_file = format!("{CLIENT_DIR}/{filename}");
let server_content = fs::read(server_file).expect("error reading server file");
let client_content = fs::read(client_file).expect("error reading client file");
assert_eq!(server_content, client_content);
}
#[test]
fn test_client_receive_windows_paths() {
let filename = "client_receive_windows_paths";
let port = "6983";
create_dir_all(format!("{SERVER_DIR}/windir").as_str())
.expect("error creating server directory");
create_file(format!("{SERVER_DIR}/windir/{filename}").as_str(), 10*1024*1024);
let _server = CommandRunner::new("target/debug/tftpd", &["-p", port, "-d", SERVER_DIR]);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new(
"target/debug/tftpc",
&[
format!(r"windir\{filename}").as_str(),
"-p",
port,
"-d",
"-rd",
CLIENT_DIR,
],
);
let status = client.wait();
assert!(status.success());
let server_file = format!("{SERVER_DIR}/windir/{filename}");
let client_file = format!("{CLIENT_DIR}/{filename}");
let server_content = fs::read(server_file).expect("error reading server file");
let client_content = fs::read(client_file).expect("error reading client file");
assert_eq!(server_content, client_content);
}
#[test]
fn test_send_curl() {
let filename = "send_curl";
let port = "6984";
initialize(format!("{SERVER_DIR}/{filename}").as_str());
// rollover=0, verbosity max, drop 1 data packet at half transfer
let _server = CommandRunner::new("target/debug/tftpd", &["-p", port, "-d", SERVER_DIR, "-R", "0", "-v", "-v", "-D", "32768"]);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new(
"curl",
&[
"-O", //use remote filename locally
"--output-dir", CLIENT_DIR,
format!("tftp://127.0.0.1:{port}/{filename}").as_str(),
"--tftp-blksize", "150", // blocksize to ensure 1 rollover
"--connect-timeout", "3", // timeout = 1s
],
);
let status = client.wait();
assert!(status.success());
}
#[test]
fn test_receive_curl() {
let filename = "receive_curl";
let port = "6985";
initialize(format!("{CLIENT_DIR}/{filename}").as_str());
// rollover=0, verbosity max, drop 1 data packet at half transfer
let _server = CommandRunner::new("target/debug/tftpd", &["-p", port, "-d", SERVER_DIR, "-R", "0", "-v", "-v", "-D", "32768"]);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new(
"curl",
&[
"--upload-file",
format!("{CLIENT_DIR}/{filename}").as_str(),
format!("tftp://127.0.0.1:{port}/").as_str(),
"--tftp-blksize", "150", // blocksize to ensure 1 rollover
"--connect-timeout", "3", // timeout = 1s
],
);
let status = client.wait();
assert!(status.success());
}
#[test]
fn test_rollover() {
let filename = "rollover";
let port = "6986";
create_dir_all(SERVER_DIR.to_string().as_str()).expect("error creating server directory");
create_file(format!("{SERVER_DIR}/{filename}").as_str(), 65540);
let _server = CommandRunner::new("target/debug/tftpd", &["-p", port, "-d", SERVER_DIR, "-R", "0", "-v", "-v", ]);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new(
"target/debug/tftpc",
&[
filename,
"-p", port,
"-d",
"-rd", CLIENT_DIR,
"-R", "0",
"-b", "1", // speed up test and ensure rollover
"-w", "32", // speed up test
"-v", "-v",
],
);
let status = client.wait();
assert!(status.success());
let server_file = format!("{SERVER_DIR}/{filename}");
let client_file = format!("{CLIENT_DIR}/{filename}");
let server_content = fs::read(server_file).expect("error reading server file");
let client_content = fs::read(client_file).expect("error reading client file");
assert_eq!(server_content, client_content);
}
#[test]
fn test_rollover_fail() {
let filename = "rollover_fail";
let port = "6987";
create_dir_all(SERVER_DIR.to_string().as_str()).expect("error creating server directory");
create_file(format!("{SERVER_DIR}/{filename}").as_str(), 65540);
let _server = CommandRunner::new("target/debug/tftpd", &["-p", port, "-d", SERVER_DIR, "-R", "0", "-v", "-v", ]);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new_piped(
"target/debug/tftpc",
&[
filename,
"-p", port,
"-d",
"-rd", CLIENT_DIR,
"-R", "1", // different from server, must fail
"-b", "1", // speed up test and ensure rollover
"-w", "32", // speed up test
"-v", "-v",
],
);
let mut stderr = client.process.stderr.take().unwrap();
let status = client.wait();
let mut buffer = String::new();
let _ = stderr.read_to_string(&mut buffer);
assert!(buffer.contains("Block counter rollover error"));
assert!(!status.success());
}
// This test creates a file corruption by dropping "data 0" packet
// (the first one after a rollover) and ensuring transfer get terminated,
// then waits for the tsize check to detect the size mismatch
#[test]
fn test_tsize() {
let filename = "tsize";
let port = "6988";
create_folders();
create_file(format!("{SERVER_DIR}/{filename}").as_str(), 65540);
let _server = CommandRunner::new("target/debug/tftpd",
&["-p", port, "-d", SERVER_DIR,
"-R", "0", "-D", "0", // rollover 0 but drop data 0!
"-v", "-v",
]);
thread::sleep(Duration::from_secs(1));
let mut client = CommandRunner::new_piped(
"target/debug/tftpc",
&[
filename,
"-p", port,
"-d",
"-rd", CLIENT_DIR,
"-R", "x", // will not fail if data 0 is missing
"-b", "1", // speed up test and ensure rollover
"-w", "30", // to ensure no ack on data 0
"-v", "-v",
],
);
let mut stderr = client.process.stderr.take().unwrap();
let status = client.wait();
let mut buffer = String::new();
let _ = stderr.read_to_string(&mut buffer);
assert!(buffer.contains("Size mismatch"));
assert!(!status.success());
}
#[test]
fn test_window() {
let filename = "window";
let port = "6989";
initialize(format!("{SERVER_DIR}/{filename}").as_str());
let _server = CommandRunner::new("target/debug/tftpd", &["-p", port, "-d", SERVER_DIR, "-v", "-v", "-D", "20458", ]);
thread::sleep(Duration::from_secs(1));
let now = Instant::now();
let mut client = CommandRunner::new(
"target/debug/tftpc",
&[
"-d",
filename,
"-p", port,
"-rd", CLIENT_DIR,
"-w", "8",
"-t", "10",
],
);
let status = client.wait();
assert!(status.success());
// Packet drop at window start should not trigger any timeout
assert!(now.elapsed() < Duration::from_secs(10));
check_files(filename);
}
#[test]
fn test_window_timeout() {
let filename = "window_timeout";
let port = "6990";
initialize(format!("{SERVER_DIR}/{filename}").as_str());
let _server = CommandRunner::new("target/debug/tftpd", &["-p", port, "-d", SERVER_DIR, "-v", "-v", "-D", "20464" ]);
thread::sleep(Duration::from_secs(1));
let now = Instant::now();
let mut client = CommandRunner::new(
"target/debug/tftpc",
&[
"-d",
filename,
"-p", port,
"-rd", CLIENT_DIR,
"-w", "8",
"-t", "6",
],
);
let status = client.wait();
assert!(status.success());
// Packet drop at window end should trigger one timeout
assert!(now.elapsed() > Duration::from_secs(6));
check_files(filename);
}
// This test checks that sender will not duplicate data packets after a double ack,
// which is called "sorcerer's apprentice syndrom" (cf RFC 1123).
#[test]
fn test_sas() {
let filename = "sas";
let port = "6991";
create_folders();
create_file(format!("{CLIENT_DIR}/{filename}").as_str(), 256);
let _server = CommandRunner::new("target/debug/tftpd",
&["-p", port, "-d", SERVER_DIR, "--overwrite", "-v", "-v" ]);
thread::sleep(Duration::from_secs(1));
// test is executed by dropping different packets wrt their position in the window
for drop in 9..27 {
let mut client = CommandRunner::new_piped(
"target/debug/tftpc",
&[
"-u", format!("{CLIENT_DIR}/{filename}").as_str(),
"-p", port,
"-b", "1", // speed up test
"-t", ".5", // speed up test
"-w", "5",
"-v", "-v", // Enable mismatch logging
"-D", drop.to_string().as_str(), // trigger ack duplication
],
);
let mut stdout = client.process.stdout.take().unwrap();
let status = client.wait();
let mut buffer = String::new();
let _ = stdout.read_to_string(&mut buffer);
assert!(buffer.split("Data packet mismatch").count() < 10);
assert!(buffer.split("Ack timeout").count() < 3);
assert!(status.success());
}
}
fn initialize(filename: &str) {
create_folders();
create_file(filename, 10*1024*1024);
}
fn create_folders() {
let _ = remove_dir_all(SERVER_DIR);
let _ = remove_dir_all(CLIENT_DIR);
create_dir_all(SERVER_DIR).expect("error creating server directory");
create_dir_all(CLIENT_DIR).expect("error creating client directory");
}
fn create_file(filename: &str, size: usize) {
Command::new("dd")
.args([
"if=/dev/urandom",
format!("of={filename}").as_str(),
format!("bs={size}").as_str(),
"count=1",
])
.spawn()
.expect("error creating test file")
.wait()
.expect("error waiting for test file creation");
}
fn check_files(filename: &str) {
let server_file = format!("{SERVER_DIR}/{filename}");
let client_file = format!("{CLIENT_DIR}/{filename}");
let server_content = fs::read(server_file).expect("error reading server file");
let client_content = fs::read(client_file).expect("error reading client file");
assert_eq!(server_content, client_content);
}
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!299 blocks