async_tftp/server/handlers/
dir.rs

1use blocking::{unblock, Unblock};
2use log::trace;
3use std::fs::{self, File};
4use std::io;
5use std::net::SocketAddr;
6use std::path::Component;
7use std::path::{Path, PathBuf};
8
9use crate::error::{Error, Result};
10use crate::packet;
11
12/// Handler that serves read requests for a directory.
13pub struct DirHandler {
14    dir: PathBuf,
15    serve_rrq: bool,
16    serve_wrq: bool,
17}
18
19pub enum DirHandlerMode {
20    /// Serve only read requests.
21    ReadOnly,
22    /// Serve only write requests.
23    WriteOnly,
24    /// Server read and write requests.
25    ReadWrite,
26}
27
28impl DirHandler {
29    /// Create new handler for directory.
30    pub fn new<P>(dir: P, flags: DirHandlerMode) -> Result<Self>
31    where
32        P: AsRef<Path>,
33    {
34        let dir = fs::canonicalize(dir.as_ref())?;
35
36        if !dir.is_dir() {
37            return Err(Error::NotDir(dir));
38        }
39
40        trace!("TFTP directory: {}", dir.display());
41
42        let serve_rrq = match flags {
43            DirHandlerMode::ReadOnly => true,
44            DirHandlerMode::WriteOnly => false,
45            DirHandlerMode::ReadWrite => true,
46        };
47
48        let serve_wrq = match flags {
49            DirHandlerMode::ReadOnly => false,
50            DirHandlerMode::WriteOnly => true,
51            DirHandlerMode::ReadWrite => true,
52        };
53
54        Ok(DirHandler {
55            dir,
56            serve_rrq,
57            serve_wrq,
58        })
59    }
60}
61
62impl crate::server::Handler for DirHandler {
63    type Reader = Unblock<File>;
64    type Writer = Unblock<File>;
65
66    async fn read_req_open(
67        &mut self,
68        _client: &SocketAddr,
69        path: &Path,
70    ) -> Result<(Self::Reader, Option<u64>), packet::Error> {
71        if !self.serve_rrq {
72            return Err(packet::Error::IllegalOperation);
73        }
74
75        let path = secure_path(&self.dir, path)?;
76
77        // Send only regular files
78        if !path.is_file() {
79            return Err(packet::Error::FileNotFound);
80        }
81
82        let path_clone = path.clone();
83        let (file, len) = unblock(move || open_file_ro(path_clone)).await?;
84        let reader = Unblock::new(file);
85
86        trace!("TFTP sending file: {}", path.display());
87
88        Ok((reader, len))
89    }
90
91    async fn write_req_open(
92        &mut self,
93        _client: &SocketAddr,
94        path: &Path,
95        size: Option<u64>,
96    ) -> Result<Self::Writer, packet::Error> {
97        if !self.serve_wrq {
98            return Err(packet::Error::IllegalOperation);
99        }
100
101        let path = secure_path(&self.dir, path)?;
102
103        let path_clone = path.clone();
104        let file = unblock(move || open_file_wo(path_clone, size)).await?;
105        let writer = Unblock::new(file);
106
107        trace!("TFTP receiving file: {}", path.display());
108
109        Ok(writer)
110    }
111}
112
113fn secure_path(
114    restricted_dir: &Path,
115    path: &Path,
116) -> Result<PathBuf, packet::Error> {
117    // Strip `/` and `./` prefixes
118    let path = path
119        .strip_prefix("/")
120        .or_else(|_| path.strip_prefix("./"))
121        .unwrap_or(path);
122
123    // Avoid directory traversal attack by filtering `../`.
124    if path.components().any(|x| x == Component::ParentDir) {
125        return Err(packet::Error::PermissionDenied);
126    }
127
128    // Path should not start from root dir or have any Windows prefixes.
129    // i.e. We accept only normal path components.
130    match path.components().next() {
131        Some(Component::Normal(_)) => {}
132        _ => return Err(packet::Error::PermissionDenied),
133    }
134
135    Ok(restricted_dir.join(path))
136}
137
138fn open_file_ro(path: PathBuf) -> io::Result<(File, Option<u64>)> {
139    let file = File::open(path)?;
140    let len = file.metadata().ok().map(|m| m.len());
141    Ok((file, len))
142}
143
144fn open_file_wo(path: PathBuf, size: Option<u64>) -> io::Result<File> {
145    let file = File::create(path)?;
146
147    if let Some(size) = size {
148        file.set_len(size)?;
149    }
150
151    Ok(file)
152}