diff --git a/server/middleware/verifyCsrf.js b/server/middleware/verifyCsrf.js index 6eca0ad..81c3daf 100644 --- a/server/middleware/verifyCsrf.js +++ b/server/middleware/verifyCsrf.js @@ -26,13 +26,14 @@ function verifyCsrf(req, res, next) { return res.status(403).json({ error: 'CSRF token missing' }); } - // Constant-time comparison to prevent timing attacks - if (cookieToken.length !== headerToken.length) { + const a = Buffer.from(cookieToken); + const b = Buffer.from(headerToken); + + // Constant-time comparison of underlying buffer lengths to prevent timing attacks + if (a.length !== b.length) { return res.status(403).json({ error: 'CSRF token invalid' }); } - const a = Buffer.from(cookieToken); - const b = Buffer.from(headerToken); if (!require('crypto').timingSafeEqual(a, b)) { return res.status(403).json({ error: 'CSRF token invalid' }); } diff --git a/server/routes/ombi.js b/server/routes/ombi.js index a2d7905..80fc07b 100644 --- a/server/routes/ombi.js +++ b/server/routes/ombi.js @@ -392,9 +392,9 @@ router.get('/webhook/status', requireAuth, async (req, res) => { requestProcessing: webhookConfig.enabled || false }, stats: metrics ? { - eventsReceived: metrics.eventCount || 0, + eventsReceived: metrics.eventsReceived || 0, pollsSkipped: metrics.pollsSkipped || 0, - lastWebhookTimestamp: metrics.lastEventTimestamp || null + lastWebhookTimestamp: metrics.lastWebhookTimestamp || null } : null }); } catch (error) { diff --git a/server/utils/downloadClients.js b/server/utils/downloadClients.js index 50a7785..9cd2193 100644 --- a/server/utils/downloadClients.js +++ b/server/utils/downloadClients.js @@ -156,6 +156,11 @@ class DownloadClientRegistry { result[type] = []; } + // Reset fallback flags for qBittorrent clients + if (client.resetFallbackFlag) { + client.resetFallbackFlag(); + } + try { const downloads = await client.getActiveDownloads(); result[type].push(...downloads); diff --git a/server/utils/logger.js b/server/utils/logger.js index 00c6df9..50cc596 100644 --- a/server/utils/logger.js +++ b/server/utils/logger.js @@ -1,16 +1,7 @@ // Copyright (c) 2026 Gordon Bolton. MIT License. -const fs = require('fs'); -const path = require('path'); - -// Use DATA_DIR so the non-root container user (UID 1000) can write logs. -// Falls back to ../../data/server.log (same directory index.js uses). -const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '../../data'); -if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); - -const logFile = fs.createWriteStream(path.join(DATA_DIR, 'server.log'), { flags: 'a' }); function logToFile(message) { - logFile.write(`[${new Date().toISOString()}] ${message}\n`); + console.log(message); } module.exports = { diff --git a/tests/integration/ombi.test.js b/tests/integration/ombi.test.js index ab70701..ef1584d 100644 --- a/tests/integration/ombi.test.js +++ b/tests/integration/ombi.test.js @@ -72,9 +72,9 @@ const OMBI_WEBHOOK_CONFIG = { }; const OMBI_WEBHOOK_METRICS = { - eventCount: 10, + eventsReceived: 10, pollsSkipped: 5, - lastEventTimestamp: 1716326400000 + lastWebhookTimestamp: 1716326400000 }; // --------------------------------------------------------------------------- diff --git a/tests/unit/downloadClients.test.js b/tests/unit/downloadClients.test.js index cd9d500..5f8cc00 100644 --- a/tests/unit/downloadClients.test.js +++ b/tests/unit/downloadClients.test.js @@ -219,11 +219,13 @@ describe('DownloadClientRegistry', () => { }); it('should get downloads grouped by client type', async () => { + const qbClient = testRegistry.getClient('qb1'); const downloadsByType = await testRegistry.getDownloadsByClientType(); expect(downloadsByType.sabnzbd).toHaveLength(1); expect(downloadsByType.qbittorrent).toHaveLength(1); expect(downloadsByType.transmission).toBeUndefined(); + expect(qbClient.resetFallbackFlag).toHaveBeenCalled(); }); it('should handle client errors gracefully', async () => { diff --git a/tests/unit/verifyCsrf.test.js b/tests/unit/verifyCsrf.test.js index 97d52af..e269b46 100644 --- a/tests/unit/verifyCsrf.test.js +++ b/tests/unit/verifyCsrf.test.js @@ -81,5 +81,14 @@ describe('verifyCsrf middleware', () => { expect(res.statusCode).toBe(403); expect(res.body.error).toBe('CSRF token invalid'); }); + + it('blocks when tokens have same character length but different byte lengths (multi-byte)', () => { + const next = vi.fn(); + const res = makeRes(); + verifyCsrf(makeReq('POST', 'cafe\u0301', 'cafes'), res, next); + expect(res.statusCode).toBe(403); + expect(res.body.error).toBe('CSRF token invalid'); + expect(next).not.toHaveBeenCalled(); + }); }); });