mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-13 13:10:31 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 037204a3eb | |||
| 1e01d4df56 | |||
| ab5ed3f488 | |||
| 8336166e0e | |||
| 42e71b9195 | |||
| ffac4f0286 | |||
| 078ef94153 |
41
.github/workflows/release.yml
vendored
41
.github/workflows/release.yml
vendored
@@ -10,18 +10,29 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build & Push
|
||||
name: Build & Push (${{ matrix.name }})
|
||||
runs-on: self-hosted
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: alpine
|
||||
dockerfile: Dockerfile
|
||||
suffix: ""
|
||||
- name: astra
|
||||
dockerfile: Dockerfile.astra
|
||||
suffix: "-astra"
|
||||
- name: redos
|
||||
dockerfile: Dockerfile.redos
|
||||
suffix: "-redos"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
@@ -37,22 +48,25 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
suffix=${{ matrix.suffix }},onlatest=true
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value=latest
|
||||
type=raw,value=latest,enable=${{ matrix.suffix == '' }}
|
||||
type=raw,value=${{ matrix.name }},enable=${{ matrix.suffix != '' }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-from: type=gha,scope=${{ matrix.name }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.name }}
|
||||
|
||||
release:
|
||||
name: GitHub Release
|
||||
@@ -71,10 +85,21 @@ jobs:
|
||||
body: |
|
||||
## Docker
|
||||
|
||||
**Alpine (standard):**
|
||||
```bash
|
||||
docker pull ghcr.io/${{ github.repository }}:${{ github.ref_name }}
|
||||
```
|
||||
|
||||
**Astra Linux SE:**
|
||||
```bash
|
||||
docker pull ghcr.io/${{ github.repository }}:${{ github.ref_name }}-astra
|
||||
```
|
||||
|
||||
**RED OS:**
|
||||
```bash
|
||||
docker pull ghcr.io/${{ github.repository }}:${{ github.ref_name }}-redos
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md)
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,3 +13,7 @@ ROADMAP*.md
|
||||
docs-site/
|
||||
docs/
|
||||
*.txt
|
||||
|
||||
## Internal files
|
||||
.internal/
|
||||
examples/
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -7,7 +7,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.19"
|
||||
version = "0.2.20"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["DevITWay <devitway@gmail.com>"]
|
||||
|
||||
63
Dockerfile.astra
Normal file
63
Dockerfile.astra
Normal file
@@ -0,0 +1,63 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
|
||||
# Build stage — static binary via musl (runs on any Linux)
|
||||
FROM rust:1.83-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache musl-dev curl
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy manifests
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY nora-registry/Cargo.toml nora-registry/
|
||||
COPY nora-storage/Cargo.toml nora-storage/
|
||||
COPY nora-cli/Cargo.toml nora-cli/
|
||||
|
||||
# Create dummy sources for dependency caching
|
||||
RUN mkdir -p nora-registry/src nora-storage/src nora-cli/src && \
|
||||
echo "fn main() {}" > nora-registry/src/main.rs && \
|
||||
echo "fn main() {}" > nora-storage/src/main.rs && \
|
||||
echo "fn main() {}" > nora-cli/src/main.rs
|
||||
|
||||
# Build dependencies only (with cache)
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
--mount=type=cache,target=/app/target \
|
||||
cargo build --release --package nora-registry && \
|
||||
rm -rf nora-registry/src nora-storage/src nora-cli/src
|
||||
|
||||
# Copy real sources
|
||||
COPY nora-registry/src nora-registry/src
|
||||
COPY nora-storage/src nora-storage/src
|
||||
COPY nora-cli/src nora-cli/src
|
||||
|
||||
# Build release binary (with cache)
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
--mount=type=cache,target=/app/target \
|
||||
touch nora-registry/src/main.rs && \
|
||||
cargo build --release --package nora-registry && \
|
||||
cp /app/target/release/nora /usr/local/bin/nora
|
||||
|
||||
# Runtime stage — scratch (compatible with Astra Linux SE, no foreign OS components)
|
||||
# Switch FROM to registry.astralinux.ru/library/alse once registry access is configured
|
||||
FROM scratch
|
||||
|
||||
# CA certificates for TLS
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
COPY --from=builder /usr/local/bin/nora /usr/local/bin/nora
|
||||
|
||||
ENV RUST_LOG=info
|
||||
ENV NORA_HOST=0.0.0.0
|
||||
ENV NORA_PORT=4000
|
||||
ENV NORA_STORAGE_MODE=local
|
||||
ENV NORA_STORAGE_PATH=/data/storage
|
||||
ENV NORA_AUTH_TOKEN_STORAGE=/data/tokens
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/nora"]
|
||||
CMD ["serve"]
|
||||
63
Dockerfile.redos
Normal file
63
Dockerfile.redos
Normal file
@@ -0,0 +1,63 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
|
||||
# Build stage — static binary via musl (runs on any Linux)
|
||||
FROM rust:1.83-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache musl-dev curl
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy manifests
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY nora-registry/Cargo.toml nora-registry/
|
||||
COPY nora-storage/Cargo.toml nora-storage/
|
||||
COPY nora-cli/Cargo.toml nora-cli/
|
||||
|
||||
# Create dummy sources for dependency caching
|
||||
RUN mkdir -p nora-registry/src nora-storage/src nora-cli/src && \
|
||||
echo "fn main() {}" > nora-registry/src/main.rs && \
|
||||
echo "fn main() {}" > nora-storage/src/main.rs && \
|
||||
echo "fn main() {}" > nora-cli/src/main.rs
|
||||
|
||||
# Build dependencies only (with cache)
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
--mount=type=cache,target=/app/target \
|
||||
cargo build --release --package nora-registry && \
|
||||
rm -rf nora-registry/src nora-storage/src nora-cli/src
|
||||
|
||||
# Copy real sources
|
||||
COPY nora-registry/src nora-registry/src
|
||||
COPY nora-storage/src nora-storage/src
|
||||
COPY nora-cli/src nora-cli/src
|
||||
|
||||
# Build release binary (with cache)
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
--mount=type=cache,target=/app/target \
|
||||
touch nora-registry/src/main.rs && \
|
||||
cargo build --release --package nora-registry && \
|
||||
cp /app/target/release/nora /usr/local/bin/nora
|
||||
|
||||
# Runtime stage — scratch (compatible with RED OS, no foreign OS components)
|
||||
# Switch FROM to registry.red-soft.ru/redos once registry access is configured
|
||||
FROM scratch
|
||||
|
||||
# CA certificates for TLS
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
COPY --from=builder /usr/local/bin/nora /usr/local/bin/nora
|
||||
|
||||
ENV RUST_LOG=info
|
||||
ENV NORA_HOST=0.0.0.0
|
||||
ENV NORA_PORT=4000
|
||||
ENV NORA_STORAGE_MODE=local
|
||||
ENV NORA_STORAGE_PATH=/data/storage
|
||||
ENV NORA_AUTH_TOKEN_STORAGE=/data/tokens
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/nora"]
|
||||
CMD ["serve"]
|
||||
@@ -63,11 +63,17 @@ impl HtpasswdAuth {
|
||||
fn is_public_path(path: &str) -> bool {
|
||||
matches!(
|
||||
path,
|
||||
"/" | "/health" | "/ready" | "/metrics" | "/v2/" | "/v2"
|
||||
"/" | "/health"
|
||||
| "/ready"
|
||||
| "/metrics"
|
||||
| "/v2/"
|
||||
| "/v2"
|
||||
| "/api/tokens"
|
||||
| "/api/tokens/list"
|
||||
| "/api/tokens/revoke"
|
||||
) || path.starts_with("/ui")
|
||||
|| path.starts_with("/api-docs")
|
||||
|| path.starts_with("/api/ui")
|
||||
|| path.starts_with("/api/tokens")
|
||||
}
|
||||
|
||||
/// Auth middleware - supports Basic auth and Bearer tokens
|
||||
@@ -404,8 +410,12 @@ mod tests {
|
||||
assert!(is_public_path("/api/ui/stats"));
|
||||
assert!(is_public_path("/api/tokens"));
|
||||
assert!(is_public_path("/api/tokens/list"));
|
||||
assert!(is_public_path("/api/tokens/revoke"));
|
||||
|
||||
// Protected paths
|
||||
assert!(!is_public_path("/api/tokens/unknown"));
|
||||
assert!(!is_public_path("/api/tokens/admin"));
|
||||
assert!(!is_public_path("/api/tokens/extra/path"));
|
||||
assert!(!is_public_path("/v2/myimage/blobs/sha256:abc"));
|
||||
assert!(!is_public_path("/v2/library/nginx/manifests/latest"));
|
||||
assert!(!is_public_path(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,7 +23,6 @@ pub fn routes() -> Router<Arc<AppState>> {
|
||||
async fn download(State(state): State<Arc<AppState>>, Path(path): Path<String>) -> 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<Arc<AppState>>, Path(path): Path<String>)
|
||||
.collect::<Vec<_>>()
|
||||
.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<Arc<AppState>>, Path(path): Path<String>)
|
||||
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<Arc<AppState>>, Path(path): Path<String>)
|
||||
"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,18 @@ async fn upload(
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_from_proxy(url: &str, timeout_secs: u64) -> Result<Vec<u8>, ()> {
|
||||
let client = reqwest::Client::builder()
|
||||
async fn fetch_from_proxy(
|
||||
client: &reqwest::Client,
|
||||
url: &str,
|
||||
timeout_secs: u64,
|
||||
) -> Result<Vec<u8>, ()> {
|
||||
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(());
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ pub fn routes() -> Router<Arc<AppState>> {
|
||||
}
|
||||
|
||||
async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<String>) -> 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<Arc<AppState>>, Path(path): Path<Str
|
||||
format!("npm/{}/metadata.json", path)
|
||||
};
|
||||
|
||||
// Extract package name for logging
|
||||
let package_name = if is_tarball {
|
||||
path.split("/-/").next().unwrap_or(&path).to_string()
|
||||
} else {
|
||||
path.clone()
|
||||
};
|
||||
|
||||
// Try local storage first
|
||||
if let Ok(data) = state.storage.get(&key).await {
|
||||
if is_tarball {
|
||||
state.metrics.record_download("npm");
|
||||
@@ -55,17 +52,12 @@ async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<Str
|
||||
return with_content_type(is_tarball, data).into_response();
|
||||
}
|
||||
|
||||
// Try proxy if configured
|
||||
if let Some(proxy_url) = &state.config.npm.proxy {
|
||||
let url = if is_tarball {
|
||||
// Tarball URL: https://registry.npmjs.org/package/-/package-version.tgz
|
||||
format!("{}/{}", proxy_url.trim_end_matches('/'), path)
|
||||
} else {
|
||||
// Metadata URL: https://registry.npmjs.org/package
|
||||
format!("{}/{}", proxy_url.trim_end_matches('/'), path)
|
||||
};
|
||||
let url = format!("{}/{}", proxy_url.trim_end_matches('/'), path);
|
||||
|
||||
if let Ok(data) = fetch_from_proxy(&url, state.config.npm.proxy_timeout).await {
|
||||
if let Ok(data) =
|
||||
fetch_from_proxy(&state.http_client, &url, state.config.npm.proxy_timeout).await
|
||||
{
|
||||
if is_tarball {
|
||||
state.metrics.record_download("npm");
|
||||
state.metrics.record_cache_miss();
|
||||
@@ -77,7 +69,6 @@ async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<Str
|
||||
));
|
||||
}
|
||||
|
||||
// Cache in local storage (fire and forget)
|
||||
let storage = state.storage.clone();
|
||||
let key_clone = key.clone();
|
||||
let data_clone = data.clone();
|
||||
@@ -85,7 +76,6 @@ async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<Str
|
||||
let _ = storage.put(&key_clone, &data_clone).await;
|
||||
});
|
||||
|
||||
// Invalidate index when caching new tarball
|
||||
if is_tarball {
|
||||
state.repo_index.invalidate("npm");
|
||||
}
|
||||
@@ -97,14 +87,18 @@ async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<Str
|
||||
StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
|
||||
async fn fetch_from_proxy(url: &str, timeout_secs: u64) -> Result<Vec<u8>, ()> {
|
||||
let client = reqwest::Client::builder()
|
||||
async fn fetch_from_proxy(
|
||||
client: &reqwest::Client,
|
||||
url: &str,
|
||||
timeout_secs: u64,
|
||||
) -> Result<Vec<u8>, ()> {
|
||||
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(());
|
||||
}
|
||||
|
||||
@@ -85,7 +85,9 @@ 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 +132,22 @@ 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 +191,14 @@ fn normalize_name(name: &str) -> String {
|
||||
}
|
||||
|
||||
/// Fetch package page from upstream
|
||||
async fn fetch_package_page(url: &str, timeout_secs: u64) -> Result<String, ()> {
|
||||
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<String, ()> {
|
||||
let response = client
|
||||
.get(url)
|
||||
.timeout(Duration::from_secs(timeout_secs))
|
||||
.header("Accept", "text/html")
|
||||
.send()
|
||||
.await
|
||||
@@ -198,14 +212,14 @@ async fn fetch_package_page(url: &str, timeout_secs: u64) -> Result<String, ()>
|
||||
}
|
||||
|
||||
/// Fetch file from upstream
|
||||
async fn fetch_file(url: &str, timeout_secs: u64) -> Result<Vec<u8>, ()> {
|
||||
let client = reqwest::Client::builder()
|
||||
async fn fetch_file(client: &reqwest::Client, url: &str, timeout_secs: u64) -> Result<Vec<u8>, ()> {
|
||||
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(());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user