1use axum::{http::StatusCode, response::IntoResponse};
2use bytes::Bytes;
3use nix_compat::{
4 narinfo::{NarInfo, Signature},
5 nix_http, nixbase32,
6 store_path::StorePath,
7};
8use snix_castore::proto::write_infused_nar_path;
9use snix_store::pathinfoservice::PathInfo;
10use tracing::{Span, instrument, warn};
11
12use crate::AppState;
13
14const NARINFO_LIMIT: usize = 2 * 1024 * 1024;
16
17#[instrument(skip_all, fields(path_info.name=%narinfo_str))]
18pub async fn head(
19 axum::extract::Path(narinfo_str): axum::extract::Path<String>,
20 axum::extract::State(AppState {
21 path_info_service, ..
22 }): axum::extract::State<AppState>,
23) -> Result<impl IntoResponse, StatusCode> {
24 let digest = nix_http::parse_narinfo_str(&narinfo_str).ok_or(StatusCode::NOT_FOUND)?;
25 Span::current().record("path_info.digest", &narinfo_str[0..32]);
26
27 if path_info_service.has(digest).await.map_err(|e| {
28 warn!(err=%e, "failed to get PathInfo");
29 StatusCode::INTERNAL_SERVER_ERROR
30 })? {
31 Ok(([("content-type", nix_http::MIME_TYPE_NARINFO)], ""))
32 } else {
33 warn!("PathInfo not found");
34 Err(StatusCode::NOT_FOUND)
35 }
36}
37
38#[instrument(skip_all, fields(path_info.name=%narinfo_str))]
39pub async fn get(
40 axum::extract::Path(narinfo_str): axum::extract::Path<String>,
41 axum::extract::State(AppState {
42 path_info_service, ..
43 }): axum::extract::State<AppState>,
44) -> Result<impl IntoResponse, StatusCode> {
45 let digest = nix_http::parse_narinfo_str(&narinfo_str).ok_or(StatusCode::NOT_FOUND)?;
46 Span::current().record("path_info.digest", &narinfo_str[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 Ok((
59 [("content-type", nix_http::MIME_TYPE_NARINFO)],
60 gen_narinfo_str(&path_info),
61 ))
62}
63
64#[instrument(skip_all, fields(path_info.name=%narinfo_str))]
65pub async fn put(
66 axum::extract::Path(narinfo_str): axum::extract::Path<String>,
67 axum::extract::State(AppState {
68 path_info_service,
69 root_nodes,
70 ..
71 }): axum::extract::State<AppState>,
72 request: axum::extract::Request,
73) -> Result<&'static str, StatusCode> {
74 let _narinfo_digest = nix_http::parse_narinfo_str(&narinfo_str).ok_or(StatusCode::UNAUTHORIZED);
75 Span::current().record("path_info.digest", &narinfo_str[0..32]);
76
77 let narinfo_bytes: Bytes = axum::body::to_bytes(request.into_body(), NARINFO_LIMIT)
78 .await
79 .map_err(|e| {
80 warn!(err=%e, "unable to fetch body");
81 StatusCode::BAD_REQUEST
82 })?;
83
84 let narinfo_str = std::str::from_utf8(narinfo_bytes.as_ref()).map_err(|e| {
86 warn!(err=%e, "unable decode body as string");
87 StatusCode::BAD_REQUEST
88 })?;
89
90 let narinfo = NarInfo::parse(narinfo_str).map_err(|e| {
91 warn!(err=%e, "unable to parse narinfo");
92 StatusCode::BAD_REQUEST
93 })?;
94
95 Span::current().record("path_info.nar_info", nixbase32::encode(&narinfo.nar_hash));
97
98 let maybe_root_node: Option<snix_castore::Node> =
101 root_nodes.read().peek(&narinfo.nar_hash).cloned();
102
103 match maybe_root_node {
104 Some(root_node) => {
105 path_info_service
107 .put(PathInfo {
108 store_path: narinfo.store_path.to_owned(),
109 node: root_node,
110 references: narinfo.references.iter().map(StorePath::to_owned).collect(),
111 nar_sha256: narinfo.nar_hash,
112 nar_size: narinfo.nar_size,
113 signatures: narinfo
114 .signatures
115 .into_iter()
116 .map(|s| {
117 Signature::<String>::new(s.name().to_string(), s.bytes().to_owned())
118 })
119 .collect(),
120 deriver: narinfo.deriver.as_ref().map(StorePath::to_owned),
121 ca: narinfo.ca,
122 })
123 .await
124 .map_err(|e| {
125 warn!(err=%e, "failed to persist the PathInfo");
126 StatusCode::INTERNAL_SERVER_ERROR
127 })?;
128
129 Ok("")
130 }
131 None => {
132 warn!("received narinfo with unknown NARHash");
133 Err(StatusCode::BAD_REQUEST)
134 }
135 }
136}
137
138fn gen_narinfo_str(path_info: &PathInfo) -> String {
140 let mut narinfo = path_info.to_narinfo();
141 let mut url = String::new();
142 write_infused_nar_path(&mut url, path_info.node.clone(), narinfo.nar_size)
143 .expect("write into string");
144 narinfo.url = &url;
145
146 narinfo.file_size = Some(narinfo.nar_size);
148
149 narinfo.to_string()
150}
151
152#[cfg(test)]
153mod tests {
154 use std::{num::NonZero, sync::Arc};
155
156 use axum::http::Method;
157 use nix_compat::nixbase32;
158 use snix_castore::{
159 blobservice::{BlobService, MemoryBlobService},
160 directoryservice::DirectoryService,
161 utils::gen_test_directory_service,
162 };
163 use snix_store::{
164 fixtures::{DUMMY_PATH_DIGEST, NAR_CONTENTS_SYMLINK, PATH_INFO, PATH_INFO_SYMLINK},
165 path_info::PathInfo,
166 pathinfoservice::PathInfoService,
167 utils::gen_test_pathinfo_service,
168 };
169 use tracing_test::traced_test;
170
171 use crate::AppState;
172
173 fn gen_server(
175 router: axum::Router<AppState>,
176 ) -> (
177 axum_test::TestServer,
178 impl BlobService,
179 impl DirectoryService,
180 impl PathInfoService,
181 ) {
182 let blob_service = Arc::new(MemoryBlobService::default());
183 let directory_service = Arc::new(gen_test_directory_service());
184 let path_info_service = Arc::new(gen_test_pathinfo_service());
185
186 let app = router.with_state(AppState::new(
187 blob_service.clone(),
188 directory_service.clone(),
189 path_info_service.clone(),
190 NonZero::new(100).unwrap(),
191 ));
192
193 (
194 axum_test::TestServer::new(app).unwrap(),
195 blob_service,
196 directory_service,
197 path_info_service,
198 )
199 }
200
201 fn gen_nix_like_narinfo(path_info: &PathInfo) -> String {
202 let mut narinfo = path_info.to_narinfo();
203
204 let url = format!("nar/{}.nar", nixbase32::encode(&path_info.nar_sha256));
205 narinfo.url = &url;
206 narinfo.to_string()
207 }
208
209 #[traced_test]
211 #[tokio::test]
212 async fn test_get_head_not_found() {
213 let (server, _blob_service, _directory_service, _path_info_service) =
214 gen_server(crate::gen_router(100));
215
216 let url = &format!("{}.narinfo", nixbase32::encode(&DUMMY_PATH_DIGEST));
217
218 server
220 .method(Method::HEAD, url)
221 .expect_failure()
222 .await
223 .assert_status_not_found();
224
225 server
227 .get(url)
228 .expect_failure()
229 .await
230 .assert_status_not_found();
231 }
232
233 #[traced_test]
235 #[tokio::test]
236 async fn test_get_head_found() {
237 let (server, _blob_service, _directory_service, path_info_service) =
238 gen_server(crate::gen_router(100));
239
240 let url = &format!("{}.narinfo", nixbase32::encode(&DUMMY_PATH_DIGEST));
241
242 path_info_service
243 .put(PATH_INFO.clone())
244 .await
245 .expect("put pathinfo");
246
247 server
248 .method(Method::HEAD, url)
249 .expect_success()
250 .await
251 .assert_status_ok();
252
253 let narinfo_bytes = server.get(url).expect_success().await.into_bytes();
255
256 assert_eq!(crate::narinfo::gen_narinfo_str(&PATH_INFO), narinfo_bytes);
257 }
258
259 #[traced_test]
261 #[tokio::test]
262 async fn test_put_without_prev_nar_fail() {
263 let (server, _blob_service, _directory_service, _path_info_service) =
264 gen_server(crate::gen_router(100));
265
266 let narinfo_str = gen_nix_like_narinfo(&PATH_INFO_SYMLINK);
270
271 server
272 .put(&format!(
273 "{}.narinfo",
274 nixbase32::encode(&PATH_INFO_SYMLINK.nar_sha256)
275 ))
276 .text(narinfo_str)
277 .content_type(nix_compat::nix_http::MIME_TYPE_NARINFO)
278 .expect_failure()
279 .await;
280 }
281
282 #[traced_test]
284 #[tokio::test]
285 async fn test_upload_nar_then_narinfo() {
286 let (server, _blob_service, _directory_service, _path_info_service) =
287 gen_server(crate::gen_router(100));
288
289 server
291 .put(&format!(
292 "/nar/{}.nar",
293 nixbase32::encode(&PATH_INFO_SYMLINK.nar_sha256)
294 ))
295 .bytes(NAR_CONTENTS_SYMLINK[..].into())
296 .expect_success()
297 .await;
298
299 let narinfo_str = gen_nix_like_narinfo(&PATH_INFO_SYMLINK);
300
301 server
303 .put(&format!(
304 "/{}.narinfo",
305 nixbase32::encode(PATH_INFO_SYMLINK.store_path.digest())
306 ))
307 .text(narinfo_str)
308 .content_type(nix_compat::nix_http::MIME_TYPE_NARINFO)
309 .expect_success()
310 .await;
311 }
312}