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