@@ -5,9 +5,47 @@ use actix_files::NamedFile;
5
5
use actix_web:: { web:: Data , HttpRequest } ;
6
6
use std:: {
7
7
io:: { Error , ErrorKind } ,
8
- path:: PathBuf ,
8
+ path:: { Component , Path , PathBuf } ,
9
9
} ;
10
10
11
+ /// Clean up invalid components in the paths and returns it. For a file
12
+ /// in the public folder, only "normal" components are valid.
13
+ fn clean_up_path ( uri : & str ) -> PathBuf {
14
+ // First split the URI as it always uses the /.
15
+ let path = PathBuf :: from_iter ( uri. split ( '/' ) ) ;
16
+
17
+ let valid_components: Vec < Component < ' _ > > = path
18
+ . components ( )
19
+ // Keep only normal components. The relative components should be
20
+ // strip by actix, but we're double checking it in case of weird encodings
21
+ // that can be interpreted as parent paths. Note this is a path that will
22
+ // be appended later to the public folder.
23
+ . filter ( |c| matches ! ( c, Component :: Normal ( _) ) )
24
+ . collect ( ) ;
25
+
26
+ // Build a new PathBuf based only on valid components
27
+ PathBuf :: from_iter ( valid_components)
28
+ }
29
+
30
+ /// Build the file path to retrieve and check if it exists. To build, it takes the project
31
+ /// root and the parsed path. You can set it the index_folder flag to manage the
32
+ /// parsed_path as a folder an look for an index.html inside it.
33
+ fn retrieve_asset_path ( root_path : & Path , file_path : & Path , index_folder : bool ) -> Option < PathBuf > {
34
+ let public_folder = root_path. join ( "public" ) ;
35
+ let asset_path = if index_folder {
36
+ public_folder. join ( file_path) . join ( "index.html" )
37
+ } else {
38
+ public_folder. join ( file_path)
39
+ } ;
40
+
41
+ // Checks the output path is a child of public folder
42
+ if asset_path. starts_with ( public_folder) && asset_path. exists ( ) && asset_path. is_file ( ) {
43
+ Some ( asset_path)
44
+ } else {
45
+ None
46
+ }
47
+ }
48
+
11
49
/// Find a static HTML file in the `public` folder. This function is used
12
50
/// when there's no direct file to be served. It will look for certain patterns
13
51
/// like "public/{uri}/index.html" and "public/{uri}.html".
@@ -17,20 +55,234 @@ pub async fn handle_assets(req: &HttpRequest) -> Result<NamedFile, Error> {
17
55
let root_path = req. app_data :: < Data < PathBuf > > ( ) . unwrap ( ) ;
18
56
let uri_path = req. path ( ) ;
19
57
20
- // File path. This is required for the wasm_handler as dynamic routes may capture static files
21
- let file_path = root_path. join ( format ! ( "public{uri_path}" ) ) ;
22
- // A.k.a pretty urls. We may access /about and this matches to /about/index.html
23
- let index_folder_path = root_path. join ( format ! ( "public{uri_path}/index.html" ) ) ;
24
- // Same as before, but the file is located at ./about.html
25
- let html_ext_path = root_path. join ( format ! ( "public{uri_path}.html" ) ) ;
58
+ // Double-check the given path path does not contain any unexpected value.
59
+ // It was previously sanitized, but this is a double check.
60
+ let parsed_path = clean_up_path ( uri_path) ;
26
61
27
- if file_path. exists ( ) {
62
+ if let Some ( file_path) = retrieve_asset_path ( root_path, & parsed_path, false ) {
63
+ // File path. This is required for the wasm_handler as dynamic routes may capture static files
28
64
NamedFile :: open_async ( file_path) . await
29
- } else if uri_path. ends_with ( '/' ) && index_folder_path. exists ( ) {
65
+ } else if let Some ( index_folder_path) = retrieve_asset_path ( root_path, & parsed_path, true ) {
66
+ // A.k.a pretty urls. We may access /about and this matches to /about/index.html
30
67
NamedFile :: open_async ( index_folder_path) . await
31
- } else if !uri_path. ends_with ( '/' ) && html_ext_path. exists ( ) {
32
- NamedFile :: open_async ( html_ext_path) . await
33
68
} else {
34
69
Err ( Error :: new ( ErrorKind :: NotFound , "The file is not present" ) )
35
70
}
36
71
}
72
+
73
+ #[ cfg( test) ]
74
+ mod tests {
75
+ use super :: * ;
76
+
77
+ #[ test]
78
+ fn test_clean_up_path ( ) {
79
+ let tests = if cfg ! ( target_os = "windows" ) {
80
+ Vec :: from ( [
81
+ ( "/" , PathBuf :: new ( ) ) ,
82
+ ( "/index.js" , PathBuf :: from ( "index.js" ) ) ,
83
+ ( "/my-folder/index.js" , PathBuf :: from ( "my-folder\\ index.js" ) ) ,
84
+ // These scenarios are unlikely as actix already filters the
85
+ // URI, but let's test them too
86
+ ( "/../index.js" , PathBuf :: from ( "index.js" ) ) ,
87
+ ( "/../../index.js" , PathBuf :: from ( "index.js" ) ) ,
88
+ ] )
89
+ } else {
90
+ Vec :: from ( [
91
+ ( "/" , PathBuf :: new ( ) ) ,
92
+ ( "/index.js" , PathBuf :: from ( "index.js" ) ) ,
93
+ ( "////index.js" , PathBuf :: from ( "index.js" ) ) ,
94
+ ( "/my-folder/index.js" , PathBuf :: from ( "my-folder/index.js" ) ) ,
95
+ // These scenarios are unlikely as actix already filters the
96
+ // URI, but let's test them too
97
+ ( "/../index.js" , PathBuf :: from ( "index.js" ) ) ,
98
+ ( "/../../index.js" , PathBuf :: from ( "index.js" ) ) ,
99
+ ] )
100
+ } ;
101
+
102
+ for ( uri, path) in tests {
103
+ assert_eq ! ( clean_up_path( uri) , path) ;
104
+ }
105
+ }
106
+
107
+ #[ test]
108
+ fn relative_asset_path_retrieval ( ) {
109
+ let ( project_root, tests) = if cfg ! ( target_os = "windows" ) {
110
+ let project_root = Path :: new ( "..\\ ..\\ tests\\ data" ) ;
111
+ let tests = Vec :: from ( [
112
+ // Existing files
113
+ (
114
+ Path :: new ( "index.html" ) ,
115
+ Some ( PathBuf :: from ( "..\\ ..\\ tests\\ data\\ public\\ index.html" ) ) ,
116
+ ) ,
117
+ (
118
+ Path :: new ( "main.css" ) ,
119
+ Some ( PathBuf :: from ( "..\\ ..\\ tests\\ data\\ public\\ main.css" ) ) ,
120
+ ) ,
121
+ // Missing files
122
+ ( Path :: new ( "" ) , None ) ,
123
+ ( Path :: new ( "unknown" ) , None ) ,
124
+ ( Path :: new ( "about" ) , None ) ,
125
+ ] ) ;
126
+
127
+ ( project_root, tests)
128
+ } else {
129
+ let project_root = Path :: new ( "../../tests/data" ) ;
130
+ let tests = Vec :: from ( [
131
+ // Existing files
132
+ (
133
+ Path :: new ( "index.html" ) ,
134
+ Some ( PathBuf :: from ( "../../tests/data/public/index.html" ) ) ,
135
+ ) ,
136
+ (
137
+ Path :: new ( "main.css" ) ,
138
+ Some ( PathBuf :: from ( "../../tests/data/public/main.css" ) ) ,
139
+ ) ,
140
+ // Missing files
141
+ ( Path :: new ( "" ) , None ) ,
142
+ ( Path :: new ( "unknown" ) , None ) ,
143
+ ( Path :: new ( "about" ) , None ) ,
144
+ ] ) ;
145
+
146
+ ( project_root, tests)
147
+ } ;
148
+
149
+ for ( file, asset_path) in tests {
150
+ assert_eq ! ( retrieve_asset_path( project_root, file, false ) , asset_path) ;
151
+ }
152
+ }
153
+
154
+ #[ test]
155
+ fn absolute_asset_path_retrieval ( ) {
156
+ let ( project_root, tests) = if cfg ! ( target_os = "windows" ) {
157
+ let project_root = Path :: new ( "..\\ ..\\ tests\\ data" ) . canonicalize ( ) . unwrap ( ) ;
158
+ let tests = Vec :: from ( [
159
+ // Existing files
160
+ (
161
+ Path :: new ( "index.html" ) ,
162
+ Some ( project_root. join ( "public\\ index.html" ) ) ,
163
+ ) ,
164
+ (
165
+ Path :: new ( "main.css" ) ,
166
+ Some ( project_root. join ( "public\\ main.css" ) ) ,
167
+ ) ,
168
+ // Missing files
169
+ ( Path :: new ( "" ) , None ) ,
170
+ ( Path :: new ( "unknown" ) , None ) ,
171
+ ( Path :: new ( "about" ) , None ) ,
172
+ ] ) ;
173
+
174
+ ( project_root, tests)
175
+ } else {
176
+ let project_root = Path :: new ( "../../tests/data" ) . canonicalize ( ) . unwrap ( ) ;
177
+
178
+ let tests = Vec :: from ( [
179
+ // Existing files
180
+ (
181
+ Path :: new ( "index.html" ) ,
182
+ Some ( project_root. join ( "public/index.html" ) ) ,
183
+ ) ,
184
+ (
185
+ Path :: new ( "main.css" ) ,
186
+ Some ( project_root. join ( "public/main.css" ) ) ,
187
+ ) ,
188
+ // Missing files
189
+ ( Path :: new ( "" ) , None ) ,
190
+ ( Path :: new ( "unknown" ) , None ) ,
191
+ ( Path :: new ( "about" ) , None ) ,
192
+ ] ) ;
193
+
194
+ ( project_root, tests)
195
+ } ;
196
+
197
+ for ( file, asset_path) in tests {
198
+ assert_eq ! ( retrieve_asset_path( & project_root, file, false ) , asset_path) ;
199
+ }
200
+ }
201
+
202
+ #[ test]
203
+ fn relative_asset_index_path_retrieval ( ) {
204
+ let ( project_root, tests) = if cfg ! ( target_os = "windows" ) {
205
+ let project_root = Path :: new ( "..\\ ..\\ tests\\ data" ) ;
206
+ let tests = Vec :: from ( [
207
+ // Existing index files
208
+ (
209
+ Path :: new ( "about" ) ,
210
+ Some ( PathBuf :: from (
211
+ "..\\ ..\\ tests\\ data\\ public\\ about\\ index.html" ,
212
+ ) ) ,
213
+ ) ,
214
+ (
215
+ Path :: new ( "" ) ,
216
+ Some ( PathBuf :: from ( "..\\ ..\\ tests\\ data\\ public\\ index.html" ) ) ,
217
+ ) ,
218
+ // Missing index files
219
+ ( Path :: new ( "main.css" ) , None ) ,
220
+ ( Path :: new ( "unknown" ) , None ) ,
221
+ ] ) ;
222
+
223
+ ( project_root, tests)
224
+ } else {
225
+ let project_root = Path :: new ( "../../tests/data" ) ;
226
+ let tests = Vec :: from ( [
227
+ // Existing index files
228
+ (
229
+ Path :: new ( "about" ) ,
230
+ Some ( PathBuf :: from ( "../../tests/data/public/about/index.html" ) ) ,
231
+ ) ,
232
+ (
233
+ Path :: new ( "" ) ,
234
+ Some ( PathBuf :: from ( "../../tests/data/public/index.html" ) ) ,
235
+ ) ,
236
+ // Missing index files
237
+ ( Path :: new ( "main.css" ) , None ) ,
238
+ ( Path :: new ( "unknown" ) , None ) ,
239
+ ] ) ;
240
+
241
+ ( project_root, tests)
242
+ } ;
243
+
244
+ for ( file, asset_path) in tests {
245
+ assert_eq ! ( retrieve_asset_path( project_root, file, true ) , asset_path) ;
246
+ }
247
+ }
248
+
249
+ #[ test]
250
+ fn absolute_asset_index_path_retrieval ( ) {
251
+ let ( project_root, tests) = if cfg ! ( target_os = "windows" ) {
252
+ let project_root = Path :: new ( "..\\ ..\\ tests\\ data" ) . canonicalize ( ) . unwrap ( ) ;
253
+ let tests = Vec :: from ( [
254
+ // Existing idnex files
255
+ (
256
+ Path :: new ( "about" ) ,
257
+ Some ( project_root. join ( "public\\ about\\ index.html" ) ) ,
258
+ ) ,
259
+ ( Path :: new ( "" ) , Some ( project_root. join ( "public\\ index.html" ) ) ) ,
260
+ // Missing index files
261
+ ( Path :: new ( "main.css" ) , None ) ,
262
+ ( Path :: new ( "unknown" ) , None ) ,
263
+ ] ) ;
264
+
265
+ ( project_root, tests)
266
+ } else {
267
+ let project_root = Path :: new ( "../../tests/data" ) . canonicalize ( ) . unwrap ( ) ;
268
+
269
+ let tests = Vec :: from ( [
270
+ // Existing index files
271
+ (
272
+ Path :: new ( "about" ) ,
273
+ Some ( project_root. join ( "public/about/index.html" ) ) ,
274
+ ) ,
275
+ ( Path :: new ( "" ) , Some ( project_root. join ( "public/index.html" ) ) ) ,
276
+ // Missing index files
277
+ ( Path :: new ( "main.css" ) , None ) ,
278
+ ( Path :: new ( "unknown" ) , None ) ,
279
+ ] ) ;
280
+
281
+ ( project_root, tests)
282
+ } ;
283
+
284
+ for ( file, asset_path) in tests {
285
+ assert_eq ! ( retrieve_asset_path( & project_root, file, true ) , asset_path) ;
286
+ }
287
+ }
288
+ }
0 commit comments