mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-13 09:40:31 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65fa80dd1b |
16
.github/dependabot.yml
vendored
16
.github/dependabot.yml
vendored
@@ -1,16 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
# GitHub Actions — обновляет версии actions в workflows
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
labels: [dependencies, ci]
|
|
||||||
|
|
||||||
# Cargo — только security-апдейты, без шума от minor/patch
|
|
||||||
- package-ecosystem: cargo
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
open-pull-requests-limit: 5
|
|
||||||
labels: [dependencies, rust]
|
|
||||||
62
.github/workflows/ci.yml
vendored
62
.github/workflows/ci.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
name: Test
|
name: Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -27,63 +27,3 @@ jobs:
|
|||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --package nora-registry
|
run: cargo test --package nora-registry
|
||||||
|
|
||||||
security:
|
|
||||||
name: Security
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
security-events: write # for uploading SARIF to GitHub Security tab
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0 # full history required for gitleaks
|
|
||||||
|
|
||||||
- name: Install Rust
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Cache cargo
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
|
|
||||||
# ── Secrets ────────────────────────────────────────────────────────────
|
|
||||||
- name: Gitleaks — scan for hardcoded secrets
|
|
||||||
run: |
|
|
||||||
curl -sL https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz \
|
|
||||||
| tar xz -C /usr/local/bin gitleaks
|
|
||||||
gitleaks detect --source . --exit-code 1 --report-format sarif --report-path gitleaks.sarif || true
|
|
||||||
continue-on-error: true # findings are reported, do not block the pipeline
|
|
||||||
|
|
||||||
# ── CVE in Rust dependencies ────────────────────────────────────────────
|
|
||||||
- name: Install cargo-audit
|
|
||||||
run: cargo install cargo-audit --locked
|
|
||||||
|
|
||||||
- name: cargo audit — RustSec advisory database
|
|
||||||
run: cargo audit
|
|
||||||
continue-on-error: true # warn only; known CVEs should not block CI until triaged
|
|
||||||
|
|
||||||
# ── Licenses, banned crates, supply chain policy ────────────────────────
|
|
||||||
- name: cargo deny — licenses and banned crates
|
|
||||||
uses: EmbarkStudios/cargo-deny-action@v2
|
|
||||||
with:
|
|
||||||
command: check
|
|
||||||
arguments: --all-features
|
|
||||||
|
|
||||||
# ── CVE scan of source tree and Cargo.lock ──────────────────────────────
|
|
||||||
- name: Trivy — filesystem scan (Cargo.lock + source)
|
|
||||||
if: always()
|
|
||||||
uses: aquasecurity/trivy-action@0.34.1
|
|
||||||
with:
|
|
||||||
scan-type: fs
|
|
||||||
scan-ref: .
|
|
||||||
format: sarif
|
|
||||||
output: trivy-fs.sarif
|
|
||||||
severity: HIGH,CRITICAL
|
|
||||||
exit-code: 0 # warn only; change to 1 to block the pipeline
|
|
||||||
|
|
||||||
- name: Upload Trivy fs results to GitHub Security tab
|
|
||||||
uses: github/codeql-action/upload-sarif@v4
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
sarif_file: trivy-fs.sarif
|
|
||||||
category: trivy-fs
|
|
||||||
|
|||||||
174
.github/workflows/release.yml
vendored
174
.github/workflows/release.yml
vendored
@@ -11,7 +11,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build & Push
|
name: Build & Push
|
||||||
runs-on: [self-hosted, nora]
|
runs-on: self-hosted
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@@ -19,16 +19,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Rust
|
- name: Set up QEMU
|
||||||
run: |
|
uses: docker/setup-qemu-action@v3
|
||||||
echo "/home/github-runner/.cargo/bin" >> $GITHUB_PATH
|
|
||||||
echo "RUSTUP_HOME=/home/github-runner/.rustup" >> $GITHUB_ENV
|
|
||||||
echo "CARGO_HOME=/home/github-runner/.cargo" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Build release binary (musl static)
|
|
||||||
run: |
|
|
||||||
cargo build --release --target x86_64-unknown-linux-musl --package nora-registry
|
|
||||||
cp target/x86_64-unknown-linux-musl/release/nora ./nora
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -40,191 +32,49 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# ── Alpine (standard) ────────────────────────────────────────────────────
|
- name: Extract metadata
|
||||||
- name: Extract metadata (alpine)
|
id: meta
|
||||||
id: meta-alpine
|
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
|
|
||||||
- name: Build and push (alpine)
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile
|
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta-alpine.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta-alpine.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha,scope=alpine
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max,scope=alpine
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
# ── RED OS ───────────────────────────────────────────────────────────────
|
|
||||||
- name: Extract metadata (redos)
|
|
||||||
id: meta-redos
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
||||||
flavor: suffix=-redos,onlatest=true
|
|
||||||
tags: |
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=raw,value=redos
|
|
||||||
|
|
||||||
- name: Build and push (redos)
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: Dockerfile.redos
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta-redos.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta-redos.outputs.labels }}
|
|
||||||
cache-from: type=gha,scope=redos
|
|
||||||
cache-to: type=gha,mode=max,scope=redos
|
|
||||||
|
|
||||||
scan:
|
|
||||||
name: Scan (${{ matrix.name }})
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- name: alpine
|
|
||||||
suffix: ""
|
|
||||||
- name: redos
|
|
||||||
suffix: "-redos"
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Log in to Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set version tag (strip leading v)
|
|
||||||
id: ver
|
|
||||||
run: echo "tag=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# ── CVE scan of the pushed image ────────────────────────────────────────
|
|
||||||
# Images are FROM scratch — no OS packages, only binary CVE scan
|
|
||||||
- name: Trivy — image scan (${{ matrix.name }})
|
|
||||||
uses: aquasecurity/trivy-action@0.30.0
|
|
||||||
with:
|
|
||||||
scan-type: image
|
|
||||||
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.ver.outputs.tag }}${{ matrix.suffix }}
|
|
||||||
format: sarif
|
|
||||||
output: trivy-image-${{ matrix.name }}.sarif
|
|
||||||
severity: HIGH,CRITICAL
|
|
||||||
exit-code: 1 # block release on HIGH/CRITICAL vulnerabilities
|
|
||||||
|
|
||||||
- name: Upload Trivy image results to GitHub Security tab
|
|
||||||
uses: github/codeql-action/upload-sarif@v4
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
sarif_file: trivy-image-${{ matrix.name }}.sarif
|
|
||||||
category: trivy-image-${{ matrix.name }}
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: GitHub Release
|
name: GitHub Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build, scan]
|
needs: build
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: read # to pull image for SBOM generation
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Log in to Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set version tag (strip leading v)
|
|
||||||
id: ver
|
|
||||||
run: echo "tag=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# ── Binary — extract from Docker image ──────────────────────────────────
|
|
||||||
- name: Extract binary from image
|
|
||||||
run: |
|
|
||||||
docker create --name nora-extract \
|
|
||||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.ver.outputs.tag }}
|
|
||||||
docker cp nora-extract:/usr/local/bin/nora ./nora-linux-amd64
|
|
||||||
docker rm nora-extract
|
|
||||||
chmod +x ./nora-linux-amd64
|
|
||||||
sha256sum ./nora-linux-amd64 > nora-linux-amd64.sha256
|
|
||||||
echo "Binary size: $(du -sh nora-linux-amd64 | cut -f1)"
|
|
||||||
cat nora-linux-amd64.sha256
|
|
||||||
|
|
||||||
# ── SBOM — Software Bill of Materials ───────────────────────────────────
|
|
||||||
- name: Generate SBOM (SPDX)
|
|
||||||
uses: anchore/sbom-action@v0
|
|
||||||
with:
|
|
||||||
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.ver.outputs.tag }}
|
|
||||||
format: spdx-json
|
|
||||||
output-file: nora-${{ github.ref_name }}.sbom.spdx.json
|
|
||||||
registry-username: ${{ github.actor }}
|
|
||||||
registry-password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Generate SBOM (CycloneDX)
|
|
||||||
uses: anchore/sbom-action@v0
|
|
||||||
with:
|
|
||||||
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.ver.outputs.tag }}
|
|
||||||
format: cyclonedx-json
|
|
||||||
output-file: nora-${{ github.ref_name }}.sbom.cdx.json
|
|
||||||
registry-username: ${{ github.actor }}
|
|
||||||
registry-password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
files: |
|
|
||||||
nora-linux-amd64
|
|
||||||
nora-linux-amd64.sha256
|
|
||||||
nora-${{ github.ref_name }}.sbom.spdx.json
|
|
||||||
nora-${{ github.ref_name }}.sbom.cdx.json
|
|
||||||
body: |
|
body: |
|
||||||
## Install
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://getnora.io/install.sh | sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Or download the binary directly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -LO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/nora-linux-amd64
|
|
||||||
chmod +x nora-linux-amd64
|
|
||||||
sudo mv nora-linux-amd64 /usr/local/bin/nora
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
**Alpine (standard):**
|
|
||||||
```bash
|
```bash
|
||||||
docker pull ghcr.io/${{ github.repository }}:${{ github.ref_name }}
|
docker pull ghcr.io/${{ github.repository }}:${{ github.ref_name }}
|
||||||
```
|
```
|
||||||
|
|
||||||
**RED OS:**
|
|
||||||
```bash
|
|
||||||
docker pull ghcr.io/${{ github.repository }}:${{ github.ref_name }}-redos
|
|
||||||
```
|
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md)
|
See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md)
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,7 +13,3 @@ ROADMAP*.md
|
|||||||
docs-site/
|
docs-site/
|
||||||
docs/
|
docs/
|
||||||
*.txt
|
*.txt
|
||||||
|
|
||||||
## Internal files
|
|
||||||
.internal/
|
|
||||||
examples/
|
|
||||||
|
|||||||
93
Cargo.lock
generated
93
Cargo.lock
generated
@@ -234,9 +234,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.11.1"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
@@ -262,9 +262,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.44"
|
version = "0.4.43"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -495,9 +495,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.9"
|
version = "1.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
@@ -1201,7 +1201,7 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nora-cli"
|
name = "nora-cli"
|
||||||
version = "0.2.22"
|
version = "0.2.18"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"flate2",
|
"flate2",
|
||||||
@@ -1215,7 +1215,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nora-registry"
|
name = "nora-registry"
|
||||||
version = "0.2.22"
|
version = "0.2.18"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -1253,7 +1253,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nora-storage"
|
name = "nora-storage"
|
||||||
version = "0.2.22"
|
version = "0.2.18"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -1412,9 +1412,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prometheus"
|
name = "prometheus"
|
||||||
version = "0.14.0"
|
version = "0.13.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a"
|
checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"fnv",
|
"fnv",
|
||||||
@@ -1422,28 +1422,14 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
"thiserror 2.0.18",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protobuf"
|
name = "protobuf"
|
||||||
version = "3.7.2"
|
version = "2.28.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4"
|
checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94"
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
"protobuf-support",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "protobuf-support"
|
|
||||||
version = "3.7.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quanta"
|
name = "quanta"
|
||||||
@@ -1462,9 +1448,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.39.2"
|
version = "0.31.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
|
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1850,11 +1836,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.0.4"
|
version = "0.6.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
|
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2153,42 +2139,44 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.0.3+spec-1.1.0"
|
version = "0.8.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c"
|
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"serde",
|
||||||
"serde_core",
|
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"toml_parser",
|
"toml_edit",
|
||||||
"toml_writer",
|
|
||||||
"winnow",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.0.0+spec-1.1.0"
|
version = "0.6.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_edit"
|
||||||
version = "1.0.9+spec-1.1.0"
|
version = "0.22.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_write",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_writer"
|
name = "toml_write"
|
||||||
version = "1.0.6+spec-1.1.0"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tonic"
|
name = "tonic"
|
||||||
@@ -2868,6 +2856,9 @@ name = "winnow"
|
|||||||
version = "0.7.14"
|
version = "0.7.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wiremock"
|
name = "wiremock"
|
||||||
@@ -3047,9 +3038,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zlib-rs"
|
name = "zlib-rs"
|
||||||
version = "0.6.2"
|
version = "0.5.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c745c48e1007337ed136dc99df34128b9faa6ed542d80a1c673cf55a6d7236c8"
|
checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.23"
|
version = "0.2.19"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["DevITWay <devitway@gmail.com>"]
|
authors = ["DevITWay <devitway@gmail.com>"]
|
||||||
|
|||||||
55
Dockerfile
55
Dockerfile
@@ -1,11 +1,58 @@
|
|||||||
# syntax=docker/dockerfile:1.4
|
# syntax=docker/dockerfile:1.4
|
||||||
# Binary is pre-built by CI (cargo build --release) and passed via context
|
|
||||||
|
# Build stage
|
||||||
|
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
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates && mkdir -p /data
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
COPY nora /usr/local/bin/nora
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy binary
|
||||||
|
COPY --from=builder /usr/local/bin/nora /usr/local/bin/nora
|
||||||
|
|
||||||
|
# Create data directory
|
||||||
|
RUN mkdir -p /data
|
||||||
|
|
||||||
|
# Default environment
|
||||||
ENV RUST_LOG=info
|
ENV RUST_LOG=info
|
||||||
ENV NORA_HOST=0.0.0.0
|
ENV NORA_HOST=0.0.0.0
|
||||||
ENV NORA_PORT=4000
|
ENV NORA_PORT=4000
|
||||||
@@ -17,5 +64,5 @@ EXPOSE 4000
|
|||||||
|
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/nora"]
|
ENTRYPOINT ["nora"]
|
||||||
CMD ["serve"]
|
CMD ["serve"]
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
# syntax=docker/dockerfile:1.4
|
|
||||||
# Binary is pre-built by CI (cargo build --release) and passed via context
|
|
||||||
# Runtime: scratch — compatible with Astra Linux SE (FSTEC certified)
|
|
||||||
# To switch to official base: replace FROM scratch with
|
|
||||||
# FROM registry.astralinux.ru/library/alse:latest
|
|
||||||
# RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
FROM alpine:3.20 AS certs
|
|
||||||
RUN apk add --no-cache ca-certificates
|
|
||||||
|
|
||||||
FROM scratch
|
|
||||||
|
|
||||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
|
||||||
COPY 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"]
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# syntax=docker/dockerfile:1.4
|
|
||||||
# Binary is pre-built by CI (cargo build --release) and passed via context
|
|
||||||
# Runtime: scratch — compatible with RED OS (FSTEC certified)
|
|
||||||
# To switch to official base: replace FROM scratch with
|
|
||||||
# FROM registry.red-soft.ru/redos/redos:8
|
|
||||||
# RUN dnf install -y ca-certificates && dnf clean all
|
|
||||||
|
|
||||||
FROM alpine:3.20 AS certs
|
|
||||||
RUN apk add --no-cache ca-certificates
|
|
||||||
|
|
||||||
FROM scratch
|
|
||||||
|
|
||||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
|
||||||
COPY 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"]
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<img src="logo.jpg" alt="NORA" height="120" />
|
# 🐿️ N○RA
|
||||||
|
|
||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/getnora-io/nora/releases)
|
[](https://github.com/getnora-io/nora/releases)
|
||||||
|
|||||||
40
deny.toml
40
deny.toml
@@ -1,40 +0,0 @@
|
|||||||
# cargo-deny configuration
|
|
||||||
# https://embarkstudios.github.io/cargo-deny/
|
|
||||||
|
|
||||||
[advisories]
|
|
||||||
# Vulnerability database (RustSec)
|
|
||||||
db-urls = ["https://github.com/rustsec/advisory-db"]
|
|
||||||
ignore = [
|
|
||||||
"RUSTSEC-2025-0119", # number_prefix unmaintained, transitive via indicatif; no fix available
|
|
||||||
]
|
|
||||||
|
|
||||||
[licenses]
|
|
||||||
# Allowed open-source licenses
|
|
||||||
allow = [
|
|
||||||
"MIT",
|
|
||||||
"Apache-2.0",
|
|
||||||
"Apache-2.0 WITH LLVM-exception",
|
|
||||||
"BSD-2-Clause",
|
|
||||||
"BSD-3-Clause",
|
|
||||||
"ISC",
|
|
||||||
"Unicode-DFS-2016",
|
|
||||||
"Unicode-3.0",
|
|
||||||
"CC0-1.0",
|
|
||||||
"OpenSSL",
|
|
||||||
"Zlib",
|
|
||||||
"CDLA-Permissive-2.0", # webpki-roots (CA certificates bundle)
|
|
||||||
"MPL-2.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[bans]
|
|
||||||
multiple-versions = "warn"
|
|
||||||
deny = [
|
|
||||||
{ name = "openssl-sys" },
|
|
||||||
{ name = "openssl" },
|
|
||||||
]
|
|
||||||
skip = []
|
|
||||||
|
|
||||||
[sources]
|
|
||||||
unknown-registry = "warn"
|
|
||||||
unknown-git = "warn"
|
|
||||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
|
||||||
5
logo.svg
5
logo.svg
@@ -1,5 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 72" width="300" height="72">
|
|
||||||
<text font-family="'SF Mono', 'Fira Code', 'Cascadia Code', monospace" font-weight="800" fill="#0f172a" letter-spacing="1">
|
|
||||||
<tspan x="8" y="58" font-size="52">N</tspan><tspan font-size="68" dy="-10" fill="#2563EB">O</tspan><tspan font-size="52" dy="10">RA</tspan>
|
|
||||||
</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 373 B |
@@ -20,4 +20,4 @@ serde_json.workspace = true
|
|||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
indicatif = "0.17"
|
indicatif = "0.17"
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
flate2 = "1.1"
|
flate2 = "1.0"
|
||||||
|
|||||||
@@ -26,18 +26,18 @@ sha2.workspace = true
|
|||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
hmac.workspace = true
|
hmac.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
toml = "1.0"
|
toml = "0.8"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
bcrypt = "0.17"
|
bcrypt = "0.17"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
prometheus = "0.14"
|
prometheus = "0.13"
|
||||||
lazy_static = "1.5"
|
lazy_static = "1.5"
|
||||||
httpdate = "1"
|
httpdate = "1"
|
||||||
utoipa = { version = "5", features = ["axum_extras"] }
|
utoipa = { version = "5", features = ["axum_extras"] }
|
||||||
utoipa-swagger-ui = { version = "9", features = ["axum", "reqwest"] }
|
utoipa-swagger-ui = { version = "9", features = ["axum", "reqwest"] }
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
flate2 = "1.1"
|
flate2 = "1.0"
|
||||||
indicatif = "0.17"
|
indicatif = "0.17"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
|
|||||||
@@ -63,17 +63,11 @@ impl HtpasswdAuth {
|
|||||||
fn is_public_path(path: &str) -> bool {
|
fn is_public_path(path: &str) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
path,
|
path,
|
||||||
"/" | "/health"
|
"/" | "/health" | "/ready" | "/metrics" | "/v2/" | "/v2"
|
||||||
| "/ready"
|
|
||||||
| "/metrics"
|
|
||||||
| "/v2/"
|
|
||||||
| "/v2"
|
|
||||||
| "/api/tokens"
|
|
||||||
| "/api/tokens/list"
|
|
||||||
| "/api/tokens/revoke"
|
|
||||||
) || path.starts_with("/ui")
|
) || path.starts_with("/ui")
|
||||||
|| path.starts_with("/api-docs")
|
|| path.starts_with("/api-docs")
|
||||||
|| path.starts_with("/api/ui")
|
|| path.starts_with("/api/ui")
|
||||||
|
|| path.starts_with("/api/tokens")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Auth middleware - supports Basic auth and Bearer tokens
|
/// Auth middleware - supports Basic auth and Bearer tokens
|
||||||
@@ -410,12 +404,8 @@ mod tests {
|
|||||||
assert!(is_public_path("/api/ui/stats"));
|
assert!(is_public_path("/api/ui/stats"));
|
||||||
assert!(is_public_path("/api/tokens"));
|
assert!(is_public_path("/api/tokens"));
|
||||||
assert!(is_public_path("/api/tokens/list"));
|
assert!(is_public_path("/api/tokens/list"));
|
||||||
assert!(is_public_path("/api/tokens/revoke"));
|
|
||||||
|
|
||||||
// Protected paths
|
// 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/myimage/blobs/sha256:abc"));
|
||||||
assert!(!is_public_path("/v2/library/nginx/manifests/latest"));
|
assert!(!is_public_path("/v2/library/nginx/manifests/latest"));
|
||||||
assert!(!is_public_path(
|
assert!(!is_public_path(
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ pub struct AppState {
|
|||||||
pub activity: ActivityLog,
|
pub activity: ActivityLog,
|
||||||
pub docker_auth: registry::DockerAuth,
|
pub docker_auth: registry::DockerAuth,
|
||||||
pub repo_index: RepoIndex,
|
pub repo_index: RepoIndex,
|
||||||
pub http_client: reqwest::Client,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -272,8 +271,6 @@ async fn run_server(config: Config, storage: Storage) {
|
|||||||
// Initialize Docker auth with proxy timeout
|
// Initialize Docker auth with proxy timeout
|
||||||
let docker_auth = registry::DockerAuth::new(config.docker.proxy_timeout);
|
let docker_auth = registry::DockerAuth::new(config.docker.proxy_timeout);
|
||||||
|
|
||||||
let http_client = reqwest::Client::new();
|
|
||||||
|
|
||||||
let state = Arc::new(AppState {
|
let state = Arc::new(AppState {
|
||||||
storage,
|
storage,
|
||||||
config,
|
config,
|
||||||
@@ -284,7 +281,6 @@ async fn run_server(config: Config, storage: Storage) {
|
|||||||
activity: ActivityLog::new(50),
|
activity: ActivityLog::new(50),
|
||||||
docker_auth,
|
docker_auth,
|
||||||
repo_index: RepoIndex::new(),
|
repo_index: RepoIndex::new(),
|
||||||
http_client,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Token routes with strict rate limiting (brute-force protection)
|
// Token routes with strict rate limiting (brute-force protection)
|
||||||
|
|||||||
@@ -167,7 +167,6 @@ async fn download_blob(
|
|||||||
// Try upstream proxies
|
// Try upstream proxies
|
||||||
for upstream in &state.config.docker.upstreams {
|
for upstream in &state.config.docker.upstreams {
|
||||||
if let Ok(data) = fetch_blob_from_upstream(
|
if let Ok(data) = fetch_blob_from_upstream(
|
||||||
&state.http_client,
|
|
||||||
&upstream.url,
|
&upstream.url,
|
||||||
&name,
|
&name,
|
||||||
&digest,
|
&digest,
|
||||||
@@ -368,7 +367,6 @@ async fn get_manifest(
|
|||||||
for upstream in &state.config.docker.upstreams {
|
for upstream in &state.config.docker.upstreams {
|
||||||
tracing::debug!(upstream_url = %upstream.url, "Trying upstream");
|
tracing::debug!(upstream_url = %upstream.url, "Trying upstream");
|
||||||
if let Ok((data, content_type)) = fetch_manifest_from_upstream(
|
if let Ok((data, content_type)) = fetch_manifest_from_upstream(
|
||||||
&state.http_client,
|
|
||||||
&upstream.url,
|
&upstream.url,
|
||||||
&name,
|
&name,
|
||||||
&reference,
|
&reference,
|
||||||
@@ -583,7 +581,6 @@ async fn list_tags_ns(
|
|||||||
|
|
||||||
/// Fetch a blob from an upstream Docker registry
|
/// Fetch a blob from an upstream Docker registry
|
||||||
async fn fetch_blob_from_upstream(
|
async fn fetch_blob_from_upstream(
|
||||||
client: &reqwest::Client,
|
|
||||||
upstream_url: &str,
|
upstream_url: &str,
|
||||||
name: &str,
|
name: &str,
|
||||||
digest: &str,
|
digest: &str,
|
||||||
@@ -597,14 +594,14 @@ async fn fetch_blob_from_upstream(
|
|||||||
digest
|
digest
|
||||||
);
|
);
|
||||||
|
|
||||||
// First try without auth
|
let client = reqwest::Client::builder()
|
||||||
let response = client
|
|
||||||
.get(&url)
|
|
||||||
.timeout(Duration::from_secs(timeout))
|
.timeout(Duration::from_secs(timeout))
|
||||||
.send()
|
.build()
|
||||||
.await
|
|
||||||
.map_err(|_| ())?;
|
.map_err(|_| ())?;
|
||||||
|
|
||||||
|
// First try without auth
|
||||||
|
let response = client.get(&url).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
|
||||||
let www_auth = response
|
let www_auth = response
|
||||||
@@ -640,7 +637,6 @@ async fn fetch_blob_from_upstream(
|
|||||||
/// Fetch a manifest from an upstream Docker registry
|
/// Fetch a manifest from an upstream Docker registry
|
||||||
/// Returns (manifest_bytes, content_type)
|
/// Returns (manifest_bytes, content_type)
|
||||||
async fn fetch_manifest_from_upstream(
|
async fn fetch_manifest_from_upstream(
|
||||||
client: &reqwest::Client,
|
|
||||||
upstream_url: &str,
|
upstream_url: &str,
|
||||||
name: &str,
|
name: &str,
|
||||||
reference: &str,
|
reference: &str,
|
||||||
@@ -656,6 +652,13 @@ async fn fetch_manifest_from_upstream(
|
|||||||
|
|
||||||
tracing::debug!(url = %url, "Fetching 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
|
// Request with Accept header for manifest types
|
||||||
let accept_header = "application/vnd.docker.distribution.manifest.v2+json, \
|
let accept_header = "application/vnd.docker.distribution.manifest.v2+json, \
|
||||||
application/vnd.docker.distribution.manifest.list.v2+json, \
|
application/vnd.docker.distribution.manifest.list.v2+json, \
|
||||||
@@ -665,7 +668,6 @@ async fn fetch_manifest_from_upstream(
|
|||||||
// First try without auth
|
// First try without auth
|
||||||
let response = client
|
let response = client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.timeout(Duration::from_secs(timeout))
|
|
||||||
.header("Accept", accept_header)
|
.header("Accept", accept_header)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pub fn routes() -> Router<Arc<AppState>> {
|
|||||||
async fn download(State(state): State<Arc<AppState>>, Path(path): Path<String>) -> Response {
|
async fn download(State(state): State<Arc<AppState>>, Path(path): Path<String>) -> Response {
|
||||||
let key = format!("maven/{}", path);
|
let key = format!("maven/{}", path);
|
||||||
|
|
||||||
|
// Extract artifact name for logging (last 2-3 path components)
|
||||||
let artifact_name = path
|
let artifact_name = path
|
||||||
.split('/')
|
.split('/')
|
||||||
.rev()
|
.rev()
|
||||||
@@ -33,6 +34,7 @@ async fn download(State(state): State<Arc<AppState>>, Path(path): Path<String>)
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("/");
|
.join("/");
|
||||||
|
|
||||||
|
// Try local storage first
|
||||||
if let Ok(data) = state.storage.get(&key).await {
|
if let Ok(data) = state.storage.get(&key).await {
|
||||||
state.metrics.record_download("maven");
|
state.metrics.record_download("maven");
|
||||||
state.metrics.record_cache_hit();
|
state.metrics.record_cache_hit();
|
||||||
@@ -45,10 +47,11 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try proxy servers
|
||||||
for proxy_url in &state.config.maven.proxies {
|
for proxy_url 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(&url, state.config.maven.proxy_timeout).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();
|
||||||
@@ -59,6 +62,7 @@ async fn download(State(state): State<Arc<AppState>>, Path(path): Path<String>)
|
|||||||
"PROXY",
|
"PROXY",
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Cache in local storage (fire and forget)
|
||||||
let storage = state.storage.clone();
|
let storage = state.storage.clone();
|
||||||
let key_clone = key.clone();
|
let key_clone = key.clone();
|
||||||
let data_clone = data.clone();
|
let data_clone = data.clone();
|
||||||
@@ -84,6 +88,7 @@ async fn upload(
|
|||||||
) -> StatusCode {
|
) -> StatusCode {
|
||||||
let key = format!("maven/{}", path);
|
let key = format!("maven/{}", path);
|
||||||
|
|
||||||
|
// Extract artifact name for logging
|
||||||
let artifact_name = path
|
let artifact_name = path
|
||||||
.split('/')
|
.split('/')
|
||||||
.rev()
|
.rev()
|
||||||
@@ -110,18 +115,14 @@ async fn upload(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_from_proxy(
|
async fn fetch_from_proxy(url: &str, timeout_secs: u64) -> Result<Vec<u8>, ()> {
|
||||||
client: &reqwest::Client,
|
let client = reqwest::Client::builder()
|
||||||
url: &str,
|
|
||||||
timeout_secs: u64,
|
|
||||||
) -> Result<Vec<u8>, ()> {
|
|
||||||
let response = client
|
|
||||||
.get(url)
|
|
||||||
.timeout(Duration::from_secs(timeout_secs))
|
.timeout(Duration::from_secs(timeout_secs))
|
||||||
.send()
|
.build()
|
||||||
.await
|
|
||||||
.map_err(|_| ())?;
|
.map_err(|_| ())?;
|
||||||
|
|
||||||
|
let response = client.get(url).send().await.map_err(|_| ())?;
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub fn routes() -> Router<Arc<AppState>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<String>) -> Response {
|
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 is_tarball = path.contains("/-/");
|
||||||
|
|
||||||
let key = if is_tarball {
|
let key = if is_tarball {
|
||||||
@@ -32,12 +33,14 @@ async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<Str
|
|||||||
format!("npm/{}/metadata.json", path)
|
format!("npm/{}/metadata.json", path)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Extract package name for logging
|
||||||
let package_name = if is_tarball {
|
let package_name = if is_tarball {
|
||||||
path.split("/-/").next().unwrap_or(&path).to_string()
|
path.split("/-/").next().unwrap_or(&path).to_string()
|
||||||
} else {
|
} else {
|
||||||
path.clone()
|
path.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Try local storage first
|
||||||
if let Ok(data) = state.storage.get(&key).await {
|
if let Ok(data) = state.storage.get(&key).await {
|
||||||
if is_tarball {
|
if is_tarball {
|
||||||
state.metrics.record_download("npm");
|
state.metrics.record_download("npm");
|
||||||
@@ -52,12 +55,17 @@ async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<Str
|
|||||||
return with_content_type(is_tarball, data).into_response();
|
return with_content_type(is_tarball, data).into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try proxy if configured
|
||||||
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 = 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)
|
||||||
|
};
|
||||||
|
|
||||||
if let Ok(data) =
|
if let Ok(data) = fetch_from_proxy(&url, state.config.npm.proxy_timeout).await {
|
||||||
fetch_from_proxy(&state.http_client, &url, state.config.npm.proxy_timeout).await
|
|
||||||
{
|
|
||||||
if is_tarball {
|
if is_tarball {
|
||||||
state.metrics.record_download("npm");
|
state.metrics.record_download("npm");
|
||||||
state.metrics.record_cache_miss();
|
state.metrics.record_cache_miss();
|
||||||
@@ -69,6 +77,7 @@ 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 storage = state.storage.clone();
|
||||||
let key_clone = key.clone();
|
let key_clone = key.clone();
|
||||||
let data_clone = data.clone();
|
let data_clone = data.clone();
|
||||||
@@ -76,6 +85,7 @@ async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<Str
|
|||||||
let _ = storage.put(&key_clone, &data_clone).await;
|
let _ = storage.put(&key_clone, &data_clone).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Invalidate index when caching new tarball
|
||||||
if is_tarball {
|
if is_tarball {
|
||||||
state.repo_index.invalidate("npm");
|
state.repo_index.invalidate("npm");
|
||||||
}
|
}
|
||||||
@@ -87,18 +97,14 @@ async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<Str
|
|||||||
StatusCode::NOT_FOUND.into_response()
|
StatusCode::NOT_FOUND.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_from_proxy(
|
async fn fetch_from_proxy(url: &str, timeout_secs: u64) -> Result<Vec<u8>, ()> {
|
||||||
client: &reqwest::Client,
|
let client = reqwest::Client::builder()
|
||||||
url: &str,
|
|
||||||
timeout_secs: u64,
|
|
||||||
) -> Result<Vec<u8>, ()> {
|
|
||||||
let response = client
|
|
||||||
.get(url)
|
|
||||||
.timeout(Duration::from_secs(timeout_secs))
|
.timeout(Duration::from_secs(timeout_secs))
|
||||||
.send()
|
.build()
|
||||||
.await
|
|
||||||
.map_err(|_| ())?;
|
.map_err(|_| ())?;
|
||||||
|
|
||||||
|
let response = client.get(url).send().await.map_err(|_| ())?;
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,9 +85,7 @@ 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(&url, state.config.pypi.proxy_timeout).await {
|
||||||
fetch_package_page(&state.http_client, &url, state.config.pypi.proxy_timeout).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);
|
||||||
return (StatusCode::OK, Html(rewritten)).into_response();
|
return (StatusCode::OK, Html(rewritten)).into_response();
|
||||||
@@ -132,22 +130,10 @@ async fn download_file(
|
|||||||
// First, fetch the package page to find the actual download URL
|
// First, fetch the package page to find the actual download URL
|
||||||
let page_url = format!("{}/{}/", proxy_url.trim_end_matches('/'), normalized);
|
let page_url = format!("{}/{}/", proxy_url.trim_end_matches('/'), normalized);
|
||||||
|
|
||||||
if let Ok(html) = fetch_package_page(
|
if let Ok(html) = fetch_package_page(&page_url, state.config.pypi.proxy_timeout).await {
|
||||||
&state.http_client,
|
|
||||||
&page_url,
|
|
||||||
state.config.pypi.proxy_timeout,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
// Find the URL for this specific file
|
// Find the URL for this specific file
|
||||||
if let Some(file_url) = find_file_url(&html, &filename) {
|
if let Some(file_url) = find_file_url(&html, &filename) {
|
||||||
if let Ok(data) = fetch_file(
|
if let Ok(data) = fetch_file(&file_url, state.config.pypi.proxy_timeout).await {
|
||||||
&state.http_client,
|
|
||||||
&file_url,
|
|
||||||
state.config.pypi.proxy_timeout,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
state.metrics.record_download("pypi");
|
state.metrics.record_download("pypi");
|
||||||
state.metrics.record_cache_miss();
|
state.metrics.record_cache_miss();
|
||||||
state.activity.push(ActivityEntry::new(
|
state.activity.push(ActivityEntry::new(
|
||||||
@@ -191,14 +177,14 @@ fn normalize_name(name: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch package page from upstream
|
/// Fetch package page from upstream
|
||||||
async fn fetch_package_page(
|
async fn fetch_package_page(url: &str, timeout_secs: u64) -> Result<String, ()> {
|
||||||
client: &reqwest::Client,
|
let client = reqwest::Client::builder()
|
||||||
url: &str,
|
.timeout(Duration::from_secs(timeout_secs))
|
||||||
timeout_secs: u64,
|
.build()
|
||||||
) -> Result<String, ()> {
|
.map_err(|_| ())?;
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
.get(url)
|
.get(url)
|
||||||
.timeout(Duration::from_secs(timeout_secs))
|
|
||||||
.header("Accept", "text/html")
|
.header("Accept", "text/html")
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
@@ -212,14 +198,14 @@ 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(url: &str, timeout_secs: u64) -> Result<Vec<u8>, ()> {
|
||||||
let response = client
|
let client = reqwest::Client::builder()
|
||||||
.get(url)
|
|
||||||
.timeout(Duration::from_secs(timeout_secs))
|
.timeout(Duration::from_secs(timeout_secs))
|
||||||
.send()
|
.build()
|
||||||
.await
|
|
||||||
.map_err(|_| ())?;
|
.map_err(|_| ())?;
|
||||||
|
|
||||||
|
let response = client.get(url).send().await.map_err(|_| ())?;
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ serde.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
toml = "1.0"
|
toml = "0.8"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
httpdate = "1"
|
httpdate = "1"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
quick-xml = { version = "0.39", features = ["serialize"] }
|
quick-xml = { version = "0.31", features = ["serialize"] }
|
||||||
|
|||||||
Reference in New Issue
Block a user