Add docstrings

This commit is contained in:
altugbakan 2023-03-12 15:01:41 +03:00
parent 43d2e85a00
commit bfb959bca8
7 changed files with 268 additions and 23 deletions

View file

@ -3,13 +3,33 @@ use std::net::Ipv4Addr;
use std::path::{Path, PathBuf};
use std::{env, process};
/// 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: Ipv4Addr,
/// 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,
}
impl Config {
/// Creates a new configuration by parsing the supplied arguments. It is
/// intended for use with [`env::args()`].
pub fn new<T>(mut args: T) -> Result<Config, Box<dyn Error>>
where
T: Iterator<Item = String>,

View file

@ -1,8 +1,22 @@
use 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")
@ -11,6 +25,8 @@ impl Convert {
}
}
/// 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((

View file

@ -1,3 +1,19 @@
#![warn(missing_docs)]
//! Multithreaded TFTP daemon implemented in pure Rust without dependencies.
//!
//! 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
//!
//! # 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.
mod config;
mod convert;
mod message;
@ -8,5 +24,10 @@ mod worker;
pub use config::Config;
pub use convert::Convert;
pub use message::Message;
pub use packet::ErrorCode;
pub use packet::Opcode;
pub use packet::OptionType;
pub use packet::Packet;
pub use packet::TransferOption;
pub use server::Server;
pub use worker::Worker;

View file

