1
- use anyhow:: { anyhow , bail, Context , Result } ;
1
+ use anyhow:: { bail, Context , Result } ;
2
2
use std:: {
3
3
collections:: HashMap ,
4
- path:: { Path , PathBuf } ,
4
+ path:: { Component , Path , PathBuf } ,
5
5
} ;
6
6
7
7
#[ derive( Debug , Hash , PartialEq , Eq ) ]
@@ -29,8 +29,13 @@ impl FileServer {
29
29
}
30
30
}
31
31
32
+ fn is_safe_relative_subpath ( path : & Path ) -> bool {
33
+ !path. is_absolute ( ) && path. components ( ) . all ( |comp| comp != Component :: ParentDir )
34
+ }
35
+
32
36
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
+
34
39
let mount_point = MountPoint {
35
40
route : route. to_owned ( ) ,
36
41
fs_path : PathBuf :: from ( fs_path) ,
@@ -56,19 +61,12 @@ impl FileServer {
56
61
self . map ( route, file_path, false )
57
62
}
58
63
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}" ) ;
62
68
}
63
69
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 > {
72
70
let file_path = self
73
71
. mount_points
74
72
. values ( )
@@ -77,7 +75,7 @@ impl FileServer {
77
75
. map ( |mp| mp. fs_path . clone ( ) ) ;
78
76
79
77
if let Some ( file_path) = file_path {
80
- return FileServer :: get_file ( file_path) ;
78
+ return Ok ( file_path) ;
81
79
}
82
80
83
81
let dir_mount_point = self
@@ -89,25 +87,48 @@ impl FileServer {
89
87
if let Some ( dir_mount_point) = dir_mount_point {
90
88
let file_name = file
91
89
. 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 ( '/' ) ;
93
92
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
+ }
98
103
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 ( ) ) ;
101
106
}
102
107
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)
104
115
}
105
116
}
106
117
107
118
#[ cfg( test) ]
108
119
mod tests {
120
+ use std:: path:: PathBuf ;
121
+
109
122
use super :: FileServer ;
110
123
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
+
111
132
#[ test]
112
133
fn test_map_dir_ok ( ) {
113
134
let fs = FileServer :: new ( ) . map_dir ( "/static" , "./relative/static" ) ;
@@ -142,4 +163,46 @@ mod tests {
142
163
143
164
assert ! ( fs. is_err( ) ) ;
144
165
}
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
+ }
145
208
}
0 commit comments