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