aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES14
-rw-r--r--Cargo.toml6
-rw-r--r--README8
-rw-r--r--src/lib.rs75
-rw-r--r--src/tftpc.rs108
-rw-r--r--src/tftpd.rs134
-rwxr-xr-xtest.sh2
7 files changed, 163 insertions, 184 deletions
diff --git a/CHANGES b/CHANGES
new file mode 100644
index 0000000..f21fa28
--- /dev/null
+++ b/CHANGES
@@ -0,0 +1,14 @@
+1.1.0 (2019-10-05)
+ * Server:
+ - chroot to destination directory if permissions are sufficient
+ - drop -d parameter, but allow directory at the end of the command line
+ * Support for some non-standard options:
+ - blksize2
+ - utimeout
+
+1.0.0 (2019-03-12)
+ * Initial release
+ * RFC 1350 (TFTP revision 2)
+ * RFC 2347 (Option Extension)
+ * RFC 2348 (Blocksize Option)
+ * RFC 2349 (Timeout Interval and Transfer Size Options)
diff --git a/Cargo.toml b/Cargo.toml
index eb4bc6a..a1adf0d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rtftp"
-version = "1.0.0"
+version = "1.1.0"
authors = ["Reiner Herrmann <reiner@reiner-h.de>"]
edition = "2018"
license = "GPL-3.0-or-later"
@@ -10,8 +10,8 @@ lto = true
panic = 'abort'
[dependencies]
-nix = "0.13.0"
-getopts = "0.2.18"
+nix = "0.15.0"
+getopts = "0.2.19"
threadpool = "1.7.1"
[[bin]]
diff --git a/README b/README
index 08b08c7..e6a5f44 100644
--- a/README
+++ b/README
@@ -10,6 +10,10 @@ Currently supported:
- RFC 2348 (Blocksize Option)
- RFC 2349 (Timeout Interval and Transfer Size Options)
+Non-standard options:
+- blksize2: block size as a power of 2
+- utimeout: timeout in microseconds
+
Use cargo to build the binaries (output dir is target/release/):
$ cargo build --release
@@ -35,12 +39,10 @@ Server:
$ ./rtftpd --help
RusTFTP
- ./rtftpd [options]
+ ./rtftpd [options] [directory]
Options:
-h, --help display usage information
- -d, --directory DIRECTORY
- directory to serve (default: current directory)
-p, --port PORT port to listen on (default: 69)
-u, --uid UID user id to run as (default: 65534)
-g, --gid GID group id to run as (default: 65534)
diff --git a/src/lib.rs b/src/lib.rs
index 9299eea..6250722 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -39,7 +39,7 @@ pub enum Mode {
#[derive(Clone, Copy)]
pub struct TftpOptions {
blksize: usize,
- timeout: u8,
+ timeout: Duration,
tsize: u64,
}
@@ -53,7 +53,7 @@ pub struct Tftp {
fn default_options() -> TftpOptions {
TftpOptions {
blksize: 512,
- timeout: 3,
+ timeout: Duration::from_secs(3),
tsize: 0,
}
}
@@ -93,6 +93,11 @@ fn octet_to_netascii(buf: &[u8]) -> Vec<u8> {
out
}
+fn blksize2(size: usize) -> usize {
+ (size + 1).next_power_of_two() >> 1
+}
+
+
impl Default for Tftp {
fn default() -> Tftp {
Tftp {
@@ -140,18 +145,14 @@ impl Tftp {
}
fn get_tftp_str(&self, buf: &[u8]) -> Option<String> {
- let mut iter = buf.iter();
-
- let len = match iter.position(|&x| x == 0) {
- Some(l) => l,
- None => return None,
- };
- let val = match String::from_utf8(buf[0..len].to_vec()) {
- Ok(v) => v,
- Err(_) => return None,
- };
-
- Some(val)
+ /* make sure the null-terminator exists */
+ buf.iter().find(|&x| *x == 0)?;
+
+ /* build string from buffer */
+ String::from_utf8(buf.iter()
+ .take_while(|&x| *x != 0)
+ .cloned()
+ .collect()).ok()
}
/// Read::read can possibly return less bytes than the requested buffer size,
@@ -162,7 +163,7 @@ impl Tftp {
/// This function will always fill the buffer completely (like expected from
/// read_exact), but also works with EOF, by filling the buffer partially and
/// returning the amount of bytes read.
- fn read_exact(&self, reader: &mut Read, buf: &mut [u8]) -> Result<usize, io::Error> {
+ fn read_exact(&self, reader: &mut dyn Read, buf: &mut [u8]) -> Result<usize, io::Error> {
let maxlen = buf.len();
let mut outbuf = Vec::with_capacity(maxlen);
let mut len = 0;
@@ -309,9 +310,24 @@ impl Tftp {
}
_ => false,
},
+ "blksize2" => match val.parse() {
+ Ok(b) if b >= 8 && b <= 32768 => {
+ /* select 2^x lower or equal the requested size */
+ self.options.blksize = blksize2(b);
+ true
+ }
+ _ => false,
+ },
"timeout" => match val.parse() {
Ok(t) if t >= 1 => {
- self.options.timeout = t;
+ self.options.timeout = Duration::from_secs(t);
+ true
+ }
+ _ => false,
+ },
+ "utimeout" => match val.parse() {
+ Ok(t) if t >= 1 => {
+ self.options.timeout = Duration::from_micros(t);
true
}
_ => false,
@@ -327,7 +343,7 @@ impl Tftp {
}
});
- sock.set_read_timeout(Some(Duration::from_secs(u64::from(self.options.timeout))))?;
+ sock.set_read_timeout(Some(self.options.timeout))?;
Ok(())
}
@@ -356,26 +372,18 @@ impl Tftp {
}
pub fn parse_file_mode_options(&self, buf: &[u8]) -> Result<(PathBuf, String, HashMap<String, String>), io::Error> {
- let dataerr = io::Error::new(io::ErrorKind::InvalidData, "invalid data received");
+ let dataerr = || io::Error::new(io::ErrorKind::InvalidData, "invalid data received");
let mut pos = 0;
- let filename = match self.get_tftp_str(&buf[pos..]) {
- Some(f) => f,
- None => return Err(dataerr),
- };
+ let filename = self.get_tftp_str(&buf[pos..]).ok_or_else(dataerr)?;
pos += filename.len() + 1;
- let filename = Path::new(&filename);
-
- let mode = match self.get_tftp_str(&buf[pos..]) {
- Some(m) => m.to_lowercase(),
- None => return Err(dataerr),
- };
+ let mode = self.get_tftp_str(&buf[pos..]).ok_or_else(dataerr)?.to_lowercase();
pos += mode.len() + 1;
let options = self.parse_options(&buf[pos..]);
- Ok((filename.to_path_buf(), mode, options))
+ Ok((Path::new(&filename).to_path_buf(), mode, options))
}
pub fn send_error(&self, socket: &UdpSocket, code: u16, msg: &str) -> Result<(), io::Error> {
@@ -663,4 +671,13 @@ mod tests {
assert_eq!(octet_to_netascii(b"\r\0\r\n"), b"\r\0\0\r\0\r\n");
assert_eq!(octet_to_netascii(b""), b"");
}
+
+ #[test]
+ fn test_blksize2() {
+ assert_eq!(blksize2(16), 16);
+ assert_eq!(blksize2(17), 16);
+ assert_eq!(blksize2(15), 8);
+ assert_eq!(blksize2(1), 1);
+ assert_eq!(blksize2(0), 0);
+ }
}
diff --git a/src/tftpc.rs b/src/tftpc.rs
index 24073d2..a134293 100644
--- a/src/tftpc.rs
+++ b/src/tftpc.rs
@@ -65,35 +65,23 @@ impl Tftpc {
fn wait_for_option_ack(&mut self, sock: &UdpSocket) -> Option<SocketAddr> {
let mut buf = [0; 512];
- match sock.peek_from(&mut buf) {
- Ok(_) => (),
- Err(_) => return None,
- };
+ sock.peek_from(&mut buf).ok()?;
let opcode = u16::from_be_bytes([buf[0], buf[1]]);
if opcode != rtftp::Opcode::OACK as u16 {
return None;
}
- let (len, remote) = match sock.recv_from(&mut buf) {
- Ok(args) => args,
- Err(_) => return None,
- };
+ let (len, remote) = sock.recv_from(&mut buf).ok()?;
let mut options = self.tftp.parse_options(&buf[2..len]);
- match self.tftp.init_tftp_options(&sock, &mut options) {
- Ok(_) => {}
- Err(_) => return None,
- }
+ self.tftp.init_tftp_options(&sock, &mut options).ok()?;
Some(remote)
}
fn wait_for_response(&self, sock: &UdpSocket, expected_opcode: rtftp::Opcode, expected_block: u16, expected_remote: Option<SocketAddr>) -> Result<Option<SocketAddr>, std::io::Error> {
let mut buf = [0; 4];
- let (len, remote) = match sock.peek_from(&mut buf) {
- Ok(args) => args,
- Err(err) => return Err(err),
- };
+ let (len, remote) = sock.peek_from(&mut buf)?;
if let Some(rem) = expected_remote {
/* verify we got a response from the same client that sent
@@ -140,24 +128,14 @@ impl Tftpc {
}
fn handle_wrq(&mut self, sock: &UdpSocket) -> Result<String, io::Error> {
- let mut file = match File::open(self.conf.filename.as_path()) {
- Ok(f) => f,
- Err(err) => return Err(err),
- };
- let err_invalidpath = io::Error::new(io::ErrorKind::InvalidInput, "Invalid path/filename");
- let filename = match self.conf.filename.file_name() {
- Some(f) => match f.to_str() {
- Some(s) => s,
- None => return Err(err_invalidpath),
- },
- None => return Err(err_invalidpath),
- };
- let metadata = match file.metadata() {
- Ok(m) => m,
- Err(_) => return Err(err_invalidpath),
- };
+ let mut file = File::open(self.conf.filename.as_path())?;
+ let err_invalidpath = || io::Error::new(io::ErrorKind::InvalidInput, "Invalid path/filename");
+
+ let filename = self.conf.filename.file_name().ok_or_else(err_invalidpath)?
+ .to_str().ok_or_else(err_invalidpath)?;
+ let metadata = file.metadata().map_err(|_| err_invalidpath())?;
if !metadata.is_file() {
- return Err(err_invalidpath);
+ return Err(err_invalidpath());
}
let tsize = self.tftp.transfersize(&mut file)?;
@@ -192,19 +170,11 @@ impl Tftpc {
}
fn handle_rrq(&mut self, sock: &UdpSocket) -> Result<String, io::Error> {
- let filename = match self.conf.filename.file_name() {
- Some(f) => f,
- None => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid path/filename")),
- };
+ let err_invalidpath = || io::Error::new(io::ErrorKind::InvalidInput, "Invalid path/filename");
+ let filename = self.conf.filename.file_name().ok_or_else(err_invalidpath)?;
let outpath = env::current_dir().expect("Can't get current directory").join(filename);
- let mut file = match File::create(outpath) {
- Ok(f) => f,
- Err(err) => return Err(err),
- };
- let filename = match self.conf.filename.to_str() {
- Some(f) => f,
- None => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid path/filename")),
- };
+ let mut file = File::create(outpath)?;
+ let filename = self.conf.filename.to_str().ok_or_else(err_invalidpath)?;
let buf = self.init_req(rtftp::Opcode::RRQ, filename, 0);
@@ -256,7 +226,7 @@ impl Tftpc {
}
}
-fn usage(opts: Options, program: String, error: Option<String>) {
+fn usage(opts: &Options, program: &str, error: Option<String>) {
if let Some(err) = error {
println!("{}\n", err);
}
@@ -264,7 +234,7 @@ fn usage(opts: Options, program: String, error: Option<String>) {
println!("{}", opts.usage(format!("RusTFTP {}\n\n{} [options] <remote>[:port]", version, program).as_str()));
}
-fn parse_commandline(args: &[String]) -> Result<Configuration, &str> {
+fn parse_commandline(args: &[String]) -> Option<Configuration> {
let program = args[0].clone();
let mut operation = None;
let mut mode = rtftp::Mode::OCTET;
@@ -277,16 +247,14 @@ fn parse_commandline(args: &[String]) -> Result<Configuration, &str> {
opts.optopt("p", "put", "upload file to remote server", "FILE");
opts.optopt("b", "blksize", format!("negotiate a different block size (default: {})", blksize).as_ref(), "SIZE");
opts.optflag("n", "netascii","use netascii mode (instead of octet)");
- let matches = match opts.parse(&args[1..]) {
- Ok(m) => m,
- Err(err) => {
- usage(opts, program, Some(err.to_string()));
- return Err("Parsing error");
- }
- };
+
+ let getopts_fail = |err: getopts::Fail| { usage(&opts, &program, Some(err.to_string())) };
+ let conv_error = |err: std::num::ParseIntError| { usage(&opts, &program, Some(err.to_string())) };
+
+ let matches = opts.parse(&args[1..]).map_err(getopts_fail).ok()?;
if matches.opt_present("h") || matches.free.len() != 1 {
- usage(opts, program, None);
- return Err("usage");
+ usage(&opts, &program, None);
+ return None;
}
if let Some(f) = matches.opt_str("g") {
@@ -299,8 +267,8 @@ fn parse_commandline(args: &[String]) -> Result<Configuration, &str> {
}
if operation.is_none() || (matches.opt_present("g") && matches.opt_present("p")) {
- usage(opts, program, Some("Exactly one of g (get) and p (put) required".to_string()));
- return Err("get put");
+ usage(&opts, &program, Some("Exactly one of g (get) and p (put) required".to_string()));
+ return None;
}
if matches.opt_present("n") {
@@ -309,25 +277,19 @@ fn parse_commandline(args: &[String]) -> Result<Configuration, &str> {
let remote_in = matches.free[0].as_str();
let remote = match remote_in.to_socket_addrs() {
- Ok(mut i) => i.next(),
+ Ok(i) => i,
Err(_) => match (remote_in, 69).to_socket_addrs() {
- Ok(mut j) => j.next(),
+ Ok(j) => j,
Err(_) => {
- usage(opts, program, Some("Failed to parse and lookup specified remote".to_string()));
- return Err("lookup");
+ usage(&opts, &program, Some("Failed to parse and lookup specified remote".to_string()));
+ return None;
}
},
- };
+ }.next();
- blksize = match matches.opt_get_default::<usize>("b", blksize) {
- Ok(b) => b,
- Err(err) => {
- usage(opts, program, Some(err.to_string()));
- return Err("blksize");
- }
- };
+ blksize = matches.opt_get_default::<usize>("b", blksize).map_err(conv_error).ok()?;
- Ok(Configuration {
+ Some(Configuration {
operation: operation.unwrap(),
mode,
filename: filename.unwrap(),
@@ -339,8 +301,8 @@ fn parse_commandline(args: &[String]) -> Result<Configuration, &str> {
fn main() {
let args: Vec<String> = env::args().collect();
let conf = match parse_commandline(&args) {
- Ok(c) => c,
- Err(_) => return,
+ Some(c) => c,
+ None => return,
};
Tftpc::new(conf).start();
diff --git a/src/tftpd.rs b/src/tftpd.rs
index c45f6e1..6513367 100644
--- a/src/tftpd.rs
+++ b/src/tftpd.rs
@@ -13,7 +13,7 @@ use std::path::{Path, PathBuf};
use std::time::Duration;
extern crate nix;
-use nix::unistd::{setresgid, setresuid, Gid, Uid};
+use nix::unistd::{chroot, setresgid, setresuid, Gid, Uid, ROOT};
extern crate getopts;
use getopts::Options;
@@ -64,24 +64,13 @@ impl Tftpd {
fn file_allowed(&self, filename: &Path) -> Option<PathBuf> {
/* get parent to check dir where file should be read/written */
- let path = self.conf.dir.join(filename);
- let path = match path.parent() {
- Some(p) => p,
- None => return None,
- };
- let path = match path.canonicalize() {
- Ok(p) => p,
- Err(_) => return None,
- };
-
- /* get last component to append to canonicalized path */
- let filename = match filename.file_name() {
- Some(f) => f,
- None => return None,
- };
- let path = path.join(filename);
+ let path = self.conf.dir.join(filename)
+ .parent()?
+ .canonicalize()
+ .ok()?;
- match path.strip_prefix(&self.conf.dir) {
+ /* check last component of given filename appended to canonicalized path */
+ match path.join(filename.file_name()?).strip_prefix(&self.conf.dir) {
Ok(p) if p != PathBuf::new() => Some(p.to_path_buf()),
_ => None,
}
@@ -192,6 +181,11 @@ impl Tftpd {
socket.set_read_timeout(Some(Duration::from_secs(5)))?;
socket.connect(cl)?;
+ if buf.len() < 2 {
+ self.tftp.send_error(&socket, 0, "Invalid request length")?;
+ return Err(io::Error::new(io::ErrorKind::Other, "invalid request length"));
+ }
+
match u16::from_be_bytes([buf[0], buf[1]]) { // opcode
o if o == rtftp::Opcode::RRQ as u16 => {
if self.conf.wo {
@@ -217,8 +211,8 @@ impl Tftpd {
}
}
- fn drop_privs(&self, uid: u32, gid: u32) -> Result<(), Box<Error>> {
- let root_uid = Uid::from_raw(0);
+ fn drop_privs(&self, uid: u32, gid: u32) -> Result<(), Box<dyn Error>> {
+ let root_uid = ROOT;
let root_gid = Gid::from_raw(0);
let unpriv_uid = Uid::from_raw(uid);
let unpriv_gid = Gid::from_raw(gid);
@@ -243,6 +237,22 @@ impl Tftpd {
Ok(())
}
+ fn chroot_destdir(&mut self) -> Result<(), nix::Error> {
+ /* chroot will only succeed if we have required permissions;
+ either running as root or having CAP_SYS_CHROOT.
+ propagate error only if chroot should have succeeded. */
+ match chroot(&self.conf.dir) {
+ Ok(_) => {
+ /* configured dir is now new root directory */
+ self.conf.dir = PathBuf::from("/");
+ Ok(())
+ },
+ Err(err) if err == nix::Error::from_errno(nix::errno::Errno::EPERM) => Ok(()),
+ Err(err) if Uid::effective() == ROOT => Err(err),
+ Err(_) => Ok(()),
+ }
+ }
+
pub fn start(&mut self) {
let socket = match UdpSocket::bind(format!("[::]:{}", self.conf.port)) {
Ok(s) => s,
@@ -251,6 +261,13 @@ impl Tftpd {
return;
}
};
+ match self.chroot_destdir() {
+ Ok(_) => {},
+ Err(err) => {
+ eprintln!("Changing root directory failed ({}).", err);
+ return;
+ }
+ }
match self.drop_privs(self.conf.uid, self.conf.gid) {
Ok(_) => (),
Err(err) => {
@@ -262,7 +279,7 @@ impl Tftpd {
match env::set_current_dir(&self.conf.dir) {
Ok(_) => (),
Err(err) => {
- eprintln!("Changing directory to {} failed ({}).", &self.conf.dir.display(), err);
+ eprintln!("Changing directory failed ({}).", err);
return;
}
}
@@ -289,90 +306,57 @@ impl Tftpd {
}
}
-fn usage(opts: Options, program: String, error: Option<String>) {
+fn usage(opts: &Options, program: &str, error: Option<String>) {
if let Some(err) = error {
println!("{}\n", err);
}
let version = rtftp::VERSION.unwrap_or("");
- println!("{}", opts.usage(format!("RusTFTP {}\n\n{} [options]", version, program).as_str()));
+ println!("{}", opts.usage(format!("RusTFTP {}\n\n{} [options] [directory]", version, program).as_str()));
}
-fn parse_commandline(args: &[String]) -> Result<Configuration, &str> {
+fn parse_commandline(args: &[String]) -> Option<Configuration> {
let program = args[0].clone();
let mut conf: Configuration = Default::default();
let mut opts = Options::new();
opts.optflag("h", "help", "display usage information");
- opts.optopt("d", "directory", "directory to serve (default: current directory)", "DIRECTORY");
opts.optopt("p", "port", format!("port to listen on (default: {})", conf.port).as_ref(), "PORT");
opts.optopt("u", "uid", format!("user id to run as (default: {})", conf.uid).as_ref(), "UID");
opts.optopt("g", "gid", format!("group id to run as (default: {})", conf.gid).as_ref(), "GID");
opts.optflag("r", "read-only", "allow only reading/downloading of files (RRQ)");
opts.optflag("w", "write-only", "allow only writing/uploading of files (WRQ)");
opts.optopt("t", "threads", format!("number of worker threads (default: {})", conf.threads).as_ref(), "N");
- let matches = match opts.parse(&args[1..]) {
- Ok(m) => m,
- Err(err) => {
- usage(opts, program, Some(err.to_string()));
- return Err("Parsing error");
- }
- };
+
+ let getopts_fail = |err: getopts::Fail| { usage(&opts, &program, Some(err.to_string())) };
+ let conv_error = |err: std::num::ParseIntError| { usage(&opts, &program, Some(err.to_string())) };
+
+ let matches = opts.parse(&args[1..]).map_err(getopts_fail).ok()?;
if matches.opt_present("h") {
- usage(opts, program, None);
- return Err("usage");
+ usage(&opts, &program, None);
+ return None;
}
- conf.port = match matches.opt_get_default("p", conf.port) {
- Ok(p) => p,
- Err(err) => {
- usage(opts, program, Some(err.to_string()));
- return Err("port");
- }
- };
- conf.uid = match matches.opt_get_default("u", conf.uid) {
- Ok(u) => u,
- Err(err) => {
- usage(opts, program, Some(err.to_string()));
- return Err("uid");
- }
- };
- conf.gid = match matches.opt_get_default("g", conf.gid) {
- Ok(g) => g,
- Err(err) => {
- usage(opts, program, Some(err.to_string()));
- return Err("gid");
- }
- };
- conf.threads = match matches.opt_get_default("t", conf.threads) {
- Ok(t) => t,
- Err(err) => {
- usage(opts, program, Some(err.to_string()));
- return Err("threads");
- }
- };
+ conf.port = matches.opt_get_default("p", conf.port).map_err(conv_error).ok()?;
+ conf.uid = matches.opt_get_default("u", conf.uid).map_err(conv_error).ok()?;
+ conf.gid = matches.opt_get_default("g", conf.gid).map_err(conv_error).ok()?;
+ conf.threads = matches.opt_get_default("t", conf.threads).map_err(conv_error).ok()?;
conf.ro = matches.opt_present("r");
conf.wo = matches.opt_present("w");
if conf.ro && conf.wo {
- usage(opts, program, Some(String::from("Only one of r (read-only) and w (write-only) allowed")));
- return Err("ro and wo");
+ usage(&opts, &program, Some(String::from("Only one of r (read-only) and w (write-only) allowed")));
+ return None;
}
- if matches.opt_present("d") {
- conf.dir = match matches.opt_str("d") {
- Some(d) => Path::new(&d).to_path_buf(),
- None => {
- usage(opts, program, None);
- return Err("directory");
- }
- };
+ if matches.free.len() > 0 {
+ conf.dir = Path::new(&matches.free[0]).to_path_buf();
}
- Ok(conf)
+ Some(conf)
}
fn main() {
let args: Vec<String> = env::args().collect();
let conf = match parse_commandline(&args) {
- Ok(c) => c,
- Err(_) => return,
+ Some(c) => c,
+ None => return,
};
Tftpd::new(conf).start();
diff --git a/test.sh b/test.sh
index 61fa6d0..7ee1800 100755
--- a/test.sh
+++ b/test.sh
@@ -52,7 +52,7 @@ tftpc() {
}
rtftpd() {
- $SSD --background --exec "$RTFTPD" --start -- -p $PORT -d "$SERVERDIR" 1>/dev/null
+ $SSD --background --exec "$RTFTPD" --start -- -p $PORT "$SERVERDIR" 1>/dev/null
}
rtftpc() {