@ -3,13 +3,33 @@ use std::{
net::{SocketAddr, UdpSocket},
};
use crate::packet::{ErrorCode, Packet, TransferOption};
use crate::{ErrorCode, Packet, TransferOption};
/// Message `struct` is used for easy message transmission of common TFTP
/// message types.
///
/// # Example
///
/// ```rust
/// use std::{net::{SocketAddr, UdpSocket}, str::FromStr};
/// use tftpd::{Message, ErrorCode};
///
/// // Send a FileNotFound error.
/// Message::send_error_to(
/// &UdpSocket::bind(SocketAddr::from_str("127.0.0.1:69").unwrap()).unwrap(),
/// &SocketAddr::from_str("127.0.0.1:1234").unwrap(),
/// ErrorCode::FileNotFound,
/// "file does not exist".to_string(),
/// );
/// ```
pub struct Message;
const MAX_REQUEST_PACKET_SIZE: usize = 512;
impl Message {
/// Sends a data packet to the socket's connected remote. See
/// [`UdpSocket`] for more information about connected
/// sockets.
pub fn send_data(
socket: &UdpSocket,
block_num: u16,
@ -20,12 +40,18 @@ impl Message {
Ok(())
}
/// Sends an acknowledgement packet to the socket's connected remote. See
/// [`UdpSocket`] for more information about connected
/// sockets.
pub fn send_ack(socket: &UdpSocket, block_number: u16) -> Result<(), Box<dyn Error>> {
socket.send(&Packet::Ack(block_number).serialize()?)?;
Ok(())
}
/// Sends an error packet to the socket's connected remote. See
/// [`UdpSocket`] for more information about connected
/// sockets.
pub fn send_error(
socket: &UdpSocket,
code: ErrorCode,
@ -36,6 +62,7 @@ impl Message {
Ok(())
}
/// Sends an error packet to the supplied [`SocketAddr`].
pub fn send_error_to<'a>(
socket: &UdpSocket,
to: &SocketAddr,
@ -58,15 +85,20 @@ impl Message {
Err(msg.into())
}
/// Sends an option acknowledgement packet to the socket's connected remote.
/// See [`UdpSocket`] for more information about connected sockets.
pub fn send_oack(
socket: &UdpSocket,
options: Vec<TransferOption>,
) -> Result<(), Box<dyn Error>> {
socket.send(&Packet::Oack { options }.serialize()?)?;
socket.send(&Packet::Oack(options).serialize()?)?;
Ok(())
}
/// Receives a packet from the socket's connected remote, and returns the
/// parsed [`Packet`]. This function cannot handle large data packets due to
/// the limited buffer size. For handling data packets, see [`Message::recv_data()`].
pub fn recv(socket: &UdpSocket) -> Result<Packet, Box<dyn Error>> {
let mut buf = [0; MAX_REQUEST_PACKET_SIZE];
let number_of_bytes = socket.recv(&mut buf)?;
@ -75,14 +107,10 @@ impl Message {
Ok(packet)
}
pub fn recv_data(socket: &UdpSocket, size: usize) -> Result<Packet, Box<dyn Error>> {
let mut buf = vec![0; size + 4];
let number_of_bytes = socket.recv(&mut buf)?;
let packet = Packet::deserialize(&buf[..number_of_bytes])?;
Ok(packet)
}
/// Receives a packet from any incoming remote request, and returns the
/// parsed [`Packet`] and the requesting [`SocketAddr`]. This function cannot handle
/// large data packets due to the limited buffer size, so it is intended for
/// only accepting incoming requests.
pub fn recv_from(socket: &UdpSocket) -> Result<(Packet, SocketAddr), Box<dyn Error>> {
let mut buf = [0; MAX_REQUEST_PACKET_SIZE];
let (number_of_bytes, from) = socket.recv_from(&mut buf)?;
@ -90,4 +118,15 @@ impl Message {
Ok((packet, from))
}
/// 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.
pub fn recv_data(socket: &UdpSocket, size: usize) -> Result<Packet, Box<dyn Error>> {
let mut buf = vec![0; size + 4];
let number_of_bytes = socket.recv(&mut buf)?;
let packet = Packet::deserialize(&buf[..number_of_bytes])?;
Ok(packet)
}
}

View file

@ -1,32 +1,62 @@
use crate::Convert;
use std::{error::Error, fmt};
/// 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,
},
Oack {
options: Vec<TransferOption>,
},
/// 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>> {
let opcode = Opcode::from_u16(Convert::to_u16(&buf[0..=1])?)?;
@ -39,29 +69,50 @@ impl Packet {
}
}
/// Serializes a [`Packet`] into a [`Vec<u8>`].
pub fn serialize(&self) -> Result<Vec<u8>, Box<dyn Error>> {
match self {
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)),
Packet::Oack(options) => Ok(serialize_oack(options)),
_ => Err("Invalid packet".into()),
}
}
}
/// 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(PartialEq)]
#[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),
@ -74,18 +125,37 @@ impl Opcode {
}
}
/// Converts a [`u16`] to a [`u8`] array with 2 elements.
pub fn as_bytes(self) -> [u8; 2] {
return (self as u16).to_be_bytes();
}
}
/// 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: usize,
}
impl TransferOption {
/// Converts a [`TransferOption`] to a [`Vec<u8>`].
pub fn as_bytes(&self) -> Vec<u8> {
[
self.option.as_str().as_bytes(),
@ -97,15 +167,32 @@ impl TransferOption {
}
}
/// 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, OptionType::from_str("blksize").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,
}
impl OptionType {
fn as_str(&self) -> &'static str {
/// Converts an [`OptionType`] to a [`str`].
pub fn as_str(&self) -> &'static str {
match self {
OptionType::BlockSize => "blksize",
OptionType::TransferSize => "tsize",
@ -113,7 +200,8 @@ impl OptionType {
}
}
fn from_str(value: &str) -> Result<Self, &'static str> {
/// Converts a [`str`] to an [`OptionType`].
pub fn from_str(value: &str) -> Result<Self, &'static str> {
match value {
"blksize" => Ok(OptionType::BlockSize),
"tsize" => Ok(OptionType::TransferSize),
@ -123,20 +211,42 @@ impl OptionType {
}
}
/// 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,
}
impl ErrorCode {
/// Converts a [`u16`] to an [`ErrorCode`].
pub fn from_u16(code: u16) -> Result<ErrorCode, &'static str> {
match code {
0 => Ok(ErrorCode::NotDefined),
@ -151,6 +261,7 @@ impl ErrorCode {
}
}
/// Converts an [`ErrorCode`] to a [`u8`] array with 2 elements.
pub fn as_bytes(self) -> [u8; 2] {
return (self as u16).to_be_bytes();
}

View file

@ -1,15 +1,31 @@
use crate::packet::{ErrorCode, Packet, TransferOption};
use crate::{Config, Message, Worker};
use crate::{ErrorCode, Packet, TransferOption};
use std::error::Error;
use std::net::{SocketAddr, UdpSocket};
use std::path::PathBuf;
/// 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 std::env;
/// use tftpd::{Config, Server};
///
/// let config = Config::new(env::args()).unwrap();
/// let server = Server::new(&config).unwrap();
/// ```
pub struct Server {
socket: UdpSocket,
directory: PathBuf,
}
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)))?;
@ -21,6 +37,7 @@ impl Server {
Ok(server)
}
/// Starts listening for connections. Note that this function does not finish running until termination.
pub fn listen(&self) {
loop {
if let Ok((packet, from)) = Message::recv_from(&self.socket) {

View file

@ -8,15 +8,32 @@ use std::{
time::Duration,
};
use crate::{
packet::{ErrorCode, OptionType, Packet, TransferOption},
Message,
};
use crate::{ErrorCode, Message, OptionType, Packet, TransferOption};
/// 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::SocketAddr, path::PathBuf, str::FromStr};
/// use tftpd::Worker;
///
/// // Send a file, responding to a read request.
/// Worker::send(
/// SocketAddr::from_str("127.0.0.1:1234").unwrap(),
/// SocketAddr::from_str("127.0.0.1:4321").unwrap(),
/// PathBuf::from_str("/home/rust/test.txt").unwrap(),
/// vec![]
/// );
/// ```
pub struct Worker;
#[derive(Debug, PartialEq, Eq)]
pub struct WorkerOptions {
struct WorkerOptions {
blk_size: usize,
t_size: usize,
timeout: u64,
@ -33,6 +50,8 @@ const DEFAULT_TIMEOUT_SECS: u64 = 5;
const DEFAULT_BLOCK_SIZE: usize = 512;
impl Worker {
/// Sends a file to the remote [`SocketAddr`] that has sent a read request using
/// a random port, asynchronously.
pub fn send(
addr: SocketAddr,
remote: SocketAddr,
@ -56,6 +75,8 @@ impl Worker {
});
}
/// Receives a file from the remote [`SocketAddr`] that has sent a write request using
/// a random port, asynchronously.
pub fn receive(
addr: SocketAddr,
remote: SocketAddr,