Skip to content

Commit b2200e9

Browse files
committed
fix: improve FileServer + write more tests
1 parent f46637b commit b2200e9

File tree

1 file changed

+86
-23
lines changed

1 file changed

+86
-23
lines changed

src/file_server.rs

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use anyhow::{anyhow, bail, Context, Result};
1+
use anyhow::{bail, Context, Result};
22
use std::{
33
collections::HashMap,
4-
path::{Path, PathBuf},
4+
path::{Component, Path, PathBuf},
55
};
66

77
#[derive(Debug, Hash, PartialEq, Eq)]
@@ -29,8 +29,13 @@ impl FileServer {
2929
}
3030
}
3131

32+
fn is_safe_relative_subpath(path: &Path) -> bool {
33+
!path.is_absolute() && path.components().all(|comp| comp != Component::ParentDir)
34+
}
35+
3236
fn map(mut self, route: &str, fs_path: &str, is_directory: bool) -> Result<Self> {
33-
let route = route.strip_suffix('/').unwrap_or(route);
37+
let route = route.trim_matches('/');
38+
3439
let mount_point = MountPoint {
3540
route: route.to_owned(),
3641
fs_path: PathBuf::from(fs_path),
@@ -56,19 +61,12 @@ impl FileServer {
5661
self.map(route, file_path, false)
5762
}
5863

59-
fn get_file(file_path: PathBuf) -> Result<PathBuf> {
60-
if !file_path.exists() {
61-
bail!("file not found: {}", file_path.display());
64+
fn get_file_path(&self, file: &str) -> Result<PathBuf> {
65+
let file = file.trim_matches('/');
66+
if !Self::is_safe_relative_subpath(Path::new(file)) {
67+
bail!("file location is not safe: {file}");
6268
}
6369

64-
if !file_path.is_file() {
65-
bail!("not a file: {}", file_path.display());
66-
}
67-
68-
Ok(file_path)
69-
}
70-
71-
pub fn handle_file_access(&self, file: &str) -> Result<PathBuf> {
7270
let file_path = self
7371
.mount_points
7472
.values()
@@ -77,7 +75,7 @@ impl FileServer {
7775
.map(|mp| mp.fs_path.clone());
7876

7977
if let Some(file_path) = file_path {
80-
return FileServer::get_file(file_path);
78+
return Ok(file_path);
8179
}
8280

8381
let dir_mount_point = self
@@ -89,25 +87,48 @@ impl FileServer {
8987
if let Some(dir_mount_point) = dir_mount_point {
9088
let file_name = file
9189
.strip_prefix(&dir_mount_point.route)
92-
.with_context(|| format!("file should have prefix: {}", dir_mount_point.route))?;
90+
.with_context(|| format!("file should have prefix: {}", dir_mount_point.route))?
91+
.trim_matches('/');
9392

94-
let safe_file_name = match Path::new(file_name).file_name() {
95-
Some(filename) => Ok(filename.to_owned()),
96-
None => Err(anyhow!("invalid file name: {file}")),
97-
}?;
93+
return Ok(dir_mount_point.fs_path.join(file_name));
94+
}
95+
96+
bail!("failed to get file path: {file}")
97+
}
98+
99+
fn validate_file_exists(file_path: &Path) -> Result<()> {
100+
if !file_path.exists() {
101+
bail!("file not found: {}", file_path.display());
102+
}
98103

99-
let file_path = dir_mount_point.fs_path.join(safe_file_name);
100-
return Self::get_file(file_path);
104+
if !file_path.is_file() {
105+
bail!("not a file: {}", file_path.display());
101106
}
102107

103-
bail!("failed to map file: {file}")
108+
Ok(())
109+
}
110+
111+
pub fn handle_file_access(&self, file: &str) -> Result<PathBuf> {
112+
let file_path = self.get_file_path(file)?;
113+
Self::validate_file_exists(&file_path)?;
114+
Ok(file_path)
104115
}
105116
}
106117

107118
#[cfg(test)]
108119
mod tests {
120+
use std::path::PathBuf;
121+
109122
use super::FileServer;
110123

124+
fn get_dummy_file_server() -> FileServer {
125+
FileServer::new()
126+
.map_file("/favicon.ico", "assets/favicon.ico")
127+
.unwrap()
128+
.map_dir("static", "assets/")
129+
.unwrap()
130+
}
131+
111132
#[test]
112133
fn test_map_dir_ok() {
113134
let fs = FileServer::new().map_dir("/static", "./relative/static");
@@ -142,4 +163,46 @@ mod tests {
142163

143164
assert!(fs.is_err());
144165
}
166+
167+
#[test]
168+
fn test_get_file_path_file_map_ok() {
169+
let fs = get_dummy_file_server();
170+
let actual_path = fs.get_file_path("/favicon.ico").unwrap();
171+
assert_eq!(PathBuf::from("assets/favicon.ico"), actual_path)
172+
}
173+
174+
#[test]
175+
fn test_get_file_path_file_map_err() {
176+
let fs = get_dummy_file_server();
177+
let res = fs.get_file_path("/not-valid.txt");
178+
assert!(res.is_err());
179+
}
180+
181+
#[test]
182+
fn test_get_file_path_dir_map_ok() {
183+
let fs = get_dummy_file_server();
184+
let actual_path = fs.get_file_path("/static/dog.png").unwrap();
185+
assert_eq!(PathBuf::from("assets/dog.png"), actual_path)
186+
}
187+
188+
#[test]
189+
fn test_get_file_path_dir_map_upward_traversal_err() {
190+
let fs = get_dummy_file_server();
191+
let res = fs.get_file_path("/static/../dog.png");
192+
assert!(res.is_err());
193+
}
194+
195+
#[test]
196+
fn test_get_file_path_dir_map_nesting_ok() {
197+
let fs = get_dummy_file_server();
198+
let actual_path = fs.get_file_path("/static/animals/snake.gif").unwrap();
199+
assert_eq!(PathBuf::from("assets/animals/snake.gif"), actual_path)
200+
}
201+
202+
#[test]
203+
fn test_get_file_path_dir_map_nesting2_ok() {
204+
let fs = get_dummy_file_server();
205+
let actual_path = fs.get_file_path("static/animals/birds/dove.jpeg/").unwrap();
206+
assert_eq!(PathBuf::from("assets/animals/birds/dove.jpeg"), actual_path)
207+
}
145208
}

0 commit comments

Comments
 (0)