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