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