signal-desktop/ts/logging/main_process_logging.ts

370 lines
9.5 KiB
TypeScript
Raw Normal View History

// Copyright 2017-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 */
/* eslint-disable no-console */
2021-10-01 18:49:59 +00:00
import { join } from 'path';
import split2 from 'split2';
import { readdirSync, createReadStream, unlinkSync, writeFileSync } from 'fs';
import type { BrowserWindow } from 'electron';
import { app, ipcMain as ipc } from 'electron';
2021-03-10 22:41:38 +00:00
import pinoms from 'pino-multi-stream';
import pino from 'pino';
import * as mkdirp from 'mkdirp';
import { filter, flatten, map, pick, sortBy } from 'lodash';
import readFirstLine from 'firstline';
import { read as readLastLines } from 'read-last-lines';
import rimraf from 'rimraf';
2021-03-10 22:41:38 +00:00
import { createStream } from 'rotating-file-stream';
2021-10-01 18:49:59 +00:00
import type { LoggerType } from '../types/Logging';
import * as log from './log';
2021-04-05 20:44:02 +00:00
import { Environment, getEnvironment } from '../environment';
import type { FetchLogIpcData, LogEntryType } from './shared';
import { LogLevel, cleanArgs, getLogLevelString, isLogEntry } from './shared';
declare global {
// We want to extend `Console`, so we need an interface.
// eslint-disable-next-line no-restricted-syntax
interface Console {
_log: typeof console.log;
_warn: typeof console.warn;
_error: typeof console.error;
}
}
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
const MAX_LOG_LINES = 1000000;
2021-10-01 18:49:59 +00:00
let globalLogger: undefined | pino.Logger;
let shouldRestart = false;
2021-04-05 20:44:02 +00:00
const isRunningFromConsole =
2021-12-09 08:06:04 +00:00
Boolean(process.stdout.isTTY) || getEnvironment() === Environment.Test;
export async function initialize(
getMainWindow: () => undefined | BrowserWindow
2021-10-01 18:49:59 +00:00
): Promise<LoggerType> {
if (globalLogger) {
throw new Error('Already called initialize!');
}
const basePath = app.getPath('userData');
2021-10-01 18:49:59 +00:00
const logPath = join(basePath, 'logs');
mkdirp.sync(logPath);
try {
await cleanupLogs(logPath);
} catch (error) {
2020-01-08 17:44:54 +00:00
const errorString = `Failed to clean logs; deleting all. Error: ${error.stack}`;
console.error(errorString);
await deleteAllLogs(logPath);
mkdirp.sync(logPath);
// If we want this log entry to persist on disk, we need to wait until we've
// set up our logging infrastructure.
setTimeout(() => {
console.error(errorString);
}, 500);
}
2021-10-01 18:49:59 +00:00
const logFile = join(logPath, 'main.log');
2021-03-10 22:41:38 +00:00
const stream = createStream(logFile, {
interval: '1d',
rotate: 3,
2021-03-10 22:41:38 +00:00
});
const onClose = () => {
globalLogger = undefined;
if (shouldRestart) {
initialize(getMainWindow);
}
};
stream.on('close', onClose);
stream.on('error', onClose);
2021-03-10 22:41:38 +00:00
const streams: pinoms.Streams = [];
streams.push({ stream });
if (isRunningFromConsole) {
2021-03-10 22:41:38 +00:00
streams.push({
level: 'debug' as const,
stream: process.stdout,
});
}
2021-03-10 22:41:38 +00:00
const logger = pinoms({
streams,
timestamp: pino.stdTimeFunctions.isoTime,
});
ipc.on('fetch-log', async event => {
const mainWindow = getMainWindow();
if (!mainWindow) {
logger.info('Logs were requested, but the main window is missing');
return;
}
let data: FetchLogIpcData;
try {
const [logEntries, rest] = await Promise.all([
fetchLogs(logPath),
fetchAdditionalLogData(mainWindow),
]);
data = {
logEntries,
...rest,
};
} catch (error) {
logger.error(`Problem loading log data: ${error.stack}`);
return;
}
try {
event.sender.send('fetched-log', data);
} catch (err: unknown) {
// NOTE(evanhahn): We don't want to send a message to a window that's closed.
// I wanted to use `event.sender.isDestroyed()` but that seems to fail.
// Instead, we attempt the send and catch the failure as best we can.
const hasUserClosedWindow = isProbablyObjectHasBeenDestroyedError(err);
if (hasUserClosedWindow) {
logger.info('Logs were requested, but it seems the window was closed');
} else {
logger.error(
'Problem replying with fetched logs',
err instanceof Error && err.stack ? err.stack : err
);
}
}
});
ipc.on('delete-all-logs', async event => {
// Restart logging when the streams will close
shouldRestart = true;
try {
await deleteAllLogs(logPath);
} catch (error) {
logger.error(`Problem deleting all logs: ${error.stack}`);
}
event.sender.send('delete-all-logs-complete');
});
globalLogger = logger;
2021-10-01 18:49:59 +00:00
return log;
}
async function deleteAllLogs(logPath: string): Promise<void> {
return new Promise((resolve, reject) => {
rimraf(
logPath,
{
disableGlob: true,
},
error => {
if (error) {
return reject(error);
}
return resolve();
}
);
});
}
async function cleanupLogs(logPath: string) {
const now = new Date();
const earliestDate = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - 3)
);
try {
const remaining = await eliminateOutOfDateFiles(logPath, earliestDate);
2021-10-01 18:49:59 +00:00
const files = filter(remaining, file => !file.start && file.end);
if (!files.length) {
return;
}
await eliminateOldEntries(files, earliestDate);
} catch (error) {
console.error(
'Error cleaning logs; deleting and starting over from scratch.',
error.stack
);
// delete and re-create the log directory
await deleteAllLogs(logPath);
mkdirp.sync(logPath);
}
}
// Exported for testing only.
export function isLineAfterDate(line: string, date: Readonly<Date>): boolean {
if (!line) {
return false;
}
try {
const data = JSON.parse(line);
return new Date(data.time).getTime() > date.getTime();
} catch (e) {
console.log('error parsing log line', e.stack, line);
return false;
}
}
// Exported for testing only.
export function eliminateOutOfDateFiles(
logPath: string,
date: Readonly<Date>
): Promise<
Array<{
path: string;
start: boolean;
end: boolean;
}>
> {
2021-10-01 18:49:59 +00:00
const files = readdirSync(logPath);
const paths = files.map(file => join(logPath, file));
return Promise.all(
2021-10-01 18:49:59 +00:00
map(paths, target =>
Promise.all([readFirstLine(target), readLastLines(target, 2)]).then(
results => {
const start = results[0];
const end = results[1].split('\n');
const file = {
path: target,
start: isLineAfterDate(start, date),
end:
isLineAfterDate(end[end.length - 1], date) ||
isLineAfterDate(end[end.length - 2], date),
};
if (!file.start && !file.end) {
2021-10-01 18:49:59 +00:00
unlinkSync(file.path);
}
return file;
}
)
)
);
}
// Exported for testing only.
export async function eliminateOldEntries(
files: ReadonlyArray<{ path: string }>,
date: Readonly<Date>
): Promise<void> {
await Promise.all(
2021-10-01 18:49:59 +00:00
map(files, file =>
fetchLog(file.path).then(lines => {
2021-10-01 18:49:59 +00:00
const recent = filter(lines, line => new Date(line.time) >= date);
const text = map(recent, line => JSON.stringify(line)).join('\n');
2021-10-01 18:49:59 +00:00
return writeFileSync(file.path, `${text}\n`);
})
)
);
}
// Exported for testing only.
export async function fetchLog(logFile: string): Promise<Array<LogEntryType>> {
const results = new Array<LogEntryType>();
const rawStream = createReadStream(logFile);
const jsonStream = rawStream.pipe(
split2(line => {
try {
return JSON.parse(line);
} catch (e) {
return undefined;
}
})
);
// Propagate fs errors down to the json stream so that for loop below handles
// them.
rawStream.on('error', error => jsonStream.emit('error', error));
for await (const line of jsonStream) {
const result = line && pick(line, ['level', 'time', 'msg']);
if (!isLogEntry(result)) {
continue;
}
results.push(result);
if (results.length > MAX_LOG_LINES) {
results.shift();
}
}
return results;
}
// Exported for testing only.
export function fetchLogs(logPath: string): Promise<Array<LogEntryType>> {
2021-10-01 18:49:59 +00:00
const files = readdirSync(logPath);
const paths = files.map(file => join(logPath, file));
// creating a manual log entry for the final log result
const fileListEntry: LogEntryType = {
level: LogLevel.Info,
time: new Date().toISOString(),
msg: `Loaded this list of log files from logPath: ${files.join(', ')}`,
};
return Promise.all(paths.map(fetchLog)).then(results => {
2021-10-01 18:49:59 +00:00
const data = flatten(results);
data.push(fileListEntry);
2021-10-01 18:49:59 +00:00
return sortBy(data, logEntry => logEntry.time);
});
}
export const fetchAdditionalLogData = (
mainWindow: BrowserWindow
): Promise<Omit<FetchLogIpcData, 'logEntries'>> =>
new Promise(resolve => {
mainWindow.webContents.send('additional-log-data-request');
ipc.once('additional-log-data-response', (_event, data) => {
resolve(data);
});
});
function logAtLevel(level: LogLevel, ...args: ReadonlyArray<unknown>) {
if (globalLogger) {
const levelString = getLogLevelString(level);
globalLogger[levelString](cleanArgs(args));
} else if (isRunningFromConsole && !process.stdout.destroyed) {
console._log(...args);
}
}
function isProbablyObjectHasBeenDestroyedError(err: unknown): boolean {
return err instanceof Error && err.message === 'Object has been destroyed';
}
// This blows up using mocha --watch, so we ensure it is run just once
if (!console._log) {
2021-10-01 18:49:59 +00:00
log.setLogAtLevel(logAtLevel);
console._log = console.log;
2021-10-01 18:49:59 +00:00
console.log = log.info;
console._error = console.error;
2021-10-01 18:49:59 +00:00
console.error = log.error;
console._warn = console.warn;
2021-10-01 18:49:59 +00:00
console.warn = log.warn;
}