diff --git a/Cargo.lock b/Cargo.lock index 81427d1..842c27d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1201,7 +1201,7 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" [[package]] name = "nora-cli" -version = "0.2.18" +version = "0.2.20" dependencies = [ "clap", "flate2", @@ -1215,7 +1215,7 @@ dependencies = [ [[package]] name = "nora-registry" -version = "0.2.18" +version = "0.2.20" dependencies = [ "async-trait", "axum", @@ -1253,7 +1253,7 @@ dependencies = [ [[package]] name = "nora-storage" -version = "0.2.18" +version = "0.2.20" dependencies = [ "axum", "base64", diff --git a/Cargo.toml b/Cargo.toml index 130c9b2..ad57397 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ ] [workspace.package] -version = "0.2.19" +version = "0.2.20" edition = "2021" license = "MIT" authors = ["DevITWay "] diff --git a/nora-registry/src/main.rs b/nora-registry/src/main.rs index 8dd9bb0..8f8ba46 100644 --- a/nora-registry/src/main.rs +++ b/nora-registry/src/main.rs @@ -85,6 +85,7 @@ pub struct AppState { pub activity: ActivityLog, pub docker_auth: registry::DockerAuth, pub repo_index: RepoIndex, + pub http_client: reqwest::Client, } #[tokio::main] @@ -271,6 +272,8 @@ async fn run_server(config: Config, storage: Storage) { // Initialize Docker auth with proxy timeout let docker_auth = registry::DockerAuth::new(config.docker.proxy_timeout); + let http_client = reqwest::Client::new(); + let state = Arc::new(AppState { storage, config, @@ -281,6 +284,7 @@ async fn run_server(config: Config, storage: Storage) { activity: ActivityLog::new(50), docker_auth, repo_index: RepoIndex::new(), + http_client, }); // Token routes with strict rate limiting (brute-force protection) diff --git a/nora-registry/src/registry/docker.rs b/nora-registry/src/registry/docker.rs index 3c0de24..7b4b163 100644 --- a/nora-registry/src/registry/docker.rs +++ b/nora-registry/src/registry/docker.rs @@ -167,6 +167,7 @@ async fn download_blob( // Try upstream proxies for upstream in &state.config.docker.upstreams { if let Ok(data) = fetch_blob_from_upstream( + &state.http_client, &upstream.url, &name, &digest, @@ -367,6 +368,7 @@ async fn get_manifest( for upstream in &state.config.docker.upstreams { tracing::debug!(upstream_url = %upstream.url, "Trying upstream"); if let Ok((data, content_type)) = fetch_manifest_from_upstream( + &state.http_client, &upstream.url, &name, &reference, @@ -581,6 +583,7 @@ async fn list_tags_ns( /// Fetch a blob from an upstream Docker registry async fn fetch_blob_from_upstream( + client: &reqwest::Client, upstream_url: &str, name: &str, digest: &str, @@ -594,13 +597,13 @@ async fn fetch_blob_from_upstream( digest ); - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(timeout)) - .build() - .map_err(|_| ())?; - // First try without auth - let response = client.get(&url).send().await.map_err(|_| ())?; + let response = client + .get(&url) + .timeout(Duration::from_secs(timeout)) + .send() + .await + .map_err(|_| ())?; let response = if response.status() == reqwest::StatusCode::UNAUTHORIZED { // Get Www-Authenticate header and fetch token @@ -637,6 +640,7 @@ async fn fetch_blob_from_upstream( /// Fetch a manifest from an upstream Docker registry /// Returns (manifest_bytes, content_type) async fn fetch_manifest_from_upstream( + client: &reqwest::Client, upstream_url: &str, name: &str, reference: &str, @@ -652,13 +656,6 @@ async fn fetch_manifest_from_upstream( tracing::debug!(url = %url, "Fetching manifest from upstream"); - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(timeout)) - .build() - .map_err(|e| { - tracing::error!(error = %e, "Failed to build HTTP client"); - })?; - // Request with Accept header for manifest types let accept_header = "application/vnd.docker.distribution.manifest.v2+json, \ application/vnd.docker.distribution.manifest.list.v2+json, \ @@ -668,6 +665,7 @@ async fn fetch_manifest_from_upstream( // First try without auth let response = client .get(&url) + .timeout(Duration::from_secs(timeout)) .header("Accept", accept_header) .send() .await diff --git a/nora-registry/src/registry/maven.rs b/nora-registry/src/registry/maven.rs index b630017..4b062f0 100644 --- a/nora-registry/src/registry/maven.rs +++ b/nora-registry/src/registry/maven.rs @@ -23,7 +23,6 @@ pub fn routes() -> Router> { async fn download(State(state): State>, Path(path): Path) -> Response { let key = format!("maven/{}", path); - // Extract artifact name for logging (last 2-3 path components) let artifact_name = path .split('/') .rev() @@ -34,7 +33,6 @@ async fn download(State(state): State>, Path(path): Path) .collect::>() .join("/"); - // Try local storage first if let Ok(data) = state.storage.get(&key).await { state.metrics.record_download("maven"); state.metrics.record_cache_hit(); @@ -47,11 +45,10 @@ async fn download(State(state): State>, Path(path): Path) return with_content_type(&path, data).into_response(); } - // Try proxy servers for proxy_url in &state.config.maven.proxies { let url = format!("{}/{}", proxy_url.trim_end_matches('/'), path); - match fetch_from_proxy(&url, state.config.maven.proxy_timeout).await { + match fetch_from_proxy(&state.http_client, &url, state.config.maven.proxy_timeout).await { Ok(data) => { state.metrics.record_download("maven"); state.metrics.record_cache_miss(); @@ -62,7 +59,6 @@ async fn download(State(state): State>, Path(path): Path) "PROXY", )); - // Cache in local storage (fire and forget) let storage = state.storage.clone(); let key_clone = key.clone(); let data_clone = data.clone(); @@ -88,7 +84,6 @@ async fn upload( ) -> StatusCode { let key = format!("maven/{}", path); - // Extract artifact name for logging let artifact_name = path .split('/') .rev() @@ -115,14 +110,14 @@ async fn upload( } } -async fn fetch_from_proxy(url: &str, timeout_secs: u64) -> Result, ()> { - let client = reqwest::Client::builder() +async fn fetch_from_proxy(client: &reqwest::Client, url: &str, timeout_secs: u64) -> Result, ()> { + let response = client + .get(url) .timeout(Duration::from_secs(timeout_secs)) - .build() + .send() + .await .map_err(|_| ())?; - let response = client.get(url).send().await.map_err(|_| ())?; - if !response.status().is_success() { return Err(()); } diff --git a/nora-registry/src/registry/npm.rs b/nora-registry/src/registry/npm.rs index e431027..7982c38 100644 --- a/nora-registry/src/registry/npm.rs +++ b/nora-registry/src/registry/npm.rs @@ -19,7 +19,6 @@ pub fn routes() -> Router> { } async fn handle_request(State(state): State>, Path(path): Path) -> Response { - // Determine if this is a tarball request or metadata request let is_tarball = path.contains("/-/"); let key = if is_tarball { @@ -33,14 +32,12 @@ async fn handle_request(State(state): State>, Path(path): Path>, Path(path): Path>, Path(path): Path>, Path(path): Path>, Path(path): Path Result, ()> { - let client = reqwest::Client::builder() +async fn fetch_from_proxy(client: &reqwest::Client, url: &str, timeout_secs: u64) -> Result, ()> { + let response = client + .get(url) .timeout(Duration::from_secs(timeout_secs)) - .build() + .send() + .await .map_err(|_| ())?; - let response = client.get(url).send().await.map_err(|_| ())?; - if !response.status().is_success() { return Err(()); } diff --git a/nora-registry/src/registry/pypi.rs b/nora-registry/src/registry/pypi.rs index a3bf22e..406df85 100644 --- a/nora-registry/src/registry/pypi.rs +++ b/nora-registry/src/registry/pypi.rs @@ -85,7 +85,7 @@ async fn package_versions( if let Some(proxy_url) = &state.config.pypi.proxy { let url = format!("{}/{}/", proxy_url.trim_end_matches('/'), normalized); - if let Ok(html) = fetch_package_page(&url, state.config.pypi.proxy_timeout).await { + if let Ok(html) = fetch_package_page(&state.http_client, &url, state.config.pypi.proxy_timeout).await { // Rewrite URLs in the HTML to point to our registry let rewritten = rewrite_pypi_links(&html, &normalized); return (StatusCode::OK, Html(rewritten)).into_response(); @@ -130,10 +130,10 @@ async fn download_file( // First, fetch the package page to find the actual download URL let page_url = format!("{}/{}/", proxy_url.trim_end_matches('/'), normalized); - if let Ok(html) = fetch_package_page(&page_url, state.config.pypi.proxy_timeout).await { + if let Ok(html) = fetch_package_page(&state.http_client, &page_url, state.config.pypi.proxy_timeout).await { // Find the URL for this specific file if let Some(file_url) = find_file_url(&html, &filename) { - if let Ok(data) = fetch_file(&file_url, state.config.pypi.proxy_timeout).await { + if let Ok(data) = fetch_file(&state.http_client, &file_url, state.config.pypi.proxy_timeout).await { state.metrics.record_download("pypi"); state.metrics.record_cache_miss(); state.activity.push(ActivityEntry::new( @@ -177,14 +177,10 @@ fn normalize_name(name: &str) -> String { } /// Fetch package page from upstream -async fn fetch_package_page(url: &str, timeout_secs: u64) -> Result { - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(timeout_secs)) - .build() - .map_err(|_| ())?; - +async fn fetch_package_page(client: &reqwest::Client, url: &str, timeout_secs: u64) -> Result { let response = client .get(url) + .timeout(Duration::from_secs(timeout_secs)) .header("Accept", "text/html") .send() .await @@ -198,14 +194,14 @@ async fn fetch_package_page(url: &str, timeout_secs: u64) -> Result } /// Fetch file from upstream -async fn fetch_file(url: &str, timeout_secs: u64) -> Result, ()> { - let client = reqwest::Client::builder() +async fn fetch_file(client: &reqwest::Client, url: &str, timeout_secs: u64) -> Result, ()> { + let response = client + .get(url) .timeout(Duration::from_secs(timeout_secs)) - .build() + .send() + .await .map_err(|_| ())?; - let response = client.get(url).send().await.map_err(|_| ())?; - if !response.status().is_success() { return Err(()); }