mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-02-21 10:16:50 +00:00
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
🎨 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.
230 lines
5.3 KiB
JavaScript
230 lines
5.3 KiB
JavaScript
// NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
|
|
/* eslint-disable more/no-then */
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const electron = require('electron');
|
|
const bunyan = require('bunyan');
|
|
const mkdirp = require('mkdirp');
|
|
const _ = require('lodash');
|
|
const readFirstLine = require('firstline');
|
|
const readLastLines = require('read-last-lines').read;
|
|
|
|
const {
|
|
app,
|
|
ipcMain: ipc,
|
|
} = electron;
|
|
const LEVELS = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'];
|
|
let logger;
|
|
|
|
|
|
module.exports = {
|
|
initialize,
|
|
getLogger,
|
|
// for tests only:
|
|
isLineAfterDate,
|
|
eliminateOutOfDateFiles,
|
|
eliminateOldEntries,
|
|
fetchLog,
|
|
fetch,
|
|
};
|
|
|
|
function initialize() {
|
|
if (logger) {
|
|
throw new Error('Already called initialize!');
|
|
}
|
|
|
|
const basePath = app.getPath('userData');
|
|
const logPath = path.join(basePath, 'logs');
|
|
mkdirp.sync(logPath);
|
|
|
|
return cleanupLogs(logPath).then(() => {
|
|
const logFile = path.join(logPath, 'log.log');
|
|
|
|
logger = bunyan.createLogger({
|
|
name: 'log',
|
|
streams: [{
|
|
level: 'debug',
|
|
stream: process.stdout,
|
|
}, {
|
|
type: 'rotating-file',
|
|
path: logFile,
|
|
period: '1d',
|
|
count: 3,
|
|
}],
|
|
});
|
|
|
|
LEVELS.forEach((level) => {
|
|
ipc.on(`log-${level}`, (first, ...rest) => {
|
|
logger[level](...rest);
|
|
});
|
|
});
|
|
|
|
ipc.on('fetch-log', (event) => {
|
|
fetch(logPath).then((data) => {
|
|
event.sender.send('fetched-log', data);
|
|
}, (error) => {
|
|
logger.error(`Problem loading log from disk: ${error.stack}`);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function cleanupLogs(logPath) {
|
|
const now = new Date();
|
|
const earliestDate = new Date(Date.UTC(
|
|
now.getUTCFullYear(),
|
|
now.getUTCMonth(),
|
|
now.getUTCDate() - 3
|
|
));
|
|
|
|
return eliminateOutOfDateFiles(logPath, earliestDate).then((remaining) => {
|
|
const files = _.filter(remaining, file => !file.start && file.end);
|
|
|
|
if (!files.length) {
|
|
return null;
|
|
}
|
|
|
|
return eliminateOldEntries(files, earliestDate);
|
|
});
|
|
}
|
|
|
|
function isLineAfterDate(line, date) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
function eliminateOutOfDateFiles(logPath, date) {
|
|
const files = fs.readdirSync(logPath);
|
|
const paths = files.map(file => path.join(logPath, file));
|
|
|
|
return Promise.all(_.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) {
|
|
fs.unlinkSync(file.path);
|
|
}
|
|
|
|
return file;
|
|
})
|
|
));
|
|
}
|
|
|
|
function eliminateOldEntries(files, date) {
|
|
const earliest = date.getTime();
|
|
|
|
return Promise.all(_.map(
|
|
files,
|
|
file => fetchLog(file.path).then((lines) => {
|
|
const recent = _.filter(lines, line => (new Date(line.time)).getTime() >= earliest);
|
|
const text = _.map(recent, line => JSON.stringify(line)).join('\n');
|
|
|
|
return fs.writeFileSync(file.path, `${text}\n`);
|
|
})
|
|
));
|
|
}
|
|
|
|
function getLogger() {
|
|
if (!logger) {
|
|
throw new Error('Logger hasn\'t been initialized yet!');
|
|
}
|
|
|
|
return logger;
|
|
}
|
|
|
|
function fetchLog(logFile) {
|
|
return new Promise((resolve, reject) => {
|
|
fs.readFile(logFile, { encoding: 'utf8' }, (err, text) => {
|
|
if (err) {
|
|
return reject(err);
|
|
}
|
|
|
|
const lines = _.compact(text.split('\n'));
|
|
const data = _.compact(lines.map((line) => {
|
|
try {
|
|
return _.pick(JSON.parse(line), ['level', 'time', 'msg']);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}));
|
|
|
|
return resolve(data);
|
|
});
|
|
});
|
|
}
|
|
|
|
function fetch(logPath) {
|
|
const files = fs.readdirSync(logPath);
|
|
const paths = files.map(file => path.join(logPath, file));
|
|
|
|
// creating a manual log entry for the final log result
|
|
const now = new Date();
|
|
const fileListEntry = {
|
|
level: 30, // INFO
|
|
time: now.toJSON(),
|
|
msg: `Loaded this list of log files from logPath: ${files.join(', ')}`,
|
|
};
|
|
|
|
return Promise.all(paths.map(fetchLog)).then((results) => {
|
|
const data = _.flatten(results);
|
|
|
|
data.push(fileListEntry);
|
|
|
|
return _.sortBy(data, 'time');
|
|
});
|
|
}
|
|
|
|
|
|
function logAtLevel(level, ...args) {
|
|
if (logger) {
|
|
// To avoid [Object object] in our log since console.log handles non-strings smoothly
|
|
const str = args.map((item) => {
|
|
if (typeof item !== 'string') {
|
|
try {
|
|
return JSON.stringify(item);
|
|
} catch (e) {
|
|
return item;
|
|
}
|
|
}
|
|
|
|
return item;
|
|
});
|
|
logger[level](str.join(' '));
|
|
} else {
|
|
console._log(...args);
|
|
}
|
|
}
|
|
|
|
// This blows up using mocha --watch, so we ensure it is run just once
|
|
if (!console._log) {
|
|
console._log = console.log;
|
|
console.log = _.partial(logAtLevel, 'info');
|
|
console._error = console.error;
|
|
console.error = _.partial(logAtLevel, 'error');
|
|
console._warn = console.warn;
|
|
console.warn = _.partial(logAtLevel, 'warn');
|
|
}
|