From 178d788dcaad6e7cdcaa3c18a0cb2cdfc1bdb362 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Sun, 23 Feb 2020 18:30:27 -0800 Subject: [PATCH] Enable auto-updating using electron-updater --- config/default.json | 4 - config/production.json | 4 +- dev-app-update.yml.sample | 8 +- main.js | 37 +++- package.json | 16 +- ts/test/updater/common_test.ts | 77 ------- ts/test/updater/signature_test.ts | 206 ------------------- ts/updater/common.ts | 249 +---------------------- ts/updater/generateKeyPair.ts | 45 ----- ts/updater/generateSignature.ts | 85 -------- ts/updater/index.ts | 41 +--- ts/updater/macos.ts | 324 ------------------------------ ts/updater/signature.ts | 112 ----------- ts/updater/updater.ts | 77 +++++++ ts/updater/windows.ts | 231 --------------------- yarn.lock | 50 ++++- 16 files changed, 178 insertions(+), 1388 deletions(-) delete mode 100644 ts/test/updater/common_test.ts delete mode 100644 ts/test/updater/signature_test.ts delete mode 100644 ts/updater/generateKeyPair.ts delete mode 100644 ts/updater/generateSignature.ts delete mode 100644 ts/updater/macos.ts delete mode 100644 ts/updater/signature.ts create mode 100644 ts/updater/updater.ts delete mode 100644 ts/updater/windows.ts diff --git a/config/default.json b/config/default.json index e0b61dac0..b08784f25 100644 --- a/config/default.json +++ b/config/default.json @@ -23,10 +23,6 @@ "port": "38157" } ], - "disableAutoUpdate": true, - "updatesUrl": "TODO", - "updatesPublicKey": - "fd7dd3de7149dc0a127909fee7de0f7620ddd0de061b37a2c303e37de802a401", "updatesEnabled": false, "openDevTools": false, "buildExpiration": 0, diff --git a/config/production.json b/config/production.json index 0967ef424..5f2141001 100644 --- a/config/production.json +++ b/config/production.json @@ -1 +1,3 @@ -{} +{ + "updatesEnabled": true +} diff --git a/dev-app-update.yml.sample b/dev-app-update.yml.sample index 04af153ba..3b1ea6914 100644 --- a/dev-app-update.yml.sample +++ b/dev-app-update.yml.sample @@ -1,5 +1,3 @@ -provider: s3 -region: us-east-1 -bucket: your-test-bucket.signal.org -path: desktop -acl: public-read +owner: +repo: +provider: github diff --git a/main.js b/main.js index b739026cf..48ca42d37 100644 --- a/main.js +++ b/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) { diff --git a/package.json b/package.json index fc6ffb706..2cbbd2f3a 100644 --- a/package.json +++ b/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" ] } } diff --git a/ts/test/updater/common_test.ts b/ts/test/updater/common_test.ts deleted file mode 100644 index 349c14fc8..000000000 --- a/ts/test/updater/common_test.ts +++ /dev/null @@ -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' - ); - }); - }); -}); diff --git a/ts/test/updater/signature_test.ts b/ts/test/updater/signature_test.ts deleted file mode 100644 index 07aff79f6..000000000 --- a/ts/test/updater/signature_test.ts +++ /dev/null @@ -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); - } - } - }); -}); diff --git a/ts/updater/common.ts b/ts/updater/common.ts index f6d98d661..1bb73decc 100644 --- a/ts/updater/common.ts +++ b/ts/updater/common.ts @@ -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 { - 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 { - 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 { - 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(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; -} diff --git a/ts/updater/generateKeyPair.ts b/ts/updater/generateKeyPair.ts deleted file mode 100644 index 8a8511e1e..000000000 --- a/ts/updater/generateKeyPair.ts +++ /dev/null @@ -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(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), - ]); -} diff --git a/ts/updater/generateSignature.ts b/ts/updater/generateSignature.ts deleted file mode 100644 index 68447f32f..000000000 --- a/ts/updater/generateSignature.ts +++ /dev/null @@ -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(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 { - const releaseDir = resolve('release'); - const files: Array = 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!"); -} diff --git a/ts/updater/index.ts b/ts/updater/index.ts index 04b6bb52c..dc1ec1310 100644 --- a/ts/updater/index.ts +++ b/ts/updater/index.ts @@ -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 ); } diff --git a/ts/updater/macos.ts b/ts/updater/macos.ts deleted file mode 100644 index 0ede2fe63..000000000 --- a/ts/updater/macos.ts +++ /dev/null @@ -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 { - 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 { - 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(); - }); - }); -} diff --git a/ts/updater/signature.ts b/ts/updater/signature.ts deleted file mode 100644 index 9bac75673..000000000 --- a/ts/updater/signature.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - const hexString = await readFile(target, 'utf8'); - - return hexToBinary(hexString); -} - -export async function writeHexToPath(target: string, data: BinaryType) { - await writeFile(target, binaryToHex(data)); -} diff --git a/ts/updater/updater.ts b/ts/updater/updater.ts new file mode 100644 index 000000000..408ce2f1a --- /dev/null +++ b/ts/updater/updater.ts @@ -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; + } +} diff --git a/ts/updater/windows.ts b/ts/updater/windows.ts deleted file mode 100644 index 1035cf040..000000000 --- a/ts/updater/windows.ts +++ /dev/null @@ -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 = 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 { - 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, - options: SpawnOptions -): Promise { - 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); - }); -} diff --git a/yarn.lock b/yarn.lock index b7a8a3a0e..e1c5b8691 100644 --- a/yarn.lock +++ b/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"