signal-desktop/ts/test-node/logging_test.ts

308 lines
9.6 KiB
TypeScript
Raw Normal View History

// Copyright 2018-2021 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
Security: Replace Unicode order overrides in attachment names As a user, when I receive a file attachment, I want to have confidence that the filename I see in the Signal Desktop app is the same as it will be on disk. To prevent user confusion when receiving files with Unicode order override characters, e.g. `test<LTRO>fig.exe` appearing as `testexe.gif`, we replace all occurrences of order overrides (`U+202D` and `U+202E`) with `U+FFFD`. **Changes** - [x] Bump `Attachment` `schemaVersion` to 2. - [x] Replace all Unicode order overrides in `attachment.filename`: `Attachment.replaceUnicodeOrderOverrides`. - [x] Add tests for existing `Attachment.upgradeSchema` - [x] Add tests for existing `Attachment.withSchemaVersion` - [x] Add tests for `Attachment.replaceUnicodeOrderOverrides` positives. - [x] Add `testcheck` generative property-based testing library (based on QuickCheck) to ensure valid filenames are preserved. --- commit 855bdbc7e647e44f73b9e1f5e6d64f734c61169a Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 13:02:01 2018 -0500 Log error stack in case of error commit 6e053ed66aee136f186568fa88aacd4814b2ab07 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 12:30:28 2018 -0500 Improve `upgradeStep` error handling commit 8c226a2523b701cb578b2137832c3eaf3475bb2b Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 12:30:08 2018 -0500 Check for expected version before upgrade Prevents out of order upgrade steps. commit 28b0675591e782169128f75429b7bab2a22307fa Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 12:29:52 2018 -0500 Reject invalid attachments commit 41f4f457dae9416dae66dc2fa2079483d1f127a9 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 12:29:36 2018 -0500 Fix upgrade pipeline order commit 3935629e91c49b8d96c1e02bd37b1b31d1180720 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 12:28:25 2018 -0500 Avoid `_.isPlainObject` Attachments are deserialized from a protocol buffer and can have a non-plain-object constructor. commit 39f6e7f622ff4885e2ccafa354e0edb5864c55d8 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 12:19:07 2018 -0500 Define basic attachment validity commit adcf7e3243cd90866cc35990c558ff7829019037 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Thu Feb 22 12:18:54 2018 -0500 Add tests for attachment upgrade pipeline commit 82fc4644d7e654eea9f348518b086497be2b0cb4 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 21 12:20:24 2018 -0500 Favor `async` / `await` over `then` commit 8fe49e3c40e78ced0b8f2eb0b678f4bae842855d Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 21 12:19:59 2018 -0500 Add `eslint-more` plugin This will enable us to disallow `then` in favor of `async` / `await`. commit 020beefb25f508ae96cf3fc099599fbbca98802b Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 21 11:31:49 2018 -0500 Remove unnecessary `async` modifiers commit 177090c5f5ad9836f0ca0a5c2f298779519e3692 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 21 11:30:55 2018 -0500 Document `operator-linebreak` ESLint rule commit 25622b7c59291cb672ae057c47e7327a564cca40 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Wed Feb 21 11:14:15 2018 -0500 Prefix internal function with `_` commit 6aa3cf5098df71e9b710064739ec49d74f81b7bf Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 19:00:07 2018 -0500 Replace all Unicode order override occurrences commit fd6e23b0a519bce3c12c5b9ac676bcd198034fed Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:48:41 2018 -0500 Whitelist `testcheck` `check` and `gen` globals commit 400bae9fac5078821813bc0ca17a5d7a72900161 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:46:57 2018 -0500 :art: Fix lint errors commit da53d3960aa7aa36b7cc1fcff414c9e929c0d9fc Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:42:42 2018 -0500 Add tests for `Attachment.withSchemaVersion` commit ec203444239d9e3c443ba88cab7ef4672151072d Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:42:17 2018 -0500 Add test for `Attachment.upgradeSchema` commit 4540d5bdf7a4279f49d2e4c6ee03f47b93df46bf Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:05:29 2018 -0500 Rename `setSchemaVersion` --> `withSchemaVersion` Put the schema version first for better readability. commit e379cf919feda31d1fa96d406c30fd38e159a11d Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:03:22 2018 -0500 Add filename sanitization to upgrade pipeline commit 1e344a0d15926fc3e17be20cd90bfa882b65f337 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:01:55 2018 -0500 Test that we preserve non-suspicious filenames commit a2452bfc98f93f82bed48b438757af2e66a6af82 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 17:00:56 2018 -0500 Add `testcheck` dependency Allows for generative property-based testing similar to Haskell’s QuickCheck. See: https://medium.com/javascript-inside/f91432247c27 commit ceb5bfd2484a77689fdb8e9edd18d4a7b093a486 Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 16:15:33 2018 -0500 Replace Unicode order override characters Prevents users from being tricked into clicking a file named `testexe.fig` that appears as `testexe.gif` due to a Unicode order override character. See: - http://unicode.org/reports/tr36/#Bidirectional_Text_Spoofing - https://krebsonsecurity.com/2011/09/right-to-left-override-aids-email-attacks/ commit bc605afb1c6af3a5ebc31a4c1523ff170eb96ffe Author: Daniel Gasienica <daniel@gasienica.ch> Date: Fri Feb 16 16:12:29 2018 -0500 Remove `CURRENT_PROCESS_VERSION` Reintroduce this whenever we need it. We currently only deal with schema version numbers within this module.
2018-02-22 18:21:53 +00:00
// NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
/* eslint-disable more/no-then */
import * as fs from 'fs';
import * as fse from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import { expect } from 'chai';
import {
eliminateOutOfDateFiles,
eliminateOldEntries,
isLineAfterDate,
fetchLog,
fetchLogs,
} from '../logging/main_process_logging';
describe('logging', () => {
const fakeLogEntry = ({
level = 30,
msg = 'hello world',
time = new Date().toISOString(),
}: {
level?: number;
msg?: string;
time?: string;
}): Record<string, unknown> => ({
level,
msg,
time,
});
const fakeLogLine = (...args: Parameters<typeof fakeLogEntry>): string =>
JSON.stringify(fakeLogEntry(...args));
let tmpDir: string;
beforeEach(async () => {
tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'signal-test-'));
});
afterEach(async () => {
await fse.remove(tmpDir);
});
describe('#isLineAfterDate', () => {
it('returns false if falsy', () => {
const actual = isLineAfterDate('', new Date());
expect(actual).to.equal(false);
});
it('returns false if invalid JSON', () => {
const actual = isLineAfterDate('{{}', new Date());
expect(actual).to.equal(false);
});
it('returns false if date is invalid', () => {
const line = JSON.stringify({ time: '2018-01-04T19:17:05.014Z' });
const actual = isLineAfterDate(line, new Date('try6'));
expect(actual).to.equal(false);
});
it('returns false if log time is invalid', () => {
const line = JSON.stringify({ time: 'try7' });
const date = new Date('2018-01-04T19:17:00.000Z');
const actual = isLineAfterDate(line, date);
expect(actual).to.equal(false);
});
it('returns false if date before provided date', () => {
const line = JSON.stringify({ time: '2018-01-04T19:17:00.000Z' });
const date = new Date('2018-01-04T19:17:05.014Z');
const actual = isLineAfterDate(line, date);
expect(actual).to.equal(false);
});
it('returns true if date is after provided date', () => {
const line = JSON.stringify({ time: '2018-01-04T19:17:05.014Z' });
const date = new Date('2018-01-04T19:17:00.000Z');
const actual = isLineAfterDate(line, date);
expect(actual).to.equal(true);
});
});
describe('#eliminateOutOfDateFiles', () => {
it('deletes an empty file', () => {
const date = new Date();
const log = '\n';
const target = path.join(tmpDir, 'log.log');
fs.writeFileSync(target, log);
return eliminateOutOfDateFiles(tmpDir, date).then(() => {
expect(fs.existsSync(target)).to.equal(false);
});
});
it('deletes a file with invalid JSON lines', () => {
const date = new Date();
const log = '{{}\n';
const target = path.join(tmpDir, 'log.log');
fs.writeFileSync(target, log);
return eliminateOutOfDateFiles(tmpDir, date).then(() => {
expect(fs.existsSync(target)).to.equal(false);
});
});
it('deletes a file with all dates before provided date', () => {
const date = new Date('2018-01-04T19:17:05.014Z');
const contents = [
JSON.stringify({ time: '2018-01-04T19:17:00.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
].join('\n');
const target = path.join(tmpDir, 'log.log');
fs.writeFileSync(target, contents);
return eliminateOutOfDateFiles(tmpDir, date).then(() => {
expect(fs.existsSync(target)).to.equal(false);
});
});
it('keeps a file with first line date before provided date', () => {
const date = new Date('2018-01-04T19:16:00.000Z');
const contents = [
JSON.stringify({ time: '2018-01-04T19:17:00.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
].join('\n');
const target = path.join(tmpDir, 'log.log');
fs.writeFileSync(target, contents);
return eliminateOutOfDateFiles(tmpDir, date).then(() => {
expect(fs.existsSync(target)).to.equal(true);
});
});
it('keeps a file with last line date before provided date', () => {
const date = new Date('2018-01-04T19:17:01.000Z');
const contents = [
JSON.stringify({ time: '2018-01-04T19:17:00.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
].join('\n');
const target = path.join(tmpDir, 'log.log');
fs.writeFileSync(target, contents);
return eliminateOutOfDateFiles(tmpDir, date).then(() => {
expect(fs.existsSync(target)).to.equal(true);
});
});
});
describe('#eliminateOldEntries', () => {
it('eliminates all non-parsing entries', () => {
const date = new Date('2018-01-04T19:17:01.000Z');
const contents = [
'random line',
fakeLogLine({ time: '2018-01-04T19:17:01.014Z' }),
fakeLogLine({ time: '2018-01-04T19:17:02.014Z' }),
fakeLogLine({ time: '2018-01-04T19:17:03.014Z' }),
].join('\n');
const expected = [
fakeLogEntry({ time: '2018-01-04T19:17:01.014Z' }),
fakeLogEntry({ time: '2018-01-04T19:17:02.014Z' }),
fakeLogEntry({ time: '2018-01-04T19:17:03.014Z' }),
];
const target = path.join(tmpDir, 'log.log');
2018-04-27 21:25:04 +00:00
const files = [
{
path: target,
},
];
fs.writeFileSync(target, contents);
return eliminateOldEntries(files, date).then(() => {
const actualEntries = fs
.readFileSync(target, 'utf8')
.split('\n')
.map(line => line.trim())
.filter(Boolean)
.map(line => JSON.parse(line));
expect(actualEntries).to.deep.equal(expected);
});
});
it('preserves all lines if before target date', () => {
const date = new Date('2018-01-04T19:17:03.000Z');
const contents = [
'random line',
fakeLogLine({ time: '2018-01-04T19:17:01.014Z' }),
fakeLogLine({ time: '2018-01-04T19:17:02.014Z' }),
fakeLogLine({ time: '2018-01-04T19:17:03.014Z' }),
].join('\n');
const expected = fakeLogEntry({ time: '2018-01-04T19:17:03.014Z' });
const target = path.join(tmpDir, 'log.log');
2018-04-27 21:25:04 +00:00
const files = [
{
path: target,
},
];
fs.writeFileSync(target, contents);
return eliminateOldEntries(files, date).then(() => {
// There should only be 1 line, so we can parse it safely.
expect(JSON.parse(fs.readFileSync(target, 'utf8'))).to.deep.equal(
expected
);
});
});
});
describe('#fetchLog', () => {
it('returns error if file does not exist', () => {
const target = 'random_file';
2018-04-27 21:25:04 +00:00
return fetchLog(target).then(
() => {
throw new Error('Expected an error!');
},
error => {
expect(error)
.to.have.property('message')
.that.match(/random_file/);
}
);
});
it('returns empty array if file has no valid JSON lines', () => {
const contents = 'line 1\nline2\n';
const target = path.join(tmpDir, 'test.log');
fs.writeFileSync(target, contents);
2018-04-27 21:25:04 +00:00
return fetchLog(target).then(result => {
expect(result).to.deep.equal([]);
});
});
it('returns just three fields in each returned line', () => {
const contents = [
JSON.stringify({
one: 1,
two: 2,
level: 30,
time: '2020-04-20T06:09:08.000Z',
msg: 'message 1',
}),
JSON.stringify({
one: 1,
two: 2,
level: 40,
time: '2021-04-20T06:09:08.000Z',
msg: 'message 2',
}),
'',
].join('\n');
2018-04-27 21:25:04 +00:00
const expected = [
{
level: 30,
time: '2020-04-20T06:09:08.000Z',
msg: 'message 1',
2018-04-27 21:25:04 +00:00
},
{
level: 40,
time: '2021-04-20T06:09:08.000Z',
msg: 'message 2',
2018-04-27 21:25:04 +00:00
},
];
const target = path.join(tmpDir, 'test.log');
fs.writeFileSync(target, contents);
2018-04-27 21:25:04 +00:00
return fetchLog(target).then(result => {
expect(result).to.deep.equal(expected);
});
});
});
describe('#fetchLogs', () => {
it('returns single entry if no files', () => {
return fetchLogs(tmpDir).then(results => {
expect(results).to.have.length(1);
expect(results[0].msg).to.match(/Loaded this list/);
});
});
it('returns sorted entries from all files', () => {
const first = [
fakeLogLine({ msg: '2', time: '2018-01-04T19:17:05.014Z' }),
'',
].join('\n');
const second = [
fakeLogLine({ msg: '1', time: '2018-01-04T19:17:00.014Z' }),
fakeLogLine({ msg: '3', time: '2018-01-04T19:18:00.014Z' }),
'',
].join('\n');
fs.writeFileSync(path.join(tmpDir, 'first.log'), first);
fs.writeFileSync(path.join(tmpDir, 'second.log'), second);
return fetchLogs(tmpDir).then(results => {
expect(results).to.have.length(4);
expect(results[0].msg).to.equal('1');
expect(results[1].msg).to.equal('2');
expect(results[2].msg).to.equal('3');
});
});
});
});