1use axum::{http::StatusCode, response::IntoResponse};
4use bytes::Bytes;
5use nix_compat::{
6 narinfo::{NarInfo, Signature},
7 nix_http,
8};
9use snix_castore::proto::write_infused_nar_path;
10use snix_store::pathinfoservice::PathInfo;
11use tracing::{Span, instrument, warn};
12
13use crate::AppState;
14
15#[instrument(skip_all, fields(path_info.digest=tracing::field::Empty))]
16pub async fn head(
17 axum::extract::Path(p): axum::extract::Path<String>,
18 axum::extract::State(AppState {
19 path_info_service, ..
20 }): axum::extract::State<AppState>,
21) -> Result<impl IntoResponse, StatusCode> {
22 let (digest, _request_type) = nix_http::parse_outhash_str(&p).ok_or(StatusCode::NOT_FOUND)?;
23 Span::current().record("path_info.digest", &p[0..32]);
24
25 if path_info_service.has(digest).await.map_err(|e| {
26 warn!(err=%e, "failed to get PathInfo");
27 StatusCode::INTERNAL_SERVER_ERROR
28 })? {
29 Ok(([("content-type", nix_http::MIME_TYPE_NARINFO)], ""))
30 } else {
31 warn!("PathInfo not found");
32 Err(StatusCode::NOT_FOUND)
33 }
34}
35
36#[instrument(skip_all, fields(path_info.digest=tracing::field::Empty))]
37pub async fn get(
38 axum::extract::Path(p): axum::extract::Path<String>,
39 axum::extract::State(AppState {
40 directory_service,
41 path_info_service,
42 ..
43 }): axum::extract::State<AppState>,
44) -> Result<impl IntoResponse, StatusCode> {
45 let (digest, request_type) = nix_http::parse_outhash_str(&p).ok_or(StatusCode::NOT_FOUND)?;
46 Span::current().record("path_info.digest", &p[0..32]);
47
48 let path_info = path_info_service
50 .get(digest)
51 .await
52 .map_err(|e| {
53 warn!(err=%e, "failed to get PathInfo");
54 StatusCode::INTERNAL_SERVER_ERROR
55 })?
56 .ok_or(StatusCode::NOT_FOUND)?;
57
58 match request_type {
59 nix_http::RequestType::Narinfo => Ok((
60 [("content-type", nix_http::MIME_TYPE_NARINFO)],
61 gen_narinfo_str(&path_info),
62 )),
63 nix_http::RequestType::Listing => {
64 let listing = snix_store::nar::produce_listing(&path_info.node, &directory_service)
66 .await
67 .map_err(|err| {
68 warn!(%err, "failed to produce listing");
69 StatusCode::INTERNAL_SERVER_ERROR
70 })?;
71
72 let listing_str = serde_json::to_string(&listing).map_err(|err| {
73 warn!(%err, "failed to serialize listing");
74 StatusCode::INTERNAL_SERVER_ERROR
75 })?;
76
77 Ok((
78 [("content-type", nix_http::MIME_TYPE_NAR_LISTING)],
79 listing_str,
80 ))
81 }
82 }
83}
84
85const NARINFO_SIZE_LIMIT: usize = 2 * 1024 * 1024;
87
88#[instrument(skip_all, fields(path_info.digest=tracing::field::Empty))]
89pub async fn put(
90 axum::extract::Path(p): axum::extract::Path<String>,
91 axum::extract::State(AppState {
92 path_info_service,
93 root_nodes,
94 ..
95 }): axum::extract::State<AppState>,
96 request: axum::extract::Request,
97) -> Result<&'static str, StatusCode> {
98 let (digest, request_type) = nix_http::parse_outhash_str(&p).ok_or(StatusCode::NOT_FOUND)?;
99 Span::current().record("path_info.digest", &p[0..32]);
100
101 match request_type {
102 nix_http::RequestType::Narinfo => {}
104 nix_http::RequestType::Listing => {
105 return Ok("");
110 }
111 }
112
113 let narinfo_bytes: Bytes = axum::body::to_bytes(request.into_body(), NARINFO_SIZE_LIMIT)
114 .await
115 .map_err(|e| {
116 warn!(err=%e, "unable to fetch body");
117 StatusCode::BAD_REQUEST
118 })?;
119
120 let narinfo_str = std::str::from_utf8(narinfo_bytes.as_ref()).map_err(|e| {
122 warn!(err=%e, "unable decode body as string");
123 StatusCode::BAD_REQUEST
124 })?;
125
126 let narinfo = NarInfo::parse(narinfo_str).map_err(|e| {
127 warn!(err=%e, "unable to parse narinfo");
128 StatusCode::BAD_REQUEST
129 })?;
130
131 if &digest != narinfo.store_path.digest() {
132 warn!("digest in URL doesn't match store path in NARInfo");
133 Err(StatusCode::BAD_REQUEST)?
134 }
135
136 let maybe_root_node: Option<snix_castore::Node> =
139 root_nodes.read().peek(&narinfo.nar_hash).cloned();
140
141 match maybe_root_node {
142 Some(root_node) => {
143 path_info_service
145 .put(PathInfo {
146 store_path: narinfo.store_path.to_owned(),
147 node: root_node,
148 references: narinfo.references.iter().map(|sp| sp.to_owned()).collect(),
149 nar_sha256: narinfo.nar_hash,
150 nar_size: narinfo.nar_size,
151 signatures: narinfo
152 .signatures
153 .into_iter()
154 .map(|s| {
155 Signature::<String>::new(s.name().to_string(), s.bytes().to_owned())
156 })
157 .collect(),
158 deriver: narinfo.deriver.as_ref().map(|sp| sp.to_owned()),
159 ca: narinfo.ca,
160 })
161 .await
162 .map_err(|e| {
163 warn!(err=%e, "failed to persist the PathInfo");
164 StatusCode::INTERNAL_SERVER_ERROR
165 })?;
166
167 Ok("")
168 }
169 None => {
170 warn!("received narinfo with unknown NARHash");
171 Err(StatusCode::BAD_REQUEST)
172 }
173 }
174}
175
176fn gen_narinfo_str(path_info: &PathInfo) -> String {
178 let mut narinfo = path_info.to_narinfo();
179 let mut url = String::new();
180 write_infused_nar_path(&mut url, path_info.node.clone(), narinfo.nar_size)
181 .expect("write into string");
182 narinfo.url = &url;
183
184 narinfo.file_size = Some(narinfo.nar_size);
186
187 narinfo.to_string()
188}
189
190#[cfg(test)]
191mod tests {
192 use std::{num::NonZero, sync::Arc};
193
194 use axum::http::Method;
195 use nix_compat::nixbase32;
196 use snix_castore::{
197 blobservice::BlobService,
198 directoryservice::DirectoryService,
199 utils::{gen_test_blob_service, gen_test_directory_service},
200 };
201 use snix_store::{
202 fixtures::{DUMMY_PATH_DIGEST, NAR_CONTENTS_SYMLINK, PATH_INFO_SYMLINK},
203 path_info::PathInfo,
204 pathinfoservice::PathInfoService,
205 utils::gen_test_pathinfo_service,
206 };
207 use tracing_test::traced_test;
208
209 use crate::AppState;
210
211 fn gen_server(
214 router: axum::Router<AppState>,
215 ) -> (
216 axum_test::TestServer,
217 impl BlobService,
218 impl DirectoryService,
219 impl PathInfoService,
220 ) {
221 let blob_service = Arc::new(gen_test_blob_service());
222 let directory_service = Arc::new(gen_test_directory_service());
223 let path_info_service = Arc::new(gen_test_pathinfo_service());
224
225 let app = router.with_state(AppState::new(
226 blob_service.clone(),
227 directory_service.clone(),
228 path_info_service.clone(),
229 NonZero::new(100).unwrap(),
230 ));
231
232 (
233 axum_test::TestServer::new(app),
234 blob_service,
235 directory_service,
236 path_info_service,
237 )
238 }
239
240 fn gen_nix_like_narinfo(path_info: &PathInfo) -> String {
241 let mut narinfo = path_info.to_narinfo();
242
243 let url = format!("nar/{}.nar", nixbase32::encode(&path_info.nar_sha256));
244 narinfo.url = &url;
245 narinfo.to_string()
246 }
247
248 #[traced_test]
251 #[tokio::test]
252 async fn test_get_head_not_found() {
253 let (server, _blob_service, _directory_service, _path_info_service) =
254 gen_server(crate::gen_router(100));
255
256 let narinfo_url = &format!("{}.narinfo", nixbase32::encode(&DUMMY_PATH_DIGEST));
257 server
258 .method(Method::HEAD, narinfo_url)
259 .expect_failure()
260 .await
261 .assert_status_not_found();
262
263 server
264 .get(narinfo_url)
265 .expect_failure()
266 .await
267 .assert_status_not_found();
268
269 let listing_url = &format!("{}.ls", nixbase32::encode(&DUMMY_PATH_DIGEST));
270 server
271 .method(Method::HEAD, listing_url)
272 .expect_failure()
273 .await
274 .assert_status_not_found();
275 server
276 .get(listing_url)
277 .expect_failure()
278 .await
279 .assert_status_not_found();
280 }
281
282 #[traced_test]
285 #[tokio::test]
286 async fn test_get_head_found() {
287 let (server, _blob_service, _directory_service, path_info_service) =
288 gen_server(crate::gen_router(100));
289
290 let narinfo_url = &format!("{}.narinfo", nixbase32::encode(&DUMMY_PATH_DIGEST));
291 path_info_service
292 .put(PATH_INFO_SYMLINK.clone())
293 .await
294 .expect("put pathinfo");
295
296 server
297 .method(Method::HEAD, narinfo_url)
298 .expect_success()
299 .await
300 .assert_status_ok();
301
302 let narinfo_bytes = server.get(narinfo_url).expect_success().await.into_bytes();
304 assert_eq!(
305 super::gen_narinfo_str(&PATH_INFO_SYMLINK),
306 narinfo_bytes,
307 "expect NARInfo to match"
308 );
309
310 let listing_url = &format!("{}.ls", nixbase32::encode(&DUMMY_PATH_DIGEST));
311 server
312 .method(Method::HEAD, listing_url)
313 .expect_success()
314 .await
315 .assert_status_ok();
316
317 let listing_bytes = server.get(listing_url).expect_success().await.into_bytes();
319 assert_eq!(
320 r#"{"root":{"target":"/nix/store/somewhereelse","type":"symlink"},"version":1}"#,
321 listing_bytes,
322 "expect listing to match"
323 );
324 }
325
326 #[traced_test]
328 #[tokio::test]
329 async fn test_put_without_prev_nar_fail() {
330 let (server, _blob_service, _directory_service, _path_info_service) =
331 gen_server(crate::gen_router(100));
332
333 let narinfo_str = gen_nix_like_narinfo(&PATH_INFO_SYMLINK);
337
338 server
339 .put(&format!(
340 "{}.narinfo",
341 nixbase32::encode(&PATH_INFO_SYMLINK.nar_sha256)
342 ))
343 .text(narinfo_str)
344 .content_type(nix_compat::nix_http::MIME_TYPE_NARINFO)
345 .expect_failure()
346 .await;
347 }
348
349 #[traced_test]
351 #[tokio::test]
352 async fn test_upload_nar_then_narinfo() {
353 let (server, _blob_service, _directory_service, _path_info_service) =
354 gen_server(crate::gen_router(100));
355
356 server
358 .put(&format!(
359 "/nar/{}.nar",
360 nixbase32::encode(&PATH_INFO_SYMLINK.nar_sha256)
361 ))
362 .bytes(NAR_CONTENTS_SYMLINK[..].into())
363 .expect_success()
364 .await;
365
366 let narinfo_str = gen_nix_like_narinfo(&PATH_INFO_SYMLINK);
367
368 server
370 .put(&format!(
371 "/{}.narinfo",
372 nixbase32::encode(PATH_INFO_SYMLINK.store_path.digest())
373 ))
374 .text(narinfo_str)
375 .content_type(nix_compat::nix_http::MIME_TYPE_NARINFO)
376 .expect_success()
377 .await;
378 }
379}