1use crate::app_state::AppState;
2use crate::get_root_node_contents;
3
4use snix_castore::PathBuf;
5
6use axum::{
7 extract::{self, State},
8 http::StatusCode,
9 response::Response,
10};
11use axum_extra::{TypedHeader, headers::Range};
12use std::path;
13use tracing::{debug, instrument};
14
15#[instrument(level = "trace", ret, skip_all, fields(maybe_path))]
16pub async fn root_node_contents(
17 maybe_path: Option<extract::Path<String>>,
18 state: State<AppState>,
19 range_header: Option<TypedHeader<Range>>,
20) -> Result<Response, StatusCode> {
21 let requested_path = maybe_path
22 .map(|extract::Path(path)| PathBuf::from_host_path(path::Path::new(&path), true))
23 .transpose()
24 .map_err(|err| {
25 debug!(%err, "User requested an invalid path");
26 StatusCode::BAD_REQUEST
27 })?;
28 let requested_path = match requested_path.as_ref() {
29 Some(p) => p.as_ref(),
30 None => &PathBuf::new(),
31 };
32
33 get_root_node_contents(
34 state.blob_service.clone(),
35 state.directory_service.clone(),
36 path::Path::new("/"),
37 &state.root_node,
38 requested_path,
39 range_header,
40 &state.index_names,
41 state.auto_index,
42 )
43 .await
44}
45
46#[cfg(test)]
47mod tests {
48 use crate::{app_state::AppConfig, router::app};
49
50 use snix_castore::{
51 B3Digest, Directory, Node,
52 blobservice::{BlobService, MemoryBlobService},
53 directoryservice::{DirectoryService, MemoryDirectoryService},
54 fixtures::{DIRECTORY_COMPLICATED, HELLOWORLD_BLOB_CONTENTS, HELLOWORLD_BLOB_DIGEST},
55 };
56
57 use axum::http::StatusCode;
58 use std::io::Cursor;
59 use std::sync::{Arc, LazyLock};
60 use tracing_test::traced_test;
61
62 fn gen_server<S: AsRef<str>>(
64 root_node: Node,
65 index_names: &[S],
66 auto_index: bool,
67 ) -> (
68 axum_test::TestServer,
69 impl BlobService + use<S>,
70 impl DirectoryService + use<S>,
71 ) {
72 let blob_service = Arc::new(MemoryBlobService::default());
73 let directory_service = Arc::new(MemoryDirectoryService::default());
74
75 let app = app(Arc::new(AppConfig {
76 blob_service: blob_service.clone(),
77 directory_service: directory_service.clone(),
78 root_node,
79 index_names: index_names
80 .iter()
81 .map(|index| index.as_ref().to_string())
82 .collect(),
83 auto_index,
84 }));
85
86 (
87 axum_test::TestServer::new(app).unwrap(),
88 blob_service,
89 directory_service,
90 )
91 }
92
93 pub const INDEX_HTML_BLOB_CONTENTS: &[u8] =
94 b"<!DOCTYPE html><html><body>Hello World!</body></html>";
95 pub static INDEX_HTML_BLOB_DIGEST: LazyLock<B3Digest> =
96 LazyLock::new(|| blake3::hash(INDEX_HTML_BLOB_CONTENTS).as_bytes().into());
97
98 pub static DIRECTORY_NESTED_WITH_SYMLINK: LazyLock<Directory> = LazyLock::new(|| {
99 Directory::try_from_iter([
100 (
101 "nested".try_into().unwrap(),
102 Node::Directory {
103 digest: DIRECTORY_WITH_SYMLINK.digest(),
104 size: DIRECTORY_WITH_SYMLINK.size(),
105 },
106 ),
107 (
108 "index.htm".try_into().unwrap(),
109 Node::File {
110 digest: INDEX_HTML_BLOB_DIGEST.clone(),
111 size: INDEX_HTML_BLOB_CONTENTS.len() as u64,
112 executable: false,
113 },
114 ),
115 (
116 "out_of_base_path_symlink".try_into().unwrap(),
117 Node::Symlink {
118 target: "../index.htm".try_into().unwrap(),
119 },
120 ),
121 ])
122 .unwrap()
123 });
124
125 pub static DIRECTORY_WITH_SYMLINK: LazyLock<Directory> = LazyLock::new(|| {
126 Directory::try_from_iter([
127 (
128 "index.html".try_into().unwrap(),
129 Node::File {
130 digest: INDEX_HTML_BLOB_DIGEST.clone(),
131 size: INDEX_HTML_BLOB_CONTENTS.len() as u64,
132 executable: false,
133 },
134 ),
135 (
136 "dot".try_into().unwrap(),
137 Node::Symlink {
138 target: ".".try_into().unwrap(),
139 },
140 ),
141 (
142 "symlink".try_into().unwrap(),
143 Node::Symlink {
144 target: "index.html".try_into().unwrap(),
145 },
146 ),
147 (
148 "dot_symlink".try_into().unwrap(),
149 Node::Symlink {
150 target: "./index.html".try_into().unwrap(),
151 },
152 ),
153 (
154 "dotdot_symlink".try_into().unwrap(),
155 Node::Symlink {
156 target: "../index.htm".try_into().unwrap(),
157 },
158 ),
159 (
160 "dotdot_same_symlink".try_into().unwrap(),
161 Node::Symlink {
162 target: "../nested/index.html".try_into().unwrap(),
163 },
164 ),
165 ])
166 .unwrap()
167 });
168
169 #[traced_test]
170 #[tokio::test]
171 async fn test_lists_directory_contents_if_auto_index_enabled() {
172 let root_node = Node::Directory {
173 digest: DIRECTORY_COMPLICATED.digest(),
174 size: DIRECTORY_COMPLICATED.size(),
175 };
176
177 let (server, _blob_service, directory_service) = gen_server::<&str>(root_node, &[], true);
179
180 directory_service
181 .put(DIRECTORY_COMPLICATED.clone())
182 .await
183 .expect("Failed to insert directory");
184
185 server
186 .get("/")
187 .expect_success()
188 .await
189 .assert_text_contains("<html><body><li><a href=\"/.keep\">.keep</a></li><li><a href=\"/aa\">aa</a></li><li><a href=\"/keep\">keep</a></li></body></html>");
190 }
191
192 #[traced_test]
193 #[tokio::test]
194 async fn test_lists_directory_contents_if_auto_index_enabled_for_nested_dir() {
195 let root_node = Node::Directory {
196 digest: DIRECTORY_NESTED_WITH_SYMLINK.digest(),
197 size: DIRECTORY_NESTED_WITH_SYMLINK.size(),
198 };
199
200 let (server, _blob_service, directory_service) = gen_server::<&str>(root_node, &[], true);
202 let mut directory_service_handle = directory_service.put_multiple_start();
203 directory_service_handle
204 .put(DIRECTORY_WITH_SYMLINK.clone())
205 .await
206 .expect("Failed to insert directory");
207 directory_service_handle
208 .put(DIRECTORY_NESTED_WITH_SYMLINK.clone())
209 .await
210 .expect("Failed to insert directory");
211 directory_service_handle
212 .close()
213 .await
214 .expect("Failed to close handle");
215
216 server
217 .get("/nested")
218 .expect_success()
219 .await
220 .assert_text_contains("<!DOCTYPE html><html><body><li><a href=\"/nested/dot\">dot</a></li><li><a href=\"/nested/dot_symlink\">dot_symlink</a></li><li><a href=\"/nested/dotdot_same_symlink\">dotdot_same_symlink</a></li><li><a href=\"/nested/dotdot_symlink\">dotdot_symlink</a></li><li><a href=\"/nested/index.html\">index.html</a></li><li><a href=\"/nested/symlink\">symlink</a></li></body></html>")
221 }
222
223 #[traced_test]
224 #[tokio::test]
225 async fn test_responds_index_file_if_configured() {
226 let root_node = Node::Directory {
227 digest: DIRECTORY_COMPLICATED.digest(),
228 size: DIRECTORY_COMPLICATED.size(),
229 };
230
231 let (server, blob_service, directory_service) =
233 gen_server::<&str>(root_node, &[".keep"], false);
234
235 directory_service
236 .put(DIRECTORY_COMPLICATED.clone())
237 .await
238 .expect("Failed to insert directory");
239
240 let mut blob_writer = blob_service.open_write().await;
241 tokio::io::copy(&mut Cursor::new(vec![]), &mut blob_writer)
242 .await
243 .expect("Failed to copy file to BlobWriter");
244 blob_writer
245 .close()
246 .await
247 .expect("Failed to close the BlobWriter");
248
249 server.get("/").expect_success().await;
250 }
251
252 #[traced_test]
253 #[tokio::test]
254 async fn test_responds_index_file_if_configured_in_nested_dir() {
255 let root_node = Node::Directory {
256 digest: DIRECTORY_NESTED_WITH_SYMLINK.digest(),
257 size: DIRECTORY_NESTED_WITH_SYMLINK.size(),
258 };
259
260 let (server, blob_service, directory_service) =
262 gen_server::<&str>(root_node, &["index.html"], false);
263
264 let mut directory_service_handle = directory_service.put_multiple_start();
265 directory_service_handle
266 .put(DIRECTORY_WITH_SYMLINK.clone())
267 .await
268 .expect("Failed to insert directory");
269 directory_service_handle
270 .put(DIRECTORY_NESTED_WITH_SYMLINK.clone())
271 .await
272 .expect("Failed to insert directory");
273 directory_service_handle
274 .close()
275 .await
276 .expect("Failed to close handle");
277
278 let mut blob_writer = blob_service.open_write().await;
279 tokio::io::copy(&mut Cursor::new(INDEX_HTML_BLOB_CONTENTS), &mut blob_writer)
280 .await
281 .expect("Failed to copy file to BlobWriter");
282 let digest = blob_writer
283 .close()
284 .await
285 .expect("Failed to close the BlobWriter");
286 assert_eq!(digest, *INDEX_HTML_BLOB_DIGEST);
287
288 server.get("/nested").expect_success().await;
289 }
290
291 #[traced_test]
292 #[tokio::test]
293 async fn test_responds_forbidden_if_no_index_configured_nor_auto_index_enabled() {
294 let root_node = Node::Directory {
295 digest: DIRECTORY_COMPLICATED.digest(),
296 size: DIRECTORY_COMPLICATED.size(),
297 };
298
299 let (server, _blob_service, directory_service) = gen_server::<&str>(root_node, &[], false);
301
302 directory_service
303 .put(DIRECTORY_COMPLICATED.clone())
304 .await
305 .expect("Failed to insert directory");
306
307 let response = server.get("/").expect_failure().await;
308 response.assert_status(StatusCode::FORBIDDEN);
309 }
310
311 #[traced_test]
312 #[tokio::test]
313 async fn test_responds_file() {
314 let root_node = Node::Directory {
315 digest: DIRECTORY_COMPLICATED.digest(),
316 size: DIRECTORY_COMPLICATED.size(),
317 };
318
319 let (server, blob_service, directory_service) = gen_server::<&str>(root_node, &[], false);
320
321 directory_service
322 .put(DIRECTORY_COMPLICATED.clone())
323 .await
324 .expect("Failed to insert directory");
325
326 let mut blob_writer = blob_service.open_write().await;
327 tokio::io::copy(&mut Cursor::new(vec![]), &mut blob_writer)
328 .await
329 .expect("Failed to copy file to BlobWriter");
330 blob_writer
331 .close()
332 .await
333 .expect("Failed to close the BlobWriter");
334
335 server.get("/.keep").expect_success().await;
336 }
337
338 #[traced_test]
339 #[tokio::test]
340 async fn test_responds_file_and_correct_content_type() {
341 let root_node = Node::Directory {
342 digest: DIRECTORY_NESTED_WITH_SYMLINK.digest(),
343 size: DIRECTORY_NESTED_WITH_SYMLINK.size(),
344 };
345
346 let (server, blob_service, directory_service) = gen_server::<&str>(root_node, &[], false);
347
348 let mut directory_service_handle = directory_service.put_multiple_start();
349 directory_service_handle
350 .put(DIRECTORY_WITH_SYMLINK.clone())
351 .await
352 .expect("Failed to insert directory");
353 directory_service_handle
354 .put(DIRECTORY_NESTED_WITH_SYMLINK.clone())
355 .await
356 .expect("Failed to insert directory");
357 directory_service_handle
358 .close()
359 .await
360 .expect("Failed to close handle");
361
362 let mut blob_writer = blob_service.open_write().await;
363 tokio::io::copy(&mut Cursor::new(INDEX_HTML_BLOB_CONTENTS), &mut blob_writer)
364 .await
365 .expect("Failed to copy file to BlobWriter");
366 let digest = blob_writer
367 .close()
368 .await
369 .expect("Failed to close the BlobWriter");
370 assert_eq!(digest, *INDEX_HTML_BLOB_DIGEST);
371
372 let response = server.get("/nested/index.html").expect_success().await;
373 response.assert_header("Content-Type", "text/html");
374 }
375
376 #[traced_test]
377 #[tokio::test]
378 async fn test_responds_redirect_if_symlink() {
379 let root_node = Node::Directory {
380 digest: DIRECTORY_COMPLICATED.digest(),
381 size: DIRECTORY_COMPLICATED.size(),
382 };
383
384 let (server, _blob_service, directory_service) = gen_server::<&str>(root_node, &[], false);
385
386 directory_service
387 .put(DIRECTORY_COMPLICATED.clone())
388 .await
389 .expect("Failed to insert directory");
390
391 let response = server.get("/aa").await;
392 response.assert_status(StatusCode::TEMPORARY_REDIRECT);
393 response.assert_header("Location", "/nix/store/somewhereelse");
394 }
395
396 #[traced_test]
397 #[tokio::test]
398 async fn test_responds_redirect_with_normalized_path_if_symlink() {
399 let root_node = Node::Directory {
400 digest: DIRECTORY_NESTED_WITH_SYMLINK.digest(),
401 size: DIRECTORY_NESTED_WITH_SYMLINK.size(),
402 };
403
404 let (server, _blob_service, directory_service) = gen_server::<&str>(root_node, &[], false);
405
406 let mut directory_service_handle = directory_service.put_multiple_start();
407 directory_service_handle
408 .put(DIRECTORY_WITH_SYMLINK.clone())
409 .await
410 .expect("Failed to insert directory");
411 directory_service_handle
412 .put(DIRECTORY_NESTED_WITH_SYMLINK.clone())
413 .await
414 .expect("Failed to insert directory");
415 directory_service_handle
416 .close()
417 .await
418 .expect("Failed to close handle");
419
420 let response = server.get("/nested/symlink").await;
421 response.assert_status(StatusCode::TEMPORARY_REDIRECT);
422 response.assert_header("Location", "/nested/index.html");
423
424 let response = server.get("/nested/dot_symlink").await;
425 response.assert_status(StatusCode::TEMPORARY_REDIRECT);
426 response.assert_header("Location", "/nested/index.html");
427
428 let response = server.get("/nested/dotdot_symlink").await;
429 response.assert_status(StatusCode::TEMPORARY_REDIRECT);
430 response.assert_header("Location", "/index.htm");
431
432 let response = server.get("/out_of_base_path_symlink").await;
433 response.assert_status(StatusCode::TEMPORARY_REDIRECT);
434 response.assert_header("Location", "/index.htm");
435
436 let response = server.get("/nested/dot").expect_failure().await;
437 response.assert_status(StatusCode::INTERNAL_SERVER_ERROR);
438 }
439
440 #[traced_test]
441 #[tokio::test]
442 async fn test_returns_bad_request_if_not_valid_path() {
443 let root_node = Node::Directory {
444 digest: DIRECTORY_COMPLICATED.digest(),
445 size: DIRECTORY_COMPLICATED.size(),
446 };
447
448 let (server, _blob_service, _directory_service) = gen_server::<&str>(root_node, &[], false);
449
450 let response = server.get("//aa").expect_failure().await;
452 response.assert_status(StatusCode::BAD_REQUEST);
453 }
454
455 #[traced_test]
456 #[tokio::test]
457 async fn test_returns_bad_request_if_root_node_is_file_and_path_requested() {
458 let root_node = Node::File {
459 digest: HELLOWORLD_BLOB_DIGEST.clone(),
460 size: HELLOWORLD_BLOB_CONTENTS.len() as u64,
461 executable: false,
462 };
463
464 let (server, _blob_service, _directory_service) = gen_server::<&str>(root_node, &[], false);
465
466 let response = server.get("/some-path").expect_failure().await;
468 response.assert_status(StatusCode::BAD_REQUEST);
469 }
470
471 #[traced_test]
472 #[tokio::test]
473 async fn test_returns_bad_request_if_root_node_is_symlink_and_path_requested() {
474 let root_node = Node::Symlink {
475 target: "/nix/store/somewhereelse".try_into().unwrap(),
476 };
477
478 let (server, _blob_service, _directory_service) = gen_server::<&str>(root_node, &[], false);
479
480 let response = server.get("/some-path").expect_failure().await;
482 response.assert_status(StatusCode::BAD_REQUEST);
483 }
484}