From bfb959bca8dd47d5c8e6d0aa3a9b6e85e6517bd9 Mon Sep 17 00:00:00 2001 From: altugbakan Date: Sun, 12 Mar 2023 15:01:41 +0300 Subject: [PATCH] Add docstrings --- src/config.rs | 20 ++++++++ src/convert.rs | 16 +++++++ src/lib.rs | 21 +++++++++ src/message.rs | 59 +++++++++++++++++++---- src/packet.rs | 125 ++++++++++++++++++++++++++++++++++++++++++++++--- src/server.rs | 19 +++++++- src/worker.rs | 31 ++++++++++-- 7 files changed, 268 insertions(+), 23 deletions(-) diff --git a/src/config.rs b/src/config.rs index dece5e6..0d90529 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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(mut args: T) -> Result> where T: Iterator, diff --git a/src/convert.rs b/src/convert.rs index ef2f98e..b4a3dd0 100644 --- a/src/convert.rs +++ b/src/convert.rs @@ -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 { 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> { match buf[start..].iter().position(|&b| b == 0x00) { Some(index) => Ok(( diff --git a/src/lib.rs b/src/lib.rs index ec561cf..003ba30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/message.rs b/src/message.rs index 488a1fc..921cf88 100644 --- a/src/message.rs +++ b/src/message.rs @@ -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> { 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, ) -> Result<(), Box> { - 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> { 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> { - 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> { 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> { + 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) + } } diff --git a/src/packet.rs b/src/packet.rs index 6237336..0e96915 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -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`]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, }, + /// Write Request `struct` Wrq { + /// Name of the requested file filename: String, + /// Transfer mode mode: String, + /// Transfer options options: Vec, }, + /// Data `struct` Data { + /// Block number block_num: u16, + /// Data data: Vec, }, + /// Acknowledgement `tuple` with block number Ack(u16), + /// Error `struct` Error { + /// Error code code: ErrorCode, + /// Error message msg: String, }, - Oack { - options: Vec, - }, + /// Option acknowledgement `tuple` with transfer options + Oack(Vec), } impl Packet { + /// Deserializes a [`u8`] slice into a [`Packet`]. pub fn deserialize(buf: &[u8]) -> Result> { let opcode = Opcode::from_u16(Convert::to_u16(&buf[0..=1])?)?; @@ -39,29 +69,50 @@ impl Packet { } } + /// Serializes a [`Packet`] into a [`Vec`]. pub fn serialize(&self) -> Result, Box> { 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 { 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`]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`]. pub fn as_bytes(&self) -> Vec { [ 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 { + /// Converts a [`str`] to an [`OptionType`]. + pub fn from_str(value: &str) -> Result { 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 { 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(); } diff --git a/src/server.rs b/src/server.rs index 852fddb..6af716a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -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> { 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) { diff --git a/src/worker.rs b/src/worker.rs index db9f1b7..a324492 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -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,