mannequin/src/nexus/proxy.rs
use super::NexusAsset;
use std::collections::HashMap;
use std::sync::Mutex;
/// Proxy-only repo: fetches crates from crates.io on demand, caches in memory.
pub struct ProxyRepo {
cache: Mutex<HashMap<String, Vec<u8>>>,
http_client: reqwest::Client,
base_url: String,
}
impl ProxyRepo {
pub fn new(base_url: &str) -> Self {
Self {
cache: Mutex::new(HashMap::new()),
http_client: tls_roots(reqwest::Client::builder())
.build()
.expect("reqwest client"),
base_url: base_url.to_string(),
}
}
pub fn list_assets(&self) -> Vec<NexusAsset> {
self.cache
.lock()
.unwrap()
.keys()
.map(|path| NexusAsset {
path: path.clone(),
download_url: format!("{}/repository/proxy-only/{}", self.base_url, path),
content_type: Some("application/octet-stream".into()),
size: None,
})
.collect()
}
pub async fn download(&self, path: &str) -> Option<Vec<u8>> {
// Check cache
if let Some(data) = self.cache.lock().unwrap().get(path) {
return Some(data.clone());
}
// Proxy from crates.io
let filename = path.rsplit('/').next()?;
if !filename.ends_with(".crate") {
return None;
}
let stem = filename.strip_suffix(".crate")?;
let crate_name = extract_crate_name(stem)?;
let url = format!(
"https://static.crates.io/crates/{}/{}.crate",
crate_name, stem
);
tracing::info!("proxy-only: fetching {}", url);
let resp = self.http_client.get(&url).send().await.ok()?;
if !resp.status().is_success() {
tracing::warn!("proxy-only: crates.io returned {}", resp.status());
return None;
}
let data = resp.bytes().await.ok()?.to_vec();
tracing::info!("proxy-only: cached {} ({} bytes)", path, data.len());
self.cache
.lock()
.unwrap()
.insert(path.to_string(), data.clone());
Some(data)
}
}
fn extract_crate_name(stem: &str) -> Option<&str> {
let mut last_sep = None;
for (i, _) in stem.match_indices('-') {
if stem.as_bytes().get(i + 1).map(|b| b.is_ascii_digit()) == Some(true) {
last_sep = Some(i);
}
}
last_sep.map(|i| &stem[..i])
}
/// reqwest 0.13 with reqwest 0.12 `rustls-tls` behaviour (2026-10-02): the ring
/// provider and the bundled Mozilla roots (webpki), not 0.13's aws-lc-rs + OS
/// trust store. Roots added later on the builder are merged in, as in 0.12.
#[allow(dead_code)]
fn tls_roots(b: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
let _ = rustls::crypto::ring::default_provider().install_default();
b.tls_certs_only(
webpki_root_certs::TLS_SERVER_ROOT_CERTS
.iter()
.filter_map(|c| reqwest::Certificate::from_der(c).ok()),
)
}