use axum::http::StatusCode; use bytes::Bytes; use nix_compat::{narinfo::NarInfo, nixbase32}; use tracing::{instrument, warn, Span}; use tvix_castore::proto::{self as castorepb, node::Node}; use tvix_store::proto::PathInfo; use crate::AppState; /// The size limit for NARInfo uploads nar-bridge receives const NARINFO_LIMIT: usize = 2 * 1024 * 1024; #[instrument(skip(path_info_service))] pub async fn head( axum::extract::Path(narinfo_str): axum::extract::Path<String>, axum::extract::State(AppState { path_info_service, .. }): axum::extract::State<AppState>, ) -> Result<&'static str, StatusCode> { let digest = parse_narinfo_str(&narinfo_str)?; Span::current().record("path_info.digest", &narinfo_str[0..32]); if path_info_service .get(digest) .await .map_err(|e| { warn!(err=%e, "failed to get PathInfo"); StatusCode::INTERNAL_SERVER_ERROR })? .is_some() { Ok("") } else { warn!("PathInfo not found"); Err(StatusCode::NOT_FOUND) } } #[instrument(skip(path_info_service))] pub async fn get( axum::extract::Path(narinfo_str): axum::extract::Path<String>, axum::extract::State(AppState { path_info_service, .. }): axum::extract::State<AppState>, ) -> Result<String, StatusCode> { let digest = parse_narinfo_str(&narinfo_str)?; Span::current().record("path_info.digest", &narinfo_str[0..32]); // fetch the PathInfo let path_info = path_info_service .get(digest) .await .map_err(|e| { warn!(err=%e, "failed to get PathInfo"); StatusCode::INTERNAL_SERVER_ERROR })? .ok_or(StatusCode::NOT_FOUND)?; let store_path = path_info.validate().map_err(|e| { warn!(err=%e, "invalid PathInfo"); StatusCode::INTERNAL_SERVER_ERROR })?; let mut narinfo = path_info.to_narinfo(store_path).ok_or_else(|| { warn!(path_info=?path_info, "PathInfo contained no NAR data"); StatusCode::INTERNAL_SERVER_ERROR })?; // encode the (unnamed) root node in the NAR url itself. let root_node = path_info .node .as_ref() .and_then(|n| n.node.as_ref()) .expect("root node must not be none") .clone() .rename("".into()); let mut buf = Vec::new(); Node::encode(&root_node, &mut buf); let url = format!( "nar/tvix-castore/{}?narsize={}", data_encoding::BASE64URL_NOPAD.encode(&buf), narinfo.nar_size, ); narinfo.url = &url; Ok(narinfo.to_string()) } #[instrument(skip(path_info_service, root_nodes, request))] pub async fn put( axum::extract::Path(narinfo_str): axum::extract::Path<String>, axum::extract::State(AppState { path_info_service, root_nodes, .. }): axum::extract::State<AppState>, request: axum::extract::Request, ) -> Result<&'static str, StatusCode> { let _narinfo_digest = parse_narinfo_str(&narinfo_str)?; Span::current().record("path_info.digest", &narinfo_str[0..32]); let narinfo_bytes: Bytes = axum::body::to_bytes(request.into_body(), NARINFO_LIMIT) .await .map_err(|e| { warn!(err=%e, "unable to fetch body"); StatusCode::BAD_REQUEST })?; // Parse the narinfo from the body. let narinfo_str = std::str::from_utf8(narinfo_bytes.as_ref()).map_err(|e| { warn!(err=%e, "unable decode body as string"); StatusCode::BAD_REQUEST })?; let narinfo = NarInfo::parse(narinfo_str).map_err(|e| { warn!(err=%e, "unable to parse narinfo"); StatusCode::BAD_REQUEST })?; // Extract the NARHash from the PathInfo. Span::current().record("path_info.nar_info", nixbase32::encode(&narinfo.nar_hash)); // populate the pathinfo. let mut pathinfo = PathInfo::from(&narinfo); // Lookup root node with peek, as we don't want to update the LRU list. // We need to be careful to not hold the RwLock across the await point. let maybe_root_node = root_nodes .read() .peek(&narinfo.nar_hash) .map(|v| v.to_owned()); match maybe_root_node { Some(root_node) => { // Set the root node from the lookup. // We need to rename the node to the narinfo storepath basename, as // that's where it's stored in PathInfo. pathinfo.node = Some(castorepb::Node { node: Some(root_node.rename(narinfo.store_path.to_string().into())), }); // Persist the PathInfo. path_info_service.put(pathinfo).await.map_err(|e| { warn!(err=%e, "failed to persist the PathInfo"); StatusCode::INTERNAL_SERVER_ERROR })?; Ok("") } None => { warn!("received narinfo with unknown NARHash"); Err(StatusCode::BAD_REQUEST) } } } /// Parses a `3mzh8lvgbynm9daj7c82k2sfsfhrsfsy.narinfo` string and returns the /// nixbase32-decoded digest. fn parse_narinfo_str(s: &str) -> Result<[u8; 20], StatusCode> { if !s.is_char_boundary(32) { warn!("invalid string, no char boundary at 32"); return Err(StatusCode::NOT_FOUND); } Ok(match s.split_at(32) { (hash_str, ".narinfo") => { // we know this is 32 bytes let hash_str_fixed: [u8; 32] = hash_str.as_bytes().try_into().unwrap(); nixbase32::decode_fixed(hash_str_fixed).map_err(|e| { warn!(err=%e, "invalid digest"); StatusCode::NOT_FOUND })? } _ => { warn!("invalid string"); return Err(StatusCode::NOT_FOUND); } }) } #[cfg(test)] mod test { use super::parse_narinfo_str; use hex_literal::hex; #[test] fn success() { assert_eq!( hex!("8a12321522fd91efbd60ebb2481af88580f61600"), parse_narinfo_str("00bgd045z0d4icpbc2yyz4gx48ak44la.narinfo").unwrap() ); } #[test] fn failure() { assert!(parse_narinfo_str("00bgd045z0d4icpbc2yyz4gx48ak44la").is_err()); assert!(parse_narinfo_str("/00bgd045z0d4icpbc2yyz4gx48ak44la").is_err()); assert!(parse_narinfo_str("000000").is_err()); assert!(parse_narinfo_str("00bgd045z0d4icpbc2yyz4gx48ak44l🦊.narinfo").is_err()); } }