Update improvements
This commit is contained in:
parent
adf21985c1
commit
9d88abdb90
4 changed files with 132 additions and 23 deletions
|
@ -1,6 +1,12 @@
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
|
||||||
import { getUpdateFileName, getVersion } from '../../updater/common';
|
import {
|
||||||
|
createTempDir,
|
||||||
|
getUpdateFileName,
|
||||||
|
getVersion,
|
||||||
|
isUpdateFileNameValid,
|
||||||
|
validatePath,
|
||||||
|
} from '../../updater/common';
|
||||||
|
|
||||||
describe('updater/signatures', () => {
|
describe('updater/signatures', () => {
|
||||||
const windows = `version: 1.23.2
|
const windows = `version: 1.23.2
|
||||||
|
@ -74,4 +80,48 @@ releaseDate: '2019-03-29T01:53:23.881Z'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#isUpdateFileNameValid', () => {
|
||||||
|
it('returns true for normal filenames', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
isUpdateFileNameValid('signal-desktop-win-1.23.2.exe'),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
isUpdateFileNameValid('signal-desktop-mac-1.23.2-beta.1.zip'),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('returns false for problematic names', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
isUpdateFileNameValid('../signal-desktop-win-1.23.2.exe'),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
isUpdateFileNameValid('%signal-desktop-mac-1.23.2-beta.1.zip'),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
isUpdateFileNameValid('@signal-desktop-mac-1.23.2-beta.1.zip'),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#validatePath', () => {
|
||||||
|
it('succeeds for simple children', async () => {
|
||||||
|
const base = await createTempDir();
|
||||||
|
validatePath(base, `${base}/child`);
|
||||||
|
validatePath(base, `${base}/child/grandchild`);
|
||||||
|
});
|
||||||
|
it('returns false for problematic names', async () => {
|
||||||
|
const base = await createTempDir();
|
||||||
|
assert.throws(() => {
|
||||||
|
validatePath(base, `${base}/../child`);
|
||||||
|
});
|
||||||
|
assert.throws(() => {
|
||||||
|
validatePath(base, '/root');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
statSync,
|
statSync,
|
||||||
writeFile as writeFileCallback,
|
writeFile as writeFileCallback,
|
||||||
} from 'fs';
|
} from 'fs';
|
||||||
import { join } from 'path';
|
import { join, normalize } from 'path';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -80,6 +80,16 @@ export async function checkForUpdates(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validatePath(basePath: string, targetPath: string) {
|
||||||
|
const normalized = normalize(targetPath);
|
||||||
|
|
||||||
|
if (!normalized.startsWith(basePath)) {
|
||||||
|
throw new Error(
|
||||||
|
`validatePath: Path ${normalized} is not under base path ${basePath}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function downloadUpdate(
|
export async function downloadUpdate(
|
||||||
fileName: string,
|
fileName: string,
|
||||||
logger: LoggerType
|
logger: LoggerType
|
||||||
|
@ -96,6 +106,9 @@ export async function downloadUpdate(
|
||||||
const targetUpdatePath = join(tempDir, fileName);
|
const targetUpdatePath = join(tempDir, fileName);
|
||||||
const targetSignaturePath = join(tempDir, getSignatureFileName(fileName));
|
const targetSignaturePath = join(tempDir, getSignatureFileName(fileName));
|
||||||
|
|
||||||
|
validatePath(tempDir, targetUpdatePath);
|
||||||
|
validatePath(tempDir, targetSignaturePath);
|
||||||
|
|
||||||
logger.info(`downloadUpdate: Downloading ${signatureUrl}`);
|
logger.info(`downloadUpdate: Downloading ${signatureUrl}`);
|
||||||
const { body } = await get(signatureUrl, getGotOptions());
|
const { body } = await get(signatureUrl, getGotOptions());
|
||||||
await writeFile(targetSignaturePath, body);
|
await writeFile(targetSignaturePath, body);
|
||||||
|
@ -228,14 +241,26 @@ export function getVersion(yaml: string): string | undefined {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validFile = /^[A-Za-z0-9\.\-]+$/;
|
||||||
|
export function isUpdateFileNameValid(name: string) {
|
||||||
|
return validFile.test(name);
|
||||||
|
}
|
||||||
|
|
||||||
export function getUpdateFileName(yaml: string) {
|
export function getUpdateFileName(yaml: string) {
|
||||||
const info = parseYaml(yaml);
|
const info = parseYaml(yaml);
|
||||||
|
|
||||||
if (info && info.path) {
|
if (!info || !info.path) {
|
||||||
return info.path;
|
throw new Error('getUpdateFileName: No path present in YAML file');
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
const path = info.path;
|
||||||
|
if (!isUpdateFileNameValid(path)) {
|
||||||
|
throw new Error(
|
||||||
|
`getUpdateFileName: Path '${path}' contains invalid characters`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseYaml(yaml: string): any {
|
function parseYaml(yaml: string): any {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { v4 as getGuid } from 'uuid';
|
||||||
import { app, autoUpdater, BrowserWindow, dialog } from 'electron';
|
import { app, autoUpdater, BrowserWindow, dialog } from 'electron';
|
||||||
import { get as getFromConfig } from 'config';
|
import { get as getFromConfig } from 'config';
|
||||||
import { gt } from 'semver';
|
import { gt } from 'semver';
|
||||||
|
import got from 'got';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
checkForUpdates,
|
checkForUpdates,
|
||||||
|
@ -79,7 +80,7 @@ async function checkDownloadAndInstall(
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
|
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
|
||||||
const verified = verifySignature(updateFilePath, version, publicKey);
|
const verified = await verifySignature(updateFilePath, version, publicKey);
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
// Note: We don't delete the cache here, because we don't want to continually
|
// 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.
|
// re-download the broken release. We will download it only once per launch.
|
||||||
|
@ -148,6 +149,7 @@ async function handToAutoUpdate(
|
||||||
logger: LoggerType
|
logger: LoggerType
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
const token = getGuid();
|
||||||
const updateFileUrl = generateFileUrl();
|
const updateFileUrl = generateFileUrl();
|
||||||
const server = createServer();
|
const server = createServer();
|
||||||
let serverUrl: string;
|
let serverUrl: string;
|
||||||
|
@ -173,6 +175,12 @@ async function handToAutoUpdate(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (url === '/token') {
|
||||||
|
writeTokenResponse(token, response);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!url || !url.startsWith(updateFileUrl)) {
|
if (!url || !url.startsWith(updateFileUrl)) {
|
||||||
write404(url, response, logger);
|
write404(url, response, logger);
|
||||||
|
|
||||||
|
@ -183,7 +191,8 @@ async function handToAutoUpdate(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
server.listen(0, '127.0.0.1', async () => {
|
||||||
|
try {
|
||||||
serverUrl = getServerUrl(server);
|
serverUrl = getServerUrl(server);
|
||||||
|
|
||||||
autoUpdater.on('error', (error: Error) => {
|
autoUpdater.on('error', (error: Error) => {
|
||||||
|
@ -196,11 +205,23 @@ async function handToAutoUpdate(
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const response = await got.get(`${serverUrl}/token`);
|
||||||
|
if (JSON.parse(response.body).token !== token) {
|
||||||
|
throw new Error(
|
||||||
|
'autoUpdater: did not receive token back from updates server'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
autoUpdater.setFeedURL({
|
autoUpdater.setFeedURL({
|
||||||
url: serverUrl,
|
url: serverUrl,
|
||||||
headers: { 'Cache-Control': 'no-cache' },
|
headers: { 'Cache-Control': 'no-cache' },
|
||||||
});
|
});
|
||||||
autoUpdater.checkForUpdates();
|
autoUpdater.checkForUpdates();
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -254,6 +275,19 @@ function writeJSONResponse(url: string, response: ServerResponse) {
|
||||||
response.end(data);
|
response.end(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeTokenResponse(token: string, response: ServerResponse) {
|
||||||
|
const data = Buffer.from(
|
||||||
|
JSON.stringify({
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
response.writeHead(200, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': data.byteLength,
|
||||||
|
});
|
||||||
|
response.end(data);
|
||||||
|
}
|
||||||
|
|
||||||
function write404(
|
function write404(
|
||||||
url: string | undefined,
|
url: string | undefined,
|
||||||
response: ServerResponse,
|
response: ServerResponse,
|
||||||
|
|
|
@ -83,7 +83,7 @@ async function checkDownloadAndInstall(
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
|
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
|
||||||
const verified = verifySignature(updateFilePath, version, publicKey);
|
const verified = await verifySignature(updateFilePath, version, publicKey);
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
// Note: We don't delete the cache here, because we don't want to continually
|
// 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.
|
// re-download the broken release. We will download it only once per launch.
|
||||||
|
@ -164,7 +164,7 @@ async function verifyAndInstall(
|
||||||
logger: LoggerType
|
logger: LoggerType
|
||||||
) {
|
) {
|
||||||
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
|
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
|
||||||
const verified = verifySignature(updateFilePath, newVersion, publicKey);
|
const verified = await verifySignature(updateFilePath, newVersion, publicKey);
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Downloaded update did not pass signature verification (version: '${newVersion}'; fileName: '${fileName}')`
|
`Downloaded update did not pass signature verification (version: '${newVersion}'; fileName: '${fileName}')`
|
||||||
|
|
Loading…
Reference in a new issue