feat: implement TFTP client and server binaries
Add command-line programs for TFTP operations: tftpd - TFTP server daemon: - Configurable root directory for serving files - Optional write support with --writable flag - Optional file overwriting with --overwrite flag - Custom port binding (default: 69) - Path traversal protection for security tftp - TFTP client: - get command for downloading files - put command for uploading files - Support for both octet and netascii modes - Verbose mode for debugging Also adds comprehensive integration tests that verify client-server communication with real network I/O. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,305 @@
|
||||
fn main() {
|
||||
println!("tftp client placeholder");
|
||||
//! TFTP Client (tftp)
|
||||
//!
|
||||
//! A simple TFTP client implementation following RFC 1350.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! ```text
|
||||
//! tftp [OPTIONS] <COMMAND>
|
||||
//!
|
||||
//! Commands:
|
||||
//! get <HOST> <REMOTE_FILE> [LOCAL_FILE] Download a file
|
||||
//! put <HOST> <LOCAL_FILE> [REMOTE_FILE] Upload a file
|
||||
//!
|
||||
//! Options:
|
||||
//! -m, --mode <MODE> Transfer mode: octet (default) or netascii
|
||||
//! -v, --verbose Enable verbose output
|
||||
//! -h, --help Print help
|
||||
//! ```
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```text
|
||||
//! # Download a file
|
||||
//! tftp get 192.168.1.1 config.txt
|
||||
//!
|
||||
//! # Download to a specific local file
|
||||
//! tftp get 192.168.1.1 config.txt local_config.txt
|
||||
//!
|
||||
//! # Upload a file
|
||||
//! tftp put 192.168.1.1 firmware.bin
|
||||
//!
|
||||
//! # Upload with netascii mode
|
||||
//! tftp -m netascii put 192.168.1.1 readme.txt
|
||||
//! ```
|
||||
|
||||
use std::{env, fs::File, path::Path, process::ExitCode};
|
||||
|
||||
use pfs_tftp::{Client, Mode};
|
||||
|
||||
/// Print usage information.
|
||||
fn print_usage(program: &str) {
|
||||
eprintln!("TFTP Client (RFC 1350)");
|
||||
eprintln!();
|
||||
eprintln!("Usage: {program} [OPTIONS] <COMMAND>");
|
||||
eprintln!();
|
||||
eprintln!("Commands:");
|
||||
eprintln!(" get <HOST> <REMOTE_FILE> [LOCAL_FILE] Download a file");
|
||||
eprintln!(" put <HOST> <LOCAL_FILE> [REMOTE_FILE] Upload a file");
|
||||
eprintln!();
|
||||
eprintln!("Options:");
|
||||
eprintln!(" -m, --mode <MODE> Transfer mode: octet (default) or netascii");
|
||||
eprintln!(" -v, --verbose Enable verbose output");
|
||||
eprintln!(" -h, --help Print help");
|
||||
eprintln!();
|
||||
eprintln!("Examples:");
|
||||
eprintln!(" {program} get 192.168.1.1 config.txt");
|
||||
eprintln!(" {program} put 192.168.1.1 firmware.bin");
|
||||
eprintln!(" {program} -m netascii get 192.168.1.1:69 readme.txt local.txt");
|
||||
}
|
||||
|
||||
/// Command to execute.
|
||||
enum Command {
|
||||
Get {
|
||||
host: String,
|
||||
remote_file: String,
|
||||
local_file: String,
|
||||
},
|
||||
Put {
|
||||
host: String,
|
||||
local_file: String,
|
||||
remote_file: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Parsed command-line arguments.
|
||||
struct Args {
|
||||
command: Command,
|
||||
mode: Mode,
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
/// Parse command-line arguments.
|
||||
fn parse_args() -> Result<Args, String> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let program = &args[0];
|
||||
|
||||
let mut mode = Mode::Octet;
|
||||
let mut verbose = false;
|
||||
|
||||
let mut i = 1;
|
||||
|
||||
// Parse options
|
||||
while i < args.len() && args[i].starts_with('-') {
|
||||
match args[i].as_str() {
|
||||
"-h" | "--help" => {
|
||||
print_usage(program);
|
||||
std::process::exit(0);
|
||||
}
|
||||
"-m" | "--mode" => {
|
||||
i += 1;
|
||||
if i >= args.len() {
|
||||
return Err("--mode requires an argument".to_string());
|
||||
}
|
||||
mode = match args[i].to_lowercase().as_str() {
|
||||
"octet" | "binary" => Mode::Octet,
|
||||
"netascii" | "ascii" => Mode::NetAscii,
|
||||
_ => return Err(format!("invalid mode: {}", args[i])),
|
||||
};
|
||||
}
|
||||
"-v" | "--verbose" => {
|
||||
verbose = true;
|
||||
}
|
||||
opt => {
|
||||
return Err(format!("unknown option: {opt}"));
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Parse command
|
||||
if i >= args.len() {
|
||||
return Err("missing command".to_string());
|
||||
}
|
||||
|
||||
let command = match args[i].as_str() {
|
||||
"get" => {
|
||||
i += 1;
|
||||
if i + 1 >= args.len() {
|
||||
return Err("get requires HOST and REMOTE_FILE".to_string());
|
||||
}
|
||||
let host = args[i].clone();
|
||||
let remote_file = args[i + 1].clone();
|
||||
let local_file = if i + 2 < args.len() {
|
||||
args[i + 2].clone()
|
||||
} else {
|
||||
// Use the filename part of the remote file
|
||||
Path::new(&remote_file)
|
||||
.file_name()
|
||||
.map_or_else(|| remote_file.clone(), |s| s.to_string_lossy().to_string())
|
||||
};
|
||||
Command::Get {
|
||||
host,
|
||||
remote_file,
|
||||
local_file,
|
||||
}
|
||||
}
|
||||
"put" => {
|
||||
i += 1;
|
||||
if i + 1 >= args.len() {
|
||||
return Err("put requires HOST and LOCAL_FILE".to_string());
|
||||
}
|
||||
let host = args[i].clone();
|
||||
let local_file = args[i + 1].clone();
|
||||
let remote_file = if i + 2 < args.len() {
|
||||
args[i + 2].clone()
|
||||
} else {
|
||||
// Use the filename part of the local file
|
||||
Path::new(&local_file)
|
||||
.file_name()
|
||||
.map_or_else(|| local_file.clone(), |s| s.to_string_lossy().to_string())
|
||||
};
|
||||
Command::Put {
|
||||
host,
|
||||
local_file,
|
||||
remote_file,
|
||||
}
|
||||
}
|
||||
cmd => {
|
||||
return Err(format!("unknown command: {cmd}"));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Args {
|
||||
command,
|
||||
mode,
|
||||
verbose,
|
||||
})
|
||||
}
|
||||
|
||||
/// Format host for connection (add default port if needed).
|
||||
fn format_host(host: &str) -> String {
|
||||
if host.contains(':') {
|
||||
host.to_string()
|
||||
} else {
|
||||
format!("{host}:69")
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn main() -> ExitCode {
|
||||
let args = match parse_args() {
|
||||
Ok(args) => args,
|
||||
Err(e) => {
|
||||
eprintln!("Error: {e}");
|
||||
eprintln!("Try '--help' for more information.");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
match args.command {
|
||||
Command::Get {
|
||||
host,
|
||||
remote_file,
|
||||
local_file,
|
||||
} => {
|
||||
let addr = format_host(&host);
|
||||
|
||||
if args.verbose {
|
||||
eprintln!("Getting '{remote_file}' from {addr} -> '{local_file}'");
|
||||
eprintln!(
|
||||
"Mode: {}",
|
||||
match args.mode {
|
||||
Mode::Octet => "octet",
|
||||
Mode::NetAscii => "netascii",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let client = match Client::new(&addr) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("Error connecting to {addr}: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
// Create output file
|
||||
let mut file = match File::create(&local_file) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("Error creating file '{local_file}': {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
// Download
|
||||
match client.get_to_writer(&remote_file, args.mode, &mut file) {
|
||||
Ok(bytes) => {
|
||||
if args.verbose {
|
||||
eprintln!("Received {bytes} bytes");
|
||||
}
|
||||
println!("Downloaded '{remote_file}' -> '{local_file}' ({bytes} bytes)");
|
||||
}
|
||||
Err(e) => {
|
||||
// Clean up partial file
|
||||
let _ = std::fs::remove_file(&local_file);
|
||||
eprintln!("Error: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Command::Put {
|
||||
host,
|
||||
local_file,
|
||||
remote_file,
|
||||
} => {
|
||||
let addr = format_host(&host);
|
||||
|
||||
if args.verbose {
|
||||
eprintln!("Putting '{local_file}' -> {addr} as '{remote_file}'");
|
||||
eprintln!(
|
||||
"Mode: {}",
|
||||
match args.mode {
|
||||
Mode::Octet => "octet",
|
||||
Mode::NetAscii => "netascii",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let client = match Client::new(&addr) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("Error connecting to {addr}: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
// Open input file
|
||||
let mut file = match File::open(&local_file) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("Error opening file '{local_file}': {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
// Upload
|
||||
match client.put_from_reader(&remote_file, args.mode, &mut file) {
|
||||
Ok(bytes) => {
|
||||
if args.verbose {
|
||||
eprintln!("Sent {bytes} bytes");
|
||||
}
|
||||
println!("Uploaded '{local_file}' -> '{remote_file}' ({bytes} bytes)");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user