diff --git a/lib/config-store.js b/lib/config-store.js index c0dedbc..c9c872f 100644 --- a/lib/config-store.js +++ b/lib/config-store.js @@ -132,6 +132,20 @@ const HISTORY_RETENTION_OPTIONS = [ { value: '100', label: 'Letzte 100 Uploads' } ]; +const DIAGNOSTIC_ERROR_MESSAGES = Object.freeze({ + DIAGNOSTIC_CONFIG_READ_FAILED: 'Die Diagnosekonfiguration konnte nicht gelesen werden', + DIAGNOSTIC_CONFIG_INVALID: 'Die Diagnosekonfiguration ist ungültig', + DIAGNOSTIC_HISTORY_NOT_FOUND: 'Die Diagnoseverlaufsdatei wurde nicht gefunden', + DIAGNOSTIC_HISTORY_READ_FAILED: 'Die Diagnoseverlaufsdatei konnte nicht gelesen werden', + DIAGNOSTIC_HISTORY_INVALID: 'Die Diagnoseverlaufsdatei ist ungültig' +}); + +function diagnosticStoreError(code) { + const error = new Error(DIAGNOSTIC_ERROR_MESSAGES[code]); + error.code = code; + return error; +} + function batchTimestampMs(batch) { const raw = batch && batch.timestamp; if (raw === null || raw === undefined || raw === '') return null; @@ -225,12 +239,27 @@ class ConfigStore { } _readHistoryFileStrict() { - const raw = fs.readFileSync(this.historyPath, 'utf-8'); - if (!raw || raw.trim().length < 2) throw new Error('Die Diagnoseverlaufsdatei ist ungültig'); - const parsed = JSON.parse(raw); + let raw; + try { + raw = fs.readFileSync(this.historyPath, 'utf-8'); + } catch (error) { + if (error && error.code === 'ENOENT') { + throw diagnosticStoreError('DIAGNOSTIC_HISTORY_NOT_FOUND'); + } + throw diagnosticStoreError('DIAGNOSTIC_HISTORY_READ_FAILED'); + } + if (!raw || raw.trim().length < 2) { + throw diagnosticStoreError('DIAGNOSTIC_HISTORY_INVALID'); + } + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + throw diagnosticStoreError('DIAGNOSTIC_HISTORY_INVALID'); + } if (Array.isArray(parsed)) return parsed; if (parsed && Array.isArray(parsed.history)) return parsed.history; - throw new Error('Die Diagnoseverlaufsdatei ist ungültig'); + throw diagnosticStoreError('DIAGNOSTIC_HISTORY_INVALID'); } _writeHistoryFileDurable(arr) { @@ -351,6 +380,31 @@ class ConfigStore { return JSON.parse(raw); } + _readConfigFileStrict() { + let raw; + try { + raw = fs.readFileSync(this.filePath, 'utf-8'); + } catch { + throw diagnosticStoreError('DIAGNOSTIC_CONFIG_READ_FAILED'); + } + if (!raw || raw.trim().length < 2) { + throw diagnosticStoreError('DIAGNOSTIC_CONFIG_INVALID'); + } + let data; + try { + data = JSON.parse(raw); + } catch { + throw diagnosticStoreError('DIAGNOSTIC_CONFIG_INVALID'); + } + if (!data || typeof data !== 'object' || Array.isArray(data) || + !data.hosters || typeof data.hosters !== 'object' || Array.isArray(data.hosters) || + !data.globalSettings || typeof data.globalSettings !== 'object' || Array.isArray(data.globalSettings) || + (data.history !== undefined && !Array.isArray(data.history))) { + throw diagnosticStoreError('DIAGNOSTIC_CONFIG_INVALID'); + } + return data; + } + _clone(obj) { try { return structuredClone(obj); } catch { return JSON.parse(JSON.stringify(obj)); } @@ -415,13 +469,7 @@ class ConfigStore { let data = null; if (strict) { - data = this._readAndParse(this.filePath); - if (!data || typeof data !== 'object' || Array.isArray(data) || - !data.hosters || typeof data.hosters !== 'object' || Array.isArray(data.hosters) || - !data.globalSettings || typeof data.globalSettings !== 'object' || Array.isArray(data.globalSettings) || - (data.history !== undefined && !Array.isArray(data.history))) { - throw new Error('Die Diagnosekonfiguration ist ungültig'); - } + data = this._readConfigFileStrict(); } else { // Try main config try { data = this._readAndParse(this.filePath); } catch {} @@ -737,10 +785,12 @@ class ConfigStore { try { return this._readHistoryFileStrict(); } catch (error) { - if (!error || error.code !== 'ENOENT') throw error; + if (!error || error.code !== 'DIAGNOSTIC_HISTORY_NOT_FOUND') throw error; } const config = this.loadDiagnosticsConfig(); - if (!Array.isArray(config.history)) throw new Error('Der Diagnoseverlauf ist ungültig'); + if (!Array.isArray(config.history)) { + throw diagnosticStoreError('DIAGNOSTIC_HISTORY_INVALID'); + } return config.history; } diff --git a/tests/config-store.test.js b/tests/config-store.test.js index cf88ed7..0e3fece 100644 --- a/tests/config-store.test.js +++ b/tests/config-store.test.js @@ -17,10 +17,36 @@ Module._load = function load(request, parent, isMain) { const ConfigStore = require('../lib/config-store'); require('../lib/secret-store').encryptField('test-initialization'); Module._load = originalLoad; +const { createCollectors } = require('../lib/diagnostics-collectors'); +const { createAgent } = require('../lib/diagnostics-agent'); +const support = require('../lib/support-bundle'); +const stats = require('../lib/stats'); let tmpDir; let store; +function thrownBy(fn) { + try { + fn(); + } catch (error) { + return error; + } + assert.fail('Expected function to throw'); +} + +function createDiagnosticsAgent(configStore, logDir) { + return createAgent(createCollectors({ + loadConfig: () => configStore.loadDiagnosticsConfig(), + loadHistory: () => configStore.loadDiagnosticsHistory(), + getAllLogPaths: () => ({ logDir }), + support, + stats, + appInfo: () => ({}), + systemInfo: () => ({}), + agentInfo: () => ({}) + })); +} + function createStore() { const fakeApp = { isPackaged: false, @@ -685,6 +711,27 @@ describe('ConfigStore', () => { assert.throws(() => store.loadDiagnosticsConfig()); }); + it('strict diagnostics config errors never expose malformed JSON or file paths', () => { + const opaque = 'opaque-config-42'; + fs.writeFileSync(store.filePath, opaque, 'utf-8'); + + let error = thrownBy(() => store.loadDiagnosticsConfig()); + assert.equal(error.code, 'DIAGNOSTIC_CONFIG_INVALID'); + assert.equal(error.message, 'Die Diagnosekonfiguration ist ungültig'); + assert.ok(!error.message.includes(opaque)); + + fs.writeFileSync(store.filePath, '{}', 'utf-8'); + error = thrownBy(() => store.loadDiagnosticsConfig()); + assert.equal(error.code, 'DIAGNOSTIC_CONFIG_INVALID'); + assert.equal(error.message, 'Die Diagnosekonfiguration ist ungültig'); + + fs.rmSync(store.filePath); + error = thrownBy(() => store.loadDiagnosticsConfig()); + assert.equal(error.code, 'DIAGNOSTIC_CONFIG_READ_FAILED'); + assert.equal(error.message, 'Die Diagnosekonfiguration konnte nicht gelesen werden'); + assert.ok(!error.message.includes(store.filePath)); + }); + it('strict diagnostics config loading decrypts current credentials and propagates decryption failures', () => { assert.equal(typeof store.loadDiagnosticsConfig, 'function'); @@ -781,12 +828,31 @@ describe('ConfigStore history split (electron-history.json)', () => { for (const invalidHistory of ['null', '{}', '{"history":null}', '{broken-history']) { fs.writeFileSync(s.historyPath, invalidHistory, 'utf-8'); - assert.throws(() => s.loadDiagnosticsHistory()); + const error = thrownBy(() => s.loadDiagnosticsHistory()); + assert.equal(error.code, 'DIAGNOSTIC_HISTORY_INVALID'); + assert.equal(error.message, 'Die Diagnoseverlaufsdatei ist ungültig'); + assert.ok(!error.message.includes(invalidHistory)); } fs.rmSync(s.historyPath); fs.mkdirSync(s.historyPath); - assert.throws(() => s.loadDiagnosticsHistory()); + const error = thrownBy(() => s.loadDiagnosticsHistory()); + assert.equal(error.code, 'DIAGNOSTIC_HISTORY_READ_FAILED'); + assert.equal(error.message, 'Die Diagnoseverlaufsdatei konnte nicht gelesen werden'); + assert.ok(!error.message.includes(s.historyPath)); + }); + + it('real diagnostics agent never returns opaque corrupt dedicated history content', () => { + const opaque = 'opaque42'; + writeConfigWithHistory(0); + fs.writeFileSync(s.historyPath, opaque, 'utf-8'); + const agent = createDiagnosticsAgent(s, dir); + + for (const operation of ['get_history', 'list_errors', 'server_health']) { + const response = agent.handle(operation, {}); + assert.deepEqual(response, { ok: false, error: 'Die Diagnoseverlaufsdatei ist ungültig' }); + assert.ok(!JSON.stringify(response).includes(opaque)); + } }); it('strict diagnostics history never falls back to stale config history when a dedicated file exists', () => {