Enable auto-updating using electron-updater
This commit is contained in:
parent
06142c880a
commit
178d788dca
|
@ -23,10 +23,6 @@
|
|||
"port": "38157"
|
||||
}
|
||||
],
|
||||
"disableAutoUpdate": true,
|
||||
"updatesUrl": "TODO",
|
||||
"updatesPublicKey":
|
||||
"fd7dd3de7149dc0a127909fee7de0f7620ddd0de061b37a2c303e37de802a401",
|
||||
"updatesEnabled": false,
|
||||
"openDevTools": false,
|
||||
"buildExpiration": 0,
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
{}
|
||||
{
|
||||
"updatesEnabled": true
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
provider: s3
|
||||
region: us-east-1
|
||||
bucket: your-test-bucket.signal.org
|
||||
path: desktop
|
||||
acl: public-read
|
||||
owner: <yourGHName>
|
||||
repo: <yourGHRepoName>
|
||||
provider: github
|
||||
|
|
37
main.js
37
main.js
|
@ -65,9 +65,7 @@ const appInstance = config.util.getEnv('NODE_APP_INSTANCE') || 0;
|
|||
const attachments = require('./app/attachments');
|
||||
const attachmentChannel = require('./app/attachment_channel');
|
||||
|
||||
// TODO: Enable when needed
|
||||
// const updater = require('./ts/updater/index');
|
||||
const updater = null;
|
||||
const updater = require('./ts/updater/index');
|
||||
|
||||
const createTrayIcon = require('./app/tray_icon');
|
||||
const ephemeralConfig = require('./app/ephemeral_config');
|
||||
|
@ -410,22 +408,40 @@ ipc.on('show-window', () => {
|
|||
showWindow();
|
||||
});
|
||||
|
||||
let updatesStarted = false;
|
||||
ipc.on('ready-for-updates', async () => {
|
||||
if (updatesStarted || !updater) {
|
||||
let isReadyForUpdates = false;
|
||||
async function readyForUpdates() {
|
||||
if (isReadyForUpdates) {
|
||||
return;
|
||||
}
|
||||
updatesStarted = true;
|
||||
|
||||
isReadyForUpdates = true;
|
||||
|
||||
// disable for now
|
||||
/*
|
||||
// First, install requested sticker pack
|
||||
const incomingUrl = getIncomingUrl(process.argv);
|
||||
if (incomingUrl) {
|
||||
handleSgnlLink(incomingUrl);
|
||||
}
|
||||
*/
|
||||
|
||||
// Second, start checking for app updates
|
||||
try {
|
||||
await updater.start(getMainWindow, locale.messages, logger);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
const log = logger || console;
|
||||
log.error(
|
||||
'Error starting update checks:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
ipc.once('ready-for-updates', readyForUpdates);
|
||||
|
||||
// Forcefully call readyForUpdates after 10 minutes.
|
||||
// This ensures we start the updater.
|
||||
const TEN_MINUTES = 10 * 60 * 1000;
|
||||
setTimeout(readyForUpdates, TEN_MINUTES);
|
||||
|
||||
function openReleaseNotes() {
|
||||
shell.openExternal(
|
||||
|
@ -842,6 +858,9 @@ async function showMainWindow(sqlKey, passwordAttempt = false) {
|
|||
}
|
||||
|
||||
setupMenu();
|
||||
|
||||
// Check updates
|
||||
readyForUpdates();
|
||||
}
|
||||
|
||||
function setupMenu(options) {
|
||||
|
|
16
package.json
16
package.json
|
@ -2,13 +2,16 @@
|
|||
"name": "session-messenger-desktop",
|
||||
"productName": "Session",
|
||||
"description": "Private messaging from your desktop",
|
||||
"repository": "https://github.com/loki-project/loki-messenger.git",
|
||||
"version": "1.0.3",
|
||||
"license": "GPL-3.0",
|
||||
"author": {
|
||||
"name": "Loki Project",
|
||||
"email": "team@loki.network"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/loki-project/session-desktop.git"
|
||||
},
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"postinstall": "electron-builder install-app-deps && rimraf node_modules/dtrace-provider",
|
||||
|
@ -25,7 +28,6 @@
|
|||
"build": "electron-builder --config.extraMetadata.environment=$SIGNAL_ENV",
|
||||
"build-release": "cross-env SIGNAL_ENV=production npm run build -- --config.directories.output=release",
|
||||
"make:linux:x64:appimage": "electron-builder build --linux appimage --x64",
|
||||
"sign-release": "node ts/updater/generateSignature.js",
|
||||
"build-module-protobuf": "pbjs --target static-module --wrap commonjs --out ts/protobuf/compiled.js protos/*.proto && pbts --out ts/protobuf/compiled.d.ts ts/protobuf/compiled.js",
|
||||
"clean-module-protobuf": "rm -f ts/protobuf/compiled.d.ts ts/protobuf/compiled.js",
|
||||
"build-protobuf": "yarn build-module-protobuf",
|
||||
|
@ -78,8 +80,9 @@
|
|||
"dompurify": "^2.0.7",
|
||||
"electron-context-menu": "^0.15.0",
|
||||
"electron-editor-context-menu": "1.1.1",
|
||||
"electron-is-dev": "0.3.0",
|
||||
"electron-is-dev": "^1.1.0",
|
||||
"electron-localshortcut": "^3.2.1",
|
||||
"electron-updater": "^4.2.2",
|
||||
"emoji-datasource": "4.0.0",
|
||||
"emoji-datasource-apple": "4.0.0",
|
||||
"emoji-js": "3.4.0",
|
||||
|
@ -139,6 +142,7 @@
|
|||
"@types/classnames": "2.2.3",
|
||||
"@types/color": "^3.0.0",
|
||||
"@types/config": "0.0.34",
|
||||
"@types/electron-is-dev": "^1.1.1",
|
||||
"@types/filesize": "3.6.0",
|
||||
"@types/fs-extra": "5.0.5",
|
||||
"@types/google-libphonenumber": "7.4.14",
|
||||
|
@ -208,7 +212,8 @@
|
|||
"build": {
|
||||
"appId": "com.loki-project.messenger-desktop",
|
||||
"afterSign": "build/notarize.js",
|
||||
"artifactName": "${name}-${os}-${version}.${ext}",
|
||||
"artifactName": "${name}-${os}-${arch}-${version}.${ext}",
|
||||
"publish": "github",
|
||||
"mac": {
|
||||
"category": "public.app-category.social-networking",
|
||||
"icon": "build/icons/mac/icon.icns",
|
||||
|
@ -318,7 +323,8 @@
|
|||
"!node_modules/@journeyapps/sqlcipher/deps/*",
|
||||
"!node_modules/@journeyapps/sqlcipher/build/*",
|
||||
"!node_modules/@journeyapps/sqlcipher/lib/binding/node-*",
|
||||
"!build/*.js"
|
||||
"!build/*.js",
|
||||
"!dev-app-update.yml"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
import { assert } from 'chai';
|
||||
|
||||
import { getUpdateFileName, getVersion } from '../../updater/common';
|
||||
|
||||
describe('updater/signatures', () => {
|
||||
const windows = `version: 1.23.2
|
||||
files:
|
||||
- url: signal-desktop-win-1.23.2.exe
|
||||
sha512: hhK+cVAb+QOK/Ln0RBcq8Rb1iPcUC0KZeT4NwLB25PMGoPmakY27XE1bXq4QlkASJN1EkYTbKf3oUJtcllziyQ==
|
||||
size: 92020776
|
||||
path: signal-desktop-win-1.23.2.exe
|
||||
sha512: hhK+cVAb+QOK/Ln0RBcq8Rb1iPcUC0KZeT4NwLB25PMGoPmakY27XE1bXq4QlkASJN1EkYTbKf3oUJtcllziyQ==
|
||||
releaseDate: '2019-03-29T16:58:08.210Z'
|
||||
`;
|
||||
const mac = `version: 1.23.2
|
||||
files:
|
||||
- url: signal-desktop-mac-1.23.2.zip
|
||||
sha512: f4pPo3WulTVi9zBWGsJPNIlvPOTCxPibPPDmRFDoXMmFm6lqJpXZQ9DSWMJumfc4BRp4y/NTQLGYI6b4WuJwhg==
|
||||
size: 105179791
|
||||
blockMapSize: 111109
|
||||
path: signal-desktop-mac-1.23.2.zip
|
||||
sha512: f4pPo3WulTVi9zBWGsJPNIlvPOTCxPibPPDmRFDoXMmFm6lqJpXZQ9DSWMJumfc4BRp4y/NTQLGYI6b4WuJwhg==
|
||||
releaseDate: '2019-03-29T16:57:16.997Z'
|
||||
`;
|
||||
const windowsBeta = `version: 1.23.2-beta.1
|
||||
files:
|
||||
- url: signal-desktop-beta-win-1.23.2-beta.1.exe
|
||||
sha512: ZHM1F3y/Y6ulP5NhbFuh7t2ZCpY4lD9BeBhPV+g2B/0p/66kp0MJDeVxTgjR49OakwpMAafA1d6y2QBail4hSQ==
|
||||
size: 92028656
|
||||
path: signal-desktop-beta-win-1.23.2-beta.1.exe
|
||||
sha512: ZHM1F3y/Y6ulP5NhbFuh7t2ZCpY4lD9BeBhPV+g2B/0p/66kp0MJDeVxTgjR49OakwpMAafA1d6y2QBail4hSQ==
|
||||
releaseDate: '2019-03-29T01:56:00.544Z'
|
||||
`;
|
||||
const macBeta = `version: 1.23.2-beta.1
|
||||
files:
|
||||
- url: signal-desktop-beta-mac-1.23.2-beta.1.zip
|
||||
sha512: h/01N0DD5Jw2Q6M1n4uLGLTCrMFxcn8QOPtLR3HpABsf3w9b2jFtKb56/2cbuJXP8ol8TkTDWKnRV6mnqnLBDw==
|
||||
size: 105182398
|
||||
blockMapSize: 110894
|
||||
path: signal-desktop-beta-mac-1.23.2-beta.1.zip
|
||||
sha512: h/01N0DD5Jw2Q6M1n4uLGLTCrMFxcn8QOPtLR3HpABsf3w9b2jFtKb56/2cbuJXP8ol8TkTDWKnRV6mnqnLBDw==
|
||||
releaseDate: '2019-03-29T01:53:23.881Z'
|
||||
`;
|
||||
|
||||
describe('#getVersion', () => {
|
||||
it('successfully gets version', () => {
|
||||
const expected = '1.23.2';
|
||||
assert.strictEqual(getVersion(windows), expected);
|
||||
assert.strictEqual(getVersion(mac), expected);
|
||||
|
||||
const expectedBeta = '1.23.2-beta.1';
|
||||
assert.strictEqual(getVersion(windowsBeta), expectedBeta);
|
||||
assert.strictEqual(getVersion(macBeta), expectedBeta);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUpdateFileName', () => {
|
||||
it('successfully gets version', () => {
|
||||
assert.strictEqual(
|
||||
getUpdateFileName(windows),
|
||||
'signal-desktop-win-1.23.2.exe'
|
||||
);
|
||||
assert.strictEqual(
|
||||
getUpdateFileName(mac),
|
||||
'signal-desktop-mac-1.23.2.zip'
|
||||
);
|
||||
assert.strictEqual(
|
||||
getUpdateFileName(windowsBeta),
|
||||
'signal-desktop-beta-win-1.23.2-beta.1.exe'
|
||||
);
|
||||
assert.strictEqual(
|
||||
getUpdateFileName(macBeta),
|
||||
'signal-desktop-beta-mac-1.23.2-beta.1.zip'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,206 +0,0 @@
|
|||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { copy } from 'fs-extra';
|
||||
|
||||
import {
|
||||
_getFileHash,
|
||||
getSignaturePath,
|
||||
loadHexFromPath,
|
||||
verifySignature,
|
||||
writeHexToPath,
|
||||
writeSignature,
|
||||
} from '../../updater/signature';
|
||||
import { createTempDir, deleteTempDir } from '../../updater/common';
|
||||
import { keyPair } from '../../updater/curve';
|
||||
|
||||
describe('updater/signatures', () => {
|
||||
it('_getFileHash returns correct hash', async () => {
|
||||
const filePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
||||
const expected =
|
||||
'7bc77f27d92d00b4a1d57c480ca86dacc43d57bc318339c92119d1fbf6b557a5';
|
||||
|
||||
const hash = await _getFileHash(filePath);
|
||||
|
||||
assert.strictEqual(expected, Buffer.from(hash).toString('hex'));
|
||||
});
|
||||
|
||||
it('roundtrips binary file writes', async () => {
|
||||
let tempDir;
|
||||
|
||||
try {
|
||||
tempDir = await createTempDir();
|
||||
|
||||
const path = join(tempDir, 'something.bin');
|
||||
const { publicKey } = keyPair();
|
||||
|
||||
await writeHexToPath(path, publicKey);
|
||||
|
||||
const fromDisk = await loadHexFromPath(path);
|
||||
|
||||
assert.strictEqual(
|
||||
Buffer.from(fromDisk).compare(Buffer.from(publicKey)),
|
||||
0
|
||||
);
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
await deleteTempDir(tempDir);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('roundtrips signature', async () => {
|
||||
let tempDir;
|
||||
|
||||
try {
|
||||
tempDir = await createTempDir();
|
||||
|
||||
const version = 'v1.23.2';
|
||||
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
||||
const updatePath = join(tempDir, 'ghost-kitty.mp4');
|
||||
await copy(sourcePath, updatePath);
|
||||
|
||||
const privateKeyPath = join(tempDir, 'private.key');
|
||||
const { publicKey, privateKey } = keyPair();
|
||||
await writeHexToPath(privateKeyPath, privateKey);
|
||||
|
||||
await writeSignature(updatePath, version, privateKeyPath);
|
||||
|
||||
const signaturePath = getSignaturePath(updatePath);
|
||||
assert.strictEqual(existsSync(signaturePath), true);
|
||||
|
||||
const verified = await verifySignature(updatePath, version, publicKey);
|
||||
assert.strictEqual(verified, true);
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
await deleteTempDir(tempDir);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('fails signature verification if version changes', async () => {
|
||||
let tempDir;
|
||||
|
||||
try {
|
||||
tempDir = await createTempDir();
|
||||
|
||||
const version = 'v1.23.2';
|
||||
const brokenVersion = 'v1.23.3';
|
||||
|
||||
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
||||
const updatePath = join(tempDir, 'ghost-kitty.mp4');
|
||||
await copy(sourcePath, updatePath);
|
||||
|
||||
const privateKeyPath = join(tempDir, 'private.key');
|
||||
const { publicKey, privateKey } = keyPair();
|
||||
await writeHexToPath(privateKeyPath, privateKey);
|
||||
|
||||
await writeSignature(updatePath, version, privateKeyPath);
|
||||
|
||||
const verified = await verifySignature(
|
||||
updatePath,
|
||||
brokenVersion,
|
||||
publicKey
|
||||
);
|
||||
assert.strictEqual(verified, false);
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
await deleteTempDir(tempDir);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('fails signature verification if signature tampered with', async () => {
|
||||
let tempDir;
|
||||
|
||||
try {
|
||||
tempDir = await createTempDir();
|
||||
|
||||
const version = 'v1.23.2';
|
||||
|
||||
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
||||
const updatePath = join(tempDir, 'ghost-kitty.mp4');
|
||||
await copy(sourcePath, updatePath);
|
||||
|
||||
const privateKeyPath = join(tempDir, 'private.key');
|
||||
const { publicKey, privateKey } = keyPair();
|
||||
await writeHexToPath(privateKeyPath, privateKey);
|
||||
|
||||
await writeSignature(updatePath, version, privateKeyPath);
|
||||
|
||||
const signaturePath = getSignaturePath(updatePath);
|
||||
const signature = Buffer.from(await loadHexFromPath(signaturePath));
|
||||
signature[4] += 3;
|
||||
await writeHexToPath(signaturePath, signature);
|
||||
|
||||
const verified = await verifySignature(updatePath, version, publicKey);
|
||||
assert.strictEqual(verified, false);
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
await deleteTempDir(tempDir);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('fails signature verification if binary file tampered with', async () => {
|
||||
let tempDir;
|
||||
|
||||
try {
|
||||
tempDir = await createTempDir();
|
||||
|
||||
const version = 'v1.23.2';
|
||||
|
||||
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
||||
const updatePath = join(tempDir, 'ghost-kitty.mp4');
|
||||
await copy(sourcePath, updatePath);
|
||||
|
||||
const privateKeyPath = join(tempDir, 'private.key');
|
||||
const { publicKey, privateKey } = keyPair();
|
||||
await writeHexToPath(privateKeyPath, privateKey);
|
||||
|
||||
await writeSignature(updatePath, version, privateKeyPath);
|
||||
|
||||
const brokenSourcePath = join(
|
||||
__dirname,
|
||||
'../../../fixtures/pixabay-Soap-Bubble-7141.mp4'
|
||||
);
|
||||
await copy(brokenSourcePath, updatePath);
|
||||
|
||||
const verified = await verifySignature(updatePath, version, publicKey);
|
||||
assert.strictEqual(verified, false);
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
await deleteTempDir(tempDir);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('fails signature verification if signed by different key', async () => {
|
||||
let tempDir;
|
||||
|
||||
try {
|
||||
tempDir = await createTempDir();
|
||||
|
||||
const version = 'v1.23.2';
|
||||
|
||||
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
||||
const updatePath = join(tempDir, 'ghost-kitty.mp4');
|
||||
await copy(sourcePath, updatePath);
|
||||
|
||||
const privateKeyPath = join(tempDir, 'private.key');
|
||||
const { publicKey } = keyPair();
|
||||
const { privateKey } = keyPair();
|
||||
await writeHexToPath(privateKeyPath, privateKey);
|
||||
|
||||
await writeSignature(updatePath, version, privateKeyPath);
|
||||
|
||||
const verified = await verifySignature(updatePath, version, publicKey);
|
||||
assert.strictEqual(verified, false);
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
await deleteTempDir(tempDir);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
|
@ -1,28 +1,4 @@
|
|||
import {
|
||||
createWriteStream,
|
||||
statSync,
|
||||
writeFile as writeFileCallback,
|
||||
} from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
// @ts-ignore
|
||||
import { createParser } from 'dashdash';
|
||||
// @ts-ignore
|
||||
import ProxyAgent from 'proxy-agent';
|
||||
import { FAILSAFE_SCHEMA, safeLoad } from 'js-yaml';
|
||||
import { gt } from 'semver';
|
||||
import { get as getFromConfig } from 'config';
|
||||
import { get, GotOptions, stream } from 'got';
|
||||
import { v4 as getGuid } from 'uuid';
|
||||
import pify from 'pify';
|
||||
import mkdirp from 'mkdirp';
|
||||
import rimraf from 'rimraf';
|
||||
import { app, BrowserWindow, dialog } from 'electron';
|
||||
|
||||
// @ts-ignore
|
||||
import * as packageJson from '../../package.json';
|
||||
import { getSignatureFileName } from './signature';
|
||||
import { BrowserWindow, dialog } from 'electron';
|
||||
|
||||
export type MessagesType = {
|
||||
[key: string]: {
|
||||
|
@ -42,90 +18,6 @@ export type LoggerType = {
|
|||
trace: LogFunction;
|
||||
};
|
||||
|
||||
const writeFile = pify(writeFileCallback);
|
||||
const mkdirpPromise = pify(mkdirp);
|
||||
const rimrafPromise = pify(rimraf);
|
||||
const { platform } = process;
|
||||
|
||||
export async function checkForUpdates(
|
||||
logger: LoggerType
|
||||
): Promise<{
|
||||
fileName: string;
|
||||
version: string;
|
||||
} | null> {
|
||||
const yaml = await getUpdateYaml();
|
||||
const version = getVersion(yaml);
|
||||
|
||||
if (!version) {
|
||||
logger.warn('checkForUpdates: no version extracted from downloaded yaml');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isVersionNewer(version)) {
|
||||
logger.info(`checkForUpdates: found newer version ${version}`);
|
||||
|
||||
return {
|
||||
fileName: getUpdateFileName(yaml),
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`checkForUpdates: ${version} is not newer; no new update available`
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function downloadUpdate(
|
||||
fileName: string,
|
||||
logger: LoggerType
|
||||
): Promise<string> {
|
||||
const baseUrl = getUpdatesBase();
|
||||
const updateFileUrl = `${baseUrl}/${fileName}`;
|
||||
|
||||
const signatureFileName = getSignatureFileName(fileName);
|
||||
const signatureUrl = `${baseUrl}/${signatureFileName}`;
|
||||
|
||||
let tempDir;
|
||||
try {
|
||||
tempDir = await createTempDir();
|
||||
const targetUpdatePath = join(tempDir, fileName);
|
||||
const targetSignaturePath = join(tempDir, getSignatureFileName(fileName));
|
||||
|
||||
logger.info(`downloadUpdate: Downloading ${signatureUrl}`);
|
||||
const { body } = await get(signatureUrl, getGotOptions());
|
||||
await writeFile(targetSignaturePath, body);
|
||||
|
||||
logger.info(`downloadUpdate: Downloading ${updateFileUrl}`);
|
||||
const downloadStream = stream(updateFileUrl, getGotOptions());
|
||||
const writeStream = createWriteStream(targetUpdatePath);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
downloadStream.on('error', error => {
|
||||
reject(error);
|
||||
});
|
||||
downloadStream.on('end', () => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
writeStream.on('error', error => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
downloadStream.pipe(writeStream);
|
||||
});
|
||||
|
||||
return targetUpdatePath;
|
||||
} catch (error) {
|
||||
if (tempDir) {
|
||||
await deleteTempDir(tempDir);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function showUpdateDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
messages: MessagesType
|
||||
|
@ -179,145 +71,6 @@ export async function showCannotUpdateDialog(
|
|||
});
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
export function getUpdateCheckUrl(): string {
|
||||
return `${getUpdatesBase()}/${getUpdatesFileName()}`;
|
||||
}
|
||||
|
||||
export function getUpdatesBase(): string {
|
||||
return getFromConfig('updatesUrl');
|
||||
}
|
||||
export function getCertificateAuthority(): string {
|
||||
return getFromConfig('certificateAuthority');
|
||||
}
|
||||
export function getProxyUrl(): string | undefined {
|
||||
return process.env.HTTPS_PROXY || process.env.https_proxy;
|
||||
}
|
||||
|
||||
export function getUpdatesFileName(): string {
|
||||
const prefix = isBetaChannel() ? 'beta' : 'latest';
|
||||
|
||||
if (platform === 'darwin') {
|
||||
return `${prefix}-mac.yml`;
|
||||
} else {
|
||||
return `${prefix}.yml`;
|
||||
}
|
||||
}
|
||||
|
||||
const hasBeta = /beta/i;
|
||||
function isBetaChannel(): boolean {
|
||||
return hasBeta.test(packageJson.version);
|
||||
}
|
||||
|
||||
function isVersionNewer(newVersion: string): boolean {
|
||||
const { version } = packageJson;
|
||||
|
||||
return gt(newVersion, version);
|
||||
}
|
||||
|
||||
export function getVersion(yaml: string): string | undefined {
|
||||
const info = parseYaml(yaml);
|
||||
|
||||
if (info && info.version) {
|
||||
return info.version;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export function getUpdateFileName(yaml: string) {
|
||||
const info = parseYaml(yaml);
|
||||
|
||||
if (info && info.path) {
|
||||
return info.path;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function parseYaml(yaml: string): any {
|
||||
return safeLoad(yaml, { schema: FAILSAFE_SCHEMA, json: true });
|
||||
}
|
||||
|
||||
async function getUpdateYaml(): Promise<string> {
|
||||
const targetUrl = getUpdateCheckUrl();
|
||||
const { body } = await get(targetUrl, getGotOptions());
|
||||
|
||||
if (!body) {
|
||||
throw new Error('Got unexpected response back from update check');
|
||||
}
|
||||
|
||||
return body.toString('utf8');
|
||||
}
|
||||
|
||||
function getGotOptions(): GotOptions<null> {
|
||||
const ca = getCertificateAuthority();
|
||||
const proxyUrl = getProxyUrl();
|
||||
const agent = proxyUrl ? new ProxyAgent(proxyUrl) : undefined;
|
||||
|
||||
return {
|
||||
agent,
|
||||
ca,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'User-Agent': 'Session Desktop (+https://getsession.org)',
|
||||
},
|
||||
useElectronNet: false,
|
||||
};
|
||||
}
|
||||
|
||||
function getBaseTempDir() {
|
||||
// We only use tmpdir() when this code is run outside of an Electron app (as in: tests)
|
||||
return app ? join(app.getPath('userData'), 'temp') : tmpdir();
|
||||
}
|
||||
|
||||
export async function createTempDir() {
|
||||
const baseTempDir = getBaseTempDir();
|
||||
const uniqueName = getGuid();
|
||||
const targetDir = join(baseTempDir, uniqueName);
|
||||
await mkdirpPromise(targetDir);
|
||||
|
||||
return targetDir;
|
||||
}
|
||||
|
||||
export async function deleteTempDir(targetDir: string) {
|
||||
const pathInfo = statSync(targetDir);
|
||||
if (!pathInfo.isDirectory()) {
|
||||
throw new Error(
|
||||
`deleteTempDir: Cannot delete path '${targetDir}' because it is not a directory`
|
||||
);
|
||||
}
|
||||
|
||||
const baseTempDir = getBaseTempDir();
|
||||
if (!targetDir.startsWith(baseTempDir)) {
|
||||
throw new Error(
|
||||
`deleteTempDir: Cannot delete path '${targetDir}' since it is not within base temp dir`
|
||||
);
|
||||
}
|
||||
|
||||
await rimrafPromise(targetDir);
|
||||
}
|
||||
|
||||
export function getPrintableError(error: Error) {
|
||||
return error && error.stack ? error.stack : error;
|
||||
}
|
||||
|
||||
export async function deleteBaseTempDir() {
|
||||
const baseTempDir = getBaseTempDir();
|
||||
await rimrafPromise(baseTempDir);
|
||||
}
|
||||
|
||||
export function getCliOptions<T>(options: any): T {
|
||||
const parser = createParser({ options });
|
||||
const cliOptions = parser.parse(process.argv);
|
||||
|
||||
if (cliOptions.help) {
|
||||
const help = parser.help().trimRight();
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(help);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return cliOptions;
|
||||
}
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
import { getCliOptions, getPrintableError } from './common';
|
||||
import { keyPair } from './curve';
|
||||
import { writeHexToPath } from './signature';
|
||||
|
||||
/* tslint:disable:no-console */
|
||||
|
||||
const OPTIONS = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Print this help and exit.',
|
||||
},
|
||||
{
|
||||
names: ['key', 'k'],
|
||||
type: 'string',
|
||||
help: 'Path where public key will go',
|
||||
default: 'public.key',
|
||||
},
|
||||
{
|
||||
names: ['private', 'p'],
|
||||
type: 'string',
|
||||
help: 'Path where private key will go',
|
||||
default: 'private.key',
|
||||
},
|
||||
];
|
||||
|
||||
type OptionsType = {
|
||||
key: string;
|
||||
private: string;
|
||||
};
|
||||
|
||||
const cliOptions = getCliOptions<OptionsType>(OPTIONS);
|
||||
go(cliOptions).catch(error => {
|
||||
console.error('Something went wrong!', getPrintableError(error));
|
||||
});
|
||||
|
||||
async function go(options: OptionsType) {
|
||||
const { key: publicKeyPath, private: privateKeyPath } = options;
|
||||
const { publicKey, privateKey } = keyPair();
|
||||
|
||||
await Promise.all([
|
||||
writeHexToPath(publicKeyPath, publicKey),
|
||||
writeHexToPath(privateKeyPath, privateKey),
|
||||
]);
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
import { join, resolve } from 'path';
|
||||
import { readdir as readdirCallback } from 'fs';
|
||||
|
||||
import pify from 'pify';
|
||||
|
||||
import { getCliOptions, getPrintableError } from './common';
|
||||
import { writeSignature } from './signature';
|
||||
|
||||
// @ts-ignore
|
||||
import * as packageJson from '../../package.json';
|
||||
|
||||
const readdir = pify(readdirCallback);
|
||||
|
||||
/* tslint:disable:no-console */
|
||||
|
||||
const OPTIONS = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Print this help and exit.',
|
||||
},
|
||||
{
|
||||
names: ['private', 'p'],
|
||||
type: 'string',
|
||||
help: 'Path to private key file (default: ./private.key)',
|
||||
default: 'private.key',
|
||||
},
|
||||
{
|
||||
names: ['update', 'u'],
|
||||
type: 'string',
|
||||
help: 'Path to the update package (default: the .exe or .zip in ./release)',
|
||||
},
|
||||
{
|
||||
names: ['version', 'v'],
|
||||
type: 'string',
|
||||
help: `Version number of this package (default: ${packageJson.version})`,
|
||||
default: packageJson.version,
|
||||
},
|
||||
];
|
||||
|
||||
type OptionsType = {
|
||||
private: string;
|
||||
update: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
const cliOptions = getCliOptions<OptionsType>(OPTIONS);
|
||||
go(cliOptions).catch(error => {
|
||||
console.error('Something went wrong!', getPrintableError(error));
|
||||
});
|
||||
|
||||
async function go(options: OptionsType) {
|
||||
const { private: privateKeyPath, version } = options;
|
||||
let { update: updatePath } = options;
|
||||
|
||||
if (!updatePath) {
|
||||
updatePath = await findUpdatePath();
|
||||
}
|
||||
|
||||
console.log('Signing with...');
|
||||
console.log(` version: ${version}`);
|
||||
console.log(` update file: ${updatePath}`);
|
||||
console.log(` private key file: ${privateKeyPath}`);
|
||||
|
||||
await writeSignature(updatePath, version, privateKeyPath);
|
||||
}
|
||||
|
||||
const IS_EXE = /\.exe$/;
|
||||
const IS_ZIP = /\.zip$/;
|
||||
async function findUpdatePath(): Promise<string> {
|
||||
const releaseDir = resolve('release');
|
||||
const files: Array<string> = await readdir(releaseDir);
|
||||
|
||||
const max = files.length;
|
||||
for (let i = 0; i < max; i += 1) {
|
||||
const file = files[i];
|
||||
const fullPath = join(releaseDir, file);
|
||||
|
||||
if (IS_EXE.test(file) || IS_ZIP.test(file)) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("No suitable file found in 'release' folder!");
|
||||
}
|
|
@ -1,14 +1,7 @@
|
|||
import { get as getFromConfig } from 'config';
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import { start as startMacOS } from './macos';
|
||||
import { start as startWindows } from './windows';
|
||||
import {
|
||||
deleteBaseTempDir,
|
||||
getPrintableError,
|
||||
LoggerType,
|
||||
MessagesType,
|
||||
} from './common';
|
||||
import { start as startUpdater } from './updater';
|
||||
import { LoggerType, MessagesType } from './common';
|
||||
|
||||
let initialized = false;
|
||||
|
||||
|
@ -17,8 +10,6 @@ export async function start(
|
|||
messages?: MessagesType,
|
||||
logger?: LoggerType
|
||||
) {
|
||||
const { platform } = process;
|
||||
|
||||
if (initialized) {
|
||||
throw new Error('updater/start: Updates have already been initialized!');
|
||||
}
|
||||
|
@ -32,6 +23,13 @@ export async function start(
|
|||
}
|
||||
|
||||
if (autoUpdateDisabled()) {
|
||||
/*
|
||||
If you really want to enable auto-updating in dev mode
|
||||
You need to create a dev-app-update.yml file.
|
||||
A sample can be found in dev-app-update.yml.sample.
|
||||
After that you can change `updatesEnabled` to `true` in the default config.
|
||||
*/
|
||||
|
||||
logger.info(
|
||||
'updater/start: Updates disabled - not starting new version checks'
|
||||
);
|
||||
|
@ -39,28 +37,11 @@ export async function start(
|
|||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteBaseTempDir();
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'updater/start: Error deleting temp dir:',
|
||||
getPrintableError(error)
|
||||
);
|
||||
}
|
||||
|
||||
if (platform === 'win32') {
|
||||
await startWindows(getMainWindow, messages, logger);
|
||||
} else if (platform === 'darwin') {
|
||||
await startMacOS(getMainWindow, messages, logger);
|
||||
} else {
|
||||
throw new Error('updater/start: Unsupported platform');
|
||||
}
|
||||
await startUpdater(getMainWindow, messages, logger);
|
||||
}
|
||||
|
||||
function autoUpdateDisabled() {
|
||||
return (
|
||||
process.platform === 'linux' ||
|
||||
process.mas ||
|
||||
!getFromConfig('updatesEnabled')
|
||||
process.mas || !getFromConfig('updatesEnabled') // From Electron: Mac App Store build
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,324 +0,0 @@
|
|||
import { createReadStream, statSync } from 'fs';
|
||||
import { createServer, IncomingMessage, Server, ServerResponse } from 'http';
|
||||
import { AddressInfo } from 'net';
|
||||
import { dirname } from 'path';
|
||||
|
||||
import { v4 as getGuid } from 'uuid';
|
||||
import { app, autoUpdater, BrowserWindow, dialog } from 'electron';
|
||||
import { get as getFromConfig } from 'config';
|
||||
import { gt } from 'semver';
|
||||
|
||||
import {
|
||||
checkForUpdates,
|
||||
deleteTempDir,
|
||||
downloadUpdate,
|
||||
getPrintableError,
|
||||
LoggerType,
|
||||
MessagesType,
|
||||
showCannotUpdateDialog,
|
||||
showUpdateDialog,
|
||||
} from './common';
|
||||
import { hexToBinary, verifySignature } from './signature';
|
||||
import { markShouldQuit } from '../../app/window_state';
|
||||
|
||||
let isChecking = false;
|
||||
const SECOND = 1000;
|
||||
const MINUTE = SECOND * 60;
|
||||
const INTERVAL = MINUTE * 30;
|
||||
|
||||
export async function start(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
messages: MessagesType,
|
||||
logger: LoggerType
|
||||
) {
|
||||
logger.info('macos/start: starting checks...');
|
||||
|
||||
loggerForQuitHandler = logger;
|
||||
app.once('quit', quitHandler);
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await checkDownloadAndInstall(getMainWindow, messages, logger);
|
||||
} catch (error) {
|
||||
logger.error('macos/start: error:', getPrintableError(error));
|
||||
}
|
||||
}, INTERVAL);
|
||||
|
||||
await checkDownloadAndInstall(getMainWindow, messages, logger);
|
||||
}
|
||||
|
||||
let fileName: string;
|
||||
let version: string;
|
||||
let updateFilePath: string;
|
||||
let loggerForQuitHandler: LoggerType;
|
||||
|
||||
async function checkDownloadAndInstall(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
messages: MessagesType,
|
||||
logger: LoggerType
|
||||
) {
|
||||
if (isChecking) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('checkDownloadAndInstall: checking for update...');
|
||||
try {
|
||||
isChecking = true;
|
||||
|
||||
const result = await checkForUpdates(logger);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { fileName: newFileName, version: newVersion } = result;
|
||||
if (fileName !== newFileName || !version || gt(newVersion, version)) {
|
||||
deleteCache(updateFilePath, logger);
|
||||
fileName = newFileName;
|
||||
version = newVersion;
|
||||
updateFilePath = await downloadUpdate(fileName, logger);
|
||||
}
|
||||
|
||||
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
|
||||
const verified = verifySignature(updateFilePath, version, publicKey);
|
||||
if (!verified) {
|
||||
// Note: We don't delete the cache here, because we don't want to continually
|
||||
// re-download the broken release. We will download it only once per launch.
|
||||
throw new Error(
|
||||
`checkDownloadAndInstall: Downloaded update did not pass signature verification (version: '${version}'; fileName: '${fileName}')`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await handToAutoUpdate(updateFilePath, logger);
|
||||
} catch (error) {
|
||||
const readOnly = 'Cannot update while running on a read-only volume';
|
||||
const message: string = error.message || '';
|
||||
if (message.includes(readOnly)) {
|
||||
logger.info('checkDownloadAndInstall: showing read-only dialog...');
|
||||
await showReadOnlyDialog(getMainWindow(), messages);
|
||||
} else {
|
||||
logger.info(
|
||||
'checkDownloadAndInstall: showing general update failure dialog...'
|
||||
);
|
||||
await showCannotUpdateDialog(getMainWindow(), messages);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
// At this point, closing the app will cause the update to be installed automatically
|
||||
// because Squirrel has cached the update file and will do the right thing.
|
||||
|
||||
logger.info('checkDownloadAndInstall: showing update dialog...');
|
||||
const shouldUpdate = await showUpdateDialog(getMainWindow(), messages);
|
||||
if (!shouldUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('checkDownloadAndInstall: calling quitAndInstall...');
|
||||
markShouldQuit();
|
||||
autoUpdater.quitAndInstall();
|
||||
} catch (error) {
|
||||
logger.error('checkDownloadAndInstall: error', getPrintableError(error));
|
||||
} finally {
|
||||
isChecking = false;
|
||||
}
|
||||
}
|
||||
|
||||
function quitHandler() {
|
||||
deleteCache(updateFilePath, loggerForQuitHandler);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
function deleteCache(filePath: string | null, logger: LoggerType) {
|
||||
if (filePath) {
|
||||
const tempDir = dirname(filePath);
|
||||
deleteTempDir(tempDir).catch(error => {
|
||||
logger.error(
|
||||
'quitHandler: error deleting temporary directory:',
|
||||
getPrintableError(error)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handToAutoUpdate(
|
||||
filePath: string,
|
||||
logger: LoggerType
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const updateFileUrl = generateFileUrl();
|
||||
const server = createServer();
|
||||
let serverUrl: string;
|
||||
|
||||
server.on('error', (error: Error) => {
|
||||
logger.error(
|
||||
'handToAutoUpdate: server had error',
|
||||
getPrintableError(error)
|
||||
);
|
||||
shutdown(server, logger);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
server.on(
|
||||
'request',
|
||||
(request: IncomingMessage, response: ServerResponse) => {
|
||||
const { url } = request;
|
||||
|
||||
if (url === '/') {
|
||||
const absoluteUrl = `${serverUrl}${updateFileUrl}`;
|
||||
writeJSONResponse(absoluteUrl, response);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!url || !url.startsWith(updateFileUrl)) {
|
||||
write404(url, response, logger);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
pipeUpdateToSquirrel(filePath, server, response, logger, reject);
|
||||
}
|
||||
);
|
||||
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
serverUrl = getServerUrl(server);
|
||||
|
||||
autoUpdater.on('error', (error: Error) => {
|
||||
logger.error('autoUpdater: error', getPrintableError(error));
|
||||
reject(error);
|
||||
});
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
logger.info('autoUpdater: update-downloaded event fired');
|
||||
shutdown(server, logger);
|
||||
resolve();
|
||||
});
|
||||
|
||||
autoUpdater.setFeedURL({
|
||||
url: serverUrl,
|
||||
headers: { 'Cache-Control': 'no-cache' },
|
||||
});
|
||||
autoUpdater.checkForUpdates();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function pipeUpdateToSquirrel(
|
||||
filePath: string,
|
||||
server: Server,
|
||||
response: ServerResponse,
|
||||
logger: LoggerType,
|
||||
reject: (error: Error) => void
|
||||
) {
|
||||
const updateFileSize = getFileSize(filePath);
|
||||
const readStream = createReadStream(filePath);
|
||||
|
||||
response.on('error', (error: Error) => {
|
||||
logger.error(
|
||||
'pipeUpdateToSquirrel: update file download request had an error',
|
||||
getPrintableError(error)
|
||||
);
|
||||
shutdown(server, logger);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
readStream.on('error', (error: Error) => {
|
||||
logger.error(
|
||||
'pipeUpdateToSquirrel: read stream error response:',
|
||||
getPrintableError(error)
|
||||
);
|
||||
shutdown(server, logger, response);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'application/zip',
|
||||
'Content-Length': updateFileSize,
|
||||
});
|
||||
|
||||
readStream.pipe(response);
|
||||
}
|
||||
|
||||
function writeJSONResponse(url: string, response: ServerResponse) {
|
||||
const data = Buffer.from(
|
||||
JSON.stringify({
|
||||
url,
|
||||
})
|
||||
);
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': data.byteLength,
|
||||
});
|
||||
response.end(data);
|
||||
}
|
||||
|
||||
function write404(
|
||||
url: string | undefined,
|
||||
response: ServerResponse,
|
||||
logger: LoggerType
|
||||
) {
|
||||
logger.error(`write404: Squirrel requested unexpected url '${url}'`);
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
}
|
||||
|
||||
function getServerUrl(server: Server) {
|
||||
const address = server.address() as AddressInfo;
|
||||
|
||||
// tslint:disable-next-line:no-http-string
|
||||
return `http://127.0.0.1:${address.port}`;
|
||||
}
|
||||
function generateFileUrl(): string {
|
||||
return `/${getGuid()}.zip`;
|
||||
}
|
||||
|
||||
function getFileSize(targetPath: string): number {
|
||||
const { size } = statSync(targetPath);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
function shutdown(
|
||||
server: Server,
|
||||
logger: LoggerType,
|
||||
response?: ServerResponse
|
||||
) {
|
||||
try {
|
||||
if (server) {
|
||||
server.close();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('shutdown: Error closing server', getPrintableError(error));
|
||||
}
|
||||
|
||||
try {
|
||||
if (response) {
|
||||
response.end();
|
||||
}
|
||||
} catch (endError) {
|
||||
logger.error(
|
||||
"shutdown: couldn't end response",
|
||||
getPrintableError(endError)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function showReadOnlyDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
messages: MessagesType
|
||||
): Promise<void> {
|
||||
const options = {
|
||||
type: 'warning',
|
||||
buttons: [messages.ok.message],
|
||||
title: messages.cannotUpdate.message,
|
||||
message: messages.readOnlyVolume.message,
|
||||
};
|
||||
|
||||
return new Promise(resolve => {
|
||||
dialog.showMessageBox(mainWindow, options, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
import { createHash } from 'crypto';
|
||||
import {
|
||||
createReadStream,
|
||||
readFile as readFileCallback,
|
||||
writeFile as writeFileCallback,
|
||||
} from 'fs';
|
||||
import { basename, dirname, join, resolve as resolvePath } from 'path';
|
||||
|
||||
import pify from 'pify';
|
||||
|
||||
import { BinaryType, sign, verify } from './curve';
|
||||
|
||||
const readFile = pify(readFileCallback);
|
||||
const writeFile = pify(writeFileCallback);
|
||||
|
||||
export async function generateSignature(
|
||||
updatePackagePath: string,
|
||||
version: string,
|
||||
privateKeyPath: string
|
||||
) {
|
||||
const privateKey = await loadHexFromPath(privateKeyPath);
|
||||
const message = await generateMessage(updatePackagePath, version);
|
||||
|
||||
return sign(privateKey, message);
|
||||
}
|
||||
|
||||
export async function verifySignature(
|
||||
updatePackagePath: string,
|
||||
version: string,
|
||||
publicKey: BinaryType
|
||||
): Promise<boolean> {
|
||||
const signaturePath = getSignaturePath(updatePackagePath);
|
||||
const signature = await loadHexFromPath(signaturePath);
|
||||
const message = await generateMessage(updatePackagePath, version);
|
||||
|
||||
return verify(publicKey, message, signature);
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
async function generateMessage(
|
||||
updatePackagePath: string,
|
||||
version: string
|
||||
): Promise<BinaryType> {
|
||||
const hash = await _getFileHash(updatePackagePath);
|
||||
const messageString = `${Buffer.from(hash).toString('hex')}-${version}`;
|
||||
|
||||
return Buffer.from(messageString);
|
||||
}
|
||||
|
||||
export async function writeSignature(
|
||||
updatePackagePath: string,
|
||||
version: string,
|
||||
privateKeyPath: string
|
||||
) {
|
||||
const signaturePath = getSignaturePath(updatePackagePath);
|
||||
const signature = await generateSignature(
|
||||
updatePackagePath,
|
||||
version,
|
||||
privateKeyPath
|
||||
);
|
||||
await writeHexToPath(signaturePath, signature);
|
||||
}
|
||||
|
||||
export async function _getFileHash(
|
||||
updatePackagePath: string
|
||||
): Promise<BinaryType> {
|
||||
const hash = createHash('sha256');
|
||||
const stream = createReadStream(updatePackagePath);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', data => {
|
||||
hash.update(data);
|
||||
});
|
||||
stream.on('close', () => {
|
||||
resolve(hash.digest());
|
||||
});
|
||||
stream.on('error', error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getSignatureFileName(fileName: string) {
|
||||
return `${fileName}.sig`;
|
||||
}
|
||||
|
||||
export function getSignaturePath(updatePackagePath: string): string {
|
||||
const updateFullPath = resolvePath(updatePackagePath);
|
||||
const updateDir = dirname(updateFullPath);
|
||||
const updateFileName = basename(updateFullPath);
|
||||
|
||||
return join(updateDir, getSignatureFileName(updateFileName));
|
||||
}
|
||||
|
||||
export function hexToBinary(target: string): BinaryType {
|
||||
return Buffer.from(target, 'hex');
|
||||
}
|
||||
|
||||
export function binaryToHex(data: BinaryType): string {
|
||||
return Buffer.from(data).toString('hex');
|
||||
}
|
||||
|
||||
export async function loadHexFromPath(target: string): Promise<BinaryType> {
|
||||
const hexString = await readFile(target, 'utf8');
|
||||
|
||||
return hexToBinary(hexString);
|
||||
}
|
||||
|
||||
export async function writeHexToPath(target: string, data: BinaryType) {
|
||||
await writeFile(target, binaryToHex(data));
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
import { autoUpdater } from 'electron-updater';
|
||||
import { BrowserWindow } from 'electron';
|
||||
import { markShouldQuit } from '../../app/window_state';
|
||||
import {
|
||||
getPrintableError,
|
||||
LoggerType,
|
||||
MessagesType,
|
||||
showCannotUpdateDialog,
|
||||
showUpdateDialog,
|
||||
} from './common';
|
||||
|
||||
let isUpdating = false;
|
||||
|
||||
const SECOND = 1000;
|
||||
const MINUTE = SECOND * 60;
|
||||
const INTERVAL = MINUTE * 30;
|
||||
|
||||
export async function start(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
messages: MessagesType,
|
||||
logger: LoggerType
|
||||
) {
|
||||
logger.info('auto-update: starting checks...');
|
||||
|
||||
autoUpdater.logger = logger;
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await checkForUpdates(getMainWindow, messages, logger);
|
||||
} catch (error) {
|
||||
logger.error('auto-update: error:', getPrintableError(error));
|
||||
}
|
||||
}, INTERVAL);
|
||||
|
||||
await checkForUpdates(getMainWindow, messages, logger);
|
||||
}
|
||||
|
||||
async function checkForUpdates(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
messages: MessagesType,
|
||||
logger: LoggerType
|
||||
) {
|
||||
if (isUpdating) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('auto-update: checking for update...');
|
||||
|
||||
try {
|
||||
// Get the update using electron-updater
|
||||
try {
|
||||
const info = await autoUpdater.checkForUpdates();
|
||||
if (!info.downloadPromise) {
|
||||
logger.info('auto-update: no update to download');
|
||||
|
||||
return;
|
||||
}
|
||||
await info.downloadPromise;
|
||||
} catch (error) {
|
||||
await showCannotUpdateDialog(getMainWindow(), messages);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Update downloaded successfully, we should ask the user to update
|
||||
logger.info('auto-update: showing update dialog...');
|
||||
const shouldUpdate = await showUpdateDialog(getMainWindow(), messages);
|
||||
if (!shouldUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('auto-update: calling quitAndInstall...');
|
||||
markShouldQuit();
|
||||
autoUpdater.quitAndInstall();
|
||||
} finally {
|
||||
isUpdating = false;
|
||||
}
|
||||
}
|
|
@ -1,231 +0,0 @@
|
|||
import { dirname, join } from 'path';
|
||||
import { spawn as spawnEmitter, SpawnOptions } from 'child_process';
|
||||
import { readdir as readdirCallback, unlink as unlinkCallback } from 'fs';
|
||||
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { get as getFromConfig } from 'config';
|
||||
import { gt } from 'semver';
|
||||
import pify from 'pify';
|
||||
|
||||
import {
|
||||
checkForUpdates,
|
||||
deleteTempDir,
|
||||
downloadUpdate,
|
||||
getPrintableError,
|
||||
LoggerType,
|
||||
MessagesType,
|
||||
showCannotUpdateDialog,
|
||||
showUpdateDialog,
|
||||
} from './common';
|
||||
import { hexToBinary, verifySignature } from './signature';
|
||||
import { markShouldQuit } from '../../app/window_state';
|
||||
|
||||
const readdir = pify(readdirCallback);
|
||||
const unlink = pify(unlinkCallback);
|
||||
|
||||
let isChecking = false;
|
||||
const SECOND = 1000;
|
||||
const MINUTE = SECOND * 60;
|
||||
const INTERVAL = MINUTE * 30;
|
||||
|
||||
export async function start(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
messages: MessagesType,
|
||||
logger: LoggerType
|
||||
) {
|
||||
logger.info('windows/start: starting checks...');
|
||||
|
||||
loggerForQuitHandler = logger;
|
||||
app.once('quit', quitHandler);
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await checkDownloadAndInstall(getMainWindow, messages, logger);
|
||||
} catch (error) {
|
||||
logger.error('windows/start: error:', getPrintableError(error));
|
||||
}
|
||||
}, INTERVAL);
|
||||
|
||||
await deletePreviousInstallers(logger);
|
||||
await checkDownloadAndInstall(getMainWindow, messages, logger);
|
||||
}
|
||||
|
||||
let fileName: string;
|
||||
let version: string;
|
||||
let updateFilePath: string;
|
||||
let installing: boolean;
|
||||
let loggerForQuitHandler: LoggerType;
|
||||
|
||||
async function checkDownloadAndInstall(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
messages: MessagesType,
|
||||
logger: LoggerType
|
||||
) {
|
||||
if (isChecking) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isChecking = true;
|
||||
|
||||
logger.info('checkDownloadAndInstall: checking for update...');
|
||||
const result = await checkForUpdates(logger);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { fileName: newFileName, version: newVersion } = result;
|
||||
if (fileName !== newFileName || !version || gt(newVersion, version)) {
|
||||
deleteCache(updateFilePath, logger);
|
||||
fileName = newFileName;
|
||||
version = newVersion;
|
||||
updateFilePath = await downloadUpdate(fileName, logger);
|
||||
}
|
||||
|
||||
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
|
||||
const verified = verifySignature(updateFilePath, version, publicKey);
|
||||
if (!verified) {
|
||||
// Note: We don't delete the cache here, because we don't want to continually
|
||||
// re-download the broken release. We will download it only once per launch.
|
||||
throw new Error(
|
||||
`Downloaded update did not pass signature verification (version: '${version}'; fileName: '${fileName}')`
|
||||
);
|
||||
}
|
||||
|
||||
logger.info('checkDownloadAndInstall: showing dialog...');
|
||||
const shouldUpdate = await showUpdateDialog(getMainWindow(), messages);
|
||||
if (!shouldUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await verifyAndInstall(updateFilePath, version, logger);
|
||||
installing = true;
|
||||
} catch (error) {
|
||||
logger.info(
|
||||
'checkDownloadAndInstall: showing general update failure dialog...'
|
||||
);
|
||||
await showCannotUpdateDialog(getMainWindow(), messages);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
markShouldQuit();
|
||||
app.quit();
|
||||
} catch (error) {
|
||||
logger.error('checkDownloadAndInstall: error', getPrintableError(error));
|
||||
} finally {
|
||||
isChecking = false;
|
||||
}
|
||||
}
|
||||
|
||||
function quitHandler() {
|
||||
if (updateFilePath && !installing) {
|
||||
verifyAndInstall(updateFilePath, version, loggerForQuitHandler).catch(
|
||||
error => {
|
||||
loggerForQuitHandler.error(
|
||||
'quitHandler: error installing:',
|
||||
getPrintableError(error)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
// This is fixed by out new install mechanisms...
|
||||
// https://github.com/signalapp/Signal-Desktop/issues/2369
|
||||
// ...but we should also clean up those old installers.
|
||||
const IS_EXE = /\.exe$/i;
|
||||
async function deletePreviousInstallers(logger: LoggerType) {
|
||||
const userDataPath = app.getPath('userData');
|
||||
const files: Array<string> = await readdir(userDataPath);
|
||||
await Promise.all(
|
||||
files.map(async file => {
|
||||
const isExe = IS_EXE.test(file);
|
||||
if (!isExe) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fullPath = join(userDataPath, file);
|
||||
try {
|
||||
await unlink(fullPath);
|
||||
} catch (error) {
|
||||
logger.error(`deletePreviousInstallers: couldn't delete file ${file}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function verifyAndInstall(
|
||||
filePath: string,
|
||||
newVersion: string,
|
||||
logger: LoggerType
|
||||
) {
|
||||
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
|
||||
const verified = verifySignature(updateFilePath, newVersion, publicKey);
|
||||
if (!verified) {
|
||||
throw new Error(
|
||||
`Downloaded update did not pass signature verification (version: '${newVersion}'; fileName: '${fileName}')`
|
||||
);
|
||||
}
|
||||
|
||||
await install(filePath, logger);
|
||||
}
|
||||
|
||||
async function install(filePath: string, logger: LoggerType): Promise<void> {
|
||||
logger.info('windows/install: installing package...');
|
||||
const args = ['--updated'];
|
||||
const options = {
|
||||
detached: true,
|
||||
stdio: 'ignore' as 'ignore', // TypeScript considers this a plain string without help
|
||||
};
|
||||
|
||||
try {
|
||||
await spawn(filePath, args, options);
|
||||
} catch (error) {
|
||||
if (error.code === 'UNKNOWN' || error.code === 'EACCES') {
|
||||
logger.warn(
|
||||
'windows/install: Error running installer; Trying again with elevate.exe'
|
||||
);
|
||||
await spawn(getElevatePath(), [filePath, ...args], options);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function deleteCache(filePath: string | null, logger: LoggerType) {
|
||||
if (filePath) {
|
||||
const tempDir = dirname(filePath);
|
||||
deleteTempDir(tempDir).catch(error => {
|
||||
logger.error(
|
||||
'deleteCache: error deleting temporary directory',
|
||||
getPrintableError(error)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
function getElevatePath() {
|
||||
const installPath = app.getAppPath();
|
||||
|
||||
return join(installPath, 'resources', 'elevate.exe');
|
||||
}
|
||||
|
||||
async function spawn(
|
||||
exe: string,
|
||||
args: Array<string>,
|
||||
options: SpawnOptions
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const emitter = spawnEmitter(exe, args, options);
|
||||
emitter.on('error', reject);
|
||||
emitter.unref();
|
||||
|
||||
// tslint:disable-next-line no-string-based-set-timeout
|
||||
setTimeout(resolve, 200);
|
||||
});
|
||||
}
|
50
yarn.lock
50
yarn.lock
|
@ -191,6 +191,13 @@
|
|||
dependencies:
|
||||
"@types/trusted-types" "*"
|
||||
|
||||
"@types/electron-is-dev@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/electron-is-dev/-/electron-is-dev-1.1.1.tgz#b48cb249b4615915b16477891160414b57b9a8c5"
|
||||
integrity sha512-axJ7z6N/FfXHf0Q6MO75Sl7gXCqAeIJMxxYd8n80FNmGev8GPHMcva31zQQX+i4B7aBUzdyVY1UfQeFxph3xVQ==
|
||||
dependencies:
|
||||
electron-is-dev "*"
|
||||
|
||||
"@types/events@*":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
||||
|
@ -392,6 +399,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45"
|
||||
integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==
|
||||
|
||||
"@types/semver@^7.1.0":
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.1.0.tgz#c8c630d4c18cd326beff77404887596f96408408"
|
||||
integrity sha512-pOKLaubrAEMUItGNpgwl0HMFPrSAFic8oSVIvfu1UwcgGNmNyK9gyhBHKmBnUTwwVvpZfkzUC0GaMgnL6P86uA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/sinon@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-4.3.1.tgz#32458f9b166cd44c23844eee4937814276f35199"
|
||||
|
@ -3039,12 +3053,7 @@ electron-is-accelerator@^0.1.0:
|
|||
resolved "https://registry.yarnpkg.com/electron-is-accelerator/-/electron-is-accelerator-0.1.2.tgz#509e510c26a56b55e17f863a4b04e111846ab27b"
|
||||
integrity sha1-UJ5RDCala1Xhf4Y6SwThEYRqsns=
|
||||
|
||||
electron-is-dev@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-0.3.0.tgz#14e6fda5c68e9e4ecbeff9ccf037cbd7c05c5afe"
|
||||
integrity sha1-FOb9pcaOnk7L7/nM8DfL18BcWv4=
|
||||
|
||||
electron-is-dev@^1.0.1:
|
||||
electron-is-dev@*, electron-is-dev@^1.0.1, electron-is-dev@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-1.1.0.tgz#b15a2a600bdc48a51a857d460e05f15b19a2522c"
|
||||
integrity sha512-Z1qA/1oHNowGtSBIcWk0pcLEqYT/j+13xUw/MYOrBUOL4X7VN0i0KCTf5SqyvMPmW5pSPKbo28wkxMxzZ20YnQ==
|
||||
|
@ -3099,6 +3108,20 @@ electron-to-chromium@^1.2.7:
|
|||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.334.tgz#0588359f4ac5c4185ebacdf5fc7e1937e2c99872"
|
||||
integrity sha512-RcjJhpsVaX0X6ntu/WSBlW9HE9pnCgXS9B8mTUObl1aDxaiOa0Lu+NMveIS5IDC+VELzhM32rFJDCC+AApVwcA==
|
||||
|
||||
electron-updater@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-4.2.2.tgz#57e106bffad16f71b1ffa3968a52a1b71c8147e6"
|
||||
integrity sha512-e/OZhr5tLW0GcgmpR5wD0ImxgKMa8pPoNWRcwRyMzTL9pGej7+ORp0t9DtI5ZBHUbObIoEbrk+6EDGUGtJf+aA==
|
||||
dependencies:
|
||||
"@types/semver" "^7.1.0"
|
||||
builder-util-runtime "8.6.0"
|
||||
fs-extra "^8.1.0"
|
||||
js-yaml "^3.13.1"
|
||||
lazy-val "^1.0.4"
|
||||
lodash.isequal "^4.5.0"
|
||||
pako "^1.0.11"
|
||||
semver "^7.1.3"
|
||||
|
||||
electron@4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-4.1.2.tgz#dc8be0f219c73d60a97675d6d3c5b040c4f50513"
|
||||
|
@ -6080,6 +6103,11 @@ lodash.isempty@^4.1.2:
|
|||
resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
|
||||
integrity sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=
|
||||
|
||||
lodash.isequal@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
|
||||
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
|
||||
|
||||
lodash.isfunction@^3.0.8:
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
|
||||
|
@ -7334,6 +7362,11 @@ package-json@^6.3.0:
|
|||
registry-url "^5.0.0"
|
||||
semver "^6.2.0"
|
||||
|
||||
pako@^1.0.11:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
||||
|
||||
pako@~1.0.5:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732"
|
||||
|
@ -9292,6 +9325,11 @@ semver@^7.1.1:
|
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.2.tgz#847bae5bce68c5d08889824f02667199b70e3d87"
|
||||
integrity sha512-BJs9T/H8sEVHbeigqzIEo57Iu/3DG6c4QoqTfbQB3BPA4zgzAomh/Fk9E7QtjWQ8mx2dgA9YCfSF4y9k9bHNpQ==
|
||||
|
||||
semver@^7.1.3:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.3.tgz#e4345ce73071c53f336445cfc19efb1c311df2a6"
|
||||
integrity sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==
|
||||
|
||||
semver@~5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
|
||||
|
|
Loading…
Reference in New Issue