mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 18:30:32 +00:00
feat: upstream auth for all protocols (Docker, Maven, npm, PyPI)
Wire up basic auth credentials for upstream registry proxying: - Docker: pass configured auth to Bearer token requests - Maven: support url|auth format in NORA_MAVEN_PROXIES env var - npm: add NORA_NPM_PROXY_AUTH env var - PyPI: add NORA_PYPI_PROXY_AUTH env var - Mask credentials in logs (never log plaintext passwords) Config examples: NORA_DOCKER_UPSTREAMS="https://registry.corp.com|user:pass" NORA_MAVEN_PROXIES="https://nexus.corp.com/maven2|user:pass" NORA_NPM_PROXY_AUTH="user:pass" NORA_PYPI_PROXY_AUTH="user:pass"
This commit is contained in:
@@ -93,7 +93,7 @@ fn default_bucket() -> String {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct MavenConfig {
|
pub struct MavenConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub proxies: Vec<String>,
|
pub proxies: Vec<MavenProxyEntry>,
|
||||||
#[serde(default = "default_timeout")]
|
#[serde(default = "default_timeout")]
|
||||||
pub proxy_timeout: u64,
|
pub proxy_timeout: u64,
|
||||||
}
|
}
|
||||||
@@ -102,6 +102,8 @@ pub struct MavenConfig {
|
|||||||
pub struct NpmConfig {
|
pub struct NpmConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub proxy: Option<String>,
|
pub proxy: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub proxy_auth: Option<String>, // "user:pass" for basic auth
|
||||||
#[serde(default = "default_timeout")]
|
#[serde(default = "default_timeout")]
|
||||||
pub proxy_timeout: u64,
|
pub proxy_timeout: u64,
|
||||||
}
|
}
|
||||||
@@ -110,6 +112,8 @@ pub struct NpmConfig {
|
|||||||
pub struct PypiConfig {
|
pub struct PypiConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub proxy: Option<String>,
|
pub proxy: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub proxy_auth: Option<String>, // "user:pass" for basic auth
|
||||||
#[serde(default = "default_timeout")]
|
#[serde(default = "default_timeout")]
|
||||||
pub proxy_timeout: u64,
|
pub proxy_timeout: u64,
|
||||||
}
|
}
|
||||||
@@ -131,6 +135,37 @@ pub struct DockerUpstream {
|
|||||||
pub auth: Option<String>, // "user:pass" for basic auth
|
pub auth: Option<String>, // "user:pass" for basic auth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maven upstream proxy configuration
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum MavenProxyEntry {
|
||||||
|
Simple(String),
|
||||||
|
Full(MavenProxy),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maven upstream proxy with optional auth
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MavenProxy {
|
||||||
|
pub url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub auth: Option<String>, // "user:pass" for basic auth
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MavenProxyEntry {
|
||||||
|
pub fn url(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
MavenProxyEntry::Simple(s) => s,
|
||||||
|
MavenProxyEntry::Full(p) => &p.url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn auth(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
MavenProxyEntry::Simple(_) => None,
|
||||||
|
MavenProxyEntry::Full(p) => p.auth.as_deref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Raw repository configuration for simple file storage
|
/// Raw repository configuration for simple file storage
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct RawConfig {
|
pub struct RawConfig {
|
||||||
@@ -177,7 +212,9 @@ fn default_timeout() -> u64 {
|
|||||||
impl Default for MavenConfig {
|
impl Default for MavenConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
proxies: vec!["https://repo1.maven.org/maven2".to_string()],
|
proxies: vec![MavenProxyEntry::Simple(
|
||||||
|
"https://repo1.maven.org/maven2".to_string(),
|
||||||
|
)],
|
||||||
proxy_timeout: 30,
|
proxy_timeout: 30,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,6 +224,7 @@ impl Default for NpmConfig {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
proxy: Some("https://registry.npmjs.org".to_string()),
|
proxy: Some("https://registry.npmjs.org".to_string()),
|
||||||
|
proxy_auth: None,
|
||||||
proxy_timeout: 30,
|
proxy_timeout: 30,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,6 +234,7 @@ impl Default for PypiConfig {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
proxy: Some("https://pypi.org/simple/".to_string()),
|
proxy: Some("https://pypi.org/simple/".to_string()),
|
||||||
|
proxy_auth: None,
|
||||||
proxy_timeout: 30,
|
proxy_timeout: 30,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,9 +416,23 @@ impl Config {
|
|||||||
self.auth.htpasswd_file = val;
|
self.auth.htpasswd_file = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maven config
|
// Maven config — supports "url1,url2" or "url1|auth1,url2|auth2"
|
||||||
if let Ok(val) = env::var("NORA_MAVEN_PROXIES") {
|
if let Ok(val) = env::var("NORA_MAVEN_PROXIES") {
|
||||||
self.maven.proxies = val.split(',').map(|s| s.trim().to_string()).collect();
|
self.maven.proxies = val
|
||||||
|
.split(',')
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| {
|
||||||
|
let parts: Vec<&str> = s.trim().splitn(2, '|').collect();
|
||||||
|
if parts.len() > 1 {
|
||||||
|
MavenProxyEntry::Full(MavenProxy {
|
||||||
|
url: parts[0].to_string(),
|
||||||
|
auth: Some(parts[1].to_string()),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
MavenProxyEntry::Simple(parts[0].to_string())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
}
|
}
|
||||||
if let Ok(val) = env::var("NORA_MAVEN_PROXY_TIMEOUT") {
|
if let Ok(val) = env::var("NORA_MAVEN_PROXY_TIMEOUT") {
|
||||||
if let Ok(timeout) = val.parse() {
|
if let Ok(timeout) = val.parse() {
|
||||||
@@ -397,6 +450,11 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// npm proxy auth
|
||||||
|
if let Ok(val) = env::var("NORA_NPM_PROXY_AUTH") {
|
||||||
|
self.npm.proxy_auth = if val.is_empty() { None } else { Some(val) };
|
||||||
|
}
|
||||||
|
|
||||||
// PyPI config
|
// PyPI config
|
||||||
if let Ok(val) = env::var("NORA_PYPI_PROXY") {
|
if let Ok(val) = env::var("NORA_PYPI_PROXY") {
|
||||||
self.pypi.proxy = if val.is_empty() { None } else { Some(val) };
|
self.pypi.proxy = if val.is_empty() { None } else { Some(val) };
|
||||||
@@ -407,6 +465,11 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PyPI proxy auth
|
||||||
|
if let Ok(val) = env::var("NORA_PYPI_PROXY_AUTH") {
|
||||||
|
self.pypi.proxy_auth = if val.is_empty() { None } else { Some(val) };
|
||||||
|
}
|
||||||
|
|
||||||
// Docker config
|
// Docker config
|
||||||
if let Ok(val) = env::var("NORA_DOCKER_PROXY_TIMEOUT") {
|
if let Ok(val) = env::var("NORA_DOCKER_PROXY_TIMEOUT") {
|
||||||
if let Ok(timeout) = val.parse() {
|
if let Ok(timeout) = val.parse() {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use axum::{
|
|||||||
routing::{delete, get, head, patch, put},
|
routing::{delete, get, head, patch, put},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
@@ -181,6 +182,7 @@ async fn download_blob(
|
|||||||
&digest,
|
&digest,
|
||||||
&state.docker_auth,
|
&state.docker_auth,
|
||||||
state.config.docker.proxy_timeout,
|
state.config.docker.proxy_timeout,
|
||||||
|
upstream.auth.as_deref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -392,6 +394,7 @@ async fn get_manifest(
|
|||||||
&reference,
|
&reference,
|
||||||
&state.docker_auth,
|
&state.docker_auth,
|
||||||
state.config.docker.proxy_timeout,
|
state.config.docker.proxy_timeout,
|
||||||
|
upstream.auth.as_deref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -733,6 +736,7 @@ async fn fetch_blob_from_upstream(
|
|||||||
digest: &str,
|
digest: &str,
|
||||||
docker_auth: &DockerAuth,
|
docker_auth: &DockerAuth,
|
||||||
timeout: u64,
|
timeout: u64,
|
||||||
|
basic_auth: Option<&str>,
|
||||||
) -> Result<Vec<u8>, ()> {
|
) -> Result<Vec<u8>, ()> {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/v2/{}/blobs/{}",
|
"{}/v2/{}/blobs/{}",
|
||||||
@@ -741,13 +745,13 @@ async fn fetch_blob_from_upstream(
|
|||||||
digest
|
digest
|
||||||
);
|
);
|
||||||
|
|
||||||
// First try without auth
|
// First try — with basic auth if configured
|
||||||
let response = client
|
let mut request = client.get(&url).timeout(Duration::from_secs(timeout));
|
||||||
.get(&url)
|
if let Some(credentials) = basic_auth {
|
||||||
.timeout(Duration::from_secs(timeout))
|
let encoded = STANDARD.encode(credentials);
|
||||||
.send()
|
request = request.header("Authorization", format!("Basic {}", encoded));
|
||||||
.await
|
}
|
||||||
.map_err(|_| ())?;
|
let response = request.send().await.map_err(|_| ())?;
|
||||||
|
|
||||||
let response = if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
let response = if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||||
// Get Www-Authenticate header and fetch token
|
// Get Www-Authenticate header and fetch token
|
||||||
@@ -758,7 +762,7 @@ async fn fetch_blob_from_upstream(
|
|||||||
.map(String::from);
|
.map(String::from);
|
||||||
|
|
||||||
if let Some(token) = docker_auth
|
if let Some(token) = docker_auth
|
||||||
.get_token(upstream_url, name, www_auth.as_deref())
|
.get_token(upstream_url, name, www_auth.as_deref(), basic_auth)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
client
|
client
|
||||||
@@ -790,6 +794,7 @@ async fn fetch_manifest_from_upstream(
|
|||||||
reference: &str,
|
reference: &str,
|
||||||
docker_auth: &DockerAuth,
|
docker_auth: &DockerAuth,
|
||||||
timeout: u64,
|
timeout: u64,
|
||||||
|
basic_auth: Option<&str>,
|
||||||
) -> Result<(Vec<u8>, String), ()> {
|
) -> Result<(Vec<u8>, String), ()> {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/v2/{}/manifests/{}",
|
"{}/v2/{}/manifests/{}",
|
||||||
@@ -806,16 +811,18 @@ async fn fetch_manifest_from_upstream(
|
|||||||
application/vnd.oci.image.manifest.v1+json, \
|
application/vnd.oci.image.manifest.v1+json, \
|
||||||
application/vnd.oci.image.index.v1+json";
|
application/vnd.oci.image.index.v1+json";
|
||||||
|
|
||||||
// First try without auth
|
// First try — with basic auth if configured
|
||||||
let response = client
|
let mut request = client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.timeout(Duration::from_secs(timeout))
|
.timeout(Duration::from_secs(timeout))
|
||||||
.header("Accept", accept_header)
|
.header("Accept", accept_header);
|
||||||
.send()
|
if let Some(credentials) = basic_auth {
|
||||||
.await
|
let encoded = STANDARD.encode(credentials);
|
||||||
.map_err(|e| {
|
request = request.header("Authorization", format!("Basic {}", encoded));
|
||||||
tracing::error!(error = %e, url = %url, "Failed to send request to upstream");
|
}
|
||||||
})?;
|
let response = request.send().await.map_err(|e| {
|
||||||
|
tracing::error!(error = %e, url = %url, "Failed to send request to upstream");
|
||||||
|
})?;
|
||||||
|
|
||||||
tracing::debug!(status = %response.status(), "Initial upstream response");
|
tracing::debug!(status = %response.status(), "Initial upstream response");
|
||||||
|
|
||||||
@@ -830,7 +837,7 @@ async fn fetch_manifest_from_upstream(
|
|||||||
tracing::debug!(www_auth = ?www_auth, "Got 401, fetching token");
|
tracing::debug!(www_auth = ?www_auth, "Got 401, fetching token");
|
||||||
|
|
||||||
if let Some(token) = docker_auth
|
if let Some(token) = docker_auth
|
||||||
.get_token(upstream_url, name, www_auth.as_deref())
|
.get_token(upstream_url, name, www_auth.as_deref(), basic_auth)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
tracing::debug!("Token acquired, retrying with auth");
|
tracing::debug!("Token acquired, retrying with auth");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) 2026 Volkov Pavel | DevITWay
|
// Copyright (c) 2026 Volkov Pavel | DevITWay
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -36,6 +37,7 @@ impl DockerAuth {
|
|||||||
registry_url: &str,
|
registry_url: &str,
|
||||||
name: &str,
|
name: &str,
|
||||||
www_authenticate: Option<&str>,
|
www_authenticate: Option<&str>,
|
||||||
|
basic_auth: Option<&str>,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let cache_key = format!("{}:{}", registry_url, name);
|
let cache_key = format!("{}:{}", registry_url, name);
|
||||||
|
|
||||||
@@ -51,7 +53,7 @@ impl DockerAuth {
|
|||||||
|
|
||||||
// Need to fetch a new token
|
// Need to fetch a new token
|
||||||
let www_auth = www_authenticate?;
|
let www_auth = www_authenticate?;
|
||||||
let token = self.fetch_token(www_auth, name).await?;
|
let token = self.fetch_token(www_auth, name, basic_auth).await?;
|
||||||
|
|
||||||
// Cache the token (default 5 minute expiry)
|
// Cache the token (default 5 minute expiry)
|
||||||
{
|
{
|
||||||
@@ -70,7 +72,12 @@ impl DockerAuth {
|
|||||||
|
|
||||||
/// Parse Www-Authenticate header and fetch token from auth server
|
/// Parse Www-Authenticate header and fetch token from auth server
|
||||||
/// Format: Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/alpine:pull"
|
/// Format: Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/alpine:pull"
|
||||||
async fn fetch_token(&self, www_authenticate: &str, name: &str) -> Option<String> {
|
async fn fetch_token(
|
||||||
|
&self,
|
||||||
|
www_authenticate: &str,
|
||||||
|
name: &str,
|
||||||
|
basic_auth: Option<&str>,
|
||||||
|
) -> Option<String> {
|
||||||
let params = parse_www_authenticate(www_authenticate)?;
|
let params = parse_www_authenticate(www_authenticate)?;
|
||||||
|
|
||||||
let realm = params.get("realm")?;
|
let realm = params.get("realm")?;
|
||||||
@@ -82,7 +89,14 @@ impl DockerAuth {
|
|||||||
|
|
||||||
tracing::debug!(url = %url, "Fetching auth token");
|
tracing::debug!(url = %url, "Fetching auth token");
|
||||||
|
|
||||||
let response = self.client.get(&url).send().await.ok()?;
|
let mut request = self.client.get(&url);
|
||||||
|
if let Some(credentials) = basic_auth {
|
||||||
|
let encoded = STANDARD.encode(credentials);
|
||||||
|
request = request.header("Authorization", format!("Basic {}", encoded));
|
||||||
|
tracing::debug!("Using basic auth for token request");
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = request.send().await.ok()?;
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
tracing::warn!(status = %response.status(), "Token request failed");
|
tracing::warn!(status = %response.status(), "Token request failed");
|
||||||
@@ -104,9 +118,15 @@ impl DockerAuth {
|
|||||||
url: &str,
|
url: &str,
|
||||||
registry_url: &str,
|
registry_url: &str,
|
||||||
name: &str,
|
name: &str,
|
||||||
|
basic_auth: Option<&str>,
|
||||||
) -> Result<reqwest::Response, ()> {
|
) -> Result<reqwest::Response, ()> {
|
||||||
// First try without auth
|
// First try — with basic auth if configured, otherwise anonymous
|
||||||
let response = self.client.get(url).send().await.map_err(|_| ())?;
|
let mut request = self.client.get(url);
|
||||||
|
if let Some(credentials) = basic_auth {
|
||||||
|
let encoded = STANDARD.encode(credentials);
|
||||||
|
request = request.header("Authorization", format!("Basic {}", encoded));
|
||||||
|
}
|
||||||
|
let response = request.send().await.map_err(|_| ())?;
|
||||||
|
|
||||||
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||||
// Extract Www-Authenticate header
|
// Extract Www-Authenticate header
|
||||||
@@ -118,7 +138,7 @@ impl DockerAuth {
|
|||||||
|
|
||||||
// Get token and retry
|
// Get token and retry
|
||||||
if let Some(token) = self
|
if let Some(token) = self
|
||||||
.get_token(registry_url, name, www_auth.as_deref())
|
.get_token(registry_url, name, www_auth.as_deref(), basic_auth)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return self
|
return self
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use axum::{
|
|||||||
routing::{get, put},
|
routing::{get, put},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -49,10 +50,17 @@ async fn download(State(state): State<Arc<AppState>>, Path(path): Path<String>)
|
|||||||
return with_content_type(&path, data).into_response();
|
return with_content_type(&path, data).into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
for proxy_url in &state.config.maven.proxies {
|
for proxy in &state.config.maven.proxies {
|
||||||
let url = format!("{}/{}", proxy_url.trim_end_matches('/'), path);
|
let url = format!("{}/{}", proxy.url().trim_end_matches('/'), path);
|
||||||
|
|
||||||
match fetch_from_proxy(&state.http_client, &url, state.config.maven.proxy_timeout).await {
|
match fetch_from_proxy(
|
||||||
|
&state.http_client,
|
||||||
|
&url,
|
||||||
|
state.config.maven.proxy_timeout,
|
||||||
|
proxy.auth(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
state.metrics.record_download("maven");
|
state.metrics.record_download("maven");
|
||||||
state.metrics.record_cache_miss();
|
state.metrics.record_cache_miss();
|
||||||
@@ -124,13 +132,14 @@ async fn fetch_from_proxy(
|
|||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
url: &str,
|
url: &str,
|
||||||
timeout_secs: u64,
|
timeout_secs: u64,
|
||||||
|
auth: Option<&str>,
|
||||||
) -> Result<Vec<u8>, ()> {
|
) -> Result<Vec<u8>, ()> {
|
||||||
let response = client
|
let mut request = client.get(url).timeout(Duration::from_secs(timeout_secs));
|
||||||
.get(url)
|
if let Some(credentials) = auth {
|
||||||
.timeout(Duration::from_secs(timeout_secs))
|
let encoded = STANDARD.encode(credentials);
|
||||||
.send()
|
request = request.header("Authorization", format!("Basic {}", encoded));
|
||||||
.await
|
}
|
||||||
.map_err(|_| ())?;
|
let response = request.send().await.map_err(|_| ())?;
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(());
|
return Err(());
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use axum::{
|
|||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -59,8 +60,13 @@ async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<Str
|
|||||||
if let Some(proxy_url) = &state.config.npm.proxy {
|
if let Some(proxy_url) = &state.config.npm.proxy {
|
||||||
let url = format!("{}/{}", proxy_url.trim_end_matches('/'), path);
|
let url = format!("{}/{}", proxy_url.trim_end_matches('/'), path);
|
||||||
|
|
||||||
if let Ok(data) =
|
if let Ok(data) = fetch_from_proxy(
|
||||||
fetch_from_proxy(&state.http_client, &url, state.config.npm.proxy_timeout).await
|
&state.http_client,
|
||||||
|
&url,
|
||||||
|
state.config.npm.proxy_timeout,
|
||||||
|
state.config.npm.proxy_auth.as_deref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
if is_tarball {
|
if is_tarball {
|
||||||
state.metrics.record_download("npm");
|
state.metrics.record_download("npm");
|
||||||
@@ -98,13 +104,14 @@ async fn fetch_from_proxy(
|
|||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
url: &str,
|
url: &str,
|
||||||
timeout_secs: u64,
|
timeout_secs: u64,
|
||||||
|
auth: Option<&str>,
|
||||||
) -> Result<Vec<u8>, ()> {
|
) -> Result<Vec<u8>, ()> {
|
||||||
let response = client
|
let mut request = client.get(url).timeout(Duration::from_secs(timeout_secs));
|
||||||
.get(url)
|
if let Some(credentials) = auth {
|
||||||
.timeout(Duration::from_secs(timeout_secs))
|
let encoded = STANDARD.encode(credentials);
|
||||||
.send()
|
request = request.header("Authorization", format!("Basic {}", encoded));
|
||||||
.await
|
}
|
||||||
.map_err(|_| ())?;
|
let response = request.send().await.map_err(|_| ())?;
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(());
|
return Err(());
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use axum::{
|
|||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -86,8 +87,13 @@ async fn package_versions(
|
|||||||
if let Some(proxy_url) = &state.config.pypi.proxy {
|
if let Some(proxy_url) = &state.config.pypi.proxy {
|
||||||
let url = format!("{}/{}/", proxy_url.trim_end_matches('/'), normalized);
|
let url = format!("{}/{}/", proxy_url.trim_end_matches('/'), normalized);
|
||||||
|
|
||||||
if let Ok(html) =
|
if let Ok(html) = fetch_package_page(
|
||||||
fetch_package_page(&state.http_client, &url, state.config.pypi.proxy_timeout).await
|
&state.http_client,
|
||||||
|
&url,
|
||||||
|
state.config.pypi.proxy_timeout,
|
||||||
|
state.config.pypi.proxy_auth.as_deref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
// Rewrite URLs in the HTML to point to our registry
|
// Rewrite URLs in the HTML to point to our registry
|
||||||
let rewritten = rewrite_pypi_links(&html, &normalized);
|
let rewritten = rewrite_pypi_links(&html, &normalized);
|
||||||
@@ -140,6 +146,7 @@ async fn download_file(
|
|||||||
&state.http_client,
|
&state.http_client,
|
||||||
&page_url,
|
&page_url,
|
||||||
state.config.pypi.proxy_timeout,
|
state.config.pypi.proxy_timeout,
|
||||||
|
state.config.pypi.proxy_auth.as_deref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -149,6 +156,7 @@ async fn download_file(
|
|||||||
&state.http_client,
|
&state.http_client,
|
||||||
&file_url,
|
&file_url,
|
||||||
state.config.pypi.proxy_timeout,
|
state.config.pypi.proxy_timeout,
|
||||||
|
state.config.pypi.proxy_auth.as_deref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -202,14 +210,17 @@ async fn fetch_package_page(
|
|||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
url: &str,
|
url: &str,
|
||||||
timeout_secs: u64,
|
timeout_secs: u64,
|
||||||
|
auth: Option<&str>,
|
||||||
) -> Result<String, ()> {
|
) -> Result<String, ()> {
|
||||||
let response = client
|
let mut request = client
|
||||||
.get(url)
|
.get(url)
|
||||||
.timeout(Duration::from_secs(timeout_secs))
|
.timeout(Duration::from_secs(timeout_secs))
|
||||||
.header("Accept", "text/html")
|
.header("Accept", "text/html");
|
||||||
.send()
|
if let Some(credentials) = auth {
|
||||||
.await
|
let encoded = STANDARD.encode(credentials);
|
||||||
.map_err(|_| ())?;
|
request = request.header("Authorization", format!("Basic {}", encoded));
|
||||||
|
}
|
||||||
|
let response = request.send().await.map_err(|_| ())?;
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(());
|
return Err(());
|
||||||
@@ -219,13 +230,18 @@ async fn fetch_package_page(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch file from upstream
|
/// Fetch file from upstream
|
||||||
async fn fetch_file(client: &reqwest::Client, url: &str, timeout_secs: u64) -> Result<Vec<u8>, ()> {
|
async fn fetch_file(
|
||||||
let response = client
|
client: &reqwest::Client,
|
||||||
.get(url)
|
url: &str,
|
||||||
.timeout(Duration::from_secs(timeout_secs))
|
timeout_secs: u64,
|
||||||
.send()
|
auth: Option<&str>,
|
||||||
.await
|
) -> Result<Vec<u8>, ()> {
|
||||||
.map_err(|_| ())?;
|
let mut request = client.get(url).timeout(Duration::from_secs(timeout_secs));
|
||||||
|
if let Some(credentials) = auth {
|
||||||
|
let encoded = STANDARD.encode(credentials);
|
||||||
|
request = request.header("Authorization", format!("Basic {}", encoded));
|
||||||
|
}
|
||||||
|
let response = request.send().await.map_err(|_| ())?;
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(());
|
return Err(());
|
||||||
|
|||||||
@@ -205,7 +205,12 @@ pub async fn api_dashboard(State(state): State<Arc<AppState>>) -> Json<Dashboard
|
|||||||
MountPoint {
|
MountPoint {
|
||||||
registry: "Maven".to_string(),
|
registry: "Maven".to_string(),
|
||||||
mount_path: "/maven2/".to_string(),
|
mount_path: "/maven2/".to_string(),
|
||||||
proxy_upstream: state.config.maven.proxies.first().cloned(),
|
proxy_upstream: state
|
||||||
|
.config
|
||||||
|
.maven
|
||||||
|
.proxies
|
||||||
|
.first()
|
||||||
|
.map(|p| p.url().to_string()),
|
||||||
},
|
},
|
||||||
MountPoint {
|
MountPoint {
|
||||||
registry: "npm".to_string(),
|
registry: "npm".to_string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user