Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b5ac2d7c5 | |||
| b5b4862e15 | |||
| 11b3296198 | |||
| 76631cd37e |
+2
-2
@@ -1224,7 +1224,7 @@ Each instance receives an `id` derived from `name` (or index if unnamed), used a
|
||||
| Concern | Mechanism |
|
||||
|---------|-----------|
|
||||
| **Secret validation** | Every webhook request must carry `X-Sofarr-Webhook-Secret` matching `SOFARR_WEBHOOK_SECRET`. Absent or wrong secret → `401`. Webhook endpoints function outside the CSRF middleware (they are not browser-initiated). |
|
||||
| **Rate limiting** | Dedicated `webhookLimiter`: 60 req/min per IP (stricter than the general 300 req/15 min limiter). |
|
||||
| **Rate limiting** | Dedicated `webhookLimiter`: 60 req/min per IP (stricter than the general 300 req/15 min limiter). Bypassed in testing/dev via `SKIP_RATE_LIMIT=1`. |
|
||||
| **Payload validation** | `validatePayload()` enforces: JSON object body, `eventType` as a non-empty string ≤ 64 chars, `eventType` in the allowlist, `instanceName` as string if present. Rejects with `400` on any violation. |
|
||||
| **Replay protection** | `isReplay()` caches a composite key `{eventType}:{instanceName}:{date}` for 5 minutes. Duplicate events within that window are acknowledged with `200 { received: true, duplicate: true }` and not processed. |
|
||||
|
||||
@@ -1232,7 +1232,7 @@ Each instance receives an `id` derived from `name` (or index if unnamed), used a
|
||||
|
||||
| Concern | Mechanism |
|
||||
|---------|-----------|
|
||||
| **Rate limiting** | 300 req/15 min general (all API routes); 10 failed attempts/15 min login limiter; 60 req/1 min webhook limiter. |
|
||||
| **Rate limiting** | General API limiter (300 req/15 min on `/api/*` prefix) exempts `/api/dashboard/cover-art` requests; Login limiter (10 attempts/15 min) employs `skipSuccessfulRequests: true` to count failed attempts only; Webhook limiter runs 60 req/1 min on `/api/webhook/*` endpoints; Root `/health` and `/ready` probes are entirely exempt. All limiters bypassable in testing via `SKIP_RATE_LIMIT=1` or `createApp({ skipRateLimits: true })`. |
|
||||
| **Secret leakage** | `sanitizeError()` (`server/utils/sanitizeError.js`) redacts secrets from error messages and logs: URL query-param secrets (`apikey=`, `token=`), HTTP auth headers (`Authorization:`, `X-Emby-Authorization:`), Bearer tokens, and basic-auth credentials in URLs. |
|
||||
| **HTTP headers** | Helmet v7: CSP with per-request nonce (`crypto.randomBytes(16)` for inline styles/scripts), HSTS, `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy`, `Permissions-Policy`. |
|
||||
| **Body size** | `express.json` body limit: 64 KB. |
|
||||
|
||||
@@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file.
|
||||
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.7.14] - 2026-05-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Undefined Reference Error in Background Poller** — Resolved a critical runtime exception in the background scheduler loop (`server/utils/poller.js`) where `logToFile` was called on cache updates but was never imported at the top of the file, previously triggering `[Poller] Poll error: logToFile is not defined` on every interval loop.
|
||||
|
||||
---
|
||||
|
||||
## [1.7.13] - 2026-05-24
|
||||
|
||||
### Changed
|
||||
|
||||
- **Comprehensive OpenAPI & Swagger Specification Remediation** — Bumped the API documentation version to `1.7.13` and fully documented all operational rate-limiting configurations, exemptions, and bypasses in `server/openapi.yaml` (including general cover-art exclusions, failed-only login trackers, webhook limiters, and rate-limit exempt root health probes).
|
||||
- **Aligned Health Check Endpoint Implementation** — Enhanced the express application factory `/health` endpoint to dynamically require and return the active version from `package.json`, keeping it fully aligned with the production entrypoint server logic.
|
||||
- **Synchronized Security & System Architecture Docs** — Aligned security matrices and threat mitigations in `SECURITY.md` and rate-limiting testing configurations in `ARCHITECTURE.md`.
|
||||
|
||||
### Added
|
||||
|
||||
- **Swagger API Coverage Verification Integration** — Implemented comprehensive assertions within `tests/integration/swagger-coverage.test.js` to dynamically verify that all newly added logging and debug endpoints (`/api/debug/*`) are fully represented in the active specification, raising test suite coverage to 876 passing checks.
|
||||
|
||||
---
|
||||
|
||||
## [1.7.12] - 2026-05-24
|
||||
|
||||
### Added
|
||||
|
||||
+7
-6
@@ -162,12 +162,13 @@ server {
|
||||
|
||||
## Rate Limits
|
||||
|
||||
| Endpoint | Limit |
|
||||
|----------|-------|
|
||||
| `POST /api/auth/login` | 10 failed attempts per 15 min per IP |
|
||||
| All `/api/*` routes | 300 requests per 15 min per IP |
|
||||
| `POST /api/webhook/*` | 60 requests per 1 min per IP (webhook-specific limiter, stricter than general) |
|
||||
| `GET /api/swagger` | No rate limit (public documentation) |
|
||||
| Endpoint | Limit | Details & Exemptions |
|
||||
|----------|-------|----------------------|
|
||||
| `POST /api/auth/login` | 10 attempts per 15 min per IP | **Only failed attempts count** (`skipSuccessfulRequests: true`). Successful requests are not counted. |
|
||||
| All `/api/*` routes | 300 requests per 15 min per IP | General rate limiting. **Exempts `/api/dashboard/cover-art` requests** to avoid page layout image loading exhaustion. |
|
||||
| `POST /api/webhook/*` | 60 requests per 1 min per IP | Webhook-specific limiter, stricter than general. |
|
||||
| `/health` and `/ready` | Exempt | Root-level liveness/readiness probes bypass rate limiters completely. |
|
||||
| `GET /api/swagger` | Exempt | Public Swagger UI documentation does not enforce rate limits. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sofarr",
|
||||
"version": "1.7.12",
|
||||
"version": "1.7.14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sofarr",
|
||||
"version": "1.7.12",
|
||||
"version": "1.7.14",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sofarr",
|
||||
"version": "1.7.12",
|
||||
"version": "1.7.14",
|
||||
"description": "A personal media download dashboard that shows your downloads 'so far' while you relax on the sofa waiting for your *arr services to finish",
|
||||
"main": "server/index.js",
|
||||
"scripts": {
|
||||
|
||||
+6
-1
@@ -15,6 +15,7 @@ const swaggerUi = require('swagger-ui-express');
|
||||
const swaggerJsdoc = require('swagger-jsdoc');
|
||||
const YAML = require('yamljs');
|
||||
const path = require('path');
|
||||
const { version } = require('../package.json');
|
||||
|
||||
const sabnzbdRoutes = require('./routes/sabnzbd');
|
||||
const sonarrRoutes = require('./routes/sonarr');
|
||||
@@ -128,13 +129,17 @@ function createApp({ skipRateLimits = false } = {}) {
|
||||
* type: number
|
||||
* description: Server uptime in seconds
|
||||
* example: 3600.5
|
||||
* version:
|
||||
* type: string
|
||||
* description: sofarr version
|
||||
* example: "1.7.14"
|
||||
* x-code-samples:
|
||||
* - lang: curl
|
||||
* label: cURL
|
||||
* source: curl http://localhost:3001/health
|
||||
*/
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', uptime: process.uptime() });
|
||||
res.json({ status: 'ok', uptime: process.uptime(), version });
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -249,7 +249,7 @@ app.use(express.json({ limit: '64kb' })); // prevent oversized JSON payloads
|
||||
* version:
|
||||
* type: string
|
||||
* description: sofarr version
|
||||
* example: "1.6.0"
|
||||
* example: "1.7.14"
|
||||
*/
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', uptime: process.uptime(), version });
|
||||
|
||||
+8
-4
@@ -12,13 +12,17 @@ info:
|
||||
4. Subsequent requests must include the cookies and send the `X-CSRF-Token` header for state-changing operations (POST, PUT, PATCH, DELETE)
|
||||
|
||||
## Rate Limiting
|
||||
- General API: 300 requests per 15 minutes per IP
|
||||
- Login: 10 failed attempts per 15 minutes per IP
|
||||
- Webhooks: 60 requests per minute per IP
|
||||
To protect the system from resource exhaustion, rate limiters are enforced at different levels:
|
||||
- **General API Limiter**: Enforces a limit of **300 requests per 15 minutes** per IP across all `/api/*` endpoints.
|
||||
- *Exemption:* Requests starting with `/api/dashboard/cover-art` are completely exempted from this limit to avoid normal dashboard image browsing triggering blocks.
|
||||
- **Login Rate Limiter**: Enforces a strict limit of **10 attempts per 15 minutes** per IP on `POST /api/auth/login`.
|
||||
- *Exemption:* This limiter only tracks and counts *failed* login attempts (`skipSuccessfulRequests: true`). Successful logins do not count towards the lockout threshold.
|
||||
- **Webhook Limiter**: Enforces a limit of **60 requests per minute** per IP on stateful webhook receiver endpoints (`/api/webhook/*`).
|
||||
- **Health and Readiness Probes**: The public `/health` and `/ready` endpoints are mounted at the root directory level rather than under `/api/*` and are completely exempt from both rate limiting and authentication controls.
|
||||
|
||||
## SSE Streaming
|
||||
Real-time updates are available via Server-Sent Events at GET /api/dashboard/stream.
|
||||
version: 1.6.0
|
||||
version: 1.7.14
|
||||
contact:
|
||||
name: sofarr
|
||||
license:
|
||||
|
||||
@@ -8,6 +8,7 @@ const {
|
||||
getRadarrInstances,
|
||||
getOmbiInstances
|
||||
} = require('./config');
|
||||
const { logToFile } = require('./logger');
|
||||
|
||||
const rawPollInterval = (process.env.POLL_INTERVAL || '').toLowerCase();
|
||||
const POLL_INTERVAL = (rawPollInterval === 'off' || rawPollInterval === 'false' || rawPollInterval === 'disabled')
|
||||
|
||||
@@ -196,6 +196,18 @@ describe('Swagger Coverage', () => {
|
||||
expect(paths['/api/ombi/webhook/test'].post).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have Debug logging endpoints documented', () => {
|
||||
const paths = openapiSpec.paths;
|
||||
|
||||
expect(paths['/api/debug/status']).toBeDefined();
|
||||
expect(paths['/api/debug/status'].get).toBeDefined();
|
||||
expect(paths['/api/debug/server-logs']).toBeDefined();
|
||||
expect(paths['/api/debug/server-logs'].get).toBeDefined();
|
||||
expect(paths['/api/debug/client-logs']).toBeDefined();
|
||||
expect(paths['/api/debug/client-logs'].get).toBeDefined();
|
||||
expect(paths['/api/debug/client-logs'].post).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return 200 for Swagger UI endpoint', async () => {
|
||||
const response = await request(app).get('/api/swagger').redirects(1);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
Reference in New Issue
Block a user