diff --git a/.github/skills/dev/maintenance/run-manual-docker-security-scan/SKILL.md b/.github/skills/dev/maintenance/run-manual-docker-security-scan/SKILL.md new file mode 100644 index 000000000..04cf99bb0 --- /dev/null +++ b/.github/skills/dev/maintenance/run-manual-docker-security-scan/SKILL.md @@ -0,0 +1,107 @@ +--- +name: run-manual-docker-security-scan +description: Guide for running a manual Docker security scan for the tracker runtime image and documenting results. Covers build, Trivy scan, CVE triage, per-CVE catalog updates, and scan history updates. Use when asked to run a manual container scan, triage Docker CVEs, or refresh security scan docs. +metadata: + author: torrust + version: "1.0" + semantic-links: + related-artifacts: + - Containerfile + - docs/security/README.md + - docs/security/docker/README.md + - docs/security/docker/scans/README.md + - docs/security/docker/scans/torrust-tracker.md + - docs/security/analysis/README.md + - docs/security/analysis/non-affecting/ +--- + +# Run Manual Docker Security Scan + +Use this workflow to run and document manual security scans for the tracker production container. + +## Scope + +- Target image: tracker runtime image built from root `Containerfile`. +- Main severity gate: `HIGH,CRITICAL`. +- Documentation outputs: + - `docs/security/docker/scans/torrust-tracker.md` + - `docs/security/docker/scans/README.md` + - `docs/security/analysis/non-affecting/CVE-*.md` (when non-affecting CVEs are analyzed) + +## Quick Commands + +```bash +# 1) Build runtime image +docker build -t torrust-tracker:local -f Containerfile . + +# 2) Gate scan (primary) +trivy image --severity HIGH,CRITICAL torrust-tracker:local + +# 3) Full context scan (optional but recommended) +trivy image --severity MEDIUM,HIGH,CRITICAL torrust-tracker:local +``` + +## Workflow + +### Step 1: Check Existing Catalog First + +Before analyzing any CVE, search the existing catalog: + +```bash +grep -R "CVE-" docs/security/analysis/non-affecting/ +``` + +If already present and `requires-recheck-when` conditions have not changed, reuse the existing verdict. + +### Step 2: Build and Scan + +- Build local runtime image from `Containerfile`. +- Run the gate scan with `HIGH,CRITICAL`. +- Run optional full scan (`MEDIUM,HIGH,CRITICAL`) to capture trend context. + +### Step 3: Update Scan History Docs + +Update: + +- `docs/security/docker/scans/torrust-tracker.md` with: + - date/time, Trivy version, totals by severity + - notable CVEs and rationale +- `docs/security/docker/scans/README.md` summary table with latest status and date. + +### Step 4: Document New Non-Affecting CVEs + +For any new non-affecting CVE, create `docs/security/analysis/non-affecting/CVE-.md` with: + +- frontmatter fields: + - `cve-id` + - `date-analyzed` + - `source` + - `status: non-affecting` + - `review-cadence` + - `requires-recheck-when` +- evidence-based explanation tied to tracker architecture +- conditions that would invalidate the current verdict + +### Step 5: Escalate Affecting CVEs + +If a CVE is affecting: + +- create/update a tracking issue +- include impact, affected component, exploitability context, and remediation plan +- update scan docs with current status and owner + +## Recheck Triggers + +Re-evaluate catalog verdicts when any of these happen: + +- `Containerfile` base image changes +- new runtime/system dependency is introduced +- code path changes that satisfy a CVE file's `requires-recheck-when` condition + +## Completion Checklist + +- [ ] `trivy` gate scan executed (`HIGH,CRITICAL`) +- [ ] scan history files updated +- [ ] new CVEs cataloged or linked to existing catalog entries +- [ ] affecting CVEs escalated +- [ ] `linter all` passes diff --git a/.github/skills/dev/pr-reviews/process-copilot-suggestions/SKILL.md b/.github/skills/dev/pr-reviews/process-copilot-suggestions/SKILL.md index 4f3ba5afc..1983e2839 100644 --- a/.github/skills/dev/pr-reviews/process-copilot-suggestions/SKILL.md +++ b/.github/skills/dev/pr-reviews/process-copilot-suggestions/SKILL.md @@ -174,7 +174,7 @@ Both are integrated into this workflow automatically. ## Example -See `docs/pr-reviews/pr-1733-copilot-suggestions.md` for a complete worked example +See `docs/pr-reviews/EXAMPLE-COMPLETED.md` for a complete worked example with all 26 Copilot suggestions processed, decided, and resolved. ## Completion Checklist diff --git a/.github/skills/usage/use-rest-api/SKILL.md b/.github/skills/usage/use-rest-api/SKILL.md new file mode 100644 index 000000000..31170b412 --- /dev/null +++ b/.github/skills/usage/use-rest-api/SKILL.md @@ -0,0 +1,155 @@ +--- +name: use-rest-api +description: Use the Torrust Tracker REST API. Covers authentication, all endpoints (stats, metrics, torrents, auth keys, whitelist), and making announce/scrape requests to verify API behaviour. Triggers on "use API", "test API", "call REST API", "query API", "API endpoint", "curl tracker", "tracker client", "announce request", or "verify API". +metadata: + author: torrust + version: "1.0" +--- + +# Use REST API + +## Prerequisites + +A running tracker with the REST API enabled. The default development config starts the API on port 1212: + +```bash +cargo run +``` + +## Skill Links + +This skill depends on these artifacts. If any of them change, review this skill. + +- `share/default/config/tracker.development.sqlite3.toml` +- `packages/axum-rest-api-server/src/v1/middlewares/auth.rs` +- `packages/axum-rest-api-server/src/routes.rs` +- `packages/axum-rest-api-server/src/v1/routes.rs` + +Use the marker `skill-link: use-rest-api` in affected artifacts. + +## Authentication + +All API endpoints (except `/api/health_check`) require an access token. + +### Header Method (preferred) + +```bash +curl -H "Authorization: Bearer MyAccessToken" http://localhost:1212/api/v1/stats +``` + +### Query Parameter Method + +```bash +curl "http://localhost:1212/api/v1/stats?token=MyAccessToken" +``` + +### Configuration + +Tokens are defined in the TOML config file under `[http_api.access_tokens]`: + +```toml +[http_api.access_tokens] +admin = "MyAccessToken" +``` + +Every token in the map has identical permissions — the label (`admin`) is just a human-readable name. + +## Endpoints + +All endpoints use `http://localhost:1212` as base (default dev config). + +### Health Check + +| Method | Endpoint | Auth | +| ------ | ------------------- | ----- | +| GET | `/api/health_check` | ❌ No | + +```bash +curl -s http://localhost:1212/api/health_check +``` + +### Stats + +| Method | Endpoint | Auth | +| ------ | ----------------- | ------ | +| GET | `/api/v1/stats` | ✅ Yes | +| GET | `/api/v1/metrics` | ✅ Yes | + +```bash +curl -s http://localhost:1212/api/v1/stats -H "Authorization: Bearer MyAccessToken" +curl -s http://localhost:1212/api/v1/metrics -H "Authorization: Bearer MyAccessToken" +``` + +### Auth Keys + +| Method | Endpoint | Auth | +| ------ | ------------------------------------ | ------ | +| POST | `/api/v1/key/{seconds_valid_or_key}` | ✅ Yes | +| DELETE | `/api/v1/key/{seconds_valid_or_key}` | ✅ Yes | +| GET | `/api/v1/keys/reload` | ✅ Yes | +| POST | `/api/v1/keys` | ✅ Yes | + +### Whitelist + +| Method | Endpoint | Auth | +| ------ | ------------------------------- | ------ | +| POST | `/api/v1/whitelist/{info_hash}` | ✅ Yes | +| DELETE | `/api/v1/whitelist/{info_hash}` | ✅ Yes | +| GET | `/api/v1/whitelist/reload` | ✅ Yes | + +### Torrents + +| Method | Endpoint | Auth | +| ------ | ----------------------------- | ------ | +| GET | `/api/v1/torrent/{info_hash}` | ✅ Yes | +| GET | `/api/v1/torrents` | ✅ Yes | + +## Making Announce Requests with the Tracker Client + +The `tracker_client` binary can make BitTorrent announce requests to verify the tracker is working. + +### UDP Announce + +```bash +cargo run -p torrust-tracker-client --bin tracker_client -- udp announce udp://localhost:6969/announce 0123456789abcdef0123456789abcdef01234567 +``` + +### HTTP Announce + +```bash +cargo run -p torrust-tracker-client --bin tracker_client -- http announce http://localhost:7070/announce 0123456789abcdef0123456789abcdef01234567 +``` + +### Scrape + +```bash +cargo run -p torrust-tracker-client --bin tracker_client -- udp scrape udp://localhost:6969/announce 0123456789abcdef0123456789abcdef01234567 +``` + +Output defaults to JSON. Use `--format text` for human-readable output. + +## Verification Workflow + +After making an announce request, verify the API reflects the activity: + +1. Check stats changed: + + ```bash + curl -s http://localhost:1212/api/v1/stats -H "Authorization: Bearer MyAccessToken" + ``` + + Expect `torrents` and `seeders` to increase. + +2. Check metrics changed: + + ```bash + curl -s http://localhost:1212/api/v1/metrics -H "Authorization: Bearer MyAccessToken" + ``` + + Expect protocol-specific counters to increase. + +3. Check tracker console logs show the request was received: + + ```text + active_peers_total=1 active_torrents_total=1 + ``` diff --git a/.github/workflows/deployment.yaml b/.github/workflows/deployment.yaml index 4e04fb936..810bf78c2 100644 --- a/.github/workflows/deployment.yaml +++ b/.github/workflows/deployment.yaml @@ -67,7 +67,6 @@ jobs: cargo publish -p torrust-tracker-axum-rest-api-server cargo publish -p torrust-tracker-axum-server cargo publish -p torrust-tracker-rest-api-client - cargo publish -p torrust-tracker-rest-api-core cargo publish -p torrust-server-lib cargo publish -p torrust-tracker cargo publish -p torrust-tracker-client diff --git a/.github/workflows/security-scan.yaml b/.github/workflows/security-scan.yaml new file mode 100644 index 000000000..6a8326d27 --- /dev/null +++ b/.github/workflows/security-scan.yaml @@ -0,0 +1,91 @@ +name: Security Scan + +on: + push: + branches: [main, develop] + paths: + - "Containerfile" + - ".github/workflows/security-scan.yaml" + + pull_request: + paths: + - "Containerfile" + - ".github/workflows/security-scan.yaml" + + # Scheduled scans are important because new CVEs appear + # even if the code or images didn't change + schedule: + - cron: "0 6 * * *" # Daily at 6 AM UTC + + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +jobs: + security-scan: + name: Security Scan + runs-on: ubuntu-latest + # Scheduled scans pull the pre-built image; push/PR triggers rebuild from + # source (Containerfile change). Rust compilation can exceed 25 min. + timeout-minutes: 45 + permissions: + contents: read + security-events: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # Scheduled scans use the pre-built develop image from Docker Hub. + # Push/PR triggers on Containerfile changes need to build from source. + - name: Determine image source + id: image-source + run: | + if [ "${{ github.event_name }}" = "schedule" ]; then + echo "method=pull" >> "$GITHUB_OUTPUT" + echo "image=torrust/tracker:develop" >> "$GITHUB_OUTPUT" + else + echo "method=build" >> "$GITHUB_OUTPUT" + echo "image=torrust-tracker:local" >> "$GITHUB_OUTPUT" + fi + + - name: Pull or build Docker image + run: | + if [ "${{ steps.image-source.outputs.method }}" = "pull" ]; then + docker pull "${{ steps.image-source.outputs.image }}" + else + docker build -t "${{ steps.image-source.outputs.image }}" -f Containerfile . + fi + + # Human-readable output in logs + # This NEVER fails the job; it's only for visibility + - name: Display vulnerabilities (table format) + uses: aquasecurity/trivy-action@0.35.0 + with: + image-ref: ${{ steps.image-source.outputs.image }} + format: "table" + severity: "HIGH,CRITICAL" + exit-code: "0" + + # SARIF generation for GitHub Code Scanning + # + # IMPORTANT: + # - exit-code MUST be 0 + # - Trivy sometimes exits with 1 even when no vulns exist + # - GitHub Security UI is responsible for enforcement + - name: Generate SARIF (Code Scanning) + uses: aquasecurity/trivy-action@0.35.0 + with: + image-ref: ${{ steps.image-source.outputs.image }} + format: "sarif" + output: "trivy-results.sarif" + severity: "HIGH,CRITICAL" + exit-code: "0" + scanners: "vuln" + + - name: Upload SARIF to Code Scanning + uses: github/codeql-action/upload-sarif@v4 + if: always() + with: + sarif_file: trivy-results.sarif diff --git a/AGENTS.md b/AGENTS.md index f9f770fab..9372c71c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,27 +60,27 @@ native IPv4/IPv6 support, private/whitelisted mode, and a management REST API. All packages live under `packages/`. The workspace version is `3.0.0-develop`. -| Package | Crate Name | Prefix / Layer | Description | -| --------------------------------- | ------------------------------------------------- | -------------- | --------------------------------------------- | -| `axum-health-check-api-server` | `torrust-tracker-axum-health-check-api-server` | `axum-*` | Health monitoring endpoint | -| `axum-http-server` | `torrust-tracker-axum-http-server` | `axum-*` | BitTorrent HTTP tracker server (BEP 3/23) | -| `axum-rest-api-server` | `torrust-tracker-axum-rest-api-server` | `axum-*` | Management REST API server | -| `axum-server` | `torrust-tracker-axum-server` | `axum-*` | Base Axum HTTP server infrastructure | -| `configuration` | `torrust-tracker-configuration` | domain | Config file parsing, environment variables | -| `events` | `torrust-tracker-events` | domain | Domain event definitions | -| `http-protocol` | `torrust-tracker-http-protocol` | `*-protocol` | HTTP tracker protocol (BEP 3/23) parsing | -| `http-core` | `torrust-tracker-http-core` | `*-core` | HTTP-specific tracker domain logic | -| `primitives` | `torrust-tracker-primitives` | domain | Core domain types (InfoHash, PeerId, ...) | -| `rest-api-client` | `torrust-tracker-rest-api-client` | client tools | REST API client library | -| `rest-api-core` | `torrust-tracker-rest-api-core` | client tools | REST API core logic | -| `swarm-coordination-registry` | `torrust-tracker-swarm-coordination-registry` | domain | Torrent/peer coordination registry | -| `test-helpers` | `torrust-tracker-test-helpers` | utilities | Mock servers, test data generation | -| `torrent-repository-benchmarking` | `torrust-tracker-torrent-repository-benchmarking` | benchmarking | Torrent storage benchmarks | -| `tracker-client` | `torrust-tracker-client` | client tools | CLI tracker interaction/testing client | -| `tracker-core` | `torrust-tracker-core` | `*-core` | Central tracker peer-management logic | -| `udp-protocol` | `torrust-tracker-udp-protocol` | `*-protocol` | UDP tracker protocol (BEP 15) framing/parsing | -| `udp-core` | `torrust-tracker-udp-core` | `*-core` | UDP-specific tracker domain logic | -| `udp-server` | `torrust-tracker-udp-server` | server | UDP tracker server implementation | +| Package | Crate Name | Prefix / Layer | Description | +| --------------------------------- | ------------------------------------------------- | --------------- | --------------------------------------------- | +| `axum-health-check-api-server` | `torrust-tracker-axum-health-check-api-server` | `axum-*` | Health monitoring endpoint | +| `axum-http-server` | `torrust-tracker-axum-http-server` | `axum-*` | BitTorrent HTTP tracker server (BEP 3/23) | +| `axum-rest-api-server` | `torrust-tracker-axum-rest-api-server` | `axum-*` | Management REST API server | +| `axum-server` | `torrust-tracker-axum-server` | `axum-*` | Base Axum HTTP server infrastructure | +| `configuration` | `torrust-tracker-configuration` | domain | Config file parsing, environment variables | +| `events` | `torrust-tracker-events` | domain | Domain event definitions | +| `http-protocol` | `torrust-tracker-http-protocol` | `*-protocol` | HTTP tracker protocol (BEP 3/23) parsing | +| `http-core` | `torrust-tracker-http-core` | `*-core` | HTTP-specific tracker domain logic | +| `primitives` | `torrust-tracker-primitives` | domain | Core domain types (InfoHash, PeerId, ...) | +| `rest-api-client` | `torrust-tracker-rest-api-client` | client tools | REST API client library | +| `rest-api-runtime-adapter` | `torrust-tracker-rest-api-runtime-adapter` | runtime adapter | REST API runtime adapter and container wiring | +| `swarm-coordination-registry` | `torrust-tracker-swarm-coordination-registry` | domain | Torrent/peer coordination registry | +| `test-helpers` | `torrust-tracker-test-helpers` | utilities | Mock servers, test data generation | +| `torrent-repository-benchmarking` | `torrust-tracker-torrent-repository-benchmarking` | benchmarking | Torrent storage benchmarks | +| `tracker-client` | `torrust-tracker-client` | client tools | CLI tracker interaction/testing client | +| `tracker-core` | `torrust-tracker-core` | `*-core` | Central tracker peer-management logic | +| `udp-protocol` | `torrust-tracker-udp-protocol` | `*-protocol` | UDP tracker protocol (BEP 15) framing/parsing | +| `udp-core` | `torrust-tracker-udp-core` | `*-core` | UDP-specific tracker domain logic | +| `udp-server` | `torrust-tracker-udp-server` | server | UDP tracker server implementation | **Extracted packages** — previously part of this workspace, now in their own standalone repositories: diff --git a/Cargo.lock b/Cargo.lock index e9815cd1e..7c027819e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4872,8 +4872,8 @@ dependencies = [ "torrust-tracker-http-core", "torrust-tracker-primitives", "torrust-tracker-rest-api-client", - "torrust-tracker-rest-api-core", "torrust-tracker-rest-api-protocol", + "torrust-tracker-rest-api-runtime-adapter", "torrust-tracker-swarm-coordination-registry", "torrust-tracker-test-helpers", "torrust-tracker-udp-core", @@ -4976,7 +4976,6 @@ dependencies = [ "torrust-tracker-primitives", "torrust-tracker-rest-api-application", "torrust-tracker-rest-api-client", - "torrust-tracker-rest-api-core", "torrust-tracker-rest-api-protocol", "torrust-tracker-rest-api-runtime-adapter", "torrust-tracker-swarm-coordination-registry", @@ -5222,28 +5221,11 @@ dependencies = [ "reqwest", "serde", "thiserror 2.0.18", + "torrust-tracker-rest-api-protocol", "url", "uuid", ] -[[package]] -name = "torrust-tracker-rest-api-core" -version = "3.0.0-develop" -dependencies = [ - "tokio", - "tokio-util", - "torrust-metrics", - "torrust-tracker-configuration", - "torrust-tracker-core", - "torrust-tracker-events", - "torrust-tracker-http-core", - "torrust-tracker-primitives", - "torrust-tracker-swarm-coordination-registry", - "torrust-tracker-test-helpers", - "torrust-tracker-udp-core", - "torrust-tracker-udp-server", -] - [[package]] name = "torrust-tracker-rest-api-protocol" version = "3.0.0-develop" @@ -5258,8 +5240,11 @@ name = "torrust-tracker-rest-api-runtime-adapter" version = "3.0.0-develop" dependencies = [ "async-trait", + "tokio", + "torrust-clock", "torrust-info-hash", "torrust-metrics", + "torrust-tracker-configuration", "torrust-tracker-core", "torrust-tracker-http-core", "torrust-tracker-primitives", diff --git a/Cargo.toml b/Cargo.toml index 8214f1dd9..88964f778 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ torrust-tracker-axum-http-server = { version = "3.0.0-develop", path = "packages torrust-tracker-axum-rest-api-server = { version = "3.0.0-develop", path = "packages/axum-rest-api-server" } torrust-tracker-axum-server = { version = "3.0.0-develop", path = "packages/axum-server" } torrust-tracker-rest-api-client = { version = "3.0.0-develop", path = "packages/rest-api-client" } -torrust-tracker-rest-api-core = { version = "3.0.0-develop", path = "packages/rest-api-core" } +torrust-tracker-rest-api-runtime-adapter = { version = "3.0.0-develop", path = "packages/rest-api-runtime-adapter" } torrust-tracker-rest-api-protocol = { version = "3.0.0-develop", path = "packages/rest-api-protocol" } torrust-server-lib = "0.1.0" torrust-clock = "3.0.0" diff --git a/Containerfile b/Containerfile index 79dc26a7f..0fc624147 100644 --- a/Containerfile +++ b/Containerfile @@ -80,7 +80,6 @@ COPY packages/http-protocol/Cargo.toml packages/http-protocol/ COPY packages/http-core/Cargo.toml packages/http-core/ COPY packages/primitives/Cargo.toml packages/primitives/ COPY packages/rest-api-client/Cargo.toml packages/rest-api-client/ -COPY packages/rest-api-core/Cargo.toml packages/rest-api-core/ COPY packages/rest-api-application/Cargo.toml packages/rest-api-application/ COPY packages/rest-api-protocol/Cargo.toml packages/rest-api-protocol/ COPY packages/rest-api-runtime-adapter/Cargo.toml packages/rest-api-runtime-adapter/ @@ -127,7 +126,6 @@ RUN mkdir -p \ packages/http-core/benches \ packages/primitives/src \ packages/rest-api-client/src \ - packages/rest-api-core/src \ packages/rest-api-application/src \ packages/rest-api-protocol/src \ packages/rest-api-runtime-adapter/src \ @@ -168,7 +166,6 @@ RUN mkdir -p \ packages/http-core/benches/http_tracker_core_benchmark.rs \ packages/primitives/src/lib.rs \ packages/rest-api-client/src/lib.rs \ - packages/rest-api-core/src/lib.rs \ packages/rest-api-application/src/lib.rs \ packages/rest-api-protocol/src/lib.rs \ packages/rest-api-runtime-adapter/src/lib.rs \ diff --git a/README.md b/README.md index f99732de2..174637eb8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Torrust Tracker -[![container_wf_b]][container_wf] [![coverage_wf_b]][coverage_wf] [![deployment_wf_b]][deployment_wf] [![testing_wf_b]][testing_wf] [![os_compat_wf_b]][os_compat_wf] [![db_compat_wf_b]][db_compat_wf] [![db_bench_wf_b]][db_bench_wf] [![docs_lint_wf_b]][docs_lint_wf] +[![container_wf_b]][container_wf] [![coverage_wf_b]][coverage_wf] [![deployment_wf_b]][deployment_wf] [![testing_wf_b]][testing_wf] [![os_compat_wf_b]][os_compat_wf] [![db_compat_wf_b]][db_compat_wf] [![db_bench_wf_b]][db_bench_wf] [![docs_lint_wf_b]][docs_lint_wf] [![security_scan_wf_b]][security_scan_wf] **Torrust Tracker** is a [BitTorrent][bittorrent] Tracker that matchmakes peers and collects statistics. Written in [Rust Language][rust] with the [Axum] web framework. **This tracker aims to be respectful to established standards, (both [formal][BEP 00] and [otherwise][torrent_source_felid]).** @@ -270,6 +270,8 @@ This project was a joint effort by [Nautilus Cyberneering GmbH][nautilus] and [D [db_bench_wf_b]: ../../actions/workflows/db-benchmarking.yaml/badge.svg [docs_lint_wf]: ../../actions/workflows/docs-lint.yaml [docs_lint_wf_b]: ../../actions/workflows/docs-lint.yaml/badge.svg +[security_scan_wf]: ../../actions/workflows/security-scan.yaml +[security_scan_wf_b]: ../../actions/workflows/security-scan.yaml/badge.svg [bittorrent]: http://bittorrent.org/ [rust]: https://www.rust-lang.org/ [axum]: https://github.com/tokio-rs/axum diff --git a/deny.toml b/deny.toml index 76e6b15f4..519af29b9 100644 --- a/deny.toml +++ b/deny.toml @@ -49,12 +49,11 @@ deny = [ "torrust-tracker-axum-rest-api-server", ] }, - # udp server — only server-layer + root + rest-api-core (pending fix) + runtime-adapter may depend on it + # udp server — only server-layer + root + runtime-adapter may depend on it { crate = "torrust-tracker-udp-server", wrappers = [ "torrust-tracker", "torrust-tracker-axum-health-check-api-server", "torrust-tracker-axum-rest-api-server", - "torrust-tracker-rest-api-core", "torrust-tracker-rest-api-runtime-adapter", ] }, @@ -84,13 +83,11 @@ deny = [ "torrust-tracker", "torrust-tracker-axum-http-server", "torrust-tracker-axum-rest-api-server", - "torrust-tracker-rest-api-core", "torrust-tracker-rest-api-runtime-adapter", ] }, { crate = "torrust-tracker-udp-core", wrappers = [ "torrust-tracker", "torrust-tracker-axum-rest-api-server", - "torrust-tracker-rest-api-core", "torrust-tracker-rest-api-runtime-adapter", "torrust-tracker-udp-server", ] }, diff --git a/docs/issues/open/1939-1938-si-1-migrate-health-check-context.md b/docs/issues/closed/1939-1938-si-1-migrate-health-check-context.md similarity index 95% rename from docs/issues/open/1939-1938-si-1-migrate-health-check-context.md rename to docs/issues/closed/1939-1938-si-1-migrate-health-check-context.md index a046348d1..1c50b1061 100644 --- a/docs/issues/open/1939-1938-si-1-migrate-health-check-context.md +++ b/docs/issues/closed/1939-1938-si-1-migrate-health-check-context.md @@ -1,18 +1,18 @@ --- doc-type: spec issue-type: task -status: planned +status: done priority: p1 epic: 1938 github-issue: 1939 -spec-path: docs/issues/open/1939-1938-si-1-migrate-health-check-context.md -last-updated-utc: 2026-06-24 - updated-reason: Updated to reference context/ module structure instead of resources/ +spec-path: docs/issues/closed/1939-1938-si-1-migrate-health-check-context.md +last-updated-utc: 2026-06-25 +updated-reason: Closed — issue implemented semantic-links: skill-links: - create-issue related-artifacts: - - docs/issues/drafts/rest-api-contract-first-migration/EPIC.md + - docs/issues/open/1938-rest-api-contract-first-migration/EPIC.md - packages/axum-rest-api-server/src/v1/context/health_check/ - packages/axum-rest-api-server/src/routes.rs - packages/rest-api-protocol/src/v1/ diff --git a/docs/issues/open/1940-1938-si-2-migrate-whitelist-context.md b/docs/issues/closed/1940-1938-si-2-migrate-whitelist-context.md similarity index 96% rename from docs/issues/open/1940-1938-si-2-migrate-whitelist-context.md rename to docs/issues/closed/1940-1938-si-2-migrate-whitelist-context.md index bcc4db4d9..d266c458e 100644 --- a/docs/issues/open/1940-1938-si-2-migrate-whitelist-context.md +++ b/docs/issues/closed/1940-1938-si-2-migrate-whitelist-context.md @@ -1,18 +1,18 @@ --- doc-type: spec issue-type: task -status: planned +status: done priority: p1 epic: 1938 github-issue: 1940 -spec-path: docs/issues/open/1940-1938-si-2-migrate-whitelist-context.md -last-updated-utc: 2026-06-24 - updated-reason: Added normalized module structure convention note +spec-path: docs/issues/closed/1940-1938-si-2-migrate-whitelist-context.md +last-updated-utc: 2026-06-26 +updated-reason: Closed — issue implemented semantic-links: skill-links: - create-issue related-artifacts: - - docs/issues/drafts/rest-api-contract-first-migration/EPIC.md + - docs/issues/open/1938-rest-api-contract-first-migration/EPIC.md - packages/axum-rest-api-server/src/v1/context/whitelist/ - packages/rest-api-protocol/src/v1/ - packages/rest-api-application/src/ diff --git a/docs/issues/open/1941-1938-si-3-migrate-auth-key-context.md b/docs/issues/closed/1941-1938-si-3-migrate-auth-key-context.md similarity index 96% rename from docs/issues/open/1941-1938-si-3-migrate-auth-key-context.md rename to docs/issues/closed/1941-1938-si-3-migrate-auth-key-context.md index 5b530eb7c..ab08fed47 100644 --- a/docs/issues/open/1941-1938-si-3-migrate-auth-key-context.md +++ b/docs/issues/closed/1941-1938-si-3-migrate-auth-key-context.md @@ -1,18 +1,18 @@ --- doc-type: spec issue-type: task -status: planned +status: done priority: p1 epic: 1938 github-issue: 1941 -spec-path: docs/issues/open/1941-1938-si-3-migrate-auth-key-context.md -last-updated-utc: 2026-06-24 - updated-reason: Updated paths to context/ and added module structure convention note +spec-path: docs/issues/closed/1941-1938-si-3-migrate-auth-key-context.md +last-updated-utc: 2026-06-26 +updated-reason: Closed — issue implemented semantic-links: skill-links: - create-issue related-artifacts: - - docs/issues/drafts/rest-api-contract-first-migration/EPIC.md + - docs/issues/open/1938-rest-api-contract-first-migration/EPIC.md - packages/axum-rest-api-server/src/v1/context/auth_key/ - packages/rest-api-protocol/src/v1/ - packages/rest-api-application/src/ @@ -122,8 +122,8 @@ See the `torrent` and `health_check` contexts for the reference pattern. - [x] `AuthKeyApiService` use-case implemented - [x] `TrackerAuthKeyAdapter` implemented in `rest-api-runtime-adapter` - [x] Axum handlers dispatch through use-case -- [ ] Pre-commit checks pass -- [ ] Pre-push checks pass +- [x] Pre-commit checks pass +- [x] Pre-push checks pass ### Progress Log diff --git a/docs/issues/open/1942-1938-si-4-migrate-stats-context.md b/docs/issues/closed/1942-1938-si-4-migrate-stats-context.md similarity index 96% rename from docs/issues/open/1942-1938-si-4-migrate-stats-context.md rename to docs/issues/closed/1942-1938-si-4-migrate-stats-context.md index d28a57e25..68090d208 100644 --- a/docs/issues/open/1942-1938-si-4-migrate-stats-context.md +++ b/docs/issues/closed/1942-1938-si-4-migrate-stats-context.md @@ -1,18 +1,18 @@ --- doc-type: spec issue-type: task -status: planned +status: done priority: p1 epic: 1938 github-issue: 1942 -spec-path: docs/issues/open/1942-1938-si-4-migrate-stats-context.md -last-updated-utc: 2026-06-24 - updated-reason: Updated paths to context/ and added module structure convention note +spec-path: docs/issues/closed/1942-1938-si-4-migrate-stats-context.md +last-updated-utc: 2026-06-27 +updated-reason: Closed — issue implemented semantic-links: skill-links: - create-issue related-artifacts: - - docs/issues/drafts/rest-api-contract-first-migration/EPIC.md + - docs/issues/open/1938-rest-api-contract-first-migration/EPIC.md - packages/axum-rest-api-server/src/v1/context/stats/ - packages/rest-api-protocol/src/v1/ - packages/rest-api-application/src/ @@ -174,7 +174,7 @@ The use-case maps domain errors to protocol error codes and returns protocol DTO | T7 | DONE | Rewire Axum handlers to use `StatsApiService` | No more tuple-state or rest-api-core calls | | T8 | DONE | Update Axum state to inject `TrackerStatsAdapter` (replacing 6+ tuples) | Single `Arc` in `v1/routes.rs` | | T9 | DONE | Remove direct internal deps from `axum-rest-api-server` stats wiring | 7+ tuple-state removed, handler uses only service | -| T10 | TODO | Verify pre-commit and pre-push checks pass | | +| T10 | DONE | Verify pre-commit and pre-push checks pass | | ## Verification / Progress @@ -186,7 +186,7 @@ The use-case maps domain errors to protocol error codes and returns protocol DTO - [x] Axum handlers dispatch through use-case - [x] Direct internal crate deps removed from Axum server stats wiring - [x] Pre-commit checks pass -- [ ] Pre-push checks pass +- [x] Pre-push checks pass ### Progress Log @@ -194,3 +194,4 @@ The use-case maps domain errors to protocol error codes and returns protocol DTO | ---------- | ---------------------------------------------------------------------------------------- | | 2026-06-24 | Draft spec created | | 2026-06-26 | Stats context migrated to contract-first architecture (Option 3: aggregation in adapter) | +| 2026-06-27 | Issue closed on GitHub — all checks passing | diff --git a/docs/issues/closed/1943-1938-si-5-deprecate-rest-api-core.md b/docs/issues/closed/1943-1938-si-5-deprecate-rest-api-core.md new file mode 100644 index 000000000..57f80e84a --- /dev/null +++ b/docs/issues/closed/1943-1938-si-5-deprecate-rest-api-core.md @@ -0,0 +1,223 @@ +--- +doc-type: spec +issue-type: task +status: done +priority: p2 +epic: 1938 +github-issue: 1943 +spec-path: docs/issues/closed/1943-1938-si-5-deprecate-rest-api-core.md +last-updated-utc: 2026-06-29 +updated-reason: Closed — issue implemented +semantic-links: + skill-links: + - create-issue + related-artifacts: + - docs/issues/open/1938-rest-api-contract-first-migration/EPIC.md + - packages/rest-api-core/ + - packages/rest-api-runtime-adapter/ + - packages/rest-api-application/ + - packages/rest-api-protocol/ + - packages/axum-rest-api-server/Cargo.toml +--- + + + +# SI-5: Deprecate `rest-api-core` and remove from workspace + +## Subissue of REST API Contract-First Migration EPIC + +## Problem + +After SI-1 through SI-4 migrate all contexts to the contract-first architecture, the `rest-api-core` package (`torrust-tracker-rest-api-core`) becomes an empty shell: + +| Current component | Absorbed by | +| -------------------------------------------------------- | ------------------------------------------------------------- | +| `TrackerHttpApiCoreContainer` (DI wiring) | `rest-api-runtime-adapter` adapters | +| `TorrentsMetrics`, `ProtocolMetrics` (metric types) | `rest-api-protocol` DTOs | +| `get_metrics()`, `get_labeled_metrics()` (orchestration) | `rest-api-application` use-cases + `rest-api-runtime-adapter` | + +It has only **one consumer** in the entire workspace: `axum-rest-api-server`. Once that consumer is migrated (SI-4 removes the stats dependency), the crate is unused. + +## Prerequisites + +- [x] SI-4 (stats migration) completed — this removes the last consumer of `rest-api-core` types from `axum-rest-api-server`. +- [x] Verify no other crate in the workspace depends on `rest-api-core`. + +## Scope + +### In Scope + +- Move any remaining useful types (metrics structs, if not already ported) to their target layers. +- Remove `torrust-tracker-rest-api-core` from `axum-rest-api-server/Cargo.toml`. +- Remove the crate from workspace `Cargo.toml` members list. +- Delete the `packages/rest-api-core/` directory. +- Remove any `deny.toml` wrapper rules referencing the crate. +- Verify no build/test breakage. + +### Out of Scope + +- Changing behaviour of existing stats endpoints (done in SI-4). + +## Implementation Plan + +| ID | Status | Task | Notes | +| --- | ------ | --------------------------------------------------------------------------------- | ----------------------------- | +| T1 | DONE | Verify all ported types exist in target layers | Must wait for SI-4 completion | +| T2 | DONE | Remove `torrust-tracker-rest-api-core` dep from `axum-rest-api-server/Cargo.toml` | | +| T3 | DONE | Remove crate from workspace `Cargo.toml` members | | +| T4 | DONE | Delete `packages/rest-api-core/` directory | | +| T5 | DONE | Update `deny.toml` if crate had wrapper rules | | +| T6 | DONE | Run pre-commit and pre-push checks | | + +## Verification / Progress + +- [x] No crate in workspace references `torrust-tracker-rest-api-core` +- [x] Workspace builds cleanly +- [x] Integration tests pass +- [x] Pre-commit checks pass +- [x] Pre-push checks pass + +## Manual Verification + +**✅ All API endpoints working correctly after removing `rest-api-core`.** + +Before committing, manually verify the REST API works correctly after removing `rest-api-core`: + +1. **Run the tracker locally** with the REST API enabled: + + ```console + cargo run -- --config share/default/config/tracker.development.sqlite3.toml + ``` + + Tracker started successfully on all ports (UDP 6868/6969, HTTP 7070/7171, API 1212). + +2. **Make test requests**: + - Request the stats endpoint: + + ```console + curl -s http://localhost:1212/api/v1/stats -H "Authorization: Bearer MyAccessToken" + ``` + + **Initial response** (all zeros): + + ```json + { + "torrents": 0, + "seeders": 0, + "completed": 6, + "leechers": 0, + "tcp4_connections_handled": 0, + "tcp4_announces_handled": 0, + "tcp4_scrapes_handled": 0, + "tcp6_connections_handled": 0, + "tcp6_announces_handled": 0, + "tcp6_scrapes_handled": 0, + "udp_requests_aborted": 0, + "udp_requests_banned": 0, + "udp_banned_ips_total": 0, + "udp_avg_connect_processing_time_ns": 0, + "udp_avg_announce_processing_time_ns": 0, + "udp_avg_scrape_processing_time_ns": 0, + "udp4_requests": 0, + "udp4_connections_handled": 0, + "udp4_announces_handled": 0, + "udp4_scrapes_handled": 0, + "udp4_responses": 0, + "udp4_errors_handled": 0, + "udp6_requests": 0, + "udp6_connections_handled": 0, + "udp6_announces_handled": 0, + "udp6_scrapes_handled": 0, + "udp6_responses": 0, + "udp6_errors_handled": 0 + } + ``` + + - Request the metrics endpoint: + + ```console + curl -s http://localhost:1212/api/v1/metrics -H "Authorization: Bearer MyAccessToken" + ``` + + **Initial response**: returned all metrics with initial samples (e.g., `tracker_core_persistent_torrents_downloads_total` with `value: 6`). + + - Make an announce request using the tracker client: + + ```console + cargo run -p torrust-tracker-client --bin tracker_client -- udp announce udp://localhost:6969/announce 0123456789abcdef0123456789abcdef01234567 + ``` + + **Announce response**: + + ```json + { + "AnnounceIpv4": { + "transaction_id": -888840697, + "announce_interval": 120, + "leechers": 0, + "seeders": 1, + "peers": [] + } + } + ``` + +3. **Verify stats and metrics changed**: + - Repeat the `/api/v1/stats` request: + + ```console + curl -s http://localhost:1212/api/v1/stats -H "Authorization: Bearer MyAccessToken" + ``` + + **Response after announce** (values changed): + + ```json + { + "torrents": 1, + "seeders": 1, + "completed": 6, + "leechers": 0, + "tcp4_connections_handled": 0, + "tcp4_announces_handled": 0, + "tcp4_scrapes_handled": 0, + "tcp6_connections_handled": 0, + "tcp6_announces_handled": 0, + "tcp6_scrapes_handled": 0, + "udp_requests_aborted": 0, + "udp_requests_banned": 0, + "udp_banned_ips_total": 0, + "udp_avg_connect_processing_time_ns": 69019, + "udp_avg_announce_processing_time_ns": 188913, + "udp_avg_scrape_processing_time_ns": 0, + "udp4_requests": 2, + "udp4_connections_handled": 1, + "udp4_announces_handled": 1, + "udp4_scrapes_handled": 0, + "udp4_responses": 2, + "udp4_errors_handled": 0, + "udp6_requests": 0, + "udp6_connections_handled": 0, + "udp6_announces_handled": 0, + "udp6_scrapes_handled": 0, + "udp6_responses": 0, + "udp6_errors_handled": 0 + } + ``` + + **Changed values**: torrents `0→1`, seeders `0→1`, `udp4_requests` `0→2`, `udp4_connections_handled` `0→1`, `udp4_announces_handled` `0→1`, `udp4_responses` `0→2`, plus average processing times populated. + + - Repeat the `/api/v1/metrics` request: returned samples with `swarm_coordination_registry_torrents_total: 1.0`, `swarm_coordination_registry_peers_added_total: 1`, `udp_tracker_core_requests_received_total: 2` (1 connect, 1 announce). + + - Tracker console logs confirmed the announce was received: + + ```text + active_peers_total=1 inactive_peers_total=0 active_torrents_total=1 inactive_torrents_total=0 + ``` + +### Progress Log + +| Date | Event | +| ---------- | ------------------------------------------------------------------------------------------ | +| 2026-06-24 | Draft spec created | +| 2026-06-29 | Implementation confirmed: move `TrackerHttpApiCoreContainer` to `rest-api-runtime-adapter` | +| 2026-06-29 | Implementation: container moved, deps removed, directory deleted | +| 2026-06-29 | Manual verification: all API endpoints working correctly (stats, metrics, announce) | diff --git a/docs/issues/open/1459-docker-security-overhaul/ISSUE.md b/docs/issues/open/1459-docker-security-overhaul/ISSUE.md new file mode 100644 index 000000000..67316b70b --- /dev/null +++ b/docs/issues/open/1459-docker-security-overhaul/ISSUE.md @@ -0,0 +1,158 @@ +--- +doc-type: issue +issue-type: task +status: completed +priority: p2 +github-issue: 1459 +spec-path: docs/issues/open/1459-docker-security-overhaul/ISSUE.md +branch: 1459-docker-security-overhaul +related-pr: "https://github.com/torrust/torrust-tracker/pull/1958" +last-updated-utc: 2026-06-29 +semantic-links: + skill-links: + - create-issue + related-artifacts: + - .github/workflows/security-scan.yaml + - Containerfile + - .github/workflows/container.yaml + - .github/skills/dev/maintenance/run-manual-docker-security-scan/SKILL.md + - docs/security/README.md + - docs/security/docker/scans/ + - docs/security/docker/README.md + - docs/security/analysis/non-affecting/ +--- + +# Issue #1459 - Docker Security Overhaul: Set Up Security Scanning Workflow + +## Problem + +The torrust-tracker Docker image contains known vulnerabilities that need to be regularly scanned and monitored. As demonstrated by the Trivy scan results, the current image has multiple security vulnerabilities including critical, high, and medium severity issues. + +## Goal + +Implement a scheduled workflow to periodically scan Docker images for vulnerabilities and misconfigurations, ensuring the security posture of the application is maintained. + +## Acceptance Criteria + +- [x] A new GitHub Actions workflow is created in `.github/workflows/security-scan.yaml` +- [x] The workflow runs on a schedule (daily) to scan the Docker image +- [x] The workflow builds the Docker image and scans it with Trivy +- [x] Vulnerability findings are reported in both human-readable and SARIF formats +- [x] The workflow integrates with the existing container build process +- [x] The README.md badge row includes the new security scan workflow badge +- [x] `docs/security/docker/scans/` is created with the first baseline scan report +- [x] `docs/security/docker/README.md` provides scanning instructions +- [x] `docs/security/README.md` provides a priority-tier security overview +- [x] Per-CVE analysis files created in `docs/security/analysis/non-affecting/` for each + MEDIUM vulnerability found in the baseline scan +- [x] `docs/security/analysis/README.md` documents the catalog strategy and recheck policy +- [x] A maintenance skill exists at + `.github/skills/dev/maintenance/run-manual-docker-security-scan/SKILL.md` + documenting how to run and document manual Docker security scans + +## Implementation Plan + +### Step 1: Create Security Scan Workflow + +Create a new workflow file `.github/workflows/security-scan.yaml` that: + +- Runs on a schedule (daily at 6 AM UTC) and on push to main/develop branches +- Builds the Docker image using the Containerfile +- Scans the image with Trivy +- Reports results in both table and SARIF formats + +### Step 2: Configure Trivy Scanning + +Configure the workflow to: + +- Use Trivy to scan the Docker image +- Report vulnerabilities in both human-readable table format and SARIF format for GitHub Code Scanning +- Generate SARIF output for integration with GitHub Security features + +### Step 3: Integrate with Existing Workflows + +Ensure the security scan workflow integrates properly with the existing container workflow. + +### Step 4: Add Workflow Badge to README.md + +Add the security scan workflow badge to the README.md header row and consistent reference links at the bottom, following the same pattern as existing workflow badges. + +### Step 5: Create Security Documentation and Run Baseline Scan + +Create `docs/security/docker/` structure mirroring the deployer's security docs pattern: + +- `docs/security/docker/README.md` — scanning instructions and context +- `docs/security/docker/scans/README.md` — scan history index table +- `docs/security/docker/scans/torrust-tracker.md` — detailed scan report with vulnerability analysis + +Run the first manual baseline scan of the production `release` stage image and document all findings, including vulnerability analysis and severity assessment. + +### Step 6: Create Top-Level Security Overview + +Create `docs/security/README.md` providing a priority-tier overview of security areas for the project, mirroring the deployer's top-level security README pattern: + +- Priority 1: Production Docker image (critical, internet-exposed) +- Priority 2: Vulnerability analysis (evaluation and tracking) +- Priority 3: Build chain security (lower-risk, build-time only) +- Current security status summary +- Scan tooling reference + +### Step 7: Create Non-Affecting CVE Catalog + +Create per-CVE analysis files in `docs/security/analysis/non-affecting/` for each +vulnerability found in the baseline scan, following this pattern: + +```text +non-affecting/ +├── CVE-2026-5435.md # glibc TSIG +├── CVE-2026-5450.md # glibc scanf +├── CVE-2026-5928.md # glibc ungetwc +├── CVE-2026-6238.md # glibc DNS response +└── CVE-2026-27171.md # zlib CRC32 +``` + +Each file includes: + +- Frontmatter with `cve-id`, `date-analyzed`, `source`, `status`, `review-cadence`, + and `requires-recheck-when` conditions +- Vulnerability description and severity +- Evidence-based rationale for why it does not affect the tracker +- Conditions that would change the verdict + +Update `docs/security/analysis/README.md` to document the catalog strategy (one catalog +for all vulnerability sources, per-CVE files preferred, with recheck policy). + +### Step 8: Add Maintenance Skill for Manual Security Scans + +Create a new skill at +`.github/skills/dev/maintenance/run-manual-docker-security-scan/SKILL.md` to standardize +how contributors run manual Docker security scans and maintain scan documentation. + +The skill should include: + +- build and scan commands (`docker build`, `trivy image`) +- triage workflow (check catalog first, then analyze) +- documentation update requirements (`docs/security/docker/scans/*` and + `docs/security/analysis/non-affecting/CVE-*.md`) +- recheck triggers and escalation path for affecting vulnerabilities + +## References + +- Original issue: https://github.com/torrust/torrust-tracker/issues/1459 +- Related issue #1630 +- Trivy documentation for GitHub Actions integration +- Tracker Deployer security scan workflow for reference: https://github.com/torrust/torrust-tracker-deployer/blob/main/.github/workflows/docker-security-scan.yml + +## Verification Plan + +### Automatic Checks + +- [ ] Workflow file is created and syntactically correct +- [ ] Workflow runs successfully on schedule +- [ ] Trivy scan produces expected output + +### Manual Verification Scenarios + +- [ ] Run workflow manually to verify it scans the image +- [ ] Verify vulnerability reports are generated correctly +- [ ] Confirm workflow integrates with existing container workflow diff --git a/docs/issues/open/1938-rest-api-contract-first-migration/EPIC.md b/docs/issues/open/1938-rest-api-contract-first-migration/EPIC.md index 53bc104b3..f52f1ee9c 100644 --- a/docs/issues/open/1938-rest-api-contract-first-migration/EPIC.md +++ b/docs/issues/open/1938-rest-api-contract-first-migration/EPIC.md @@ -1,13 +1,13 @@ --- doc-type: epic issue-type: task -status: planned +status: in_progress priority: p1 epic: 1938 github-issue: 1938 spec-path: docs/issues/open/1938-rest-api-contract-first-migration/EPIC.md epic-owner: josecelano -last-updated-utc: 2026-06-24 +last-updated-utc: 2026-06-29 semantic-links: skill-links: - create-issue @@ -28,23 +28,22 @@ semantic-links: ## Goal -Progressively migrate all remaining REST API contexts (`health_check`, `whitelist`, `auth_key`, `stats`) from direct tracker-internal wiring to the contract-first layered architecture (protocol → application → runtime-adapter → axum transport), following the pattern validated by [SI-33 (#1930)](../../open/1930-1669-si-33-rest-api-contract-first-architecture.md) PoC. +Migrate all remaining REST API contexts (`health_check`, `whitelist`, `auth_key`, `stats`) from direct tracker-internal wiring to the contract-first layered architecture (protocol → application → runtime-adapter → axum transport), following the pattern validated by [SI-33 (#1930)](../../open/1930-1669-si-33-rest-api-contract-first-architecture.md) PoC. -## Why This Is Needed +All context migrations are **complete** (SI-1 through SI-5 closed). The only remaining open item is SI-6 (`ApiClient` high-level typed client). -[SI-33](../../open/1930-1669-si-33-rest-api-contract-first-architecture.md) validated the contract-first architecture with a single endpoint (torrent detail). The remaining contexts still have the old coupling: +## Why This Is Needed -- Axum handlers call tracker internals directly (`tracker-core`, `udp-core`, `http-core`, `udp-server`). -- DTO/response types are defined locally in the Axum server, not in `rest-api-protocol`. -- No port traits or use-case services exist for these contexts. -- The forbidden dependency edges (`axum-rest-api-server → tracker-core` etc.) still exist for non-torrent contexts. +Before this EPIC, the REST API had a mixture of architectures: -Migrating all contexts to the new architecture will: +- **`torrent` context** (SI-33 PoC) already used the contract-first architecture. +- **All other contexts** (`health_check`, `whitelist`, `auth_key`, `stats`) still had the old coupling: + - Axum handlers calling tracker internals directly (`tracker-core`, `udp-core`, `http-core`, `udp-server`). + - DTO/response types defined locally in the Axum server, not in `rest-api-protocol`. + - No port traits or use-case services existed for these contexts. + - Forbidden dependency edges (`axum-rest-api-server → tracker-core` etc.) still existed for non-torrent contexts. -- Allow removing direct internal crate dependencies from `axum-rest-api-server` (currently 7+ internal crate deps for non-torrent contexts). -- Make each context testable at the application layer without Axum. -- Provide a clear path toward a tracker-agnostic REST API standard. -- Complete the architectural vision started by SI-33. +This EPIC eliminated that coupling. The remaining open item (SI-6) is about improving the client API, not the server architecture. ## Relationship to SI-33 @@ -54,14 +53,15 @@ This EPIC is the follow-up work identified in [SI-33](../../open/1930-1669-si-33 The contexts are ordered by complexity and dependency depth. Follow-up tasks (SI-5, SI-6) come after all contexts are migrated: -| Order | Context / Task | Effort | Handlers | Tracker Deps | Rationale | -| ----- | ------------------------------- | ------ | -------- | ------------------------ | ------------------------------------------------ | -| 1 | SI-1: `health_check` | Small | 1 | None | Trivial starter — no tracker deps | -| 2 | SI-2: `whitelist` | Medium | 3 | `tracker-core` only | Clean pattern, no DTOs needed | -| 3 | SI-3: `auth_key` | Medium | 4 | `tracker-core` + `clock` | Form DTOs + validation, 4 endpoints | -| 4 | SI-4: `stats` | Large | 2 | 5+ crates | 28-field DTO, Prometheus, multi-repo aggregation | -| 5 | SI-5: deprecate `rest-api-core` | Small | — | — | Cleanup after all contexts migrated | -| 6 | SI-6: introduce `ApiClient` | Medium | — | — | Typed high-level wrapper over `ApiHttpClient` | +| Order | Context / Task | Effort | Handlers | Tracker Deps | Status | +| ----- | -------------------------------- | ------ | -------- | ------------------------ | ------ | +| 1 | SI-1: `health_check` | Small | 1 | None | ✅ | +| 2 | SI-2: `whitelist` | Medium | 3 | `tracker-core` only | ✅ | +| 3 | SI-3: `auth_key` | Medium | 4 | `tracker-core` + `clock` | ✅ | +| 4 | SI-4: `stats` | Large | 2 | 5+ crates | ✅ | +| 5 | SI-5: deprecate `rest-api-core` | Small | — | — | ✅ | +| 6 | SI-6: introduce `ApiClient` | Medium | — | — | ❌ | +| 7 | SI-7: review tests + align v1 ns | Small | — | — | 🏗️ | ## Context Status Summary @@ -69,62 +69,28 @@ The contexts are ordered by complexity and dependency depth. Follow-up tasks (SI | ------------------------------- | :-----------: | :------------: | :---------: | :-------: | :--------------: | --------------------------------------------------------------------------------- | | `torrent` | 2 ✅ done | ✅ | ✅ | ✅ | ✅ | Reference pattern — lives under `v1::context::torrent::resources::torrent` | | SI-1: `health_check` | 1 ✅ done | ✅ | ❌ N/A | ❌ N/A | ❌ N/A | No tracker deps — DTOs under `v1::context::health_check::resources::health_check` | -| SI-2: `whitelist` | 3 | ❌ | ❌ | ❌ | ❌ | Reuses `ActionStatus` | -| SI-3: `auth_key` | 4 | ❌ | ❌ | ❌ | ❌ | Form DTOs + `clock` | -| SI-4: `stats` | 2 | ❌ | ❌ | ❌ | ❌ | 28-field DTO, SI-30 traits | -| SI-5: deprecate `rest-api-core` | — | — | — | — | — | Post-migration cleanup | -| SI-6: introduce `ApiClient` | — | — | — | — | — | Typed wrapper over `ApiHttpClient` | +| SI-2: `whitelist` | 3 ✅ done | ✅ | ✅ | ✅ | ✅ | Reuses `ActionStatus` | +| SI-3: `auth_key` | 4 ✅ done | ✅ | ✅ | ✅ | ✅ | Form DTOs + `clock` | +| SI-4: `stats` | 2 ✅ done | ✅ | ✅ | ✅ | ✅ | 28-field DTO, SI-30 traits | +| SI-5: deprecate `rest-api-core` | — | — | — | — | — | ✅ done — crate removed from workspace | +| SI-6: introduce `ApiClient` | — | — | — | — | — | ❌ pending — typed wrapper over `ApiHttpClient` | ## Scope -### In Scope - -- Create protocol DTOs (request/response/error types) in `rest-api-protocol` for each remaining context. - Each context follows a normalized module structure under `packages/rest-api-protocol/src/v1/context/`: - - ```text - context/ - └── / - ├── mod.rs # context docs + pub mod resources; - └── resources/ - ├── mod.rs # pub mod ; - └── .rs # DTO definitions - ``` - - See the `torrent` context for the reference pattern. -- Define port traits in `rest-api-application` for each context's query/command operations. - These are flat files named after the context in `packages/rest-api-application/src/ports/`: - - ```text - ports/ - ├── mod.rs # pub mod torrent; pub mod whitelist; ... - └── .rs # port trait definition - ``` - -- Implement use-case services in `rest-api-application`. - Similarly flat files in `packages/rest-api-application/src/use_cases/`: - - ```text - use_cases/ - ├── mod.rs # pub mod torrent; pub mod whitelist; ... - └── .rs # use-case service implementation - ``` - -- Implement runtime adapters in `rest-api-runtime-adapter` wrapping tracker internals. - Flat files in `packages/rest-api-runtime-adapter/src/adapters/`: - - ```text - adapters/ - ├── mod.rs # pub mod torrent; pub mod whitelist; ... - └── .rs # adapter implementation - ``` - -- Rewire Axum handlers to dispatch through use cases instead of direct internals. -- Update tests to use adapter conversion functions. -- Remove internal crate dependencies from `axum-rest-api-server` as contexts are migrated. -- Update `deny.toml` layer bans as dependencies are removed. -- Deprecate and clean up `rest-api-core` after all contexts are migrated (SI-5). -- Introduce `ApiClient` — a high-level typed client wrapping `ApiHttpClient` with protocol DTOs (SI-6). +### In Scope (completed for SI-1 through SI-5) + +The following scope items have been completed across sub-issues SI-1 through SI-5: + +- ✅ Create protocol DTOs (request/response/error types) in `rest-api-protocol` for each context. +- ✅ Define port traits in `rest-api-application` for each context's operations. +- ✅ Implement use-case services in `rest-api-application`. +- ✅ Implement runtime adapters in `rest-api-runtime-adapter` wrapping tracker internals. +- ✅ Rewire Axum handlers to dispatch through use cases instead of direct internals. +- ✅ Remove internal crate dependencies from `axum-rest-api-server` as contexts were migrated. +- ✅ Update `deny.toml` layer bans as dependencies were removed. +- ✅ Deprecate and clean up `rest-api-core` (SI-5). +- ❌ **SI-6 (pending)**: Introduce `ApiClient` — a high-level typed client wrapping `ApiHttpClient` with protocol DTOs. +- 🏗️ **SI-7 (in progress)**: Review tests and align v1 namespace across REST API packages. ### Out of Scope @@ -136,12 +102,13 @@ The contexts are ordered by complexity and dependency depth. Follow-up tasks (SI ## Sub-issues -- [#1939](https://github.com/torrust/torrust-tracker/issues/1939) — [SI-1](../1939-1938-si-1-migrate-health-check-context.md): Migrate `health_check` context -- [#1940](https://github.com/torrust/torrust-tracker/issues/1940) — [SI-2](../1940-1938-si-2-migrate-whitelist-context.md): Migrate `whitelist` context -- [#1941](https://github.com/torrust/torrust-tracker/issues/1941) — [SI-3](../1941-1938-si-3-migrate-auth-key-context.md): Migrate `auth_key` context -- [#1942](https://github.com/torrust/torrust-tracker/issues/1942) — [SI-4](../1942-1938-si-4-migrate-stats-context.md): Migrate `stats` context -- [#1943](https://github.com/torrust/torrust-tracker/issues/1943) — [SI-5](../1943-1938-si-5-deprecate-rest-api-core.md): Deprecate `rest-api-core` and remove from workspace +- [#1939](https://github.com/torrust/torrust-tracker/issues/1939) — [SI-1](../../closed/1939-1938-si-1-migrate-health-check-context.md): Migrate `health_check` context ✅ closed +- [#1940](https://github.com/torrust/torrust-tracker/issues/1940) — [SI-2](../../closed/1940-1938-si-2-migrate-whitelist-context.md): Migrate `whitelist` context ✅ closed +- [#1941](https://github.com/torrust/torrust-tracker/issues/1941) — [SI-3](../../closed/1941-1938-si-3-migrate-auth-key-context.md): Migrate `auth_key` context ✅ closed +- [#1942](https://github.com/torrust/torrust-tracker/issues/1942) — [SI-4](../../closed/1942-1938-si-4-migrate-stats-context.md): Migrate `stats` context ✅ closed +- [#1943](https://github.com/torrust/torrust-tracker/issues/1943) — [SI-5](../../closed/1943-1938-si-5-deprecate-rest-api-core.md): Deprecate `rest-api-core` and remove from workspace ✅ closed - [#1944](https://github.com/torrust/torrust-tracker/issues/1944) — [SI-6](../1944-1938-si-6-align-rest-api-client.md): Introduce `ApiClient` — a high-level typed client over protocol DTOs +- [#1959](https://github.com/torrust/torrust-tracker/issues/1959) — [SI-7](../1959-1938-si-7-review-tests-align-v1-namespace.md): Review tests and align v1 namespace across REST API packages ## Contract Evolution Governance @@ -165,26 +132,28 @@ Types that are not exposed over the wire (e.g., internal Rust enums used only fo ## Dependency Removal Tracking -The following table maps each internal crate dependency to the sub-issue that removes it from `axum-rest-api-server/Cargo.toml`: +The following table maps each internal crate dependency to the sub-issue that removed it from `axum-rest-api-server/Cargo.toml`: -| Dependency | Removed by | Notes | -| ----------------------------- | ---------------------------------- | -------------------------------------------------- | -| `tracker-core` | SI-2 (whitelist) + SI-3 (auth_key) | Both contexts must finish | -| `http-core` | SI-4 (stats) | Via stats repository port | -| `udp-core` | SI-4 (stats) | Via SI-30 `BanningStats`, `UdpCoreStatsRepository` | -| `udp-server` | SI-4 (stats) | Via SI-30 `UdpServerStatsRepository` | -| `rest-api-core` | SI-5 (deprecate) | After all contexts migrated | -| `swarm-coordination-registry` | SI-4 (stats) | Via stats repository port | -| `clock` | SI-3 (auth_key) | Moved to runtime adapter | +| Dependency | Removed by | Status | +| ----------------------------- | ---------------------------------- | ------ | +| `tracker-core` | SI-2 (whitelist) + SI-3 (auth_key) | ✅ | +| `http-core` | SI-4 (stats) | ✅ | +| `udp-core` | SI-4 (stats) | ✅ | +| `udp-server` | SI-4 (stats) | ✅ | +| `rest-api-core` | SI-5 (deprecate) | ✅ | +| `swarm-coordination-registry` | SI-4 (stats) | ✅ | +| `clock` | SI-3 (auth_key) | ✅ | ## Success Criteria -- All 10 non-torrent Axum handler functions dispatch through application use-case services. -- All response DTOs live in `rest-api-protocol`; none are defined locally in Axum server. -- All direct `tracker-core`, `udp-core`, `http-core`, `udp-server`, `rest-api-core`, and `swarm-coordination-registry` imports are removed from `axum-rest-api-server`. -- `deny.toml` layer bans enforce the new dependency rules. -- All pre-commit and pre-push checks pass. -- Integration tests continue to pass without behavioural changes. +- ✅ All 10 non-torrent Axum handler functions dispatch through application use-case services. +- ✅ All response DTOs live in `rest-api-protocol`; none are defined locally in Axum server. +- ✅ All direct `tracker-core`, `udp-core`, `http-core`, `udp-server`, `rest-api-core`, and `swarm-coordination-registry` imports are removed from `axum-rest-api-server`. +- ✅ `deny.toml` layer bans enforce the new dependency rules. +- ✅ All pre-commit and pre-push checks pass. +- ✅ Integration tests continue to pass without behavioural changes. +- ❌ **SI-6 pending**: Introduce `ApiClient` high-level typed client. +- 🏗️ **SI-7 in progress**: Review tests and align v1 namespace. ## Progress Tracking @@ -195,3 +164,9 @@ The following table maps each internal crate dependency to the sub-issue that re | 2026-06-24 | Draft EPIC created after SI-33 PoC validation | | 2026-06-24 | SI-1 (health_check) implemented — protocol DTOs migrated | | 2026-06-24 | Specs updated to document normalized `context/` module structure for all protocol DTOs | +| 2026-06-25 | SI-1 closed on GitHub | +| 2026-06-26 | SI-2 (whitelist) and SI-3 (auth_key) closed on GitHub | +| 2026-06-27 | SI-4 (stats) closed on GitHub | +| 2026-06-29 | SI-5 (rest-api-core deprecation) closed on GitHub | +| 2026-06-29 | Closed issue specs moved to `docs/issues/closed/` with updated frontmatter | +| 2026-06-29 | SI-7 (review tests + align v1 ns) added — remaining task: SI-6 (ApiClient) | diff --git a/docs/issues/open/1943-1938-si-5-deprecate-rest-api-core.md b/docs/issues/open/1943-1938-si-5-deprecate-rest-api-core.md deleted file mode 100644 index 76ecc7883..000000000 --- a/docs/issues/open/1943-1938-si-5-deprecate-rest-api-core.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -doc-type: spec -issue-type: task -status: planned -priority: p2 -epic: 1938 -github-issue: 1943 -spec-path: docs/issues/open/1943-1938-si-5-deprecate-rest-api-core.md -last-updated-utc: 2026-06-24 -semantic-links: - skill-links: - - create-issue - related-artifacts: - - docs/issues/drafts/rest-api-contract-first-migration/EPIC.md - - packages/rest-api-core/ - - packages/rest-api-runtime-adapter/ - - packages/rest-api-application/ - - packages/rest-api-protocol/ - - packages/axum-rest-api-server/Cargo.toml ---- - - - -# SI-5: Deprecate `rest-api-core` and remove from workspace - -## Subissue of REST API Contract-First Migration EPIC - -## Problem - -After SI-1 through SI-4 migrate all contexts to the contract-first architecture, the `rest-api-core` package (`torrust-tracker-rest-api-core`) becomes an empty shell: - -| Current component | Absorbed by | -| -------------------------------------------------------- | ------------------------------------------------------------- | -| `TrackerHttpApiCoreContainer` (DI wiring) | `rest-api-runtime-adapter` adapters | -| `TorrentsMetrics`, `ProtocolMetrics` (metric types) | `rest-api-protocol` DTOs | -| `get_metrics()`, `get_labeled_metrics()` (orchestration) | `rest-api-application` use-cases + `rest-api-runtime-adapter` | - -It has only **one consumer** in the entire workspace: `axum-rest-api-server`. Once that consumer is migrated (SI-4 removes the stats dependency), the crate is unused. - -## Prerequisites - -- [ ] SI-4 (stats migration) completed — this removes the last consumer of `rest-api-core` types from `axum-rest-api-server`. -- [ ] Verify no other crate in the workspace depends on `rest-api-core`. - -## Scope - -### In Scope - -- Move any remaining useful types (metrics structs, if not already ported) to their target layers. -- Remove `torrust-tracker-rest-api-core` from `axum-rest-api-server/Cargo.toml`. -- Remove the crate from workspace `Cargo.toml` members list. -- Delete the `packages/rest-api-core/` directory. -- Remove any `deny.toml` wrapper rules referencing the crate. -- Verify no build/test breakage. - -### Out of Scope - -- Changing behaviour of existing stats endpoints (done in SI-4). - -## Implementation Plan - -| ID | Status | Task | Notes | -| --- | ------ | --------------------------------------------------------------------------------- | ----------------------------- | -| T1 | TODO | Verify all ported types exist in target layers | Must wait for SI-4 completion | -| T2 | TODO | Remove `torrust-tracker-rest-api-core` dep from `axum-rest-api-server/Cargo.toml` | | -| T3 | TODO | Remove crate from workspace `Cargo.toml` members | | -| T4 | TODO | Delete `packages/rest-api-core/` directory | | -| T5 | TODO | Update `deny.toml` if crate had wrapper rules | | -| T6 | TODO | Run pre-commit and pre-push checks | | - -## Verification / Progress - -- [ ] No crate in workspace references `torrust-tracker-rest-api-core` -- [ ] Workspace builds cleanly -- [ ] Integration tests pass -- [ ] Pre-commit checks pass -- [ ] Pre-push checks pass - -### Progress Log - -| Date | Event | -| ---------- | ------------------ | -| 2026-06-24 | Draft spec created | diff --git a/docs/issues/open/1944-1938-si-6-align-rest-api-client.md b/docs/issues/open/1944-1938-si-6-align-rest-api-client.md index 7c22b7329..6373d395f 100644 --- a/docs/issues/open/1944-1938-si-6-align-rest-api-client.md +++ b/docs/issues/open/1944-1938-si-6-align-rest-api-client.md @@ -11,7 +11,7 @@ semantic-links: skill-links: - create-issue related-artifacts: - - docs/issues/drafts/rest-api-contract-first-migration/EPIC.md + - docs/issues/open/1938-rest-api-contract-first-migration/EPIC.md - packages/rest-api-client/ - packages/rest-api-protocol/ - packages/rest-api-client/src/v1/client.rs @@ -24,6 +24,16 @@ semantic-links: ## Subissue of REST API Contract-First Migration EPIC +## Clarifying Decisions (from AI agent Q&A with user) + +- **`AddKeyForm`**: Use the protocol package's `AddKeyForm` (with field `opt_seconds_valid`) and remove the local `AddKeyForm` from client. +- **`ClientError` enum variants**: + - `TransportError(reqwest::Error)` — network/connection failures + - `ApiError { status: StatusCode, body: String }` — non-2xx responses with the error body + - `DeserializationError(reqwest::Error)` — JSON parsing failures +- **Public `get()` function**: Keep as a public free function (used by health_check tests directly). +- **Re-export strategy**: Re-export both `ApiClient` and `ApiHttpClient` from the crate root for ergonomics. + ## Problem The REST API client package (`torrust-tracker-rest-api-client`) currently exposes only a **low-level** `Client` struct where all 10 methods return raw `reqwest::Response` values. Callers must manually deserialize responses and handle errors. Some internal methods use `.unwrap()`, panicking on transport errors. @@ -175,23 +185,23 @@ impl ApiClient { | ID | Status | Task | Notes | | --- | ------ | ------------------------------------------------------------ | ------------------------------------------------ | -| T1 | TODO | Rename `Client` → `ApiHttpClient` in `client.rs` | Compiler catches all references | -| T2 | TODO | Add `rest-api-protocol` to `rest-api-client/Cargo.toml` | | -| T3 | TODO | Define `ClientError` enum | Wraps reqwest error, deserialization, API errors | -| T4 | TODO | Add `ApiClient` struct before `ApiHttpClient` in `client.rs` | New high-level typed client | -| T5 | TODO | Implement typed methods on `ApiClient` for all endpoints | Returns `Result` | -| T6 | TODO | Verify pre-commit and pre-push checks pass | | +| T1 | DONE | Rename `Client` → `ApiHttpClient` in `client.rs` | Compiler catches all references | +| T2 | DONE | Add `rest-api-protocol` to `rest-api-client/Cargo.toml` | | +| T3 | DONE | Define `ClientError` enum | Wraps reqwest error, deserialization, API errors | +| T4 | DONE | Add `ApiClient` struct before `ApiHttpClient` in `client.rs` | New high-level typed client | +| T5 | DONE | Implement typed methods on `ApiClient` for all endpoints | Returns `Result` | +| T6 | DONE | Verify pre-commit and pre-push checks pass | | ## Verification / Progress -- [ ] `Client` renamed to `ApiHttpClient` across the codebase -- [ ] `rest-api-protocol` added as dependency -- [ ] `ClientError` enum defined -- [ ] `ApiClient` struct with typed methods for all endpoints added -- [ ] `ApiClient` appears before `ApiHttpClient` in `client.rs` -- [ ] All existing tests pass unchanged -- [ ] Pre-commit checks pass -- [ ] Pre-push checks pass +- [x] `Client` renamed to `ApiHttpClient` across the codebase +- [x] `rest-api-protocol` added as dependency +- [x] `ClientError` enum defined +- [x] `ApiClient` struct with typed methods for all endpoints added +- [x] `ApiClient` appears before `ApiHttpClient` in `client.rs` +- [x] All existing tests pass unchanged +- [x] Pre-commit checks pass +- [x] Pre-push checks pass ### Progress Log diff --git a/docs/issues/open/1959-1938-si-7-review-tests-align-v1-namespace.md b/docs/issues/open/1959-1938-si-7-review-tests-align-v1-namespace.md new file mode 100644 index 000000000..3b1af141d --- /dev/null +++ b/docs/issues/open/1959-1938-si-7-review-tests-align-v1-namespace.md @@ -0,0 +1,116 @@ +--- +doc-type: spec +issue-type: task +status: planned +priority: p3 +epic: 1938 +github-issue: 1959 +spec-path: docs/issues/open/1959-1938-si-7-review-tests-align-v1-namespace.md +last-updated-utc: 2026-06-29 +semantic-links: + skill-links: + - create-issue + related-artifacts: + - docs/issues/open/1938-rest-api-contract-first-migration/EPIC.md + - packages/axum-rest-api-server/src/v1/context/torrent/resources/torrent.rs + - packages/axum-rest-api-server/src/v1/context/torrent/resources/peer.rs + - packages/rest-api-runtime-adapter/src/conversion.rs + - packages/rest-api-application/src/ + - packages/rest-api-runtime-adapter/src/ + - packages/rest-api-protocol/src/ + - packages/axum-rest-api-server/src/ + - packages/rest-api-client/src/ +--- + + + +# SI-7: Review tests and align v1 namespace across REST API packages + +## Subissue of REST API Contract-First Migration EPIC + +## Problem + +During the contract-first migration (SI-1 through SI-5), production code was moved from `axum-rest-api-server` to the new layered packages (`rest-api-protocol`, `rest-api-application`, `rest-api-runtime-adapter`). However, some unit tests were left behind in the wrong package, and the `v1` namespace is not consistently applied across all packages. + +### Issue 1: Tests in wrong packages + +The file `packages/axum-rest-api-server/src/v1/context/torrent/resources/torrent.rs` contains two unit tests that test functions defined in `rest-api-runtime-adapter::conversion`: + +- `torrent_resource_should_be_converted_from_torrent_info()` — tests `conversion::from_domain_info()` +- `torrent_resource_list_item_should_be_converted_from_the_basic_torrent_info()` — tests `conversion::list_item_from_domain()` + +These tests should live alongside the production code they test, in `rest-api-runtime-adapter`. + +Additionally, `packages/axum-rest-api-server/src/v1/context/torrent/resources/peer.rs` is a stub file containing only a doc comment saying _"Protocol DTOs are defined in `rest-api-protocol`."_ — it has no production code and should be removed. + +A review of the whole `axum-rest-api-server` package is needed to identify all such cases. + +### Issue 2: Inconsistent v1 namespace + +The API packages use the `v1` module inconsistently: + +| Package | Has `v1` module? | Notes | +| -------------------------- | ------------------ | -------------------------------------------- | +| `rest-api-protocol` | ✅ `src/v1/mod.rs` | Canonical home for v1 DTOs | +| `axum-rest-api-server` | ✅ `src/v1/` | Axum handlers, routes, responses | +| `rest-api-client` | ✅ `src/v1/` | Client for v1 endpoints | +| `rest-api-application` | ❌ No `v1` | Ports and use-cases at top level | +| `rest-api-runtime-adapter` | ❌ No `v1` | Adapters, container, conversion at top level | + +For `rest-api-application` and `rest-api-runtime-adapter`, the content is specific to the v1 API contract. Adding a `v1` module would align them with the other packages and make the version boundary explicit. + +## Scope + +### In Scope + +#### Part A: Move misplaced tests + +- Move the two conversion tests from `axum-rest-api-server/src/v1/context/torrent/resources/torrent.rs` to `rest-api-runtime-adapter/src/conversion.rs` (or a new `tests/` module in that package). +- Remove the empty stub file `axum-rest-api-server/src/v1/context/torrent/resources/peer.rs` and its module declaration. +- Review the entire `axum-rest-api-server` package for any other tests that test code from other packages. + +#### Part B: Align v1 namespace + +- Add `src/v1/` module to `rest-api-application` and move `ports/` and `use_cases/` under it. +- Add `src/v1/` module to `rest-api-runtime-adapter` and move `adapters/`, `container.rs`, `conversion.rs` under it. +- Update all internal imports across the workspace to use the new paths. +- Update `lib.rs` in both packages to re-export from `v1`. + +### Out of Scope + +- Changing test logic or adding new tests — only moving existing tests. +- Changing the Axum server test infrastructure or integration tests. +- Creating the SI-6 `ApiClient` implementation. + +## Implementation Plan + +### Part A: Move misplaced tests + +| ID | Status | Task | Notes | +| --- | ------ | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| A1 | TODO | Move conversion tests from `axum-rest-api-server` to `rest-api-runtime-adapter::conversion` | Tests for `from_domain_info()` and `list_item_from_domain()` | +| A2 | TODO | Remove empty `axum-rest-api-server/src/v1/context/torrent/resources/peer.rs` stub | Only doc comment, no code | +| A3 | TODO | Clean up module declarations after removing peer.rs | Remove `pub mod peer;` from `resources/mod.rs` | +| A4 | TODO | Review the whole `axum-rest-api-server/` package for similar misplaced tests | Check all context handlers, responses, routes | + +### Part B: Align v1 namespace + +| ID | Status | Task | Notes | +| --- | ------ | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| B1 | TODO | Add `v1/` module to `rest-api-application`, move `ports/` and `use_cases/` under it | Update `lib.rs` | +| B2 | TODO | Add `v1/` module to `rest-api-runtime-adapter`, move `adapters/`, `container.rs`, `conversion.rs` under it | Update `lib.rs` | +| B3 | TODO | Update internal imports across workspace | For `rest-api-application` and `rest-api-runtime-adapter` consumers | +| B4 | TODO | Verify workspace builds cleanly | `cargo build` | +| B5 | TODO | Pre-commit and pre-push checks pass | | + +## Verification / Progress + +- [ ] A1: Conversion tests moved to `rest-api-runtime-adapter` +- [ ] A2: Empty `peer.rs` stub removed +- [ ] A3: Module declarations cleaned up +- [ ] A4: No other misplaced tests found in `axum-rest-api-server` +- [ ] B1: `rest-api-application` has `v1/` module with ports + use-cases +- [ ] B2: `rest-api-runtime-adapter` has `v1/` module with adapters + container + conversion +- [ ] B3: All internal imports updated +- [ ] B4: Workspace builds cleanly +- [ ] B5: Pre-commit and pre-push checks pass diff --git a/docs/issues/open/1964-rename-number-of-downloads-btree-map-type-alias.md b/docs/issues/open/1964-rename-number-of-downloads-btree-map-type-alias.md new file mode 100644 index 000000000..f7365233a --- /dev/null +++ b/docs/issues/open/1964-rename-number-of-downloads-btree-map-type-alias.md @@ -0,0 +1,159 @@ +--- +doc-type: issue +issue-type: task +status: planned +priority: p2 +github-issue: 1964 +spec-path: docs/issues/open/1964-rename-number-of-downloads-btree-map-type-alias.md +branch: "1964-rename-number-of-downloads-btree-map" +related-pr: null +last-updated-utc: 2026-06-30 12:00 +semantic-links: + skill-links: + - create-issue + related-artifacts: + - packages/primitives/src/lib.rs + - packages/tracker-core/src/databases/traits/torrent_metrics.rs + - packages/tracker-core/src/databases/driver/sqlite/torrent_metrics_store.rs + - packages/tracker-core/src/databases/driver/mysql/torrent_metrics_store.rs + - packages/tracker-core/src/databases/driver/postgres/torrent_metrics_store.rs + - packages/tracker-core/src/statistics/persisted/downloads.rs + - packages/tracker-core/src/torrent/repository/in_memory.rs + - packages/tracker-core/src/torrent/manager.rs + - packages/swarm-coordination-registry/src/swarm/registry.rs + - packages/torrent-repository-benchmarking/src/repository/mod.rs + - packages/torrent-repository-benchmarking/src/repository/ + - packages/torrent-repository-benchmarking/tests/ +--- + + + +# Issue #1964 - Rename `NumberOfDownloadsBTreeMap` to `NumberOfDownloadsPerInfoHash` + +## Goal + +Rename the type alias `NumberOfDownloadsBTreeMap` to `NumberOfDownloadsPerInfoHash` so the name +expresses the _intent_ of the type ("downloads per info-hash") rather than its internal +implementation (`BTreeMap`). + +## Background + +The type alias is defined in `packages/primitives/src/lib.rs`: + +```rust +pub type NumberOfDownloads = u32; +pub type NumberOfDownloadsBTreeMap = BTreeMap; +``` + +It represents the number of completed downloads per info-hash and serves as the persistence +boundary for torrent download counts — used by all three database drivers (SQLite, MySQL, +PostgreSQL) when loading torrent metrics from the database. + +The current name `NumberOfDownloadsBTreeMap` leaks the implementation detail (`BTreeMap`). If the +underlying collection were ever changed (e.g., to a `HashMap`), the name would become misleading +and need a follow-up rename. + +The sibling type `NumberOfDownloads` is named after _what_ it represents, not _how_ it's stored +(`u32`). The pair should follow the same convention. + +A workspace-wide search found 19 source files and 4 documentation files referencing this alias, +making this a low-risk but moderately broad rename. + +## Scope + +### In Scope + +- Rename `NumberOfDownloadsBTreeMap` to `NumberOfDownloadsPerInfoHash` in `packages/primitives/src/lib.rs` +- Update all references across the workspace (~19 source files + 4 doc files) +- Verify `linter all` and the full test suite pass + +### Out of Scope + +- Changing the underlying collection type (`BTreeMap` → something else) +- Renaming other type aliases in the codebase +- Changing the `NumberOfDownloads` alias (already well-named) + +## Implementation Plan + +Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`. + +| ID | Status | Task | Notes / Expected Output | +| --- | ------ | ------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| T1 | TODO | Rename definition in primitives crate | Change `NumberOfDownloadsBTreeMap` to `NumberOfDownloadsPerInfoHash` in `packages/primitives/src/lib.rs` | +| T2 | TODO | Update core domain references | Update imports/usages in `tracker-core`, `swarm-coordination-registry`, etc. | +| T3 | TODO | Update benchmarking references | Update imports/usages in `torrent-repository-benchmarking` crate and tests | +| T4 | TODO | Update documentation | Update the 4 doc files referencing the old name | +| T5 | TODO | Run full verification | `linter all`, `cargo test --workspace`, pre-commit checks | + +## Progress Tracking + +### Workflow Checkpoints + +- [x] Spec drafted in `docs/issues/drafts/` +- [ ] Spec reviewed and approved by user/maintainer +- [ ] GitHub issue created and issue number added to this spec +- [ ] (Optional, recommended for complex issues) Spec-only PR merged into `develop` before implementation +- [ ] Implementation completed +- [ ] Automatic verification completed (`linter all`, relevant tests, and any pre-push checks) +- [ ] Manual verification scenarios executed and recorded (status + evidence) +- [ ] Acceptance criteria reviewed after implementation and updated with evidence +- [ ] Reviewer validated acceptance criteria and updated checkboxes +- [ ] Committer verified spec progress is up to date before commit +- [ ] Issue closed and spec moved from `docs/issues/open/` to `docs/issues/closed/` + +### Progress Log + +- 2026-06-30 12:00 UTC - Copilot - Spec draft created + +## Acceptance Criteria + +- [ ] AC1: `NumberOfDownloadsBTreeMap` no longer appears anywhere in the codebase +- [ ] AC2: `NumberOfDownloadsPerInfoHash` is the sole name for the type alias +- [ ] AC3: All tests pass (`cargo test --workspace`) +- [ ] AC4: `linter all` exits with code `0` +- [ ] AC5: Pre-commit checks pass +- [ ] Manual verification scenarios are executed and documented (status + evidence) +- [ ] Acceptance criteria are re-reviewed after implementation and reflect actual behavior + +## Verification Plan + +### Automatic Checks + +- `linter all` +- `cargo test --workspace` +- Pre-commit checks (`./contrib/dev-tools/git/hooks/pre-commit.sh`) + +### Manual Verification Scenarios + +Status values: `TODO`, `IN_PROGRESS`, `DONE`, `FAILED`, `BLOCKED`. + +| ID | Scenario | Command/Steps | Expected Result | Status | Evidence | +| --- | --------------------------- | ----------------------------------------------------------------------- | ------------------------------------------ | ------ | ---------------------------- | +| M1 | Build succeeds after rename | `cargo build --workspace` | Zero errors, no warnings related to rename | TODO | {log/output/screenshot/path} | +| M2 | grep confirms no old name | `grep -r "NumberOfDownloadsBTreeMap" --include="*.rs" --include="*.md"` | No matches found | TODO | {log/output/screenshot/path} | + +### Acceptance Verification + +| AC ID | Status (`TODO`/`DONE`) | Evidence | +| ----- | ---------------------- | ------------------ | +| AC1 | TODO | {test/log/PR link} | +| AC2 | TODO | {test/log/PR link} | +| AC3 | TODO | {test/log/PR link} | +| AC4 | TODO | {test/log/PR link} | +| AC5 | TODO | {test/log/PR link} | + +## Risks and Trade-offs + +- **Risk**: Mass rename could miss a reference if a file uses a differently-formatted reference + (e.g., macro-generated code). **Mitigation**: grep for the old name after the rename to confirm + zero matches. +- **Risk**: External consumers of `torrust-tracker-primitives` (crates.io) could break if they + depend on the old name. **Mitigation**: Check if any published reverse-dependencies use this + type. The crate has minimal external consumers and the type is internal-facing. + +## References + +- Definition: `packages/primitives/src/lib.rs` (line 71) +- Usage sites: 19 source files across `tracker-core`, `swarm-coordination-registry`, + `torrent-repository-benchmarking`, and their tests +- Docs: 4 documentation files in `docs/issues/` referencing the type diff --git a/docs/issues/open/1965-1669-si-34-consolidate-duplicate-http-types/ISSUE.md b/docs/issues/open/1965-1669-si-34-consolidate-duplicate-http-types/ISSUE.md new file mode 100644 index 000000000..a1c1fe82c --- /dev/null +++ b/docs/issues/open/1965-1669-si-34-consolidate-duplicate-http-types/ISSUE.md @@ -0,0 +1,197 @@ +--- +doc-type: issue +issue-type: task +status: planned +priority: p1 +github-issue: 1965 +spec-path: docs/issues/open/1965-1669-si-34-consolidate-duplicate-http-types/ISSUE.md +issue-folder: docs/issues/open/1965-1669-si-34-consolidate-duplicate-http-types/ +branch: "1965-1669-si-34-consolidate-duplicate-http-types" +related-pr: null +last-updated-utc: 2026-06-30 12:00 +semantic-links: + skill-links: + - create-issue + - run-tracker-locally + related-artifacts: + - docs/issues/open/1669-overhaul-packages/EPIC.md + - docs/issues/open/1669-overhaul-packages/DECISIONS.md + - packages/http-protocol/src/v1/requests/ + - packages/http-protocol/src/v1/responses/ + - packages/axum-http-server/tests/server/requests/ + - packages/axum-http-server/tests/server/responses/ + - packages/tracker-client/src/http/client/requests/ + - packages/tracker-client/src/http/client/responses/ + - .github/skills/dev/environment-setup/run-tracker-locally/SKILL.md +--- + + + +# Issue #1965 - EPIC 1669 SI-34: Consolidate Duplicate HTTP Types into `http-protocol` + +> **Parent EPIC**: [#1669 — Overhaul: Packages](https://github.com/torrust/torrust-tracker/issues/1669) +> **EPIC Reference**: `docs/issues/open/1669-overhaul-packages/EPIC.md` +> +> **Issue type**: Folder issue — manual verification evidence and command logs will be documented +> in a separate `manual-verification.md` file alongside this spec inside the issue folder at +> `docs/issues/open/1965-1669-si-34-consolidate-duplicate-http-types/`. + +## Goal + +Eliminate duplicate HTTP request/response type definitions across the workspace by consolidating +them into `packages/http-protocol`, and add the `http-protocol` dependency to `tracker-client` so +both consumers import from a single source of truth. + +## Background + +Three crate locations define overlapping HTTP request and response types: + +1. **`packages/http-protocol/src/v1/{requests,responses}/`** — server-side protocol parsing (production library) +2. **`packages/axum-http-server/tests/server/{requests,responses}/`** — test helpers (test-only code) +3. **`packages/tracker-client/src/http/client/{requests,responses}/`** — tracker client library (production library) + +Locations (2) and (3) define their own copies of types that semantically belong in (1): + +- `axum-http-server` **has** `http-protocol` as a dependency, but its tests define their own types instead of using it +- `tracker-client` does **not** depend on `http-protocol` at all + +The duplication creates maintenance burden: any change to these types must be replicated in two +or three places. Several types (especially `Error`, `Compact`, `CompactPeer`, `CompactPeerList`, +scrape `Query`/`QueryBuilder`/`QueryParams`, `ByteArray20`, `InfoHash`, `percent_encode_byte_array`) +are byte-for-byte identical between locations (2) and (3). + +The `http-protocol` crate is the canonical home for HTTP tracker protocol types. Client-side +parsing/serialization types are a natural extension of this crate, not a separate concern. + +## Scope + +### In Scope + +- Add client-side request construction and response deserialization types to `packages/http-protocol` + (e.g., query builders, response structs with `serde_bencode` derives) +- Replace duplicate types in `packages/axum-http-server/tests/server/` with imports from `http-protocol` +- Replace duplicate types in `packages/tracker-client/src/http/client/` with imports from `http-protocol` +- Add `http-protocol` as a dependency of `tracker-client` +- Create a `use-tracker-client` skill in `.github/skills/usage/` capturing the manual verification learnings +- Verify all tests pass and no functionality regresses + +### Out of Scope + +- Merging `packages/http-protocol` with other protocol crates +- Changing the public API of `http-protocol` beyond what's needed for consolidation +- Removing or refactoring the server-side types in `http-protocol` +- Changing how `axum-http-server` production code uses `http-protocol` + +## Implementation Plan + +Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`. + +| ID | Status | Task | Notes / Expected Output | +| --- | ------ | --------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| T1 | TODO | Survey duplicate types and decide merge strategy | Catalog exact types to move; identify which location has the "best" version | +| T2 | TODO | Add client-side types to `http-protocol` | Move query builders, response deserialization structs, and shared helpers | +| T3 | TODO | Add `http-protocol` dependency to `tracker-client` | Update `Cargo.toml`, verify dependency tree | +| T4 | TODO | Replace duplicate types in `tracker-client` | Delete local copies, update imports to `http-protocol` | +| T5 | TODO | Replace duplicate types in `axum-http-server` tests | Delete local copies, update imports to `http-protocol` | +| T6 | TODO | Run full verification | `linter all`, `cargo test --workspace`, pre-commit, pre-push | +| T7 | TODO | Create `use-tracker-client` skill | New skill in `.github/skills/usage/use-tracker-client/` with learnings from manual verification | + +## Progress Tracking + +### Workflow Checkpoints + +- [x] Spec drafted in `docs/issues/drafts/` +- [ ] Spec reviewed and approved by user/maintainer +- [ ] GitHub issue created and issue number added to this spec +- [ ] (Optional, recommended for complex issues) Spec-only PR merged into `develop` before implementation +- [ ] Implementation completed +- [ ] Automatic verification completed (`linter all`, relevant tests, and any pre-push checks) +- [ ] Manual verification scenarios executed and recorded (status + evidence) +- [ ] Acceptance criteria reviewed after implementation and updated with evidence +- [ ] Reviewer validated acceptance criteria and updated checkboxes +- [ ] Committer verified spec progress is up to date before commit +- [ ] Issue closed and spec moved from `docs/issues/open/` to `docs/issues/closed/` + +### Progress Log + +- 2026-06-30 12:00 UTC - Copilot - Spec draft created + +## Acceptance Criteria + +- [ ] AC1: No HTTP request/response types are duplicated between `http-protocol`, `axum-http-server` tests, and `tracker-client` +- [ ] AC2: `tracker-client` depends on `http-protocol` and imports types from it instead of defining its own +- [ ] AC3: `axum-http-server` tests import types from `http-protocol` instead of defining their own +- [ ] AC4: All existing tests pass (`cargo test --workspace`) +- [ ] AC5: `linter all` exits with code `0` +- [ ] AC6: Pre-commit and pre-push checks pass +- [ ] AC7: No `deps.rs` or layer-violation regressions +- [ ] AC8: `use-tracker-client` skill is created in `.github/skills/usage/` with proper YAML frontmatter and instructions +- [ ] Manual verification scenarios are executed and documented (status + evidence) +- [ ] Acceptance criteria are re-reviewed after implementation and reflect actual behavior + +## Verification Plan + +### Automatic Checks + +- `linter all` +- `cargo test --workspace` +- Pre-commit checks (`./contrib/dev-tools/git/hooks/pre-commit.sh`) +- Pre-push checks (`./contrib/dev-tools/git/hooks/pre-push.sh`) +- `cargo machete` (no unused dependencies introduced) + +### Manual Verification Scenarios + +Status values: `TODO`, `IN_PROGRESS`, `DONE`, `FAILED`, `BLOCKED`. + +All manual verification evidence — including full command output, troubleshooting notes, and +step-by-step logs — will be recorded in a separate `manual-verification.md` file inside the +issue folder. The Evidence column below links to the relevant section of that file. + +**Skills used during manual verification**: + +- **Run tracker locally**: [`../../../../.github/skills/dev/environment-setup/run-tracker-locally/SKILL.md`](../../../../.github/skills/dev/environment-setup/run-tracker-locally/SKILL.md) — start the tracker with default development configuration +- **Tracker client**: No dedicated skill exists yet. A `use-tracker-client` skill will be created + in `../../../../.github/skills/usage/` as the final step of this issue, capturing the learnings from the + manual verification process. + +| ID | Scenario | Command/Steps | Expected Result | Status | Evidence | +| --- | ----------------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------- | ------ | ------------------------------------ | +| M1 | HTTP tracker announces work with tracker-client | Run `tracker_client http announce` against a local tracker; verify request/response flow | Same behavior as before the consolidation | TODO | `manual-verification.md#m1-announce` | +| M2 | HTTP scrape works with tracker-client | Run `tracker_client http scrape` against a local tracker | Same behavior as before | TODO | `manual-verification.md#m2-scrape` | +| M3 | axum-http-server integration tests pass | `cargo test -p torrust-tracker-axum-http-server --test integration` | All tests pass | TODO | `manual-verification.md#m3-tests` | +| M4 | No duplicate type definitions remain | `grep` for key struct names (e.g., `struct Query`, `struct CompactPeer`) in old paths | Only imports, no local definitions for merged types | TODO | `manual-verification.md#m4-grep` | + +### Acceptance Verification + +| AC ID | Status (`TODO`/`DONE`) | Evidence | +| ----- | ---------------------- | ------------------ | +| AC1 | TODO | {test/log/PR link} | +| AC2 | TODO | {test/log/PR link} | +| AC3 | TODO | {test/log/PR link} | +| AC4 | TODO | {test/log/PR link} | +| AC5 | TODO | {test/log/PR link} | +| AC6 | TODO | {test/log/PR link} | +| AC7 | TODO | {test/log/PR link} | +| AC8 | TODO | {test/log/PR link} | + +## Risks and Trade-offs + +- **Risk**: Client-side types differ subtly between `tracker-client` and `axum-http-server` tests + (e.g., `Event` default variant, `numwant` field presence). **Mitigation**: The implementer must + survey both versions and ensure the consolidated type in `http-protocol` accommodates both use + cases. Where differences are intentional, use configuration (e.g., builder methods, `Option` + fields) rather than separate types. +- **Risk**: Adding `http-protocol` as a dependency of `tracker-client` increases compile time for + the client. **Mitigation**: `http-protocol` is already a lightweight crate with few transitive + dependencies; the impact should be negligible. +- **Risk**: The consolidation might change the public API of `http-protocol`, potentially breaking + external consumers. **Mitigation**: Review all existing `pub` exports and ensure backward + compatibility, or bump the version appropriately with clear changelog entries. + +## References + +- Parent EPIC: [#1669](https://github.com/torrust/torrust-tracker/issues/1669) +- EPIC spec: `docs/issues/open/1669-overhaul-packages/EPIC.md` +- Decisions log: `docs/issues/open/1669-overhaul-packages/DECISIONS.md` +- Duplicate analysis: exploration performed 2026-06-30 by Copilot +- Related ADR: `docs/adrs/20260527175600_keep_protocol_and_domain_types_decoupled.md` diff --git a/docs/issues/open/1965-1669-si-34-consolidate-duplicate-http-types/manual-verification.md b/docs/issues/open/1965-1669-si-34-consolidate-duplicate-http-types/manual-verification.md new file mode 100644 index 000000000..cf13f977a --- /dev/null +++ b/docs/issues/open/1965-1669-si-34-consolidate-duplicate-http-types/manual-verification.md @@ -0,0 +1,123 @@ +--- +doc-type: issue +issue-type: task +status: planned +priority: p1 +github-issue: 1965 +spec-path: docs/issues/open/1965-1669-si-34-consolidate-duplicate-http-types/manual-verification.md +last-updated-utc: 2026-06-30 12:00 +--- + +# Manual Verification — Issue #1965 (EPIC 1669 SI-34) + +> This file records manual verification evidence for the issue. +> It is populated during implementation. +> +> Skills used: +> +> - Run tracker locally: `.github/skills/dev/environment-setup/run-tracker-locally/SKILL.md` +> - Tracker client: skill to be created as part of this issue (T7) + +--- + +## M1: HTTP tracker announces work with tracker-client + +| Field | Value | +| ---------------- | ------ | +| **Status** | `TODO` | +| **Date** | | +| **Performed by** | | + +### Steps + +```text +(To be filled during verification) +``` + +### Output + +```text +(To be filled during verification) +``` + +### Result + +(To be filled: PASS / FAIL with notes) + +--- + +## M2: HTTP scrape works with tracker-client + +| Field | Value | +| ---------------- | ------ | +| **Status** | `TODO` | +| **Date** | | +| **Performed by** | | + +### Steps + +```text +(To be filled during verification) +``` + +### Output + +```text +(To be filled during verification) +``` + +### Result + +(To be filled: PASS / FAIL with notes) + +--- + +## M3: axum-http-server integration tests pass + +| Field | Value | +| ---------------- | ------ | +| **Status** | `TODO` | +| **Date** | | +| **Performed by** | | + +### Steps + +```text +(To be filled during verification) +``` + +### Output + +```text +(To be filled during verification) +``` + +### Result + +(To be filled: PASS / FAIL with notes) + +--- + +## M4: No duplicate type definitions remain + +| Field | Value | +| ---------------- | ------ | +| **Status** | `TODO` | +| **Date** | | +| **Performed by** | | + +### Steps + +```text +(To be filled during verification) +``` + +### Output + +```text +(To be filled during verification) +``` + +### Result + +(To be filled: PASS / FAIL with notes) diff --git a/docs/issues/open/1966-1669-si-35-consolidate-duplicate-udp-types.md b/docs/issues/open/1966-1669-si-35-consolidate-duplicate-udp-types.md new file mode 100644 index 000000000..ac36075a5 --- /dev/null +++ b/docs/issues/open/1966-1669-si-35-consolidate-duplicate-udp-types.md @@ -0,0 +1,219 @@ +--- +doc-type: issue +issue-type: task +status: planned +priority: p2 +github-issue: 1966 +spec-path: docs/issues/open/1966-1669-si-35-consolidate-duplicate-udp-types.md +branch: "1966-1669-si-35-consolidate-duplicate-udp-types" +related-pr: null +last-updated-utc: 2026-06-30 12:00 +semantic-links: + skill-links: + - create-issue + related-artifacts: + - docs/issues/open/1669-overhaul-packages/EPIC.md + - docs/issues/open/1669-overhaul-packages/DECISIONS.md + - packages/udp-protocol/src/ + - packages/udp-core/src/event.rs + - packages/udp-server/src/event.rs + - packages/udp-server/src/lib.rs + - packages/tracker-client/src/udp/mod.rs + - docs/adrs/20260527175600_keep_protocol_and_domain_types_decoupled.md + - packages/primitives/src/announce.rs + - packages/http-protocol/src/v1/requests/announce.rs + - packages/http-protocol/src/v1/responses/announce.rs + - packages/http-protocol/src/v1/responses/scrape.rs +--- + + + +# Issue #1966 - EPIC 1669 SI-35: Consolidate Duplicate UDP Types + +> **Parent EPIC**: [#1669 — Overhaul: Packages](https://github.com/torrust/torrust-tracker/issues/1669) +> **EPIC Reference**: `docs/issues/open/1669-overhaul-packages/EPIC.md` + +## Goal + +Eliminate duplicate type definitions and constants in the UDP tracker packages by consolidating +them into their canonical locations. + +## Background + +A workspace-wide audit of UDP-related packages found that the UDP layer is significantly cleaner +than the HTTP layer — the core protocol types (`ConnectRequest`, `ConnectResponse`, +`AnnounceRequest`, `AnnounceResponse`, `ScrapeRequest`, `ScrapeResponse`, `Request`, `Response`, +`ErrorResponse`, `ResponsePeer`, `TorrentScrapeStatistics`) are defined exclusively in +`packages/udp-protocol/src/` and imported everywhere else. This is the correct architecture. + +However, three duplications were found: + +### 🔴 `ConnectionContext` — full copy-paste + +The struct and its entire `impl` block are duplicated between: + +| | `packages/udp-core/src/event.rs` (line 26) | `packages/udp-server/src/event.rs` (line 85) | +| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| **Fields** | `pub client_socket_addr: SocketAddr`, `pub server_service_binding: ServiceBinding` | `client_socket_addr: SocketAddr` (private), `server_service_binding: ServiceBinding` (private) | +| **Methods** | `new()`, `client_socket_addr()`, `server_socket_addr()`, `client_address_ip_family()`, `client_address_ip_type()` | `new()`, `client_socket_addr()`, `server_socket_addr()`, `client_address_ip_family()`, `client_address_ip_type()` | +| **Derive** | `Debug, PartialEq, Eq, Clone` | `Debug, PartialEq, Eq, Clone` | +| **`From for LabelSet`** | Yes | Yes | + +The only difference is field visibility (`pub` in core, private in server). The impl blocks are +identical. One should be the canonical definition and the other should import it. + +### 🟡 `MAX_PACKET_SIZE` — same constant, two locations + +| Package | File | Value | +| -------------------------------------------------- | ------------------------------------------ | ------ | +| `packages/udp-server/src/lib.rs` (line 651) | `pub const MAX_PACKET_SIZE: usize = 1496;` | `1496` | +| `packages/tracker-client/src/udp/mod.rs` (line 11) | `pub const MAX_PACKET_SIZE: usize = 1496;` | `1496` | + +The `tracker-client` already depends on `udp-protocol`. This constant could live in +`udp-protocol` and be shared by both consumers. + +### 🟡 `PROTOCOL_ID` — dead code copy + +| Package | Symbol | Value | Visibility | +| -------------------------------------------------- | --------------------- | ------------------- | ------------ | +| `packages/udp-protocol/src/connect.rs` (line 15) | `PROTOCOL_IDENTIFIER` | `4_497_486_125_440` | `pub(crate)` | +| `packages/tracker-client/src/udp/mod.rs` (line 14) | `PROTOCOL_ID` | `0x0417_2710_1980` | `pub` | + +Same magic constant with different names. `PROTOCOL_ID` in `tracker-client` is **unused** — a +grep shows no references to it anywhere. It should be removed. + +### 🟢 Intentional duplications (not in scope) + +The following are kept separate per +[ADR 20260527175600](docs/adrs/20260527175600_keep_protocol_and_domain_types_decoupled.md) +and are **not** addressed by this issue: + +- `AnnounceEvent` — `udp-protocol` vs `primitives` (wire type vs domain type) +- `InfoHash` — `udp-protocol` vs `torrust_info_hash` (wire type vs domain type) +- `NumberOfBytes` — `udp-protocol` vs `primitives` vs `http-protocol` (wire type vs domain type) + +These types currently have comments like `// Intentionally kept in...` or `// Intentional boundary duplication` but +do not explicitly reference the ADR. As part of this issue, each location will gain a `// adr:` comment so +future contributors understand the architectural reasoning and do not accidentally re-couple the types. + +**Code locations to annotate**: + +- `packages/udp-protocol/src/common.rs` — `InfoHash` (line 20) and `NumberOfBytes` (line 46) +- `packages/http-protocol/src/v1/requests/announce.rs` — `NumberOfBytes` (line 28) +- `packages/http-protocol/src/v1/responses/announce.rs` — `Announce` DTO (line 11) +- `packages/http-protocol/src/v1/responses/scrape.rs` — scrape response DTOs (lines 10, 20) +- `packages/primitives/src/announce.rs` — `AnnounceEvent` (line 91) + +## Scope + +### In Scope + +- Consolidate `ConnectionContext` into a single canonical definition (likely in `udp-core`) +- Move `MAX_PACKET_SIZE` to `udp-protocol` and import it in both `udp-server` and `tracker-client` +- Remove the unused `PROTOCOL_ID` constant from `tracker-client` +- Add `adr:` comments to the code locations listed under "Intentional duplications" referencing + ADR `docs/adrs/20260527175600_keep_protocol_and_domain_types_decoupled.md`, so future + contributors understand why the duplication exists and do not accidentally re-couple the types +- Verify all tests pass and no functionality regresses + +### Out of Scope + +- Merging protocol-level types (`AnnounceEvent`, `InfoHash`, `NumberOfBytes`) — governed by ADR +- Changing the public API of `udp-protocol` beyond what's needed for consolidation +- Refactoring the UDP server architecture + +## Implementation Plan + +Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`. + +| ID | Status | Task | Notes / Expected Output | +| --- | ------ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| T1 | TODO | Consolidate `ConnectionContext` into `udp-core` | Make `udp-server` import from `udp-core` instead of defining its own copy | +| T2 | TODO | Move `MAX_PACKET_SIZE` to `udp-protocol` | Add `pub const MAX_PACKET_SIZE: usize = 1496;` to `udp-protocol`; update imports | +| T3 | TODO | Remove dead `PROTOCOL_ID` from `tracker-client` | Delete the unused constant | +| T4 | TODO | Add `adr:` comments for intentional duplications | Annotate `udp-protocol/src/common.rs`, `http-protocol/src/v1/requests/announce.rs`, `http-protocol/src/v1/responses/announce.rs`, `http-protocol/src/v1/responses/scrape.rs`, and `primitives/src/announce.rs` with `// adr: docs/adrs/20260527175600...` comments | +| T5 | TODO | Run full verification | `linter all`, `cargo test --workspace`, pre-commit, pre-push | + +## Progress Tracking + +### Workflow Checkpoints + +- [x] Spec drafted in `docs/issues/drafts/` +- [ ] Spec reviewed and approved by user/maintainer +- [ ] GitHub issue created and issue number added to this spec +- [ ] (Optional, recommended for complex issues) Spec-only PR merged into `develop` before implementation +- [ ] Implementation completed +- [ ] Automatic verification completed (`linter all`, relevant tests, and any pre-push checks) +- [ ] Manual verification scenarios executed and recorded (status + evidence) +- [ ] Acceptance criteria reviewed after implementation and updated with evidence +- [ ] Reviewer validated acceptance criteria and updated checkboxes +- [ ] Committer verified spec progress is up to date before commit +- [ ] Issue closed and spec moved from `docs/issues/open/` to `docs/issues/closed/` + +### Progress Log + +- 2026-06-30 12:00 UTC - Copilot - Spec draft created + +## Acceptance Criteria + +- [ ] AC1: `ConnectionContext` is defined in exactly one location (imported by the other) +- [ ] AC2: `MAX_PACKET_SIZE` is defined in `udp-protocol` and imported by both `udp-server` and `tracker-client` +- [ ] AC3: `PROTOCOL_ID` no longer exists in `tracker-client` +- [ ] AC4: Each location listed in the "Intentional duplications" section has an `adr:` comment referencing the ADR +- [ ] AC5: All existing tests pass (`cargo test --workspace`) +- [ ] AC6: `linter all` exits with code `0` +- [ ] AC7: Pre-commit and pre-push checks pass +- [ ] Manual verification scenarios are executed and documented (status + evidence) +- [ ] Acceptance criteria are re-reviewed after implementation and reflect actual behavior + +## Verification Plan + +### Automatic Checks + +- `linter all` +- `cargo test --workspace` +- Pre-commit checks (`./contrib/dev-tools/git/hooks/pre-commit.sh`) +- Pre-push checks (`./contrib/dev-tools/git/hooks/pre-push.sh`) +- `cargo machete` (no unused dependencies introduced) + +### Manual Verification Scenarios + +Status values: `TODO`, `IN_PROGRESS`, `DONE`, `FAILED`, `BLOCKED`. + +| ID | Scenario | Command/Steps | Expected Result | Status | Evidence | +| --- | ---------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------- | ------ | ---------------------------- | +| M1 | UDP tracker announces work with tracker-client | Run `tracker_client udp announce` against a local tracker; verify request/response flow | Same behavior as before the consolidation | TODO | {log/output/screenshot/path} | +| M2 | UDP scrape works with tracker-client | Run `tracker_client udp scrape` against a local tracker | Same behavior as before | TODO | {log/output/screenshot/path} | +| M3 | udp-server tests pass | `cargo test -p torrust-tracker-udp-server` | All tests pass | TODO | {log/output/screenshot/path} | +| M4 | No duplicate definitions remain | `grep` for `ConnectionContext` and `MAX_PACKET_SIZE` across workspace | Only one definition each | TODO | {log/output/screenshot/path} | + +### Acceptance Verification + +| AC ID | Status (`TODO`/`DONE`) | Evidence | +| ----- | ---------------------- | ------------------ | +| AC1 | TODO | {test/log/PR link} | +| AC2 | TODO | {test/log/PR link} | +| AC3 | TODO | {test/log/PR link} | +| AC4 | TODO | {test/log/PR link} | +| AC5 | TODO | {test/log/PR link} | +| AC6 | TODO | {test/log/PR link} | +| AC7 | TODO | {test/log/PR link} | + +## Risks and Trade-offs + +- **Risk**: `ConnectionContext` has different field visibility (`pub` in core, private in server). + **Mitigation**: The consolidated definition should use `pub` fields (or provide accessor methods) + so both consumers can use it without friction. +- **Risk**: Moving `MAX_PACKET_SIZE` to `udp-protocol` changes its visibility scope. + **Mitigation**: Make it `pub` in `udp-protocol`; both consumers already depend on it. +- **Risk**: Removing `PROTOCOL_ID` could break something if it's used via macro or build script. + **Mitigation**: The grep confirmed zero references; removal is safe. + +## References + +- Parent EPIC: [#1669](https://github.com/torrust/torrust-tracker/issues/1669) +- EPIC spec: `docs/issues/open/1669-overhaul-packages/EPIC.md` +- Decisions log: `docs/issues/open/1669-overhaul-packages/DECISIONS.md` +- Duplicate analysis: exploration performed 2026-06-30 by Copilot +- Related ADR: `docs/adrs/20260527175600_keep_protocol_and_domain_types_decoupled.md` +- Related HTTP consolidation issue: `docs/issues/drafts/1669-si-34-consolidate-duplicate-http-types-into-http-protocol.md` diff --git a/docs/issues/open/1969-1938-si-8-eliminate-unwraps-from-rest-api-client.md b/docs/issues/open/1969-1938-si-8-eliminate-unwraps-from-rest-api-client.md new file mode 100644 index 000000000..a63b868af --- /dev/null +++ b/docs/issues/open/1969-1938-si-8-eliminate-unwraps-from-rest-api-client.md @@ -0,0 +1,101 @@ +--- +doc-type: spec +issue-type: task +status: planned +priority: p2 +epic: 1938 +github-issue: 1969 +spec-path: docs/issues/open/1969-1938-si-8-eliminate-unwraps-from-rest-api-client.md +last-updated-utc: 2026-06-30 +semantic-links: + skill-links: + - create-issue + related-artifacts: + - docs/issues/open/1938-rest-api-contract-first-migration/EPIC.md + - packages/rest-api-client/ + - packages/rest-api-client/src/v1/client.rs +--- + + + +# SI-8: Eliminate all unwraps from the REST API client package + +## Subissue of REST API Contract-First Migration EPIC + +## Goal + +Eliminate all `.unwrap()` calls from the `torrust-tracker-rest-api-client` package. Every operation that can fail must return a `Result`. For operations that are provably infallible, replace bare `.unwrap()` with an explicit `.expect("infallible: ...")` that documents why the operation cannot fail. + +## Background + +The `ApiClient` was made fully panic-free in SI-6 (PR #1968). However, the low-level `ApiHttpClient` and several free functions/helpers in `client.rs` still contain `.unwrap()` and `.expect()` calls that can panic at runtime. + +The calls fall into two categories: + +### Transport unwraps (must return `Result`) + +These are real failure points — network errors, URL parsing failures, etc. They must return `Result`: + +1. **11 public `ApiHttpClient` methods** — thin wrappers that delegate to fallible `*_result()` counterparts but `.unwrap()` the result. +2. **`post_empty()`, `post_form()`** (private) — same wrapper-with-unwrap pattern. +3. **`get()` (pub method on `ApiHttpClient`)** — same pattern. +4. **`get()` (pub free function)** — thin wrapper around `get_result()`. +5. **`get_request()` (pub on `ApiHttpClient`)** — calls `base_url()` which already returns `Result`. + +### Infallible conversions (replace `unwrap` with `expect`) + +These are provably infallible operations where a descriptive `expect` message is the right pattern: + +1. **`headers_with_request_id()`** — `Uuid::to_string()` always produces a valid ASCII string, and `HeaderValue::from_str()` for ASCII strings never fails. +2. **`headers_with_auth_token()`** — same pattern, pre-formatted token string. +3. **`get_request_with_query_result()` auth token inserts** — 2 token-to-HeaderValue conversions, same provably-infallible pattern. + +## Scope + +### In Scope + +- Change all panicking public `ApiHttpClient` methods to return `Result` instead of `Response`. +- Update all caller sites across the repository (contract tests, E2E tests, integration tests) to handle the new `Result` return types. +- Change helper functions (`post_empty`, `post_form`, `get`, `get_request`, `get()`) to return `Result`. +- Replace bare `.unwrap()` with `.expect("infallible: ...")` in `headers_with_request_id()`, `headers_with_auth_token()`, and `get_request_with_query_result()` auth token inserts. +- Update issue spec and documentation. + +### Out of Scope + +- Changing the `ApiHttpClient`'s HTTP transport or connection model. +- Adding retry/timeout policy (tracked separately). +- Removing the `ApiClient`/`ApiHttpClient` two-tier architecture. + +## Implementation Plan + +| ID | Status | Task | Notes | +| --- | ------ | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| T1 | TODO | Move `ApiHttpClient` public methods to return `Result` | 10 methods + `get()` method — return `ClientError` | +| T2 | TODO | Update all callers in contract tests (`packages/axum-rest-api-server/tests/`) | ~65 `ApiHttpClient::new(...)` call sites | +| T3 | TODO | Update callers in `src/console/ci/qbittorrent_e2e/tracker/client.rs` | E2E test runner wrapper | +| T4 | TODO | Update callers in `tests/servers/api/contract/stats/mod.rs` | Integration test | +| T5 | TODO | Replace bare `.unwrap()` with `.expect("infallible: ...")` for provably infallible conversions | `headers_with_request_id`, `headers_with_auth_token`, auth token inserts | +| T6 | TODO | Verify pre-commit and pre-push checks pass | | + +## Verification / Progress + +- [ ] All `ApiHttpClient` public methods return `Result` +- [ ] No bare `.unwrap()` calls remain (only `.expect("infallible: ...")` for provably infallible operations) +- [ ] All contract tests pass unchanged (except for updated `.unwrap()` calls on test side) +- [ ] E2E tests compile +- [ ] Pre-commit checks pass +- [ ] Pre-push checks pass + +## Acceptance Criteria + +- `ApiHttpClient` never panics on transport/URL failures; all errors are returned as `ClientError` +- Provably infallible conversions use `.expect("infallible: ...")` with a clear rationale +- No regressions in existing tests +- `linter all` passes + +### Progress Log + +| Date | Event | +| ---------- | ------------------ | +| 2026-06-30 | Spec drafted | +| 2026-06-30 | Spec moved to open | diff --git a/docs/packages.md b/docs/packages.md index a4dd94f3c..69eb24ef9 100644 --- a/docs/packages.md +++ b/docs/packages.md @@ -32,7 +32,6 @@ packages/ ├── primitives ├── rest-api-application ├── rest-api-client -├── rest-api-core ├── rest-api-protocol ├── rest-api-runtime-adapter ├── swarm-coordination-registry @@ -143,7 +142,7 @@ level, catching violations in pre-commit hooks and CI before merge. | Edge | Description | Current violations | | --------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------ | -| `core -> server` | Core must not depend on delivery-layer packages | `rest-api-core -> udp-server` | +| `core -> server` | Core must not depend on delivery-layer packages | None (historical `rest-api-core` removed in SI-5) | | `tracker-core -> core` | Tracker core must not depend on its protocol-specific wrappers | None | | `tracker-core -> protocol` | Tracker core must not depend on protocol parsing crates | None | | `tracker-core -> server` | Tracker core must not depend on server crates | None | @@ -186,16 +185,18 @@ For example, `torrust-tracker-udp-server` can only be depended on by: - `torrust-tracker` (root binary) - `torrust-tracker-axum-rest-api-server` - `torrust-tracker-axum-health-check-api-server` -- `torrust-tracker-rest-api-core` (known violation — see below) +- `torrust-tracker-rest-api-runtime-adapter` A core package like `torrust-tracker-http-core` adding `udp-server` as a dependency would be immediately rejected by `cargo deny check bans`. ### Known exceptions -- `torrust-tracker-rest-api-core` depends on `torrust-tracker-udp-server` - — a known violation tracked separately. Until it is fixed, the wrapper - list for `udp-server` includes `rest-api-core`. +None. The `rest-api-core` package was removed in SI-5 after its last consumer +(`axum-rest-api-server`) was migrated to use the `rest-api-runtime-adapter` +container. See issue [#1943][1943]. + +[1943]: https://github.com/torrust/torrust-tracker/issues/1943 ### Maintenance @@ -223,38 +224,37 @@ See `deny.toml` for the complete configuration. ## Package Catalog -| Package | Description | Key Responsibilities | -| --------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------- | -| **axum-\*** | | | -| `axum-server` | Base Axum HTTP server infrastructure | HTTP server lifecycle management | -| `axum-http-server` | BitTorrent HTTP tracker (BEP 3/23) | Handle announce/scrape requests | -| `axum-rest-api-server` | Management REST API (transport) | HTTP routing, request extraction, response serialization, middleware | -| `axum-health-check-api-server` | Health monitoring endpoint | System health reporting | -| **REST API** | Contract-first layers | See [REST API architecture](#rest-api-contract-first-architecture) | -| `rest-api-protocol` | REST API protocol contract | Versioned DTOs, error schemas, auth semantics | -| `rest-api-application` | REST API application | Port traits, use-case services, domain-error mapping | -| `rest-api-runtime-adapter` | REST API runtime adapter | Tracker-specific port implementations, domain→DTO conversions | -| `rest-api-core` | REST API core (deprecated) | Legacy integration container — to be replaced by application + adapter layers | -| **Core Components** | | | -| `http-core` | HTTP-specific implementation | Request validation, Response formatting | -| `udp-core` | UDP-specific implementation | Connectionless request handling | -| `tracker-core` | Central tracker logic | Peer management | -| **Protocols** | | | -| `http-protocol` | HTTP tracker protocol (BEP 3/23) | Announce/scrape request parsing | -| `udp-protocol` | UDP tracker protocol (BEP 15) | UDP message framing/parsing | -| **Domain** | | | -| `swarm-coordination-registry` | Peer swarm registry | Torrent/peer coordination | -| `configuration` | Runtime configuration | Config file parsing, Environment variables | -| `primitives` | Domain-specific types | PeerId, Peer, SwarmMetadata | -| `events` | Async event bus | Inter-package communication | -| **Utilities** | | | -| `test-helpers` | Testing utilities | Mock servers, Test data generation | -| **Client Tools** | | | -| `tracker-client` (`packages/`) | Tracker client library | Generic tracker client library | -| `rest-api-client` | API client library | REST API integration | -| **Benchmarking** | | | -| `torrent-repository-benchmarking` | Torrent storage benchmarks | Criterion benchmarks | -| `persistence-benchmark` | Persistence layer benchmarks | SQLite/MySQL/PostgreSQL benchmarks | +| Package | Description | Key Responsibilities | +| --------------------------------- | ------------------------------------ | -------------------------------------------------------------------- | +| **axum-\*** | | | +| `axum-server` | Base Axum HTTP server infrastructure | HTTP server lifecycle management | +| `axum-http-server` | BitTorrent HTTP tracker (BEP 3/23) | Handle announce/scrape requests | +| `axum-rest-api-server` | Management REST API (transport) | HTTP routing, request extraction, response serialization, middleware | +| `axum-health-check-api-server` | Health monitoring endpoint | System health reporting | +| **REST API** | Contract-first layers | See [REST API architecture](#rest-api-contract-first-architecture) | +| `rest-api-protocol` | REST API protocol contract | Versioned DTOs, error schemas, auth semantics | +| `rest-api-application` | REST API application | Port traits, use-case services, domain-error mapping | +| `rest-api-runtime-adapter` | REST API runtime adapter | Tracker-specific port implementations, domain→DTO conversions | +| **Core Components** | | | +| `http-core` | HTTP-specific implementation | Request validation, Response formatting | +| `udp-core` | UDP-specific implementation | Connectionless request handling | +| `tracker-core` | Central tracker logic | Peer management | +| **Protocols** | | | +| `http-protocol` | HTTP tracker protocol (BEP 3/23) | Announce/scrape request parsing | +| `udp-protocol` | UDP tracker protocol (BEP 15) | UDP message framing/parsing | +| **Domain** | | | +| `swarm-coordination-registry` | Peer swarm registry | Torrent/peer coordination | +| `configuration` | Runtime configuration | Config file parsing, Environment variables | +| `primitives` | Domain-specific types | PeerId, Peer, SwarmMetadata | +| `events` | Async event bus | Inter-package communication | +| **Utilities** | | | +| `test-helpers` | Testing utilities | Mock servers, Test data generation | +| **Client Tools** | | | +| `tracker-client` (`packages/`) | Tracker client library | Generic tracker client library | +| `rest-api-client` | API client library | REST API integration | +| **Benchmarking** | | | +| `torrent-repository-benchmarking` | Torrent storage benchmarks | Criterion benchmarks | +| `persistence-benchmark` | Persistence layer benchmarks | SQLite/MySQL/PostgreSQL benchmarks | ### Extracted Packages diff --git a/docs/pr-reviews/pr-1733-copilot-suggestions.md b/docs/pr-reviews/EXAMPLE-COMPLETED.md similarity index 96% rename from docs/pr-reviews/pr-1733-copilot-suggestions.md rename to docs/pr-reviews/EXAMPLE-COMPLETED.md index 90b06139d..eec879eff 100644 --- a/docs/pr-reviews/pr-1733-copilot-suggestions.md +++ b/docs/pr-reviews/EXAMPLE-COMPLETED.md @@ -6,9 +6,9 @@ semantic-links: - docs/pr-reviews/README.md --- -# PR #1733 Copilot Suggestions Tracking +# PR # Copilot Suggestions Tracking (EXAMPLE - COMPLETED) -Source: Copilot PR review threads for https://github.com/torrust/torrust-tracker/pull/1733 +Source: Copilot PR review threads for https://github.com/torrust/torrust-tracker/pull/ Status legend: @@ -18,9 +18,9 @@ Status legend: ## Processing Log -- 2026-05-06: Started processing suggestions (downloaded 26 threads from PR #1733) -- 2026-05-06: Applied code/doc fixes and committed changes -- 2026-05-06: Resolved all 26 threads in PR #1733 +- : Started processing suggestions (downloaded 26 threads from PR #) +- : Applied code/doc fixes and committed changes +- : Resolved all 26 threads in PR # All suggestions (action and no-action) have been processed and marked resolved. diff --git a/docs/pr-reviews/README.md b/docs/pr-reviews/README.md index bf70ec3c6..9bf99c41a 100644 --- a/docs/pr-reviews/README.md +++ b/docs/pr-reviews/README.md @@ -15,7 +15,7 @@ This directory contains tools and templates for managing GitHub Copilot code rev ## Files - [docs/templates/COPILOT-SUGGESTIONS-TEMPLATE.md](../templates/COPILOT-SUGGESTIONS-TEMPLATE.md) — Reusable template for tracking and processing Copilot suggestions on any PR. Copy and customize for each new PR. -- **pr-1733-copilot-suggestions.md** — Example of a completed suggestion review for PR #1733, showing how to document decisions, process suggestions, and track resolutions. +- **EXAMPLE-COMPLETED.md** — Example of a completed suggestion review, showing how to document decisions, process suggestions, and track resolutions. Use uppercase `EXAMPLE` files as reference; copy the template from `docs/templates/COPILOT-SUGGESTIONS-TEMPLATE.md` for new PRs. ## Workflow @@ -33,4 +33,4 @@ This directory contains tools and templates for managing GitHub Copilot code rev ## Example -See `pr-1733-copilot-suggestions.md` for a complete example where all 26 Copilot suggestions were reviewed, processed, and resolved. +See `EXAMPLE-COMPLETED.md` for a complete example where all 26 Copilot suggestions were reviewed, processed, and resolved. diff --git a/docs/pr-reviews/pr-1967-copilot-suggestions.md b/docs/pr-reviews/pr-1967-copilot-suggestions.md new file mode 100644 index 000000000..e382fd447 --- /dev/null +++ b/docs/pr-reviews/pr-1967-copilot-suggestions.md @@ -0,0 +1,44 @@ +--- +semantic-links: + skill-links: + - process-copilot-suggestions + related-artifacts: + - .github/skills/dev/pr-reviews/process-copilot-suggestions/SKILL.md +--- + + + + +# PR #1967 Copilot Suggestions Tracking + +Source: Copilot PR review threads for https://github.com/torrust/torrust-tracker/pull/1967 + +Status legend: + +- `action`: code/docs change applied +- `no-action`: suggestion reviewed; no code change needed +- `resolved`: thread resolved in PR + +## Workflow + +1. Download all review threads (including resolved/outdated state and thread IDs). +2. Add one row per thread in the Suggestions table. +3. Process suggestions one by one: + - decide `action` or `no-action` + - if `action`, apply change and validate + - if needed, commit changes + - resolve the PR thread +4. Set `Thread State` to `resolved` once resolved in PR. + +## Processing Log + +- 2026-06-30: Started processing suggestions. +- 2026-06-30: Completed processing suggestions. + +## Suggestions + +| # | Thread ID | Path | URL | Suggestion Summary | Decision | Status | Thread State | +| --- | ----------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------ | ------ | ------------ | +| 1 | `PRRT_kwDOGp2yqc6NNrmf` | `docs/issues/open/1965-1669-si-34-consolidate-duplicate-http-types/ISSUE.md` | [Comment](https://github.com/torrust/torrust-tracker/pull/1967#discussion_r3497403152) | Relative link `../../.github/skills/...` is broken — needs 4 `..` segments from the nested folder | action — fix the relative link | DONE | RESOLVED | +| 2 | `PRRT_kwDOGp2yqc6NNrnB` | `docs/issues/open/1965-1669-si-34-consolidate-duplicate-http-types/manual-verification.md` | [Comment](https://github.com/torrust/torrust-tracker/pull/1967#discussion_r3497403199) | Missing YAML frontmatter for docs metadata consistency | action — add YAML frontmatter | DONE | RESOLVED | +| 3 | `PRRT_kwDOGp2yqc6NNrnc` | `docs/issues/open/1966-1669-si-35-consolidate-duplicate-udp-types.md` | [Comment](https://github.com/torrust/torrust-tracker/pull/1967#discussion_r3497403232) | AC numbering duplicates AC5 and skips AC7 — renumber to align with table | action — fix AC numbering | DONE | RESOLVED | diff --git a/docs/security/README.md b/docs/security/README.md new file mode 100644 index 000000000..9e96cbd12 --- /dev/null +++ b/docs/security/README.md @@ -0,0 +1,86 @@ +# Security Overview + +This directory documents security considerations for the Torrust Tracker project, organized by priority level. + +## Priority Levels + +Security effort should be distributed according to exposure and risk. The highest-priority areas are those that directly affect end users in production. + +### Priority 1 — Production Docker Image (Critical) + +**Directory**: [`docker/`](docker/) + +The production Docker image (`release` stage based on `gcr.io/distroless/cc-debian13`) is the most critical security surface. It is exposed to the internet and runs continuously. Any vulnerability here directly affects tracker users. + +**Scope**: + +- The tracker binary +- OS base layer (distroless/cc-debian13) +- Transitive library dependencies (glibc, zlib) + +**Scan history**: [`docker/scans/`](docker/scans/) + +--- + +### Priority 2 — Vulnerability Analysis (Important) + +**Directory**: [`analysis/`](analysis/) + +When a security issue is detected (Docker Scout, dependabot, manual audit, container image scans), we create an analysis document to evaluate whether it actually affects the tracker. + +**Scope**: + +- Evaluating CVEs reported by scanning tools +- Documenting non-affecting vulnerabilities +- Tracking affecting vulnerabilities with remediation plans + +**Documents**: + +- [Analysis README](analysis/README.md) — process and document index +- [Non-affecting CVEs](analysis/non-affecting/) — analyzed and accepted vulnerabilities + +--- + +### Priority 3 — Build Chain Security (Standard) + +The build-time images (`chef`, `tester`, `gcc` stages in the `Containerfile`) are a **lower-risk surface** because: + +- They run only during CI/CD or local builds +- They are not exposed to the internet +- They produce the final artifact but are not deployed themselves + +This priority increases if the build images are ever used in a long-running service. + +**Scope**: + +- Base build images: `rust:trixie`, `rust:slim-trixie`, `gcc:trixie` +- Rust dependency vulnerabilities (`cargo audit` / RustSec) +- CI/CD pipeline security + +--- + +## Scan Tooling + +| Tool | Purpose | Run Command | +| ----- | ------------------------- | ---------------------------------------------- | +| Trivy | Docker image CVE scanning | `trivy image --severity HIGH,CRITICAL ` | + +## Current Security Status + +### Production Image + +See [`docker/scans/README.md`](docker/scans/README.md) for the latest status of the production `release` stage image. + +### Vulnerability Analysis + +See [`analysis/README.md`](analysis/README.md) for cataloged vulnerability evaluations. + +**Non-affecting CVE catalog**: [`analysis/non-affecting/`](analysis/non-affecting/) — +per-CVE files documenting why each vulnerability does not affect the tracker and what +conditions would change the verdict. + +## Related Documentation + +- [Docker Image Security](docker/README.md) — scanning instructions and scan history +- [Security Analysis](analysis/README.md) — CVE evaluation process +- [`SECURITY.md`](../../SECURITY.md) — project security policy and reporting diff --git a/docs/security/analysis/README.md b/docs/security/analysis/README.md index 5bfb6f0d0..1603635de 100644 --- a/docs/security/analysis/README.md +++ b/docs/security/analysis/README.md @@ -6,6 +6,7 @@ semantic-links: - Containerfile - docs/security/analysis/non-affecting/ - docs/adrs/20260603000000_keep_unit_tests_inside_container_build.md + - docs/security/docker/scans/torrust-tracker.md --- # Security Analysis @@ -29,39 +30,68 @@ container image vulnerability scanning), we create an analysis document here to: docs/security/analysis/ ├── README.md # This file — index and process ├── non-affecting/ # Vulnerabilities that do NOT affect us -│ └── {date}_{descriptive-name}.md -└── ... # (future) Affecting vulnerabilities go here +│ ├── CVE-{id}.md # Per-CVE files (preferred for individual CVEs) +│ └── {date}_{source}.md # Bulk scan/event files (for bulk triage) +└── affecting/ # (future) Vulnerabilities that DO affect us +``` + +## Catalog Strategy + +We use **one catalog** for all vulnerability sources (Docker scans, cargo-audit, dependabot, +etc.). A vulnerability is a vulnerability regardless of origin. + +### Per-CVE Files (preferred) + +Individual CVEs from container scans are documented in their own file: + +```text +non-affecting/ +├── CVE-2026-5435.md # glibc TSIG +├── CVE-2026-5450.md # glibc scanf +├── CVE-2026-5928.md # glibc ungetwc +├── CVE-2026-6238.md # glibc DNS response +└── CVE-2026-27171.md # zlib CRC32 +``` + +**Advantages**: + +- `grep -r CVE-2026-5435` finds it instantly. +- Fast to check "have we seen this before?" on any new scan. +- Each file carries its own `review-date`, `review-cadence`, and `requires-recheck-when` + in frontmatter. + +### Bulk Scan/Event Files + +For bulk triage (e.g. a full Docker Scout report with dozens of CVEs), a single event-based +file can be used instead of creating individual CVE files. Example: + +```text +non-affecting/ +└── 2026-06-10_containerfile-trixie-cves.md # Bulk triage of 100+ CVEs ``` ## Process ### When a security warning appears -1. **Check the catalog**: search `docs/security/analysis/non-affecting/` to see if this - vulnerability has already been analyzed. If it has, you're done — the document explains - why it doesn't affect us and what to watch for. +1. **Check the catalog**: `grep -r '' docs/security/analysis/non-affecting/` to + see if this vulnerability has already been analyzed. If it has, verify the + `requires-recheck-when` conditions still hold. If they do, you're done. -2. **If not yet cataloged**: create a new analysis document in `non-affecting/` (or an - appropriate subfolder) following the template below. +2. **If not yet cataloged**: create a new per-CVE analysis document in `non-affecting/` + following the template below. 3. **If it DOES affect us**: escalate immediately. Create an issue and a fix. The analysis document should describe the impact, affected components, and remediation plan. -### Analysis Document Template - -Each analysis document should include: - -- **Date of analysis** -- **Source of the warning** (tool, scanner, CVE database, etc.) -- **Vulnerability summary** — what CVEs, what packages, what severity -- **Why it does not affect us** — a clear rationale tied to our architecture -- **Future actions** — periodic review cadence, conditions that would change the status -- **References** — links to the original warning, Docker Hub layers, CVE entries, etc. +### Recheck Policy -### Review Cadence +Non-affecting verdicts can become stale when dependencies or code change. Each per-CVE file +has a `requires-recheck-when` field that specifies the conditions under which the verdict +must be re-evaluated. -Non-affecting vulnerabilities should be reviewed: +**Triggers for recheck**: -- At least **quarterly** (or when the relevant base image is updated). -- Immediately if the affected image begins being used in a **different context** (e.g., if - a build-stage image becomes part of the runtime). +- A change to the `Containerfile` base image. +- A new system dependency added (e.g., via new `FROM` stage or package install). +- Any change that affects the `requires-recheck-when` condition documented in the CVE file. diff --git a/docs/security/analysis/non-affecting/2026-06-10_containerfile-trixie-cves.md b/docs/security/analysis/non-affecting/2026-06-10_containerfile-trixie-cves.md index 79edd3f9a..e010ca6df 100644 --- a/docs/security/analysis/non-affecting/2026-06-10_containerfile-trixie-cves.md +++ b/docs/security/analysis/non-affecting/2026-06-10_containerfile-trixie-cves.md @@ -3,6 +3,7 @@ date-analyzed: 2026-06-10 source: Docker DX (docker-language-server) / Docker Scout status: non-affecting review-cadence: quarterly +requires-recheck-when: any build-stage image (`chef`, `tester`, `gcc`) is used in a runtime context image-digest: sha256:19dfb952582d0e17841fdb8cd70febfb6cb0761c4e0cd84f3cb1f07bb3281a8d semantic-links: related-artifacts: diff --git a/docs/security/analysis/non-affecting/CVE-2026-27171.md b/docs/security/analysis/non-affecting/CVE-2026-27171.md new file mode 100644 index 000000000..5a0397cdb --- /dev/null +++ b/docs/security/analysis/non-affecting/CVE-2026-27171.md @@ -0,0 +1,39 @@ +--- +cve-id: CVE-2026-27171 +date-analyzed: 2026-06-29 +source: Trivy (Docker image scan) +status: non-affecting +review-cadence: quarterly +requires-recheck-when: tracker statically links or dynamically loads the system zlib library +related-artifacts: + - Containerfile + - docs/security/docker/scans/torrust-tracker.md +--- + +# CVE-2026-27171 — zlib: Denial of Service via infinite loop in CRC32 combine functions + +## Vulnerability + +Denial of Service via infinite loop in zlib's CRC32 combine function. An attacker can +cause an infinite loop by calling `crc32_combine()` or `crc32_combine64()` with crafted +inputs. + +- **Severity**: MEDIUM +- **Package**: zlib1g 1:1.3.dfsg+really1.3.1-1+b1 +- **Link**: https://avd.aquasec.com/nvd/cve-2026-27171 + +## Why It Does NOT Affect Us + +The tracker uses `tower-http` with `compression-full` for optional HTTP response +compression middleware (gzip, brotli, zstd). However, this uses `flate2` → `miniz_oxide` +(pure Rust implementation of zlib), **not** the system `zlib1g` library. The system +`zlib1g` is only pulled in as a transitive dependency of distroless base OS packages, not +used by the tracker binary itself. Additionally, CRC32 combine is a specialized function +not exercised by normal compression/decompression. + +## Conditions That Would Change This Verdict + +- If the tracker starts to statically link or dynamically load the system `zlib1g` for + compression +- If the Rust `flate2` crate switches from its default pure-Rust backend (`miniz_oxide`) + to the system zlib backend diff --git a/docs/security/analysis/non-affecting/CVE-2026-5435.md b/docs/security/analysis/non-affecting/CVE-2026-5435.md new file mode 100644 index 000000000..49ce27e39 --- /dev/null +++ b/docs/security/analysis/non-affecting/CVE-2026-5435.md @@ -0,0 +1,37 @@ +--- +cve-id: CVE-2026-5435 +date-analyzed: 2026-06-29 +source: Trivy (Docker image scan) +status: non-affecting +review-cadence: quarterly +requires-recheck-when: production code adds DNS resolution that calls glibc resolver functions +related-artifacts: + - Containerfile + - docs/security/docker/scans/torrust-tracker.md +--- + +# CVE-2026-5435 — glibc: Out-of-bounds write via TSIG record processing + +## Vulnerability + +Out-of-bounds write in glibc's DNS TSIG (Transaction Signature) record processing. An +attacker can trigger this by sending a crafted DNS response with a malicious TSIG record, +potentially leading to memory corruption. + +- **Severity**: MEDIUM +- **Package**: libc6 (glibc) 2.41-12+deb13u3 +- **Link**: https://avd.aquasec.com/nvd/cve-2026-5435 + +## Why It Does NOT Affect Us + +The tracker server performs **no DNS resolution** in its production code paths. Peer IP +resolution is done from HTTP headers (`X-Forwarded-For`) or socket addresses, not DNS. +The only DNS resolution occurs inside `sqlx` lazily when connecting to MySQL/PostgreSQL +databases, which does not use glibc's TSIG record processing path. + +## Conditions That Would Change This Verdict + +- If the tracker server adds code that performs DNS resolution using glibc resolver + functions (`gethostbyname`, `res_nquery`, `getaddrinfo`, etc.) +- If `sqlx` DNS resolution ever triggers the TSIG code path (unlikely — TSIG is + specific to DNSSEC-secured zone transfers, not normal A/AAAA lookups) diff --git a/docs/security/analysis/non-affecting/CVE-2026-5450.md b/docs/security/analysis/non-affecting/CVE-2026-5450.md new file mode 100644 index 000000000..2f4bf64de --- /dev/null +++ b/docs/security/analysis/non-affecting/CVE-2026-5450.md @@ -0,0 +1,34 @@ +--- +cve-id: CVE-2026-5450 +date-analyzed: 2026-06-29 +source: Trivy (Docker image scan) +status: non-affecting +review-cadence: quarterly +requires-recheck-when: production code adds C stdio input parsing via scanf-family functions +related-artifacts: + - Containerfile + - docs/security/docker/scans/torrust-tracker.md +--- + +# CVE-2026-5450 — glibc: Heap Buffer Overflow in `scanf` with `%mc` format specifier + +## Vulnerability + +Heap buffer overflow in the `scanf` family with the `%mc` format specifier. An attacker +can trigger memory corruption by providing crafted input to a program that uses `scanf`, +`sscanf`, `fscanf`, etc. + +- **Severity**: MEDIUM +- **Package**: libc6 (glibc) 2.41-12+deb13u3 +- **Link**: https://avd.aquasec.com/nvd/cve-2026-5450 + +## Why It Does NOT Affect Us + +The tracker codebase contains **zero uses** of `scanf`, `sscanf`, or any C stdio input +functions. All input parsing is done in safe Rust. A search of the entire codebase confirms +no occurrences of any scanf-family functions. + +## Conditions That Would Change This Verdict + +- If new code adds C FFI calls that use `scanf`-family functions on untrusted input +- If a new dependency links a native library that uses `scanf` on attacker-controlled data diff --git a/docs/security/analysis/non-affecting/CVE-2026-5928.md b/docs/security/analysis/non-affecting/CVE-2026-5928.md new file mode 100644 index 000000000..1c07b2b30 --- /dev/null +++ b/docs/security/analysis/non-affecting/CVE-2026-5928.md @@ -0,0 +1,34 @@ +--- +cve-id: CVE-2026-5928 +date-analyzed: 2026-06-29 +source: Trivy (Docker image scan) +status: non-affecting +review-cadence: quarterly +requires-recheck-when: production code adds wide character I/O via ungetwc-family functions +related-artifacts: + - Containerfile + - docs/security/docker/scans/torrust-tracker.md +--- + +# CVE-2026-5928 — glibc: Information disclosure or denial of service via `ungetwc` function + +## Vulnerability + +Information disclosure or denial of service via the `ungetwc` (wide character un-get) +function in glibc. An attacker can potentially read freed memory or cause a crash by +manipulating the wide character input buffer. + +- **Severity**: MEDIUM +- **Package**: libc6 (glibc) 2.41-12+deb13u3 +- **Link**: https://avd.aquasec.com/nvd/cve-2026-5928 + +## Why It Does NOT Affect Us + +The tracker does not use wide character I/O functions (`ungetwc`, `fgetwc`, `fputwc`, +etc.). The codebase contains no usage of these functions. + +## Conditions That Would Change This Verdict + +- If new code adds wide character stream I/O via glibc C FFI +- If a new dependency links a native library that exercises `ungetwc` on attacker-controlled + data diff --git a/docs/security/analysis/non-affecting/CVE-2026-6238.md b/docs/security/analysis/non-affecting/CVE-2026-6238.md new file mode 100644 index 000000000..a10b1c3dc --- /dev/null +++ b/docs/security/analysis/non-affecting/CVE-2026-6238.md @@ -0,0 +1,38 @@ +--- +cve-id: CVE-2026-6238 +date-analyzed: 2026-06-29 +source: Trivy (Docker image scan) +status: non-affecting +review-cadence: quarterly +requires-recheck-when: production code adds DNS resolution via glibc resolver functions +related-artifacts: + - Containerfile + - docs/security/docker/scans/torrust-tracker.md +--- + +# CVE-2026-6238 — glibc: Application crash or uninitialized memory read via crafted DNS response + +## Vulnerability + +Application crash or uninitialized memory read via crafted DNS response. This affects +glibc's internal DNS resolution functions (`gethostbyname`, `res_nquery`, etc.). An +attacker controlling a DNS server can respond with a malicious packet that causes memory +corruption. + +- **Severity**: MEDIUM +- **Package**: libc6 (glibc) 2.41-12+deb13u3 +- **Link**: https://avd.aquasec.com/nvd/cve-2026-6238 + +## Why It Does NOT Affect Us + +The tracker server performs **no DNS resolution** directly. Peer IP resolution is done +from HTTP headers (`X-Forwarded-For`) or socket addresses, not hostname lookup. Database +hostname resolution is done lazily inside `sqlx` at first query time using Tokio's async +DNS or system `getaddrinfo`, which does not call the affected glibc resolver path. + +## Conditions That Would Change This Verdict + +- If the tracker server adds code that performs DNS resolution using glibc resolver + functions directly (`gethostbyname`, `res_nquery`, etc.) +- If `sqlx` is upgraded to a version that uses a different resolver triggering this path + (unlikely — `sqlx` delegates to Tokio/OS resolution, not glibc resolver internals) diff --git a/docs/security/docker/README.md b/docs/security/docker/README.md new file mode 100644 index 000000000..384881088 --- /dev/null +++ b/docs/security/docker/README.md @@ -0,0 +1,62 @@ +# Docker Image Security + +This directory covers security scanning for the Torrust Tracker Docker image. + +## Purpose + +Regular security scanning ensures that the tracker's container image is free from known +vulnerabilities. This documentation provides: + +- Instructions for running security scans on the tracker image +- Scan history and current status +- Vulnerability management decisions + +## Automated Scanning + +See the [Security Scan workflow](../../../.github/workflows/security-scan.yaml) for automated +scheduled scanning via GitHub Actions. + +## Manual Scanning with Trivy + +### Installation + +```bash +# macOS +brew install trivy + +# Linux (Debian/Ubuntu) +sudo apt-get install trivy + +# Or use Docker +docker run --rm aquasec/trivy:latest image +``` + +### Scan Commands + +**Build the image**: + +```bash +docker build -t torrust-tracker:local -f Containerfile . +``` + +**Scan for HIGH and CRITICAL only** (standard production check): + +```bash +trivy image --severity HIGH,CRITICAL torrust-tracker:local +``` + +**Scan with all severities** (full report): + +```bash +trivy image --severity MEDIUM,HIGH,CRITICAL torrust-tracker:local +``` + +### Severity Levels + +- `CRITICAL`: Exploitable vulnerabilities with severe impact +- `HIGH`: Significant vulnerabilities requiring attention +- `MEDIUM`: Moderate vulnerabilities (tracked for awareness) + +## Scan Results + +See [`scans/`](scans/) for the full scan history. diff --git a/docs/security/docker/scans/README.md b/docs/security/docker/scans/README.md new file mode 100644 index 000000000..ae9614919 --- /dev/null +++ b/docs/security/docker/scans/README.md @@ -0,0 +1,21 @@ +# Docker Image Scan Results + +Historical security scan results for the Torrust Tracker Docker image. + +## Current Status Summary + +| Image | Stage | MEDIUM | HIGH | CRITICAL | Status | Last Scan | Details | +| ----------------- | ------- | ------ | ---- | -------- | -------- | ------------ | -------------------------- | +| `torrust-tracker` | release | 5 | 0 | 0 | ✅ Clean | Jun 29, 2026 | [View](torrust-tracker.md) | + +## Build and Scan + +```bash +# Build the production release image +docker build -t torrust-tracker:local -f Containerfile . + +# Scan +trivy image --severity HIGH,CRITICAL torrust-tracker:local +``` + +See [`../README.md`](../README.md) for detailed scanning instructions. diff --git a/docs/security/docker/scans/torrust-tracker.md b/docs/security/docker/scans/torrust-tracker.md new file mode 100644 index 000000000..7bd374ad7 --- /dev/null +++ b/docs/security/docker/scans/torrust-tracker.md @@ -0,0 +1,94 @@ +# Torrust Tracker - Security Scans + +Security scan history for the `torrust-tracker` Docker image. + +## Current Status + +| Stage | MEDIUM | HIGH | CRITICAL | Status | Last Scan | +| ------- | ------ | ---- | -------- | -------- | ------------ | +| release | 5 | 0 | 0 | ✅ Clean | Jun 29, 2026 | + +## Build & Scan Commands + +**Build the image**: + +```bash +docker build -t torrust-tracker:local -f Containerfile . +``` + +**Run Trivy security scan**: + +```bash +trivy image --severity HIGH,CRITICAL torrust-tracker:local +``` + +**Full scan with all severities**: + +```bash +trivy image --severity MEDIUM,HIGH,CRITICAL torrust-tracker:local +``` + +## Scan History + +### June 29, 2026 - Baseline + +**Image**: `torrust-tracker:local` +**Trivy Version**: 0.69.3 +**Scan Mode**: `--severity MEDIUM,HIGH,CRITICAL` +**Base OS**: Debian 13.5 (trixie, distroless/cc-debian13) +**Status**: ✅ **Clean** — 5 MEDIUM, 0 HIGH, 0 CRITICAL + +#### Summary + +This is the baseline scan for the Torrust Tracker production runtime image. The image uses +`gcr.io/distroless/cc-debian13` as the runtime base, which is a minimal distroless image. +All 5 findings are MEDIUM-severity CVEs in OS base libraries (`libc6` and `zlib1g`). + +#### Vulnerability Details (all MEDIUM) + +| CVE | Package | Installed Version | Title | +| -------------- | ------- | --------------------------- | ------------------------------------------------------------------------------ | +| CVE-2026-5435 | libc6 | 2.41-12+deb13u3 | glibc: Out-of-bounds write via TSIG record processing | +| CVE-2026-5450 | libc6 | 2.41-12+deb13u3 | glibc: Heap Buffer Overflow in `scanf` with `%mc` format specifier | +| CVE-2026-5928 | libc6 | 2.41-12+deb13u3 | glibc: Information disclosure or denial of service via `ungetwc` function | +| CVE-2026-6238 | libc6 | 2.41-12+deb13u3 | glibc: Application crash or uninitialized memory read via crafted DNS response | +| CVE-2026-27171 | zlib1g | 1:1.3.dfsg+really1.3.1-1+b1 | zlib: Denial of Service via infinite loop in CRC32 combine functions | + +#### Analysis + +- **CVE-2026-5435** (TSIG record processing): Affects DNS TSIG record handling in glibc. + The tracker server performs **no DNS resolution** in its production code paths. Peer IP + resolution is done from HTTP headers (`X-Forwarded-For`) or socket addresses, not DNS. + The only DNS resolution occurs inside `sqlx` lazily when connecting to MySQL/PostgreSQL + databases, which does not use glibc's TSIG record processing path. **Non-affecting**. + +- **CVE-2026-5450** (scanf `%mc`): Heap buffer overflow in the `scanf` family with the + `%mc` format specifier. The tracker codebase contains **zero uses** of `scanf`, `sscanf`, + or any C stdio input functions. Input parsing is done entirely in safe Rust. + **Non-affecting**. + +- **CVE-2026-5928** (ungetwc): Information disclosure or DoS via `ungetwc`. The tracker + does not use wide character I/O functions (`ungetwc`, `fgetwc`, etc.). **Non-affecting**. + +- **CVE-2026-6238** (DNS response): Crash or uninitialized memory read via crafted DNS + response. This affects glibc's internal DNS resolution (`gethostbyname`, `res_nquery`, + etc.). The tracker server performs **no DNS resolution** directly — peer IP resolution + is from HTTP headers/socket addresses, not hostname lookup. Database hostname resolution + is done lazily inside `sqlx` at first query time, which does not trigger the affected + code path. **Non-affecting**. + +- **CVE-2026-27171** (zlib CRC32): DoS via infinite loop in CRC32 combine function. + The tracker uses `tower-http` with `compression-full` for optional HTTP response + compression middleware (gzip, brotli, zstd). However, this uses `flate2` → `miniz_oxide` + (pure Rust implementation of zlib), **not** the system `zlib1g` library. The system `zlib1g` + is only pulled in as a transitive dependency of distroless base OS packages, not used by + the tracker binary itself. Additionally, CRC32 combine is a specialized function not + exercised by normal compression/decompression. **Non-affecting**. + +#### Next Steps + +- All 5 MEDIUM CVEs are **non-affecting** for the current runtime — accepted risk. +- Re-scan quarterly to track OS package updates from the distroless base image. +- If the base image is updated, re-scan and update this report. +- Consider filing issues for specific CVEs if they become fixable (e.g., via Debian security + updates to the distroless base). diff --git a/packages/AGENTS.md b/packages/AGENTS.md index 9f89afa5a..a857557da 100644 --- a/packages/AGENTS.md +++ b/packages/AGENTS.md @@ -16,9 +16,12 @@ depend on packages in the same layer or a lower one. │ axum-http-server axum-rest-api-server │ │ axum-health-check-api-server udp-server │ ├────────────────────────────────────────────────────────────────┤ +│ Runtime Adapter │ +│ rest-api-runtime-adapter │ +├────────────────────────────────────────────────────────────────┤ │ Core (domain layer) │ -│ http-core udp-core tracker-core │ -│ rest-api-core swarm-coordination-registry │ +│ http-core udp-core tracker-core │ +│ swarm-coordination-registry │ ├────────────────────────────────────────────────────────────────┤ │ Protocols │ │ http-protocol udp-protocol │ @@ -64,7 +67,7 @@ dependency injection. | `tracker-core` | Central peer management: announce/scrape handlers, auth, whitelist, database abstraction (SQLite/MySQL drivers in `src/databases/driver/`) | | `http-core` | HTTP-specific validation and response formatting | | `udp-core` | UDP connection cookies, crypto, banning logic | -| `rest-api-core` | REST API statistics and container wiring | +| `rest-api-runtime-adapter` | REST API runtime adapter and container wiring (Runtime Adapter layer) | | `swarm-coordination-registry` | Registry of torrents and their peer swarms | ### Protocols (`*-protocol`) diff --git a/packages/axum-rest-api-server/Cargo.toml b/packages/axum-rest-api-server/Cargo.toml index d3e31d003..31edde9dd 100644 --- a/packages/axum-rest-api-server/Cargo.toml +++ b/packages/axum-rest-api-server/Cargo.toml @@ -31,7 +31,6 @@ thiserror = "2" tokio = { version = "1", features = [ "macros", "net", "rt-multi-thread", "signal", "sync" ] } torrust-tracker-axum-server = { version = "3.0.0-develop", path = "../axum-server" } torrust-tracker-rest-api-client = { version = "3.0.0-develop", path = "../rest-api-client" } -torrust-tracker-rest-api-core = { version = "3.0.0-develop", path = "../rest-api-core" } torrust-tracker-rest-api-application = { version = "3.0.0-develop", path = "../rest-api-application" } torrust-tracker-rest-api-protocol = { version = "3.0.0-develop", path = "../rest-api-protocol" } torrust-tracker-rest-api-runtime-adapter = { version = "3.0.0-develop", path = "../rest-api-runtime-adapter" } diff --git a/packages/axum-rest-api-server/src/routes.rs b/packages/axum-rest-api-server/src/routes.rs index 050904ef9..2d4e360dd 100644 --- a/packages/axum-rest-api-server/src/routes.rs +++ b/packages/axum-rest-api-server/src/routes.rs @@ -17,7 +17,7 @@ use axum::{BoxError, Router, middleware}; use hyper::{Request, StatusCode}; use torrust_server_lib::logging::Latency; use torrust_tracker_configuration::AccessTokens; -use torrust_tracker_rest_api_core::container::TrackerHttpApiCoreContainer; +use torrust_tracker_rest_api_runtime_adapter::v1::container::TrackerHttpApiCoreContainer; use tower::ServiceBuilder; use tower::timeout::TimeoutLayer; use tower_http::LatencyUnit; diff --git a/packages/axum-rest-api-server/src/server.rs b/packages/axum-rest-api-server/src/server.rs index 90cf10395..3b8faedc0 100644 --- a/packages/axum-rest-api-server/src/server.rs +++ b/packages/axum-rest-api-server/src/server.rs @@ -40,7 +40,7 @@ use torrust_server_lib::signals::{Halted, Started}; use torrust_tracker_axum_server::custom_axum_server::{self, TimeoutAcceptor}; use torrust_tracker_axum_server::signals::graceful_shutdown; use torrust_tracker_configuration::AccessTokens; -use torrust_tracker_rest_api_core::container::TrackerHttpApiCoreContainer; +use torrust_tracker_rest_api_runtime_adapter::v1::container::TrackerHttpApiCoreContainer; use tracing::{Level, instrument}; use super::routes::router; @@ -310,7 +310,7 @@ mod tests { use torrust_server_lib::registar::Registar; use torrust_tracker_axum_server::tsl::make_rust_tls; use torrust_tracker_configuration::{Configuration, logging}; - use torrust_tracker_rest_api_core::container::TrackerHttpApiCoreContainer; + use torrust_tracker_rest_api_runtime_adapter::v1::container::TrackerHttpApiCoreContainer; use torrust_tracker_test_helpers::configuration::ephemeral_public; use crate::server::{ApiServer, Launcher}; diff --git a/packages/axum-rest-api-server/src/testing/environment.rs b/packages/axum-rest-api-server/src/testing/environment.rs index 4ff565127..7b610ea3d 100644 --- a/packages/axum-rest-api-server/src/testing/environment.rs +++ b/packages/axum-rest-api-server/src/testing/environment.rs @@ -9,7 +9,7 @@ use torrust_tracker_core::container::TrackerCoreContainer; use torrust_tracker_http_core::container::HttpTrackerCoreContainer; use torrust_tracker_primitives::peer; use torrust_tracker_rest_api_client::connection_info::{ConnectionInfo, Origin}; -use torrust_tracker_rest_api_core::container::TrackerHttpApiCoreContainer; +use torrust_tracker_rest_api_runtime_adapter::v1::container::TrackerHttpApiCoreContainer; use torrust_tracker_swarm_coordination_registry::container::SwarmCoordinationRegistryContainer; use torrust_tracker_udp_core::container::UdpTrackerCoreContainer; use torrust_tracker_udp_server::container::UdpTrackerServerContainer; diff --git a/packages/axum-rest-api-server/src/v1/context/auth_key/handlers.rs b/packages/axum-rest-api-server/src/v1/context/auth_key/handlers.rs index aa5d3ae4b..f840361ef 100644 --- a/packages/axum-rest-api-server/src/v1/context/auth_key/handlers.rs +++ b/packages/axum-rest-api-server/src/v1/context/auth_key/handlers.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use axum::extract::{self, Path, State}; use axum::response::Response; use serde::Deserialize; -use torrust_tracker_rest_api_application::use_cases::auth_key::AuthKeyApiService; +use torrust_tracker_rest_api_application::v1::use_cases::auth_key::AuthKeyApiService; use torrust_tracker_rest_api_protocol::v1::context::auth_key::forms::add_key_form::AddKeyForm; use super::responses::{ diff --git a/packages/axum-rest-api-server/src/v1/context/auth_key/routes.rs b/packages/axum-rest-api-server/src/v1/context/auth_key/routes.rs index 3fa0d0a11..7d5d509e3 100644 --- a/packages/axum-rest-api-server/src/v1/context/auth_key/routes.rs +++ b/packages/axum-rest-api-server/src/v1/context/auth_key/routes.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use axum::Router; use axum::routing::{get, post}; -use torrust_tracker_rest_api_application::use_cases::auth_key::AuthKeyApiService; +use torrust_tracker_rest_api_application::v1::use_cases::auth_key::AuthKeyApiService; use super::handlers::{add_auth_key_handler, delete_auth_key_handler, generate_auth_key_handler, reload_keys_handler}; diff --git a/packages/axum-rest-api-server/src/v1/context/stats/handlers.rs b/packages/axum-rest-api-server/src/v1/context/stats/handlers.rs index 051c7a362..f0e3a0177 100644 --- a/packages/axum-rest-api-server/src/v1/context/stats/handlers.rs +++ b/packages/axum-rest-api-server/src/v1/context/stats/handlers.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use axum::extract::{Query, State}; use axum::response::Response; use serde::Deserialize; -use torrust_tracker_rest_api_application::use_cases::stats::StatsApiService; +use torrust_tracker_rest_api_application::v1::use_cases::stats::StatsApiService; use super::responses::{labeled_metrics_response, labeled_stats_response, metrics_response, stats_response}; diff --git a/packages/axum-rest-api-server/src/v1/context/stats/routes.rs b/packages/axum-rest-api-server/src/v1/context/stats/routes.rs index f013c92ee..d5954f010 100644 --- a/packages/axum-rest-api-server/src/v1/context/stats/routes.rs +++ b/packages/axum-rest-api-server/src/v1/context/stats/routes.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use axum::Router; use axum::routing::get; -use torrust_tracker_rest_api_application::use_cases::stats::StatsApiService; +use torrust_tracker_rest_api_application::v1::use_cases::stats::StatsApiService; use super::handlers::{get_metrics_handler, get_stats_handler}; diff --git a/packages/axum-rest-api-server/src/v1/context/torrent/handlers.rs b/packages/axum-rest-api-server/src/v1/context/torrent/handlers.rs index ea8b51b51..d7ba0509a 100644 --- a/packages/axum-rest-api-server/src/v1/context/torrent/handlers.rs +++ b/packages/axum-rest-api-server/src/v1/context/torrent/handlers.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Deserializer, de}; use thiserror::Error; use torrust_info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; -use torrust_tracker_rest_api_application::use_cases::torrent::TorrentApiService; +use torrust_tracker_rest_api_application::v1::use_cases::torrent::TorrentApiService; use super::responses::{torrent_info_response, torrent_list_response, torrent_not_known_response}; use crate::InfoHashParam; diff --git a/packages/axum-rest-api-server/src/v1/context/torrent/resources/mod.rs b/packages/axum-rest-api-server/src/v1/context/torrent/resources/mod.rs index 8e31036d3..1c5d8f6cb 100644 --- a/packages/axum-rest-api-server/src/v1/context/torrent/resources/mod.rs +++ b/packages/axum-rest-api-server/src/v1/context/torrent/resources/mod.rs @@ -1,4 +1,3 @@ //! API resources for the [`torrent`](crate::v1::context::torrent) //! API context. -pub mod peer; pub mod torrent; diff --git a/packages/axum-rest-api-server/src/v1/context/torrent/resources/peer.rs b/packages/axum-rest-api-server/src/v1/context/torrent/resources/peer.rs deleted file mode 100644 index 1405aa129..000000000 --- a/packages/axum-rest-api-server/src/v1/context/torrent/resources/peer.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! `Peer` and Peer `Id` API resources. -//! -//! Protocol DTOs are defined in `torrust-tracker-rest-api-protocol`. diff --git a/packages/axum-rest-api-server/src/v1/context/torrent/resources/torrent.rs b/packages/axum-rest-api-server/src/v1/context/torrent/resources/torrent.rs index dc158a187..3b7371f90 100644 --- a/packages/axum-rest-api-server/src/v1/context/torrent/resources/torrent.rs +++ b/packages/axum-rest-api-server/src/v1/context/torrent/resources/torrent.rs @@ -1,67 +1,3 @@ //! `Torrent` and `ListItem` API resources. //! //! Protocol DTOs are defined in `torrust-tracker-rest-api-protocol`. -//! This module only contains unit tests for domain→DTO conversions. - -#[cfg(test)] -mod tests { - use std::net::{IpAddr, Ipv4Addr, SocketAddr}; - use std::str::FromStr; - - use torrust_clock::DurationSinceUnixEpoch; - use torrust_info_hash::InfoHash; - use torrust_tracker_core::torrent::services::{BasicInfo, Info}; - use torrust_tracker_primitives::{AnnounceEvent, NumberOfBytes, PeerId, peer}; - use torrust_tracker_rest_api_protocol::v1::context::torrent::resources::torrent::{ListItem, Torrent}; - use torrust_tracker_rest_api_runtime_adapter::conversion; - - fn sample_peer() -> peer::Peer { - peer::Peer { - peer_id: PeerId(*b"-qB00000000000000000"), - peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080), - updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0), - uploaded: NumberOfBytes::new(0), - downloaded: NumberOfBytes::new(0), - left: NumberOfBytes::new(0), - event: AnnounceEvent::Started, - } - } - - #[test] - fn torrent_resource_should_be_converted_from_torrent_info() { - assert_eq!( - conversion::from_domain_info(Info { - info_hash: InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(), // DevSkim: ignore DS173237 - seeders: 1, - completed: 2, - leechers: 3, - peers: Some(vec![sample_peer()]), - }), - Torrent { - info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), // DevSkim: ignore DS173237 - seeders: 1, - completed: 2, - leechers: 3, - peers: Some(vec![conversion::from_domain_peer(sample_peer())]), - } - ); - } - - #[test] - fn torrent_resource_list_item_should_be_converted_from_the_basic_torrent_info() { - assert_eq!( - conversion::list_item_from_domain(&BasicInfo { - info_hash: InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(), // DevSkim: ignore DS173237 - seeders: 1, - completed: 2, - leechers: 3, - }), - ListItem { - info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), // DevSkim: ignore DS173237 - seeders: 1, - completed: 2, - leechers: 3, - } - ); - } -} diff --git a/packages/axum-rest-api-server/src/v1/context/torrent/routes.rs b/packages/axum-rest-api-server/src/v1/context/torrent/routes.rs index 423a30f7a..b960582d5 100644 --- a/packages/axum-rest-api-server/src/v1/context/torrent/routes.rs +++ b/packages/axum-rest-api-server/src/v1/context/torrent/routes.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use axum::Router; use axum::routing::get; -use torrust_tracker_rest_api_application::use_cases::torrent::TorrentApiService; +use torrust_tracker_rest_api_application::v1::use_cases::torrent::TorrentApiService; use super::handlers::{get_torrent_handler, get_torrents_handler}; diff --git a/packages/axum-rest-api-server/src/v1/context/whitelist/handlers.rs b/packages/axum-rest-api-server/src/v1/context/whitelist/handlers.rs index 571bee86c..a2aed9146 100644 --- a/packages/axum-rest-api-server/src/v1/context/whitelist/handlers.rs +++ b/packages/axum-rest-api-server/src/v1/context/whitelist/handlers.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use axum::extract::{Path, State}; use axum::response::Response; use torrust_info_hash::InfoHash; -use torrust_tracker_rest_api_application::use_cases::whitelist::WhitelistApiService; +use torrust_tracker_rest_api_application::v1::use_cases::whitelist::WhitelistApiService; use super::responses::{ failed_to_reload_whitelist_response, failed_to_remove_torrent_from_whitelist_response, failed_to_whitelist_torrent_response, diff --git a/packages/axum-rest-api-server/src/v1/context/whitelist/routes.rs b/packages/axum-rest-api-server/src/v1/context/whitelist/routes.rs index d4728b1df..1f2375308 100644 --- a/packages/axum-rest-api-server/src/v1/context/whitelist/routes.rs +++ b/packages/axum-rest-api-server/src/v1/context/whitelist/routes.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use axum::Router; use axum::routing::{delete, get, post}; -use torrust_tracker_rest_api_application::use_cases::whitelist::WhitelistApiService; +use torrust_tracker_rest_api_application::v1::use_cases::whitelist::WhitelistApiService; use super::handlers::{add_torrent_to_whitelist_handler, reload_whitelist_handler, remove_torrent_from_whitelist_handler}; diff --git a/packages/axum-rest-api-server/src/v1/middlewares/auth.rs b/packages/axum-rest-api-server/src/v1/middlewares/auth.rs index 9b5ec2320..b62520335 100644 --- a/packages/axum-rest-api-server/src/v1/middlewares/auth.rs +++ b/packages/axum-rest-api-server/src/v1/middlewares/auth.rs @@ -8,6 +8,7 @@ //! 1. As a `Bearer` token in the `Authorization` header. //! 2. As a `token` GET param in the URL. //! +//! skill-link: use-rest-api //! Using the `Authorization` header: //! //! ```console diff --git a/packages/axum-rest-api-server/src/v1/routes.rs b/packages/axum-rest-api-server/src/v1/routes.rs index 1bba296e0..e8cbb4bd4 100644 --- a/packages/axum-rest-api-server/src/v1/routes.rs +++ b/packages/axum-rest-api-server/src/v1/routes.rs @@ -2,15 +2,15 @@ use std::sync::Arc; use axum::Router; -use torrust_tracker_rest_api_application::use_cases::auth_key::AuthKeyApiService; -use torrust_tracker_rest_api_application::use_cases::stats::StatsApiService; -use torrust_tracker_rest_api_application::use_cases::torrent::TorrentApiService; -use torrust_tracker_rest_api_application::use_cases::whitelist::WhitelistApiService; -use torrust_tracker_rest_api_core::container::TrackerHttpApiCoreContainer; -use torrust_tracker_rest_api_runtime_adapter::adapters::auth_key::TrackerAuthKeyAdapter; -use torrust_tracker_rest_api_runtime_adapter::adapters::stats::TrackerStatsAdapter; -use torrust_tracker_rest_api_runtime_adapter::adapters::torrent::TrackerTorrentQueryAdapter; -use torrust_tracker_rest_api_runtime_adapter::adapters::whitelist::TrackerWhitelistAdapter; +use torrust_tracker_rest_api_application::v1::use_cases::auth_key::AuthKeyApiService; +use torrust_tracker_rest_api_application::v1::use_cases::stats::StatsApiService; +use torrust_tracker_rest_api_application::v1::use_cases::torrent::TorrentApiService; +use torrust_tracker_rest_api_application::v1::use_cases::whitelist::WhitelistApiService; +use torrust_tracker_rest_api_runtime_adapter::v1::adapters::auth_key::TrackerAuthKeyAdapter; +use torrust_tracker_rest_api_runtime_adapter::v1::adapters::stats::TrackerStatsAdapter; +use torrust_tracker_rest_api_runtime_adapter::v1::adapters::torrent::TrackerTorrentQueryAdapter; +use torrust_tracker_rest_api_runtime_adapter::v1::adapters::whitelist::TrackerWhitelistAdapter; +use torrust_tracker_rest_api_runtime_adapter::v1::container::TrackerHttpApiCoreContainer; use super::context::{auth_key, stats, torrent, whitelist}; diff --git a/packages/axum-rest-api-server/tests/server/v1/contract/authentication.rs b/packages/axum-rest-api-server/tests/server/v1/contract/authentication.rs index 24cc4193e..e9fc8d396 100644 --- a/packages/axum-rest-api-server/tests/server/v1/contract/authentication.rs +++ b/packages/axum-rest-api-server/tests/server/v1/contract/authentication.rs @@ -4,7 +4,7 @@ mod given_that_the_token_is_only_provided_in_the_authentication_header { use torrust_tracker_rest_api_client::common::http::Query; use torrust_tracker_rest_api_client::connection_info::ConnectionInfo; use torrust_tracker_rest_api_client::v1::client::{ - AUTH_BEARER_TOKEN_HEADER_PREFIX, Client, headers_with_auth_token, headers_with_request_id, + AUTH_BEARER_TOKEN_HEADER_PREFIX, ApiHttpClient, headers_with_auth_token, headers_with_request_id, }; use torrust_tracker_test_helpers::logging::logs_contains_a_line_with; use torrust_tracker_test_helpers::{configuration, logging}; @@ -20,7 +20,7 @@ mod given_that_the_token_is_only_provided_in_the_authentication_header { let token = env.get_connection_info().api_token.unwrap(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_request_with_query("stats", Query::default(), Some(headers_with_auth_token(&token))) .await; @@ -48,7 +48,7 @@ mod given_that_the_token_is_only_provided_in_the_authentication_header { .expect("the auth token is not a valid header value"), ); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_request_with_query("stats", Query::default(), Some(headers)) .await; @@ -83,7 +83,7 @@ mod given_that_the_token_is_only_provided_in_the_authentication_header { let connection_info = ConnectionInfo::anonymous(env.get_connection_info().origin); - let response = Client::new(connection_info) + let response = ApiHttpClient::new(connection_info) .unwrap() .get_request_with_query("stats", Query::default(), Some(headers)) .await; @@ -103,7 +103,7 @@ mod given_that_the_token_is_only_provided_in_the_query_param { use torrust_tracker_axum_rest_api_server::testing::environment::Started; use torrust_tracker_rest_api_client::common::http::{Query, QueryParam}; use torrust_tracker_rest_api_client::connection_info::ConnectionInfo; - use torrust_tracker_rest_api_client::v1::client::{Client, TOKEN_PARAM_NAME, headers_with_request_id}; + use torrust_tracker_rest_api_client::v1::client::{ApiHttpClient, TOKEN_PARAM_NAME, headers_with_request_id}; use torrust_tracker_test_helpers::logging::logs_contains_a_line_with; use torrust_tracker_test_helpers::{configuration, logging}; use uuid::Uuid; @@ -120,7 +120,7 @@ mod given_that_the_token_is_only_provided_in_the_query_param { let connection_info = ConnectionInfo::anonymous(env.get_connection_info().origin); - let response = Client::new(connection_info) + let response = ApiHttpClient::new(connection_info) .unwrap() .get_request_with_query( "stats", @@ -144,7 +144,7 @@ mod given_that_the_token_is_only_provided_in_the_query_param { let connection_info = ConnectionInfo::anonymous(env.get_connection_info().origin); - let response = Client::new(connection_info) + let response = ApiHttpClient::new(connection_info) .unwrap() .get_request_with_query( "stats", @@ -173,7 +173,7 @@ mod given_that_the_token_is_only_provided_in_the_query_param { let connection_info = ConnectionInfo::anonymous(env.get_connection_info().origin); - let response = Client::new(connection_info) + let response = ApiHttpClient::new(connection_info) .unwrap() .get_request_with_query( "stats", @@ -203,7 +203,7 @@ mod given_that_the_token_is_only_provided_in_the_query_param { let connection_info = ConnectionInfo::anonymous(env.get_connection_info().origin); // At the beginning of the query component - let response = Client::new(connection_info) + let response = ApiHttpClient::new(connection_info) .unwrap() .get_request(&format!("torrents?token={token}&limit=1")) .await; @@ -211,7 +211,7 @@ mod given_that_the_token_is_only_provided_in_the_query_param { assert_eq!(response.status(), 200); // At the end of the query component - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_request(&format!("torrents?limit=1&token={token}")) .await; @@ -227,7 +227,7 @@ mod given_that_not_token_is_provided { use torrust_tracker_axum_rest_api_server::testing::environment::Started; use torrust_tracker_rest_api_client::common::http::Query; use torrust_tracker_rest_api_client::connection_info::ConnectionInfo; - use torrust_tracker_rest_api_client::v1::client::{Client, headers_with_request_id}; + use torrust_tracker_rest_api_client::v1::client::{ApiHttpClient, headers_with_request_id}; use torrust_tracker_test_helpers::logging::logs_contains_a_line_with; use torrust_tracker_test_helpers::{configuration, logging}; use uuid::Uuid; @@ -244,7 +244,7 @@ mod given_that_not_token_is_provided { let connection_info = ConnectionInfo::anonymous(env.get_connection_info().origin); - let response = Client::new(connection_info) + let response = ApiHttpClient::new(connection_info) .unwrap() .get_request_with_query("stats", Query::default(), Some(headers_with_request_id(request_id))) .await; @@ -263,7 +263,7 @@ mod given_that_not_token_is_provided { mod given_that_token_is_provided_via_get_param_and_authentication_header { use torrust_tracker_axum_rest_api_server::testing::environment::Started; use torrust_tracker_rest_api_client::common::http::{Query, QueryParam}; - use torrust_tracker_rest_api_client::v1::client::{Client, TOKEN_PARAM_NAME, headers_with_auth_token}; + use torrust_tracker_rest_api_client::v1::client::{ApiHttpClient, TOKEN_PARAM_NAME, headers_with_auth_token}; use torrust_tracker_test_helpers::{configuration, logging}; #[tokio::test] @@ -276,7 +276,7 @@ mod given_that_token_is_provided_via_get_param_and_authentication_header { let non_authorized_token = "NonAuthorizedToken"; - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_request_with_query( "stats", diff --git a/packages/axum-rest-api-server/tests/server/v1/contract/context/auth_key.rs b/packages/axum-rest-api-server/tests/server/v1/contract/context/auth_key.rs index c9dba6bfb..36c1d7074 100644 --- a/packages/axum-rest-api-server/tests/server/v1/contract/context/auth_key.rs +++ b/packages/axum-rest-api-server/tests/server/v1/contract/context/auth_key.rs @@ -3,7 +3,7 @@ use std::time::Duration; use serde::Serialize; use torrust_tracker_axum_rest_api_server::testing::environment::Started; use torrust_tracker_core::authentication::Key; -use torrust_tracker_rest_api_client::v1::client::{AddKeyForm, Client, headers_with_request_id}; +use torrust_tracker_rest_api_client::v1::client::{AddKeyForm, ApiHttpClient, headers_with_request_id}; use torrust_tracker_test_helpers::logging::logs_contains_a_line_with; use torrust_tracker_test_helpers::{configuration, logging}; use uuid::Uuid; @@ -24,12 +24,12 @@ async fn should_allow_generating_a_new_random_auth_key() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .add_auth_key( AddKeyForm { opt_key: None, - seconds_valid: Some(60), + opt_seconds_valid: Some(60), }, Some(headers_with_request_id(request_id)), ) @@ -57,12 +57,12 @@ async fn should_allow_uploading_a_preexisting_auth_key() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .add_auth_key( AddKeyForm { opt_key: Some("Xc1L4PbQJSFGlrgSRZl8wxSFAuMa21z5".to_string()), - seconds_valid: Some(60), + opt_seconds_valid: Some(60), }, Some(headers_with_request_id(request_id)), ) @@ -90,12 +90,12 @@ async fn should_not_allow_generating_a_new_auth_key_for_unauthenticated_users() let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_invalid_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_invalid_token(env.get_connection_info().origin)) .unwrap() .add_auth_key( AddKeyForm { opt_key: None, - seconds_valid: Some(60), + opt_seconds_valid: Some(60), }, Some(headers_with_request_id(request_id)), ) @@ -110,12 +110,12 @@ async fn should_not_allow_generating_a_new_auth_key_for_unauthenticated_users() let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_no_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_no_token(env.get_connection_info().origin)) .unwrap() .add_auth_key( AddKeyForm { opt_key: None, - seconds_valid: Some(60), + opt_seconds_valid: Some(60), }, Some(headers_with_request_id(request_id)), ) @@ -141,12 +141,12 @@ async fn should_fail_when_the_auth_key_cannot_be_generated() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .add_auth_key( AddKeyForm { opt_key: None, - seconds_valid: Some(60), + opt_seconds_valid: Some(60), }, Some(headers_with_request_id(request_id)), ) @@ -179,7 +179,7 @@ async fn should_allow_deleting_an_auth_key() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .delete_auth_key(&auth_key.key.to_string(), Some(headers_with_request_id(request_id))) .await; @@ -214,7 +214,7 @@ async fn should_fail_generating_a_new_auth_key_when_the_provided_key_is_invalid( for invalid_key in invalid_keys { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .post_form( "keys", @@ -254,7 +254,7 @@ async fn should_fail_generating_a_new_auth_key_when_the_key_duration_is_invalid( for invalid_key_duration in invalid_key_durations { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .post_form( "keys", @@ -291,7 +291,7 @@ async fn should_fail_deleting_an_auth_key_when_the_key_id_is_invalid() { for invalid_auth_key in &invalid_auth_keys { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .delete_auth_key(invalid_auth_key, Some(headers_with_request_id(request_id))) .await; @@ -321,7 +321,7 @@ async fn should_fail_when_the_auth_key_cannot_be_deleted() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .delete_auth_key(&auth_key.key.to_string(), Some(headers_with_request_id(request_id))) .await; @@ -355,7 +355,7 @@ async fn should_not_allow_deleting_an_auth_key_for_unauthenticated_users() { let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_invalid_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_invalid_token(env.get_connection_info().origin)) .unwrap() .delete_auth_key(&auth_key.key.to_string(), Some(headers_with_request_id(request_id))) .await; @@ -378,7 +378,7 @@ async fn should_not_allow_deleting_an_auth_key_for_unauthenticated_users() { let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_no_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_no_token(env.get_connection_info().origin)) .unwrap() .delete_auth_key(&auth_key.key.to_string(), Some(headers_with_request_id(request_id))) .await; @@ -409,7 +409,7 @@ async fn should_allow_reloading_keys() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .reload_keys(Some(headers_with_request_id(request_id))) .await; @@ -437,7 +437,7 @@ async fn should_fail_when_keys_cannot_be_reloaded() { force_database_error(&env.container.tracker_core_container.database_stores.schema_migrator).await; - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .reload_keys(Some(headers_with_request_id(request_id))) .await; @@ -468,7 +468,7 @@ async fn should_not_allow_reloading_keys_for_unauthenticated_users() { let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_invalid_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_invalid_token(env.get_connection_info().origin)) .unwrap() .reload_keys(Some(headers_with_request_id(request_id))) .await; @@ -482,7 +482,7 @@ async fn should_not_allow_reloading_keys_for_unauthenticated_users() { let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_no_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_no_token(env.get_connection_info().origin)) .unwrap() .reload_keys(Some(headers_with_request_id(request_id))) .await; @@ -501,7 +501,7 @@ mod deprecated_generate_key_endpoint { use torrust_tracker_axum_rest_api_server::testing::environment::Started; use torrust_tracker_core::authentication::Key; - use torrust_tracker_rest_api_client::v1::client::{Client, headers_with_request_id}; + use torrust_tracker_rest_api_client::v1::client::{ApiHttpClient, headers_with_request_id}; use torrust_tracker_test_helpers::logging::logs_contains_a_line_with; use torrust_tracker_test_helpers::{configuration, logging}; use uuid::Uuid; @@ -521,7 +521,7 @@ mod deprecated_generate_key_endpoint { let seconds_valid = 60; - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .generate_auth_key(seconds_valid, None) .await; @@ -549,14 +549,14 @@ mod deprecated_generate_key_endpoint { let request_id = Uuid::new_v4(); let seconds_valid = 60; - let response = Client::new(connection_with_invalid_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_invalid_token(env.get_connection_info().origin)) .unwrap() .generate_auth_key(seconds_valid, Some(headers_with_request_id(request_id))) .await; assert_token_not_valid(response).await; - let response = Client::new(connection_with_no_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_no_token(env.get_connection_info().origin)) .unwrap() .generate_auth_key(seconds_valid, None) .await; @@ -584,7 +584,7 @@ mod deprecated_generate_key_endpoint { ]; for invalid_key_duration in invalid_key_durations { - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .post_empty(&format!("key/{invalid_key_duration}"), None) .await; @@ -605,7 +605,7 @@ mod deprecated_generate_key_endpoint { let request_id = Uuid::new_v4(); let seconds_valid = 60; - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .generate_auth_key(seconds_valid, Some(headers_with_request_id(request_id))) .await; diff --git a/packages/axum-rest-api-server/tests/server/v1/contract/context/stats.rs b/packages/axum-rest-api-server/tests/server/v1/contract/context/stats.rs index 9d24087ee..610457b18 100644 --- a/packages/axum-rest-api-server/tests/server/v1/contract/context/stats.rs +++ b/packages/axum-rest-api-server/tests/server/v1/contract/context/stats.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use torrust_info_hash::InfoHash; use torrust_tracker_axum_rest_api_server::testing::environment::Started; use torrust_tracker_primitives::peer::fixture::PeerBuilder; -use torrust_tracker_rest_api_client::v1::client::{Client, headers_with_request_id}; +use torrust_tracker_rest_api_client::v1::client::{ApiHttpClient, headers_with_request_id}; use torrust_tracker_rest_api_protocol::v1::context::stats::resources::stats::Stats; use torrust_tracker_test_helpers::logging::logs_contains_a_line_with; use torrust_tracker_test_helpers::{configuration, logging}; @@ -26,7 +26,7 @@ async fn should_allow_getting_tracker_statistics() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_tracker_statistics(Some(headers_with_request_id(request_id))) .await; @@ -81,7 +81,7 @@ async fn should_not_allow_getting_tracker_statistics_for_unauthenticated_users() let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_invalid_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_invalid_token(env.get_connection_info().origin)) .unwrap() .get_tracker_statistics(Some(headers_with_request_id(request_id))) .await; @@ -95,7 +95,7 @@ async fn should_not_allow_getting_tracker_statistics_for_unauthenticated_users() let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_no_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_no_token(env.get_connection_info().origin)) .unwrap() .get_tracker_statistics(Some(headers_with_request_id(request_id))) .await; diff --git a/packages/axum-rest-api-server/tests/server/v1/contract/context/torrent.rs b/packages/axum-rest-api-server/tests/server/v1/contract/context/torrent.rs index 052bed556..6d3166d00 100644 --- a/packages/axum-rest-api-server/tests/server/v1/contract/context/torrent.rs +++ b/packages/axum-rest-api-server/tests/server/v1/contract/context/torrent.rs @@ -4,9 +4,9 @@ use torrust_info_hash::InfoHash; use torrust_tracker_axum_rest_api_server::testing::environment::Started; use torrust_tracker_primitives::peer::fixture::PeerBuilder; use torrust_tracker_rest_api_client::common::http::{Query, QueryParam}; -use torrust_tracker_rest_api_client::v1::client::{Client, headers_with_request_id}; +use torrust_tracker_rest_api_client::v1::client::{ApiHttpClient, headers_with_request_id}; use torrust_tracker_rest_api_protocol::v1::context::torrent::resources::torrent::{self, Torrent}; -use torrust_tracker_rest_api_runtime_adapter::conversion; +use torrust_tracker_rest_api_runtime_adapter::v1::conversion; use torrust_tracker_test_helpers::logging::logs_contains_a_line_with; use torrust_tracker_test_helpers::{configuration, logging}; use uuid::Uuid; @@ -30,7 +30,7 @@ async fn should_allow_getting_all_torrents() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_torrents(Query::empty(), Some(headers_with_request_id(request_id))) .await; @@ -64,7 +64,7 @@ async fn should_allow_limiting_the_torrents_in_the_result() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_torrents( Query::params([QueryParam::new("limit", "1")].to_vec()), @@ -101,7 +101,7 @@ async fn should_allow_the_torrents_result_pagination() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_torrents( Query::params([QueryParam::new("offset", "1")].to_vec()), @@ -137,7 +137,7 @@ async fn should_allow_getting_a_list_of_torrents_providing_infohashes() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_torrents( Query::params( @@ -184,7 +184,7 @@ async fn should_fail_getting_torrents_when_the_offset_query_parameter_cannot_be_ for invalid_offset in &invalid_offsets { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_torrents( Query::params([QueryParam::new("offset", invalid_offset)].to_vec()), @@ -213,7 +213,7 @@ async fn should_fail_getting_torrents_when_the_limit_query_parameter_cannot_be_p for invalid_limit in &invalid_limits { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_torrents( Query::params([QueryParam::new("limit", invalid_limit)].to_vec()), @@ -242,7 +242,7 @@ async fn should_fail_getting_torrents_when_the_info_hash_parameter_is_invalid() for invalid_info_hash in &invalid_info_hashes { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_torrents( Query::params([QueryParam::new("info_hash", invalid_info_hash)].to_vec()), @@ -268,7 +268,7 @@ async fn should_not_allow_getting_torrents_for_unauthenticated_users() { let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_invalid_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_invalid_token(env.get_connection_info().origin)) .unwrap() .get_torrents(Query::empty(), Some(headers_with_request_id(request_id))) .await; @@ -282,7 +282,7 @@ async fn should_not_allow_getting_torrents_for_unauthenticated_users() { let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_no_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_no_token(env.get_connection_info().origin)) .unwrap() .get_torrents(Query::default(), Some(headers_with_request_id(request_id))) .await; @@ -311,7 +311,7 @@ async fn should_allow_getting_a_torrent_info() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_torrent(&info_hash.to_string(), Some(headers_with_request_id(request_id))) .await; @@ -340,7 +340,7 @@ async fn should_fail_while_getting_a_torrent_info_when_the_torrent_does_not_exis let request_id = Uuid::new_v4(); let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); // DevSkim: ignore DS173237 - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_torrent(&info_hash.to_string(), Some(headers_with_request_id(request_id))) .await; @@ -359,7 +359,7 @@ async fn should_fail_getting_a_torrent_info_when_the_provided_infohash_is_invali for invalid_infohash in &invalid_infohashes_returning_bad_request() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_torrent(invalid_infohash, Some(headers_with_request_id(request_id))) .await; @@ -370,7 +370,7 @@ async fn should_fail_getting_a_torrent_info_when_the_provided_infohash_is_invali for invalid_infohash in &invalid_infohashes_returning_not_found() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .get_torrent(invalid_infohash, Some(headers_with_request_id(request_id))) .await; @@ -393,7 +393,7 @@ async fn should_not_allow_getting_a_torrent_info_for_unauthenticated_users() { let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_invalid_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_invalid_token(env.get_connection_info().origin)) .unwrap() .get_torrent(&info_hash.to_string(), Some(headers_with_request_id(request_id))) .await; @@ -407,7 +407,7 @@ async fn should_not_allow_getting_a_torrent_info_for_unauthenticated_users() { let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_no_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_no_token(env.get_connection_info().origin)) .unwrap() .get_torrent(&info_hash.to_string(), Some(headers_with_request_id(request_id))) .await; diff --git a/packages/axum-rest-api-server/tests/server/v1/contract/context/whitelist.rs b/packages/axum-rest-api-server/tests/server/v1/contract/context/whitelist.rs index 7e5298deb..baa075ac9 100644 --- a/packages/axum-rest-api-server/tests/server/v1/contract/context/whitelist.rs +++ b/packages/axum-rest-api-server/tests/server/v1/contract/context/whitelist.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use torrust_info_hash::InfoHash; use torrust_tracker_axum_rest_api_server::testing::environment::Started; -use torrust_tracker_rest_api_client::v1::client::{Client, headers_with_request_id}; +use torrust_tracker_rest_api_client::v1::client::{ApiHttpClient, headers_with_request_id}; use torrust_tracker_test_helpers::logging::logs_contains_a_line_with; use torrust_tracker_test_helpers::{configuration, logging}; use uuid::Uuid; @@ -24,7 +24,7 @@ async fn should_allow_whitelisting_a_torrent() { let request_id = Uuid::new_v4(); let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); // DevSkim: ignore DS173237 - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .whitelist_a_torrent(&info_hash, Some(headers_with_request_id(request_id))) .await; @@ -49,7 +49,7 @@ async fn should_allow_whitelisting_a_torrent_that_has_been_already_whitelisted() let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); // DevSkim: ignore DS173237 - let api_client = Client::new(env.get_connection_info()).unwrap(); + let api_client = ApiHttpClient::new(env.get_connection_info()).unwrap(); let request_id = Uuid::new_v4(); @@ -78,7 +78,7 @@ async fn should_not_allow_whitelisting_a_torrent_for_unauthenticated_users() { let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_invalid_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_invalid_token(env.get_connection_info().origin)) .unwrap() .whitelist_a_torrent(&info_hash, Some(headers_with_request_id(request_id))) .await; @@ -92,7 +92,7 @@ async fn should_not_allow_whitelisting_a_torrent_for_unauthenticated_users() { let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_no_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_no_token(env.get_connection_info().origin)) .unwrap() .whitelist_a_torrent(&info_hash, Some(headers_with_request_id(request_id))) .await; @@ -119,7 +119,7 @@ async fn should_fail_when_the_torrent_cannot_be_whitelisted() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .whitelist_a_torrent(&info_hash, Some(headers_with_request_id(request_id))) .await; @@ -143,7 +143,7 @@ async fn should_fail_whitelisting_a_torrent_when_the_provided_infohash_is_invali let request_id = Uuid::new_v4(); for invalid_infohash in &invalid_infohashes_returning_bad_request() { - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .whitelist_a_torrent(invalid_infohash, Some(headers_with_request_id(request_id))) .await; @@ -154,7 +154,7 @@ async fn should_fail_whitelisting_a_torrent_when_the_provided_infohash_is_invali let request_id = Uuid::new_v4(); for invalid_infohash in &invalid_infohashes_returning_not_found() { - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .whitelist_a_torrent(invalid_infohash, Some(headers_with_request_id(request_id))) .await; @@ -183,7 +183,7 @@ async fn should_allow_removing_a_torrent_from_the_whitelist() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .remove_torrent_from_whitelist(&hash, Some(headers_with_request_id(request_id))) .await; @@ -210,7 +210,7 @@ async fn should_not_fail_trying_to_remove_a_non_whitelisted_torrent_from_the_whi let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .remove_torrent_from_whitelist(&non_whitelisted_torrent_hash, Some(headers_with_request_id(request_id))) .await; @@ -229,7 +229,7 @@ async fn should_fail_removing_a_torrent_from_the_whitelist_when_the_provided_inf for invalid_infohash in &invalid_infohashes_returning_bad_request() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .remove_torrent_from_whitelist(invalid_infohash, Some(headers_with_request_id(request_id))) .await; @@ -240,7 +240,7 @@ async fn should_fail_removing_a_torrent_from_the_whitelist_when_the_provided_inf for invalid_infohash in &invalid_infohashes_returning_not_found() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .remove_torrent_from_whitelist(invalid_infohash, Some(headers_with_request_id(request_id))) .await; @@ -270,7 +270,7 @@ async fn should_fail_when_the_torrent_cannot_be_removed_from_the_whitelist() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .remove_torrent_from_whitelist(&hash, Some(headers_with_request_id(request_id))) .await; @@ -303,7 +303,7 @@ async fn should_not_allow_removing_a_torrent_from_the_whitelist_for_unauthentica let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_invalid_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_invalid_token(env.get_connection_info().origin)) .unwrap() .remove_torrent_from_whitelist(&hash, Some(headers_with_request_id(request_id))) .await; @@ -324,7 +324,7 @@ async fn should_not_allow_removing_a_torrent_from_the_whitelist_for_unauthentica let request_id = Uuid::new_v4(); - let response = Client::new(connection_with_no_token(env.get_connection_info().origin)) + let response = ApiHttpClient::new(connection_with_no_token(env.get_connection_info().origin)) .unwrap() .remove_torrent_from_whitelist(&hash, Some(headers_with_request_id(request_id))) .await; @@ -357,7 +357,7 @@ async fn should_allow_reload_the_whitelist_from_the_database() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .reload_whitelist(Some(headers_with_request_id(request_id))) .await; @@ -396,7 +396,7 @@ async fn should_fail_when_the_whitelist_cannot_be_reloaded_from_the_database() { let request_id = Uuid::new_v4(); - let response = Client::new(env.get_connection_info()) + let response = ApiHttpClient::new(env.get_connection_info()) .unwrap() .reload_whitelist(Some(headers_with_request_id(request_id))) .await; diff --git a/packages/rest-api-application/src/lib.rs b/packages/rest-api-application/src/lib.rs index aeb870d23..e880d9953 100644 --- a/packages/rest-api-application/src/lib.rs +++ b/packages/rest-api-application/src/lib.rs @@ -14,5 +14,4 @@ //! - Axum server routing or middleware. //! - Tracker internal database or domain logic. //! - Protocol DTOs (those belong to `rest-api-protocol`). -pub mod ports; -pub mod use_cases; +pub mod v1; diff --git a/packages/rest-api-application/src/v1/mod.rs b/packages/rest-api-application/src/v1/mod.rs new file mode 100644 index 000000000..e66a1927f --- /dev/null +++ b/packages/rest-api-application/src/v1/mod.rs @@ -0,0 +1,5 @@ +//! Version 1 of the Torrust Tracker REST API application layer. +//! +//! This module contains all v1-specific port traits and use-case services. +pub mod ports; +pub mod use_cases; diff --git a/packages/rest-api-application/src/ports/auth_key.rs b/packages/rest-api-application/src/v1/ports/auth_key.rs similarity index 100% rename from packages/rest-api-application/src/ports/auth_key.rs rename to packages/rest-api-application/src/v1/ports/auth_key.rs diff --git a/packages/rest-api-application/src/ports/mod.rs b/packages/rest-api-application/src/v1/ports/mod.rs similarity index 100% rename from packages/rest-api-application/src/ports/mod.rs rename to packages/rest-api-application/src/v1/ports/mod.rs diff --git a/packages/rest-api-application/src/ports/stats.rs b/packages/rest-api-application/src/v1/ports/stats.rs similarity index 100% rename from packages/rest-api-application/src/ports/stats.rs rename to packages/rest-api-application/src/v1/ports/stats.rs diff --git a/packages/rest-api-application/src/ports/torrent.rs b/packages/rest-api-application/src/v1/ports/torrent.rs similarity index 100% rename from packages/rest-api-application/src/ports/torrent.rs rename to packages/rest-api-application/src/v1/ports/torrent.rs diff --git a/packages/rest-api-application/src/ports/whitelist.rs b/packages/rest-api-application/src/v1/ports/whitelist.rs similarity index 100% rename from packages/rest-api-application/src/ports/whitelist.rs rename to packages/rest-api-application/src/v1/ports/whitelist.rs diff --git a/packages/rest-api-application/src/use_cases/auth_key.rs b/packages/rest-api-application/src/v1/use_cases/auth_key.rs similarity index 97% rename from packages/rest-api-application/src/use_cases/auth_key.rs rename to packages/rest-api-application/src/v1/use_cases/auth_key.rs index eb1bde71c..ff8405444 100644 --- a/packages/rest-api-application/src/use_cases/auth_key.rs +++ b/packages/rest-api-application/src/v1/use_cases/auth_key.rs @@ -5,7 +5,7 @@ use torrust_tracker_rest_api_protocol::v1::context::auth_key::forms::add_key_form::AddKeyForm; use torrust_tracker_rest_api_protocol::v1::context::auth_key::resources::auth_key::{AuthKey, AuthKeyError}; -use crate::ports::auth_key::AuthKeyPort; +use crate::v1::ports::auth_key::AuthKeyPort; /// Use-case service for auth-key-related API operations. /// diff --git a/packages/rest-api-application/src/use_cases/mod.rs b/packages/rest-api-application/src/v1/use_cases/mod.rs similarity index 100% rename from packages/rest-api-application/src/use_cases/mod.rs rename to packages/rest-api-application/src/v1/use_cases/mod.rs diff --git a/packages/rest-api-application/src/use_cases/stats.rs b/packages/rest-api-application/src/v1/use_cases/stats.rs similarity index 95% rename from packages/rest-api-application/src/use_cases/stats.rs rename to packages/rest-api-application/src/v1/use_cases/stats.rs index 4f206ab53..51eefb8de 100644 --- a/packages/rest-api-application/src/use_cases/stats.rs +++ b/packages/rest-api-application/src/v1/use_cases/stats.rs @@ -3,7 +3,7 @@ //! Orchestrates calls to the [`StatsQueryPort`] to retrieve tracker metrics. use torrust_tracker_rest_api_protocol::v1::context::stats::resources::stats::{LabeledStats, Stats}; -use crate::ports::stats::StatsQueryPort; +use crate::v1::ports::stats::StatsQueryPort; /// Use-case service for stats-related API operations. /// diff --git a/packages/rest-api-application/src/use_cases/torrent.rs b/packages/rest-api-application/src/v1/use_cases/torrent.rs similarity index 96% rename from packages/rest-api-application/src/use_cases/torrent.rs rename to packages/rest-api-application/src/v1/use_cases/torrent.rs index 47c182e10..bcbda9673 100644 --- a/packages/rest-api-application/src/use_cases/torrent.rs +++ b/packages/rest-api-application/src/v1/use_cases/torrent.rs @@ -3,7 +3,7 @@ use torrust_info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; use torrust_tracker_rest_api_protocol::v1::context::torrent::resources::torrent::{ListItem, Torrent}; -use crate::ports::torrent::TorrentQueryPort; +use crate::v1::ports::torrent::TorrentQueryPort; /// Use-case service for torrent-related API operations. /// diff --git a/packages/rest-api-application/src/use_cases/whitelist.rs b/packages/rest-api-application/src/v1/use_cases/whitelist.rs similarity index 96% rename from packages/rest-api-application/src/use_cases/whitelist.rs rename to packages/rest-api-application/src/v1/use_cases/whitelist.rs index 05c0b6e6d..ab1e3a5ac 100644 --- a/packages/rest-api-application/src/use_cases/whitelist.rs +++ b/packages/rest-api-application/src/v1/use_cases/whitelist.rs @@ -5,7 +5,7 @@ use torrust_info_hash::InfoHash; use torrust_tracker_rest_api_protocol::v1::context::whitelist::resources::whitelist::WhitelistError; -use crate::ports::whitelist::WhitelistCommandPort; +use crate::v1::ports::whitelist::WhitelistCommandPort; /// Use-case service for whitelist-related API operations. /// diff --git a/packages/rest-api-client/Cargo.toml b/packages/rest-api-client/Cargo.toml index f57aea95d..31e012f20 100644 --- a/packages/rest-api-client/Cargo.toml +++ b/packages/rest-api-client/Cargo.toml @@ -19,5 +19,6 @@ hyper = "1" reqwest = { version = "0", features = [ "json", "query" ] } serde = { version = "1", features = [ "derive" ] } thiserror = "2" +torrust-tracker-rest-api-protocol = { version = "3.0.0-develop", path = "../rest-api-protocol" } url = { version = "2", features = [ "serde" ] } uuid = { version = "1", features = [ "v4" ] } diff --git a/packages/rest-api-client/src/v1/client.rs b/packages/rest-api-client/src/v1/client.rs index fadef6bac..0385e09dc 100644 --- a/packages/rest-api-client/src/v1/client.rs +++ b/packages/rest-api-client/src/v1/client.rs @@ -1,8 +1,15 @@ use std::time::Duration; use hyper::{HeaderMap, header}; -use reqwest::{Error, Response}; +use reqwest::{Response, StatusCode}; use serde::Serialize; +use serde::de::DeserializeOwned; +use thiserror::Error; +// Re-export AddKeyForm from the protocol package for backwards compatibility. +pub use torrust_tracker_rest_api_protocol::v1::context::auth_key::forms::add_key_form::AddKeyForm; +use torrust_tracker_rest_api_protocol::v1::context::auth_key::resources::auth_key::AuthKey; +use torrust_tracker_rest_api_protocol::v1::context::stats::resources::stats::Stats; +use torrust_tracker_rest_api_protocol::v1::context::torrent::resources::torrent::{ListItem, Torrent}; use url::Url; use uuid::Uuid; @@ -15,19 +22,219 @@ pub const AUTH_BEARER_TOKEN_HEADER_PREFIX: &str = "Bearer"; const API_PATH: &str = "api/v1/"; const DEFAULT_REQUEST_TIMEOUT_IN_SECS: u64 = 5; -/// API Client +/// Error type for [`ApiClient`] operations. +#[derive(Debug, Error)] +pub enum ClientError { + /// A transport-level error (connection refused, timeout, DNS failure, etc.). + #[error("transport error: {0}")] + TransportError(#[source] reqwest::Error), + + /// The API returned a non-2xx status code. + #[error("API error: {status} - {body}")] + ApiError { + /// The HTTP status code returned by the API. + status: StatusCode, + /// The response body (error message). + body: String, + }, + + /// Failed to deserialize the API response body into the expected type. + #[error("deserialization error: {0}")] + DeserializationError(#[source] reqwest::Error), + + /// An internal error (URL construction failure, etc.). + #[error("internal error: {0}")] + InternalError(String), +} + +impl From for ClientError { + fn from(err: reqwest::Error) -> Self { + Self::TransportError(err) + } +} + +/// High-level typed client for the Torrust Tracker REST API. +/// +/// Wraps [`ApiHttpClient`] and returns protocol DTOs from `rest-api-protocol`. +/// Never panics — all errors are returned as [`ClientError`]. +pub struct ApiClient { + inner: ApiHttpClient, +} + +impl ApiClient { + /// Creates a new `ApiClient`. + /// + /// # Errors + /// + /// Returns [`ClientError::TransportError`] if the HTTP client cannot be built. + pub fn new(connection_info: ConnectionInfo) -> Result { + Ok(Self { + inner: ApiHttpClient::new(connection_info).map_err(ClientError::TransportError)?, + }) + } + + /// Returns a reference to the inner [`ApiHttpClient`] for low-level operations. + #[must_use] + pub fn inner(&self) -> &ApiHttpClient { + &self.inner + } + + /// Generates a new random authentication key valid for `seconds_valid`. + /// + /// # Errors + /// + /// Returns [`ClientError::TransportError`] if the request fails. + /// Returns [`ClientError::ApiError`] if the API returns a non-2xx status. + /// Returns [`ClientError::DeserializationError`] if the response cannot be parsed. + pub async fn generate_auth_key(&self, seconds_valid: i32) -> Result { + let response = self.inner.post_empty_result(&format!("key/{seconds_valid}"), None).await?; + Self::parse_response(response).await + } + + /// Adds a new authentication key using the provided form data. + /// + /// # Errors + /// + /// Returns [`ClientError::TransportError`] if the request fails. + /// Returns [`ClientError::ApiError`] if the API returns a non-2xx status. + /// Returns [`ClientError::DeserializationError`] if the response cannot be parsed. + pub async fn add_auth_key(&self, form: AddKeyForm) -> Result { + let response = self.inner.post_form_result("keys", &form, None).await?; + Self::parse_response(response).await + } + + /// Deletes an authentication key. + /// + /// # Errors + /// + /// Returns [`ClientError::TransportError`] if the request fails. + /// Returns [`ClientError::ApiError`] if the API returns a non-2xx status. + pub async fn delete_auth_key(&self, key: &str) -> Result<(), ClientError> { + let response = self.inner.delete_result(&format!("key/{key}"), None).await?; + Self::check_success(response).await + } + + /// Reloads authentication keys from the database. + /// + /// # Errors + /// + /// Returns [`ClientError::TransportError`] if the request fails. + /// Returns [`ClientError::ApiError`] if the API returns a non-2xx status. + pub async fn reload_keys(&self) -> Result<(), ClientError> { + let response = self.inner.get_result("keys/reload", Query::default(), None).await?; + Self::check_success(response).await + } + + /// Whitelists a torrent by info hash. + /// + /// # Errors + /// + /// Returns [`ClientError::TransportError`] if the request fails. + /// Returns [`ClientError::ApiError`] if the API returns a non-2xx status. + pub async fn whitelist_a_torrent(&self, info_hash: &str) -> Result<(), ClientError> { + let response = self.inner.post_empty_result(&format!("whitelist/{info_hash}"), None).await?; + Self::check_success(response).await + } + + /// Removes a torrent from the whitelist. + /// + /// # Errors + /// + /// Returns [`ClientError::TransportError`] if the request fails. + /// Returns [`ClientError::ApiError`] if the API returns a non-2xx status. + pub async fn remove_torrent_from_whitelist(&self, info_hash: &str) -> Result<(), ClientError> { + let response = self.inner.delete_result(&format!("whitelist/{info_hash}"), None).await?; + Self::check_success(response).await + } + + /// Reloads the whitelist from the database. + /// + /// # Errors + /// + /// Returns [`ClientError::TransportError`] if the request fails. + /// Returns [`ClientError::ApiError`] if the API returns a non-2xx status. + pub async fn reload_whitelist(&self) -> Result<(), ClientError> { + let response = self.inner.get_result("whitelist/reload", Query::default(), None).await?; + Self::check_success(response).await + } + + /// Gets a single torrent by info hash. + /// + /// # Errors + /// + /// Returns [`ClientError::TransportError`] if the request fails. + /// Returns [`ClientError::ApiError`] if the API returns a non-2xx status. + /// Returns [`ClientError::DeserializationError`] if the response cannot be parsed. + pub async fn get_torrent(&self, info_hash: &str) -> Result { + let response = self + .inner + .get_result(&format!("torrent/{info_hash}"), Query::default(), None) + .await?; + Self::parse_response(response).await + } + + /// Gets a list of torrents matching the query parameters. + /// + /// # Errors + /// + /// Returns [`ClientError::TransportError`] if the request fails. + /// Returns [`ClientError::ApiError`] if the API returns a non-2xx status. + /// Returns [`ClientError::DeserializationError`] if the response cannot be parsed. + pub async fn get_torrents(&self, params: Query) -> Result, ClientError> { + let response = self.inner.get_result("torrents", params, None).await?; + Self::parse_response(response).await + } + + /// Gets tracker statistics. + /// + /// # Errors + /// + /// Returns [`ClientError::TransportError`] if the request fails. + /// Returns [`ClientError::ApiError`] if the API returns a non-2xx status. + /// Returns [`ClientError::DeserializationError`] if the response cannot be parsed. + pub async fn get_tracker_statistics(&self) -> Result { + let response = self.inner.get_result("stats", Query::default(), None).await?; + Self::parse_response(response).await + } + + /// Parses a successful response into the expected DTO type. + async fn parse_response(response: Response) -> Result { + let status = response.status(); + if !status.is_success() { + let body = response.text().await.map_err(ClientError::TransportError)?; + return Err(ClientError::ApiError { status, body }); + } + response.json::().await.map_err(ClientError::DeserializationError) + } + + /// Checks that the response has a 2xx status code, ignoring the body. + async fn check_success(response: Response) -> Result<(), ClientError> { + let status = response.status(); + if !status.is_success() { + let body = response.text().await.map_err(ClientError::TransportError)?; + return Err(ClientError::ApiError { status, body }); + } + Ok(()) + } +} + +/// Low-level HTTP transport for the Torrust Tracker REST API. +/// +/// Handles connection info, URL building, auth headers, and raw HTTP requests. +/// Returns [`reqwest::Response`] directly. For a typed high-level API, use +/// [`ApiClient`]. #[allow(clippy::struct_field_names)] -pub struct Client { +pub struct ApiHttpClient { connection_info: ConnectionInfo, base_path: String, http_client: reqwest::Client, } -impl Client { +impl ApiHttpClient { /// # Errors /// /// Will return an error if the HTTP client can't be created. - pub fn new(connection_info: ConnectionInfo) -> Result { + pub fn new(connection_info: ConnectionInfo) -> Result { let client = reqwest::Client::builder() .timeout(Duration::from_secs(DEFAULT_REQUEST_TIMEOUT_IN_SECS)) .build()?; @@ -39,61 +246,137 @@ impl Client { }) } + /// Generates a new random authentication key valid for `seconds_valid`. + /// + /// # Panics + /// + /// Will panic if the request can't be sent. pub async fn generate_auth_key(&self, seconds_valid: i32, headers: Option) -> Response { - self.post_empty(&format!("key/{seconds_valid}"), headers).await + self.post_empty_result(&format!("key/{seconds_valid}"), headers) + .await + .unwrap() } + /// Adds a new authentication key using the provided form data. + /// + /// # Panics + /// + /// Will panic if the request can't be sent. pub async fn add_auth_key(&self, add_key_form: AddKeyForm, headers: Option) -> Response { - self.post_form("keys", &add_key_form, headers).await + self.post_form_result("keys", &add_key_form, headers).await.unwrap() } + /// Deletes an authentication key. + /// + /// # Panics + /// + /// Will panic if the request can't be sent. pub async fn delete_auth_key(&self, key: &str, headers: Option) -> Response { - self.delete(&format!("key/{key}"), headers).await + self.delete_result(&format!("key/{key}"), headers).await.unwrap() } + /// Reloads authentication keys from the database. + /// + /// # Panics + /// + /// Will panic if the request can't be sent. pub async fn reload_keys(&self, headers: Option) -> Response { - self.get("keys/reload", Query::default(), headers).await + self.get_result("keys/reload", Query::default(), headers).await.unwrap() } + /// Whitelists a torrent by info hash. + /// + /// # Panics + /// + /// Will panic if the request can't be sent. pub async fn whitelist_a_torrent(&self, info_hash: &str, headers: Option) -> Response { - self.post_empty(&format!("whitelist/{info_hash}"), headers).await + self.post_empty_result(&format!("whitelist/{info_hash}"), headers) + .await + .unwrap() } + /// Removes a torrent from the whitelist. + /// + /// # Panics + /// + /// Will panic if the request can't be sent. pub async fn remove_torrent_from_whitelist(&self, info_hash: &str, headers: Option) -> Response { - self.delete(&format!("whitelist/{info_hash}"), headers).await + self.delete_result(&format!("whitelist/{info_hash}"), headers).await.unwrap() } + /// Reloads the whitelist from the database. + /// + /// # Panics + /// + /// Will panic if the request can't be sent. pub async fn reload_whitelist(&self, headers: Option) -> Response { - self.get("whitelist/reload", Query::default(), headers).await + self.get_result("whitelist/reload", Query::default(), headers).await.unwrap() } + /// Gets a single torrent by info hash. + /// + /// # Panics + /// + /// Will panic if the request can't be sent. pub async fn get_torrent(&self, info_hash: &str, headers: Option) -> Response { - self.get(&format!("torrent/{info_hash}"), Query::default(), headers).await + self.get_result(&format!("torrent/{info_hash}"), Query::default(), headers) + .await + .unwrap() } + /// Gets a list of torrents matching the query parameters. + /// + /// # Panics + /// + /// Will panic if the request can't be sent. pub async fn get_torrents(&self, params: Query, headers: Option) -> Response { - self.get("torrents", params, headers).await + self.get_result("torrents", params, headers).await.unwrap() } + /// Gets tracker statistics. + /// + /// # Panics + /// + /// Will panic if the request can't be sent. pub async fn get_tracker_statistics(&self, headers: Option) -> Response { - self.get("stats", Query::default(), headers).await + self.get_result("stats", Query::default(), headers).await.unwrap() } + /// Performs a GET request. + /// + /// # Panics + /// + /// Will panic if the request can't be sent. pub async fn get(&self, path: &str, params: Query, headers: Option) -> Response { + self.get_result(path, params, headers).await.unwrap() + } + + /// Fallible version of [`Self::get`] that returns a `Result` instead of panicking. + pub(crate) async fn get_result( + &self, + path: &str, + params: Query, + headers: Option, + ) -> Result { let mut query: Query = params; if let Some(token) = &self.connection_info.api_token { query.add_param(QueryParam::new(TOKEN_PARAM_NAME, token)); } - self.get_request_with_query(path, query, headers).await + self.get_request_with_query_result(path, query, headers).await } /// # Panics /// /// Will panic if the request can't be sent pub async fn post_empty(&self, path: &str, headers: Option) -> Response { - let builder = self.http_client.post(self.base_url(path).clone()); + self.post_empty_result(path, headers).await.unwrap() + } + + /// Fallible version of [`Self::post_empty`] that returns a `Result` instead of panicking. + pub(crate) async fn post_empty_result(&self, path: &str, headers: Option) -> Result { + let builder = self.http_client.post(self.base_url(path)?.clone()); let builder = match headers { Some(headers) => builder.headers(headers), @@ -105,14 +388,24 @@ impl Client { None => builder, }; - builder.send().await.unwrap() + Ok(builder.send().await?) } /// # Panics /// /// Will panic if the request can't be sent pub async fn post_form(&self, path: &str, form: &T, headers: Option) -> Response { - let builder = self.http_client.post(self.base_url(path).clone()).json(&form); + self.post_form_result(path, form, headers).await.unwrap() + } + + /// Fallible version of [`Self::post_form`] that returns a `Result` instead of panicking. + pub(crate) async fn post_form_result( + &self, + path: &str, + form: &T, + headers: Option, + ) -> Result { + let builder = self.http_client.post(self.base_url(path)?.clone()).json(&form); let builder = match headers { Some(headers) => builder.headers(headers), @@ -124,14 +417,12 @@ impl Client { None => builder, }; - builder.send().await.unwrap() + Ok(builder.send().await?) } - /// # Panics - /// - /// Will panic if the request can't be sent - async fn delete(&self, path: &str, headers: Option) -> Response { - let builder = self.http_client.delete(self.base_url(path).clone()); + /// Fallible version of [`Self::delete`] that returns a `Result` instead of panicking. + async fn delete_result(&self, path: &str, headers: Option) -> Result { + let builder = self.http_client.delete(self.base_url(path)?.clone()); let builder = match headers { Some(headers) => builder.headers(headers), @@ -143,13 +434,24 @@ impl Client { None => builder, }; - builder.send().await.unwrap() + Ok(builder.send().await?) } /// # Panics /// /// Will panic if it can't convert the authentication token to a `HeaderValue`. pub async fn get_request_with_query(&self, path: &str, params: Query, headers: Option) -> Response { + self.get_request_with_query_result(path, params, headers).await.unwrap() + } + + /// Fallible version of [`Self::get_request_with_query`] that returns a `Result` instead of panicking. + pub(crate) async fn get_request_with_query_result( + &self, + path: &str, + params: Query, + headers: Option, + ) -> Result { + let url = self.base_url(path)?; match &self.connection_info.api_token { Some(token) => { let headers = if let Some(headers) = headers { @@ -185,18 +487,22 @@ impl Client { headers }; - get(self.base_url(path), Some(params), Some(headers)).await + get_result(url, Some(params), Some(headers)).await } - None => get(self.base_url(path), Some(params), headers).await, + None => get_result(url, Some(params), headers).await, } } + /// # Panics + /// + /// Will panic if the request can't be sent pub async fn get_request(&self, path: &str) -> Response { - get(self.base_url(path), None, None).await + get(self.base_url(path).unwrap(), None, None).await } - fn base_url(&self, path: &str) -> Url { - Url::parse(&format!("{}{}{path}", self.connection_info.origin, self.base_path)).unwrap() + fn base_url(&self, path: &str) -> Result { + Url::parse(&format!("{}{}{path}", self.connection_info.origin, self.base_path)) + .map_err(|e| ClientError::InternalError(format!("invalid URL: {e}"))) } } @@ -204,10 +510,14 @@ impl Client { /// /// Will panic if the request can't be sent pub async fn get(path: Url, query: Option, headers: Option) -> Response { + get_result(path, query, headers).await.unwrap() +} + +/// Fallible version of [`get`] that returns a `Result` instead of panicking. +pub(crate) async fn get_result(path: Url, query: Option, headers: Option) -> Result { let client = reqwest::Client::builder() .timeout(Duration::from_secs(DEFAULT_REQUEST_TIMEOUT_IN_SECS)) - .build() - .unwrap(); + .build()?; let mut request_builder = client.get(path); @@ -219,7 +529,7 @@ pub async fn get(path: Url, query: Option, headers: Option) -> request_builder = request_builder.headers(headers); } - request_builder.send().await.unwrap() + request_builder.send().await.map_err(ClientError::TransportError) } /// Returns a `HeaderMap` with a request id header. @@ -256,10 +566,3 @@ pub fn headers_with_auth_token(token: &str) -> HeaderMap { ); headers } - -#[derive(Serialize, Debug)] -pub struct AddKeyForm { - #[serde(rename = "key")] - pub opt_key: Option, - pub seconds_valid: Option, -} diff --git a/packages/rest-api-client/src/v1/mod.rs b/packages/rest-api-client/src/v1/mod.rs index b9babe5bc..104437df8 100644 --- a/packages/rest-api-client/src/v1/mod.rs +++ b/packages/rest-api-client/src/v1/mod.rs @@ -1 +1,3 @@ pub mod client; + +pub use client::{ApiClient, ApiHttpClient}; diff --git a/packages/rest-api-core/Cargo.toml b/packages/rest-api-core/Cargo.toml deleted file mode 100644 index c1580345d..000000000 --- a/packages/rest-api-core/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -authors.workspace = true -description = "A library with the core functionality needed to implement a BitTorrent UDP tracker." -documentation.workspace = true -edition.workspace = true -homepage.workspace = true -keywords = [ "api", "bittorrent", "core", "library", "tracker" ] -license.workspace = true -name = "torrust-tracker-rest-api-core" -publish.workspace = true -readme = "README.md" -repository.workspace = true -rust-version.workspace = true -version.workspace = true - -[dependencies] -torrust-tracker-http-core = { version = "3.0.0-develop", path = "../http-core" } -torrust-tracker-core = { version = "3.0.0-develop", path = "../tracker-core" } -torrust-tracker-udp-core = { version = "3.0.0-develop", path = "../udp-core" } -tokio = { version = "1", features = [ "macros", "net", "rt-multi-thread", "signal", "sync" ] } -tokio-util = "0.7.15" -torrust-tracker-configuration = { version = "3.0.0-develop", path = "../configuration" } -torrust-metrics = "0.1.0" -torrust-tracker-primitives = { version = "3.0.0-develop", path = "../primitives" } -torrust-tracker-swarm-coordination-registry = { version = "3.0.0-develop", path = "../swarm-coordination-registry" } -torrust-tracker-udp-server = { version = "3.0.0-develop", path = "../udp-server" } - -[dev-dependencies] -torrust-tracker-events = { version = "3.0.0-develop", path = "../events" } -torrust-tracker-test-helpers = { version = "3.0.0-develop", path = "../test-helpers" } diff --git a/packages/rest-api-core/LICENSE b/packages/rest-api-core/LICENSE deleted file mode 100644 index 0ad25db4b..000000000 --- a/packages/rest-api-core/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/packages/rest-api-core/README.md b/packages/rest-api-core/README.md deleted file mode 100644 index 96bf17bf7..000000000 --- a/packages/rest-api-core/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# BitTorrent UDP Tracker Core library - -A library with the core functionality needed to implement the Torrust Tracker API - -## Documentation - -[Crate documentation](https://docs.rs/torrust-tracker-api-core). - -## License - -The project is licensed under the terms of the [GNU AFFERO GENERAL PUBLIC LICENSE](./LICENSE). diff --git a/packages/rest-api-core/src/lib.rs b/packages/rest-api-core/src/lib.rs deleted file mode 100644 index ddf1d9afd..000000000 --- a/packages/rest-api-core/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod container; -pub mod statistics; diff --git a/packages/rest-api-core/src/statistics/metrics.rs b/packages/rest-api-core/src/statistics/metrics.rs deleted file mode 100644 index ecdecd130..000000000 --- a/packages/rest-api-core/src/statistics/metrics.rs +++ /dev/null @@ -1,118 +0,0 @@ -use torrust_tracker_primitives::swarm_metadata::AggregateActiveSwarmMetadata; - -/// Metrics collected by the tracker at the swarm layer. -#[derive(Copy, Clone, Debug, PartialEq, Default)] -pub struct TorrentsMetrics { - /// Total number of peers that have ever completed downloading. - pub total_downloaded: u64, - - /// Total number of seeders. - pub total_complete: u64, - - /// Total number of leechers. - pub total_incomplete: u64, - - /// Total number of torrents. - pub total_torrents: u64, -} - -impl From for TorrentsMetrics { - fn from(value: AggregateActiveSwarmMetadata) -> Self { - Self { - total_downloaded: value.total_downloaded, - total_complete: value.total_complete, - total_incomplete: value.total_incomplete, - total_torrents: value.total_torrents, - } - } -} - -/// Metrics collected by the tracker at the delivery layer. -/// -/// - Number of connections handled -/// - Number of `announce` requests handled -/// - Number of `scrape` request handled -/// -/// These metrics are collected for each connection type: UDP and HTTP -/// and also for each IP version used by the peers: IPv4 and IPv6. -#[derive(Debug, PartialEq, Default)] -pub struct ProtocolMetrics { - /// Total number of TCP (HTTP tracker) connections from IPv4 peers. - /// Since the HTTP tracker spec does not require a handshake, this metric - /// increases for every HTTP request. - #[deprecated(since = "3.1.0")] - pub tcp4_connections_handled: u64, - - /// Total number of TCP (HTTP tracker) `announce` requests from IPv4 peers. - pub tcp4_announces_handled: u64, - - /// Total number of TCP (HTTP tracker) `scrape` requests from IPv4 peers. - pub tcp4_scrapes_handled: u64, - - /// Total number of TCP (HTTP tracker) connections from IPv6 peers. - #[deprecated(since = "3.1.0")] - pub tcp6_connections_handled: u64, - - /// Total number of TCP (HTTP tracker) `announce` requests from IPv6 peers. - pub tcp6_announces_handled: u64, - - /// Total number of TCP (HTTP tracker) `scrape` requests from IPv6 peers. - pub tcp6_scrapes_handled: u64, - - // UDP - /// Total number of UDP (UDP tracker) requests aborted. - pub udp_requests_aborted: u64, - - /// Total number of UDP (UDP tracker) requests banned. - pub udp_requests_banned: u64, - - /// Total number of banned IPs. - pub udp_banned_ips_total: u64, - - /// Average rounded time spent processing UDP connect requests. - pub udp_avg_connect_processing_time_ns: u64, - - /// Average rounded time spent processing UDP announce requests. - pub udp_avg_announce_processing_time_ns: u64, - - /// Average rounded time spent processing UDP scrape requests. - pub udp_avg_scrape_processing_time_ns: u64, - - // UDPv4 - /// Total number of UDP (UDP tracker) requests from IPv4 peers. - pub udp4_requests: u64, - - /// Total number of UDP (UDP tracker) connections from IPv4 peers. - pub udp4_connections_handled: u64, - - /// Total number of UDP (UDP tracker) `announce` requests from IPv4 peers. - pub udp4_announces_handled: u64, - - /// Total number of UDP (UDP tracker) `scrape` requests from IPv4 peers. - pub udp4_scrapes_handled: u64, - - /// Total number of UDP (UDP tracker) responses from IPv4 peers. - pub udp4_responses: u64, - - /// Total number of UDP (UDP tracker) `error` requests from IPv4 peers. - pub udp4_errors_handled: u64, - - // UDPv6 - /// Total number of UDP (UDP tracker) requests from IPv6 peers. - pub udp6_requests: u64, - - /// Total number of UDP (UDP tracker) `connection` requests from IPv6 peers. - pub udp6_connections_handled: u64, - - /// Total number of UDP (UDP tracker) `announce` requests from IPv6 peers. - pub udp6_announces_handled: u64, - - /// Total number of UDP (UDP tracker) `scrape` requests from IPv6 peers. - pub udp6_scrapes_handled: u64, - - /// Total number of UDP (UDP tracker) responses from IPv6 peers. - pub udp6_responses: u64, - - /// Total number of UDP (UDP tracker) `error` requests from IPv6 peers. - pub udp6_errors_handled: u64, -} diff --git a/packages/rest-api-core/src/statistics/mod.rs b/packages/rest-api-core/src/statistics/mod.rs deleted file mode 100644 index a3c8a4b0e..000000000 --- a/packages/rest-api-core/src/statistics/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod metrics; -pub mod services; diff --git a/packages/rest-api-core/src/statistics/services.rs b/packages/rest-api-core/src/statistics/services.rs deleted file mode 100644 index be648bb2d..000000000 --- a/packages/rest-api-core/src/statistics/services.rs +++ /dev/null @@ -1,259 +0,0 @@ -use std::sync::Arc; - -use tokio::sync::RwLock; -use torrust_metrics::metric_collection::MetricCollection; -use torrust_tracker_core::torrent::repository::in_memory::InMemoryTorrentRepository; -use torrust_tracker_udp_core::services::banning::BanService; -use torrust_tracker_udp_core::{self}; -use torrust_tracker_udp_server::statistics::{self as udp_server_statistics}; - -use super::metrics::TorrentsMetrics; -use crate::statistics::metrics::ProtocolMetrics; - -/// All the metrics collected by the tracker. -#[derive(Debug, PartialEq)] -pub struct TrackerMetrics { - /// Domain level metrics. - /// - /// General metrics for all torrents (number of seeders, leechers, etcetera) - pub torrents_metrics: TorrentsMetrics, - - /// Application level metrics. Usage statistics/metrics. - /// - /// Metrics about how the tracker is been used (number of udp announce requests, number of http scrape requests, etcetera) - pub protocol_metrics: ProtocolMetrics, -} - -/// It returns all the [`TrackerMetrics`] -pub async fn get_metrics( - in_memory_torrent_repository: Arc, - tracker_core_stats_repository: Arc, - http_stats_repository: Arc, - udp_server_stats_repository: Arc, -) -> TrackerMetrics { - TrackerMetrics { - torrents_metrics: get_torrents_metrics(in_memory_torrent_repository, tracker_core_stats_repository).await, - protocol_metrics: get_protocol_metrics(http_stats_repository.clone(), udp_server_stats_repository.clone()).await, - } -} - -async fn get_torrents_metrics( - in_memory_torrent_repository: Arc, - - tracker_core_stats_repository: Arc, -) -> TorrentsMetrics { - let aggregate_active_swarm_metadata = in_memory_torrent_repository.get_aggregate_swarm_metadata().await; - - let mut torrents_metrics: TorrentsMetrics = aggregate_active_swarm_metadata.into(); - torrents_metrics.total_downloaded = tracker_core_stats_repository.get_torrents_downloads_total().await; - - torrents_metrics -} - -#[allow(deprecated)] -#[allow(clippy::too_many_lines)] -async fn get_protocol_metrics( - http_stats_repository: Arc, - udp_server_stats_repository: Arc, -) -> ProtocolMetrics { - let http_stats = http_stats_repository.get_stats().await; - let udp_server_stats = udp_server_stats_repository.get_stats().await; - - // TCPv4 - - let tcp4_announces_handled = http_stats.tcp4_announces_handled(); - let tcp4_scrapes_handled = http_stats.tcp4_scrapes_handled(); - - // TCPv6 - - let tcp6_announces_handled = http_stats.tcp6_announces_handled(); - let tcp6_scrapes_handled = http_stats.tcp6_scrapes_handled(); - - // UDP - - let udp_requests_aborted = udp_server_stats.udp_requests_aborted_total(); - let udp_requests_banned = udp_server_stats.udp_requests_banned_total(); - let udp_banned_ips_total = udp_server_stats.udp_banned_ips_total(); - let udp_avg_connect_processing_time_ns = udp_server_stats.udp_avg_connect_processing_time_ns_averaged(); - let udp_avg_announce_processing_time_ns = udp_server_stats.udp_avg_announce_processing_time_ns_averaged(); - let udp_avg_scrape_processing_time_ns = udp_server_stats.udp_avg_scrape_processing_time_ns_averaged(); - - // UDPv4 - - let udp4_requests = udp_server_stats.udp4_requests_received_total(); - let udp4_connections_handled = udp_server_stats.udp4_connect_requests_accepted_total(); - let udp4_announces_handled = udp_server_stats.udp4_announce_requests_accepted_total(); - let udp4_scrapes_handled = udp_server_stats.udp4_scrape_requests_accepted_total(); - let udp4_responses = udp_server_stats.udp4_responses_sent_total(); - let udp4_errors_handled = udp_server_stats.udp4_errors_total(); - - // UDPv6 - - let udp6_requests = udp_server_stats.udp6_requests_received_total(); - let udp6_connections_handled = udp_server_stats.udp6_connect_requests_accepted_total(); - let udp6_announces_handled = udp_server_stats.udp6_announce_requests_accepted_total(); - let udp6_scrapes_handled = udp_server_stats.udp6_scrape_requests_accepted_total(); - let udp6_responses = udp_server_stats.udp6_responses_sent_total(); - let udp6_errors_handled = udp_server_stats.udp6_errors_total(); - - // For backward compatibility we keep the `tcp4_connections_handled` and - // `tcp6_connections_handled` metrics. They don't make sense for the HTTP - // tracker, but we keep them for now. In new major versions we should remove - // them. - - ProtocolMetrics { - // TCPv4 - tcp4_connections_handled: tcp4_announces_handled + tcp4_scrapes_handled, - tcp4_announces_handled, - tcp4_scrapes_handled, - // TCPv6 - tcp6_connections_handled: tcp6_announces_handled + tcp6_scrapes_handled, - tcp6_announces_handled, - tcp6_scrapes_handled, - // UDP - udp_requests_aborted, - udp_requests_banned, - udp_banned_ips_total, - udp_avg_connect_processing_time_ns, - udp_avg_announce_processing_time_ns, - udp_avg_scrape_processing_time_ns, - // UDPv4 - udp4_requests, - udp4_connections_handled, - udp4_announces_handled, - udp4_scrapes_handled, - udp4_responses, - udp4_errors_handled, - // UDPv6 - udp6_requests, - udp6_connections_handled, - udp6_announces_handled, - udp6_scrapes_handled, - udp6_responses, - udp6_errors_handled, - } -} - -#[derive(Debug, PartialEq)] -pub struct TrackerLabeledMetrics { - pub metrics: MetricCollection, -} - -/// It returns all the [`TrackerLabeledMetrics`] -/// -/// # Panics -/// -/// Will panic if the metrics cannot be merged. This could happen if the -/// packages are producing duplicate metric names, for example. -pub async fn get_labeled_metrics( - in_memory_torrent_repository: Arc, - ban_service: Arc>, - swarms_stats_repository: Arc, - tracker_core_stats_repository: Arc, - http_stats_repository: Arc, - udp_stats_repository: Arc, - udp_server_stats_repository: Arc, -) -> TrackerLabeledMetrics { - let _torrents_metrics = in_memory_torrent_repository.get_aggregate_swarm_metadata(); - let _udp_banned_ips_total = ban_service.read().await.get_banned_ips_total(); - - let swarms_stats = swarms_stats_repository.get_metrics().await; - let tracker_core_stats = tracker_core_stats_repository.get_metrics().await; - let http_stats = http_stats_repository.get_stats().await; - let udp_stats_repository = udp_stats_repository.get_stats().await; - let udp_server_stats = udp_server_stats_repository.get_stats().await; - - // Merge all the metrics into a single collection - let mut metrics = MetricCollection::default(); - - metrics - .merge(&swarms_stats.metric_collection) - .expect("msg: failed to merge torrent repository metrics"); - metrics - .merge(&tracker_core_stats.metric_collection) - .expect("msg: failed to merge tracker core metrics"); - metrics - .merge(&http_stats.metric_collection) - .expect("msg: failed to merge HTTP core metrics"); - metrics - .merge(&udp_stats_repository.metric_collection) - .expect("failed to merge UDP core metrics"); - metrics - .merge(&udp_server_stats.metric_collection) - .expect("failed to merge UDP server metrics"); - - TrackerLabeledMetrics { metrics } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use tokio::sync::RwLock; - use tokio_util::sync::CancellationToken; - use torrust_tracker_configuration::Configuration; - use torrust_tracker_core::container::TrackerCoreContainer; - use torrust_tracker_core::{self}; - use torrust_tracker_events::bus::SenderStatus; - use torrust_tracker_http_core::event::bus::EventBus; - use torrust_tracker_http_core::event::sender::Broadcaster; - use torrust_tracker_http_core::statistics::event::listener::run_event_listener; - use torrust_tracker_http_core::statistics::repository::Repository; - use torrust_tracker_swarm_coordination_registry::container::SwarmCoordinationRegistryContainer; - use torrust_tracker_test_helpers::configuration; - use torrust_tracker_udp_core::services::banning::BanService; - - use crate::statistics::metrics::{ProtocolMetrics, TorrentsMetrics}; - use crate::statistics::services::{TrackerMetrics, get_metrics}; - - pub fn tracker_configuration() -> Configuration { - configuration::ephemeral() - } - - #[tokio::test] - async fn the_statistics_service_should_return_the_tracker_metrics() { - let cancellation_token = CancellationToken::new(); - - let config = tracker_configuration(); - let core_config = Arc::new(config.core.clone()); - - let swarm_coordination_registry_container = - Arc::new(SwarmCoordinationRegistryContainer::initialize(SenderStatus::Enabled)); - - let tracker_core_container = - TrackerCoreContainer::initialize_from(&core_config, &swarm_coordination_registry_container.clone()).await; - - let _ban_service = Arc::new(RwLock::new(BanService::new(10))); - - // HTTP core stats - let http_core_broadcaster = Broadcaster::default(); - let http_stats_repository = Arc::new(Repository::new()); - let http_stats_event_bus = Arc::new(EventBus::new( - config.core.tracker_usage_statistics.into(), - http_core_broadcaster.clone(), - )); - - if config.core.tracker_usage_statistics { - let _unused = run_event_listener(http_stats_event_bus.receiver(), cancellation_token, &http_stats_repository); - } - - // UDP server stats - let udp_server_stats_repository = Arc::new(torrust_tracker_udp_server::statistics::repository::Repository::new()); - - let tracker_metrics = get_metrics( - tracker_core_container.in_memory_torrent_repository.clone(), - tracker_core_container.stats_repository.clone(), - http_stats_repository.clone(), - udp_server_stats_repository.clone(), - ) - .await; - - assert_eq!( - tracker_metrics, - TrackerMetrics { - torrents_metrics: TorrentsMetrics::default(), - protocol_metrics: ProtocolMetrics::default(), - } - ); - } -} diff --git a/packages/rest-api-runtime-adapter/Cargo.toml b/packages/rest-api-runtime-adapter/Cargo.toml index 57632846e..73a401df9 100644 --- a/packages/rest-api-runtime-adapter/Cargo.toml +++ b/packages/rest-api-runtime-adapter/Cargo.toml @@ -14,6 +14,7 @@ rust-version.workspace = true version.workspace = true [dependencies] +torrust-tracker-configuration = { version = "3.0.0-develop", path = "../configuration" } torrust-tracker-rest-api-application = { version = "3.0.0-develop", path = "../rest-api-application" } torrust-tracker-rest-api-protocol = { version = "3.0.0-develop", path = "../rest-api-protocol" } torrust-tracker-core = { version = "3.0.0-develop", path = "../tracker-core" } @@ -25,3 +26,7 @@ torrust-tracker-swarm-coordination-registry = { version = "3.0.0-develop", path torrust-metrics = "0.1.0" torrust-info-hash = "=0.2.0" async-trait = "0.1" +tokio = { version = "1", features = [ "sync" ] } + +[dev-dependencies] +torrust-clock = "3.0.0" diff --git a/packages/rest-api-runtime-adapter/src/lib.rs b/packages/rest-api-runtime-adapter/src/lib.rs index 7cde91a69..2d11ab94b 100644 --- a/packages/rest-api-runtime-adapter/src/lib.rs +++ b/packages/rest-api-runtime-adapter/src/lib.rs @@ -14,5 +14,4 @@ //! - Protocol DTOs (those belong to `rest-api-protocol`). //! - Use-case services (those belong to `rest-api-application`). //! - Axum server routing or middleware. -pub mod adapters; -pub mod conversion; +pub mod v1; diff --git a/packages/rest-api-runtime-adapter/src/adapters/auth_key.rs b/packages/rest-api-runtime-adapter/src/v1/adapters/auth_key.rs similarity index 97% rename from packages/rest-api-runtime-adapter/src/adapters/auth_key.rs rename to packages/rest-api-runtime-adapter/src/v1/adapters/auth_key.rs index dbd8300de..c730c4c12 100644 --- a/packages/rest-api-runtime-adapter/src/adapters/auth_key.rs +++ b/packages/rest-api-runtime-adapter/src/v1/adapters/auth_key.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use async_trait::async_trait; use torrust_tracker_core::authentication::handler::{AddKeyRequest, KeysHandler}; use torrust_tracker_core::authentication::{Key, PeerKey}; -use torrust_tracker_rest_api_application::ports::auth_key::AuthKeyPort; +use torrust_tracker_rest_api_application::v1::ports::auth_key::AuthKeyPort; use torrust_tracker_rest_api_protocol::v1::context::auth_key::forms::add_key_form::AddKeyForm; use torrust_tracker_rest_api_protocol::v1::context::auth_key::resources::auth_key::{AuthKey, AuthKeyError}; diff --git a/packages/rest-api-runtime-adapter/src/adapters/mod.rs b/packages/rest-api-runtime-adapter/src/v1/adapters/mod.rs similarity index 100% rename from packages/rest-api-runtime-adapter/src/adapters/mod.rs rename to packages/rest-api-runtime-adapter/src/v1/adapters/mod.rs diff --git a/packages/rest-api-runtime-adapter/src/adapters/stats.rs b/packages/rest-api-runtime-adapter/src/v1/adapters/stats.rs similarity index 98% rename from packages/rest-api-runtime-adapter/src/adapters/stats.rs rename to packages/rest-api-runtime-adapter/src/v1/adapters/stats.rs index 9438f6cc7..83ddac976 100644 --- a/packages/rest-api-runtime-adapter/src/adapters/stats.rs +++ b/packages/rest-api-runtime-adapter/src/v1/adapters/stats.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use async_trait::async_trait; use torrust_metrics::metric_collection::MetricCollection; use torrust_tracker_core::torrent::repository::in_memory::InMemoryTorrentRepository; -use torrust_tracker_rest_api_application::ports::stats::StatsQueryPort; +use torrust_tracker_rest_api_application::v1::ports::stats::StatsQueryPort; use torrust_tracker_rest_api_protocol::v1::context::stats::resources::stats::{LabeledStats, Stats}; /// Adapter that queries all tracker-internal data sources and converts /// domain types to protocol DTOs. diff --git a/packages/rest-api-runtime-adapter/src/adapters/torrent.rs b/packages/rest-api-runtime-adapter/src/v1/adapters/torrent.rs similarity index 95% rename from packages/rest-api-runtime-adapter/src/adapters/torrent.rs rename to packages/rest-api-runtime-adapter/src/v1/adapters/torrent.rs index 0f69cde44..0b304af1f 100644 --- a/packages/rest-api-runtime-adapter/src/adapters/torrent.rs +++ b/packages/rest-api-runtime-adapter/src/v1/adapters/torrent.rs @@ -6,7 +6,7 @@ use torrust_info_hash::InfoHash; use torrust_tracker_core::torrent::repository::in_memory::InMemoryTorrentRepository; use torrust_tracker_core::torrent::services; use torrust_tracker_primitives::pagination::Pagination; -use torrust_tracker_rest_api_application::ports::torrent::TorrentQueryPort; +use torrust_tracker_rest_api_application::v1::ports::torrent::TorrentQueryPort; use torrust_tracker_rest_api_protocol::v1::context::torrent::resources::torrent::{ListItem, Torrent}; use super::super::conversion; diff --git a/packages/rest-api-runtime-adapter/src/adapters/whitelist.rs b/packages/rest-api-runtime-adapter/src/v1/adapters/whitelist.rs similarity index 94% rename from packages/rest-api-runtime-adapter/src/adapters/whitelist.rs rename to packages/rest-api-runtime-adapter/src/v1/adapters/whitelist.rs index a354572f9..536281450 100644 --- a/packages/rest-api-runtime-adapter/src/adapters/whitelist.rs +++ b/packages/rest-api-runtime-adapter/src/v1/adapters/whitelist.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use async_trait::async_trait; use torrust_info_hash::InfoHash; use torrust_tracker_core::whitelist::manager::WhitelistManager; -use torrust_tracker_rest_api_application::ports::whitelist::WhitelistCommandPort; +use torrust_tracker_rest_api_application::v1::ports::whitelist::WhitelistCommandPort; use torrust_tracker_rest_api_protocol::v1::context::whitelist::resources::whitelist::WhitelistError; /// Adapter that wraps [`WhitelistManager`] and implements the diff --git a/packages/rest-api-core/src/container.rs b/packages/rest-api-runtime-adapter/src/v1/container.rs similarity index 89% rename from packages/rest-api-core/src/container.rs rename to packages/rest-api-runtime-adapter/src/v1/container.rs index c2e356caa..4afebe35c 100644 --- a/packages/rest-api-core/src/container.rs +++ b/packages/rest-api-runtime-adapter/src/v1/container.rs @@ -1,3 +1,10 @@ +//! Dependency injection container for the REST API server. +//! +//! Wires all tracker internal components (swarm registry, HTTP/UDP cores, etc.) +//! into a single container that the Axum server uses to construct adapters. +//! +//! This was previously in `rest-api-core` and was moved here as part of SI-5 +//! (deprecation of `rest-api-core`). use std::sync::Arc; use tokio::sync::RwLock; @@ -10,6 +17,8 @@ use torrust_tracker_udp_core::services::banning::BanService; use torrust_tracker_udp_core::{self}; use torrust_tracker_udp_server::container::UdpTrackerServerContainer; +/// Container that holds all the internal tracker components needed by the +/// REST API server. pub struct TrackerHttpApiCoreContainer { pub http_api_config: Arc, diff --git a/packages/rest-api-runtime-adapter/src/conversion.rs b/packages/rest-api-runtime-adapter/src/v1/conversion.rs similarity index 50% rename from packages/rest-api-runtime-adapter/src/conversion.rs rename to packages/rest-api-runtime-adapter/src/v1/conversion.rs index 65f668a97..8eecb0ea9 100644 --- a/packages/rest-api-runtime-adapter/src/conversion.rs +++ b/packages/rest-api-runtime-adapter/src/v1/conversion.rs @@ -63,3 +63,67 @@ pub fn list_item_from_domain(basic_info: &BasicInfo) -> ListItem { leechers: basic_info.leechers, } } + +#[cfg(test)] +mod tests { + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::str::FromStr; + + use torrust_clock::DurationSinceUnixEpoch; + use torrust_info_hash::InfoHash; + use torrust_tracker_core::torrent::services::{BasicInfo, Info}; + use torrust_tracker_primitives::{AnnounceEvent, NumberOfBytes, PeerId, peer}; + use torrust_tracker_rest_api_protocol::v1::context::torrent::resources::torrent::{ListItem, Torrent}; + + use super::*; + + fn sample_peer() -> peer::Peer { + peer::Peer { + peer_id: PeerId(*b"-qB00000000000000000"), + peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080), + updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0), + uploaded: NumberOfBytes::new(0), + downloaded: NumberOfBytes::new(0), + left: NumberOfBytes::new(0), + event: AnnounceEvent::Started, + } + } + + #[test] + fn torrent_resource_should_be_converted_from_torrent_info() { + assert_eq!( + from_domain_info(Info { + info_hash: InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(), // DevSkim: ignore DS173237 + seeders: 1, + completed: 2, + leechers: 3, + peers: Some(vec![sample_peer()]), + }), + Torrent { + info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), // DevSkim: ignore DS173237 + seeders: 1, + completed: 2, + leechers: 3, + peers: Some(vec![from_domain_peer(sample_peer())]), + } + ); + } + + #[test] + fn torrent_resource_list_item_should_be_converted_from_the_basic_torrent_info() { + assert_eq!( + list_item_from_domain(&BasicInfo { + info_hash: InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(), // DevSkim: ignore DS173237 + seeders: 1, + completed: 2, + leechers: 3, + }), + ListItem { + info_hash: "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_string(), // DevSkim: ignore DS173237 + seeders: 1, + completed: 2, + leechers: 3, + } + ); + } +} diff --git a/packages/rest-api-runtime-adapter/src/v1/mod.rs b/packages/rest-api-runtime-adapter/src/v1/mod.rs new file mode 100644 index 000000000..d5ca09557 --- /dev/null +++ b/packages/rest-api-runtime-adapter/src/v1/mod.rs @@ -0,0 +1,7 @@ +//! Version 1 of the Torrust Tracker REST API runtime adapter. +//! +//! This module contains all v1-specific adapter implementations, +//! the dependency injection container, and domain→protocol DTO conversions. +pub mod adapters; +pub mod container; +pub mod conversion; diff --git a/project-words.txt b/project-words.txt index ec32c47d7..9196fd97e 100644 --- a/project-words.txt +++ b/project-words.txt @@ -13,6 +13,8 @@ alloca analyse analysed appuser +aquasec +aquasecurity argjson artefacts Arvid @@ -48,7 +50,6 @@ Bragilevsky bufs buildid BuildKit -bottlenecked Buildx byteorder callgrind @@ -91,6 +92,7 @@ debuginfo defence depgraph Deque +dfsg DGRAM Dihc Dijke @@ -98,6 +100,7 @@ distroless distros dler Dmqcd +DNSSEC dockerhub doctest downloadedi @@ -114,25 +117,33 @@ eprint eprintln Eray eventfd +exploitability fastrand fdbased fdget +fgetwc filesd finalises flamegraph flamegraphs +flate +flate2 fnix footgun formalised formalises formatjson fput +fputwc fract Freebox frontmatter Frostegård +fscanf Garnham gecos +getaddrinfo +gethostbyname ghac Gibibytes Glrg @@ -190,6 +201,8 @@ lcov leafification leecher leechers +libc +libc6 libheif libhwloc libraw @@ -210,6 +223,8 @@ microbenchmark microbenchmarks middlewares millis +miniz +miniz_oxide misresolved mktemp mmap @@ -222,7 +237,6 @@ multimap myacicontext mysqladmin mysqld -ñaca Naim nanos newkey @@ -238,6 +252,7 @@ nonblocking nonroot Norberg notnull +nquery numwant nvCFlJCq7fz7Qx6KoKTDiMZvns8l5Kw7 objcopy @@ -323,7 +338,9 @@ rustfmt Rustls rustup Ryzen +sarif savepath +scanf sccache Seedable serde @@ -343,9 +360,9 @@ specialised sqllite sqlx srcset +sscanf stabilised subissue -Subissue Subissues subkey subsec @@ -379,9 +396,13 @@ torrustracker trackerid Trackon triaging -trixie +trivy +trivy-action +trivy-results +Trixie trunc tryhackx +TSIG tslconfig ttwu typenum @@ -391,8 +412,8 @@ Unamed unconfigured UNCONN underflows +ungetwc uninit -Uninit unittests unparked Unparker @@ -435,3 +456,4 @@ xxxxxxxxxxxxxxxxxxxxd yyyyyyyyyyyyyyyyyyyyd zerocopy zstd +ñaca diff --git a/share/default/config/tracker.development.sqlite3.toml b/share/default/config/tracker.development.sqlite3.toml index d40eba34c..03228aeb3 100644 --- a/share/default/config/tracker.development.sqlite3.toml +++ b/share/default/config/tracker.development.sqlite3.toml @@ -1,4 +1,5 @@ # skill-link: run-tracker-locally +# skill-link: use-rest-api [metadata] app = "torrust-tracker" purpose = "configuration" diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index 1ae267693..4ea4d9e68 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -30,7 +30,7 @@ use torrust_tracker_axum_rest_api_server::Version; use torrust_tracker_axum_rest_api_server::server::{ApiServer, Launcher}; use torrust_tracker_axum_server::tsl::make_rust_tls; use torrust_tracker_configuration::AccessTokens; -use torrust_tracker_rest_api_core::container::TrackerHttpApiCoreContainer; +use torrust_tracker_rest_api_runtime_adapter::v1::container::TrackerHttpApiCoreContainer; use tracing::instrument; /// This is the message that the "launcher" spawned task sends to the main @@ -104,7 +104,7 @@ mod tests { use torrust_server_lib::registar::Registar; use torrust_tracker_axum_rest_api_server::Version; - use torrust_tracker_rest_api_core::container::TrackerHttpApiCoreContainer; + use torrust_tracker_rest_api_runtime_adapter::v1::container::TrackerHttpApiCoreContainer; use torrust_tracker_test_helpers::configuration::ephemeral_public; use crate::bootstrap::app::initialize_global_services; diff --git a/src/console/ci/qbittorrent_e2e/tracker/client.rs b/src/console/ci/qbittorrent_e2e/tracker/client.rs index f517d0af5..afb802f4a 100644 --- a/src/console/ci/qbittorrent_e2e/tracker/client.rs +++ b/src/console/ci/qbittorrent_e2e/tracker/client.rs @@ -1,11 +1,11 @@ //! Tracker REST API client, scoped to E2E test needs. //! -//! Wraps the official [`torrust_tracker_rest_api_client::v1::Client`] so that +//! Wraps the official [`torrust_tracker_rest_api_client::v1::client::ApiHttpClient`] so that //! future scenario steps can call any REST API endpoint through the same client //! without having to reconstruct connection details each time. use anyhow::Context; use torrust_tracker_rest_api_client::connection_info::{ConnectionInfo, Origin}; -use torrust_tracker_rest_api_client::v1::client::Client; +use torrust_tracker_rest_api_client::v1::client::ApiHttpClient; use torrust_tracker_rest_api_protocol::v1::context::torrent::resources::torrent::Torrent; use super::super::types::InfoHash; @@ -14,9 +14,9 @@ use super::config_builder::TrackerConfig; /// Wrapper around the official Torrust Tracker REST API client. /// /// Provides typed, high-level helpers for the endpoints used in E2E test scenarios. -/// All other endpoints are still reachable through the inner [`Client`]. +/// All other endpoints are still reachable through the inner [`ApiHttpClient`]. pub(crate) struct TrackerApiClient { - inner: Client, + inner: ApiHttpClient, } impl TrackerApiClient { @@ -32,7 +32,7 @@ impl TrackerApiClient { let connection_info = ConnectionInfo::authenticated(origin, tracker_config.access_token()); - let inner = Client::new(connection_info).context("failed to build tracker REST API client")?; + let inner = ApiHttpClient::new(connection_info).context("failed to build tracker REST API client")?; Ok(Self { inner }) } diff --git a/src/container.rs b/src/container.rs index fcfaafce3..de4097d67 100644 --- a/src/container.rs +++ b/src/container.rs @@ -6,7 +6,7 @@ use torrust_server_lib::registar::Registar; use torrust_tracker_configuration::{Configuration, HttpApi}; use torrust_tracker_core::container::TrackerCoreContainer; use torrust_tracker_http_core::container::{HttpTrackerCoreContainer, HttpTrackerCoreServices}; -use torrust_tracker_rest_api_core::container::TrackerHttpApiCoreContainer; +use torrust_tracker_rest_api_runtime_adapter::v1::container::TrackerHttpApiCoreContainer; use torrust_tracker_swarm_coordination_registry::container::SwarmCoordinationRegistryContainer; use torrust_tracker_udp_core::container::{UdpTrackerCoreContainer, UdpTrackerCoreServices}; use torrust_tracker_udp_core::{self}; diff --git a/tests/servers/api/contract/stats/mod.rs b/tests/servers/api/contract/stats/mod.rs index 6e1fe8c40..b77863d42 100644 --- a/tests/servers/api/contract/stats/mod.rs +++ b/tests/servers/api/contract/stats/mod.rs @@ -9,7 +9,7 @@ use torrust_tracker_client::http::client::Client as HttpTrackerClient; use torrust_tracker_client::http::client::requests::announce::QueryBuilder; use torrust_tracker_lib::app; use torrust_tracker_rest_api_client::connection_info::{ConnectionInfo, Origin}; -use torrust_tracker_rest_api_client::v1::client::Client as TrackerApiClient; +use torrust_tracker_rest_api_client::v1::client::ApiHttpClient as TrackerApiClient; #[tokio::test] async fn the_stats_api_endpoint_should_return_the_global_stats() {