From 71b841c6f21582648bcc6af47120fa180d12ce35 Mon Sep 17 00:00:00 2001 From: Sascha Mann Date: Sun, 8 Nov 2020 17:15:46 +0100 Subject: [PATCH] Verify checksums (#49) Co-authored-by: Derk-Jan Karrenbeld --- __tests__/installer.test.ts | 46 +++++++++++++++---------------- __tests__/main.test.ts | 50 --------------------------------- lib/installer.js | 55 +++++++++++++++++++++++++++++++------ src/installer.ts | 53 ++++++++++++++++++++++++++++++----- 4 files changed, 115 insertions(+), 89 deletions(-) delete mode 100644 __tests__/main.test.ts diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index 9790735..3a23c92 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -34,35 +34,35 @@ const toolDir = path.join(__dirname, 'runner', 'tools') const tempDir = path.join(__dirname, 'runner', 'temp') const fixtureDir = path.join(__dirname, 'fixtures') -process.env['RUNNER_TOOL_CACHE'] = toolDir; -process.env['RUNNER_TEMP'] = tempDir; +process.env['RUNNER_TOOL_CACHE'] = toolDir +process.env['RUNNER_TEMP'] = tempDir import * as installer from '../src/installer' describe('version matching tests', () => { describe('specific versions', () => { - it('Doesn\'t change the version when given a valid semver version', async () => { - expect(await installer.getJuliaVersion([], '1.0.5')).toEqual('1.0.5') - expect(await installer.getJuliaVersion(['v1.0.5', 'v1.0.6'], '1.0.5')).toEqual('1.0.5') - expect(await installer.getJuliaVersion(['v1.0.4', 'v1.0.5'], '1.0.5')).toEqual('1.0.5') - expect(await installer.getJuliaVersion(['v1.0.4'], '1.0.5')).toEqual('1.0.5') - expect(await installer.getJuliaVersion([], '1.3.0-alpha')).toEqual('1.3.0-alpha') - expect(await installer.getJuliaVersion(['v1.2.0', 'v1.3.0-alpha', 'v1.3.0-rc1', 'v1.3.0'], '1.3.0-alpha')).toEqual('1.3.0-alpha') - expect(await installer.getJuliaVersion([], '1.3.0-rc2')).toEqual('1.3.0-rc2') + it('Doesn\'t change the version when given a valid semver version', () => { + expect(installer.getJuliaVersion([], '1.0.5')).toEqual('1.0.5') + expect(installer.getJuliaVersion(['v1.0.5', 'v1.0.6'], '1.0.5')).toEqual('1.0.5') + expect(installer.getJuliaVersion(['v1.0.4', 'v1.0.5'], '1.0.5')).toEqual('1.0.5') + expect(installer.getJuliaVersion(['v1.0.4'], '1.0.5')).toEqual('1.0.5') + expect(installer.getJuliaVersion([], '1.3.0-alpha')).toEqual('1.3.0-alpha') + expect(installer.getJuliaVersion(['v1.2.0', 'v1.3.0-alpha', 'v1.3.0-rc1', 'v1.3.0'], '1.3.0-alpha')).toEqual('1.3.0-alpha') + expect(installer.getJuliaVersion([], '1.3.0-rc2')).toEqual('1.3.0-rc2') }) - it('Doesn\'t change the version when given `nightly`', async () => { - expect(await installer.getJuliaVersion([], 'nightly')).toEqual('nightly') - expect(await installer.getJuliaVersion(testVersions, 'nightly')).toEqual('nightly') + it('Doesn\'t change the version when given `nightly`', () => { + expect(installer.getJuliaVersion([], 'nightly')).toEqual('nightly') + expect(installer.getJuliaVersion(testVersions, 'nightly')).toEqual('nightly') }) }) describe('version ranges', () => { - it('Chooses the highest available version that matches the input', async () => { - expect(await installer.getJuliaVersion(testVersions, '1')).toEqual('1.2.0') - expect(await installer.getJuliaVersion(testVersions, '1.0')).toEqual('1.0.5') - expect(await installer.getJuliaVersion(testVersions, '^1.3.0-rc1')).toEqual('1.3.0-rc4') - expect(await installer.getJuliaVersion(testVersions, '^1.2.0-rc1')).toEqual('1.2.0') + it('Chooses the highest available version that matches the input', () => { + expect(installer.getJuliaVersion(testVersions, '1')).toEqual('1.2.0') + expect(installer.getJuliaVersion(testVersions, '1.0')).toEqual('1.0.5') + expect(installer.getJuliaVersion(testVersions, '^1.3.0-rc1')).toEqual('1.3.0-rc4') + expect(installer.getJuliaVersion(testVersions, '^1.2.0-rc1')).toEqual('1.2.0') }) }) @@ -96,16 +96,16 @@ describe('installer tests', () => { describe('versions.json parsing', () => { // Instead of downloading versions.json, use fixtures/versions.json beforeEach(() => { - nock('https://julialang-s3.julialang.org') - .get('/bin/versions.json') - .replyWithFile(200, path.join(fixtureDir, 'versions.json')) + nock('https://julialang-s3.julialang.org').persist() + .get('/bin/versions.json') + .replyWithFile(200, path.join(fixtureDir, 'versions.json')) }) - + afterEach(() => { nock.cleanAll() nock.enableNetConnect() }) - + it('Extracts the list of available versions', async () => { expect(await (await installer.getJuliaVersions(await installer.getJuliaVersionInfo())).sort()).toEqual(testVersions.sort()) }) diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts deleted file mode 100644 index 5db125b..0000000 --- a/__tests__/main.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as installer from '../src/installer' - -import * as semver from 'semver' - -const testVersions = ['v1.3.0-rc4', 'v1.3.0-rc3', 'v1.3.0-rc2', 'v1.0.5', 'v1.2.0', 'v1.3.0-rc1', 'v1.2.0-rc3', 'v1.3.0-alpha', 'v1.2.0-rc2', 'v1.2.0-rc1', 'v1.1.1', 'v1.0.4', 'v1.1.0', 'v1.1.0-rc2', 'v1.1.0-rc1', 'v1.0.3', 'v1.0.2', 'v1.0.1', 'v1.0.0'] - -describe('installer tests', () => { - describe('version matching', () => { - describe('specific versions', () => { - it('Doesn\'t change the version when given a valid semver version', async () => { - expect(await installer.getJuliaVersion([], '1.0.5')).toEqual('1.0.5') - expect(await installer.getJuliaVersion(['v1.0.5', 'v1.0.6'], '1.0.5')).toEqual('1.0.5') - expect(await installer.getJuliaVersion(['v1.0.4', 'v1.0.5'], '1.0.5')).toEqual('1.0.5') - expect(await installer.getJuliaVersion(['v1.0.4'], '1.0.5')).toEqual('1.0.5') - expect(await installer.getJuliaVersion([], '1.3.0-alpha')).toEqual('1.3.0-alpha') - expect(await installer.getJuliaVersion(['v1.2.0', 'v1.3.0-alpha', 'v1.3.0-rc1', 'v1.3.0'], '1.3.0-alpha')).toEqual('1.3.0-alpha') - expect(await installer.getJuliaVersion([], '1.3.0-rc2')).toEqual('1.3.0-rc2') - }) - it('Doesn\'t change the version when given `nightly`', async () => { - expect(await installer.getJuliaVersion([], 'nightly')).toEqual('nightly') - expect(await installer.getJuliaVersion(testVersions, 'nightly')).toEqual('nightly') - }) - }) - describe('version ranges', () => { - it('Chooses the highest available version that matches the input', async () => { - expect(await installer.getJuliaVersion(testVersions, '1')).toEqual('1.2.0') - expect(await installer.getJuliaVersion(testVersions, '1.0')).toEqual('1.0.5') - expect(await installer.getJuliaVersion(testVersions, '^1.3.0-rc1')).toEqual('1.3.0-rc4') - expect(await installer.getJuliaVersion(testVersions, '^1.2.0-rc1')).toEqual('1.2.0') - }) - }) - describe('invalid version range (#38)', () => { - it('Throws an error if a version range does not match any available version', () => { - expect(() => { - installer.getJuliaVersion(['v1.5.0-rc1', 'v1.5.0-beta1', 'v1.4.2', 'v1.4.1', 'v1.4.0', 'v1.4.0-rc2', 'v1.4.0-rc1'], '1.6') - }).toThrowError() - }) - }) - }) - describe('node-semver behaviour', () => { - describe('Windows installer change', () => { - it('Correctly understands >1.4.0', () => { - expect(semver.gtr('1.4.0-rc1', '1.3', {includePrerelease: true})).toBeTruthy() - expect(semver.gtr('1.4.0-DEV', '1.3', {includePrerelease: true})).toBeTruthy() - expect(semver.gtr('1.3.1', '1.3', {includePrerelease: true})).toBeFalsy() - expect(semver.gtr('1.3.2-rc1', '1.3', {includePrerelease: true})).toBeFalsy() - }) - }) - }) -}) diff --git a/lib/installer.js b/lib/installer.js index 7989fbf..371360f 100644 --- a/lib/installer.js +++ b/lib/installer.js @@ -18,6 +18,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(require("@actions/core")); const exec = __importStar(require("@actions/exec")); const tc = __importStar(require("@actions/tool-cache")); +const crypto = __importStar(require("crypto")); const fs = __importStar(require("fs")); const os = __importStar(require("os")); const path = __importStar(require("path")); @@ -35,6 +36,24 @@ const archMap = { // Store information about the environment const osPlat = os.platform(); // possible values: win32 (Windows), linux (Linux), darwin (macOS) core.debug(`platform: ${osPlat}`); +/** + * @returns The SHA256 checksum of a given file. + */ +function calculateChecksum(file) { + return __awaiter(this, void 0, void 0, function* () { + const hash = crypto.createHash('sha256'); + const input = fs.createReadStream(file); + return new Promise((resolve, reject) => { + input.on('data', (chunk) => { + hash.update(chunk); + }); + input.on('end', () => { + const digest = hash.digest('hex'); + digest ? resolve(digest) : reject(new Error(`Could not calculate checksum of file ${file}: digest was empty.`)); + }); + }); + }); +} /** * @returns The content of the downloaded versions.json file as object. */ @@ -99,27 +118,45 @@ function getNightlyFileName(arch) { } return `julia-latest${versionExt}.${ext}`; } -function getDownloadURL(versionInfo, version, arch) { +function getFileInfo(versionInfo, version, arch) { + if (version == 'nightly') { + return null; + } + for (let file of versionInfo[version].files) { + if (file.os == osMap[osPlat] && file.arch == archMap[arch]) { + return file; + } + } + throw `Could not find ${archMap[arch]}/${version} binaries`; +} +exports.getFileInfo = getFileInfo; +function getDownloadURL(fileInfo, version, arch) { // nightlies if (version == 'nightly') { const baseURL = 'https://julialangnightlies-s3.julialang.org/bin'; return `${baseURL}/${osMap[osPlat]}/${arch}/${getNightlyFileName(arch)}`; } - for (let file of versionInfo[version].files) { - if (file.os == osMap[osPlat] && file.arch == archMap[arch]) { - core.debug(file); - return file.url; - } - } - throw `Could not find ${archMap[arch]}/${version} binaries`; + return fileInfo.url; } exports.getDownloadURL = getDownloadURL; function installJulia(versionInfo, version, arch) { return __awaiter(this, void 0, void 0, function* () { // Download Julia - const downloadURL = getDownloadURL(versionInfo, version, arch); + const fileInfo = getFileInfo(versionInfo, version, arch); + const downloadURL = getDownloadURL(fileInfo, version, arch); core.debug(`downloading Julia from ${downloadURL}`); const juliaDownloadPath = yield tc.downloadTool(downloadURL); + // Verify checksum + if (version != 'nightly') { + const checkSum = yield calculateChecksum(juliaDownloadPath); + if (fileInfo.sha256 != checkSum) { + throw new Error(`Checksum of downloaded file does not match the expected checksum from versions.json.\nExpected: ${fileInfo.sha256}\nGot: ${checkSum}`); + } + core.debug(`Checksum of downloaded file matches expected checksum: ${checkSum}`); + } + else { + core.debug('Skipping checksum check for nightly binaries.'); + } const tempInstallDir = fs.mkdtempSync(`julia-${arch}-${version}-`); // Install it switch (osPlat) { diff --git a/src/installer.ts b/src/installer.ts index 43b57bb..ad2cdc3 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -2,6 +2,7 @@ import * as core from '@actions/core' import * as exec from '@actions/exec' import * as tc from '@actions/tool-cache' +import * as crypto from 'crypto' import * as fs from 'fs' import * as os from 'os' import * as path from 'path' @@ -23,6 +24,25 @@ const archMap = { const osPlat = os.platform() // possible values: win32 (Windows), linux (Linux), darwin (macOS) core.debug(`platform: ${osPlat}`) +/** + * @returns The SHA256 checksum of a given file. + */ +async function calculateChecksum(file: string): Promise { + const hash = crypto.createHash('sha256') + const input = fs.createReadStream(file) + + return new Promise((resolve, reject) => { + input.on('data', (chunk) => { + hash.update(chunk) + }) + + input.on('end', () => { + const digest = hash.digest('hex') + digest ? resolve(digest) : reject(new Error(`Could not calculate checksum of file ${file}: digest was empty.`)) + }) + }) +} + /** * @returns The content of the downloaded versions.json file as object. */ @@ -90,29 +110,48 @@ function getNightlyFileName(arch: string): string { return `julia-latest${versionExt}.${ext}` } -export function getDownloadURL(versionInfo, version: string, arch: string): string { - // nightlies +export function getFileInfo(versionInfo, version: string, arch: string) { if (version == 'nightly') { - const baseURL = 'https://julialangnightlies-s3.julialang.org/bin' - return `${baseURL}/${osMap[osPlat]}/${arch}/${getNightlyFileName(arch)}` + return null } for (let file of versionInfo[version].files) { if (file.os == osMap[osPlat] && file.arch == archMap[arch]) { - core.debug(file) - return file.url + return file } } throw `Could not find ${archMap[arch]}/${version} binaries` } +export function getDownloadURL(fileInfo, version: string, arch: string): string { + // nightlies + if (version == 'nightly') { + const baseURL = 'https://julialangnightlies-s3.julialang.org/bin' + return `${baseURL}/${osMap[osPlat]}/${arch}/${getNightlyFileName(arch)}` + } + + return fileInfo.url +} + export async function installJulia(versionInfo, version: string, arch: string): Promise { // Download Julia - const downloadURL = getDownloadURL(versionInfo, version, arch) + const fileInfo = getFileInfo(versionInfo, version, arch) + const downloadURL = getDownloadURL(fileInfo, version, arch) core.debug(`downloading Julia from ${downloadURL}`) const juliaDownloadPath = await tc.downloadTool(downloadURL) + // Verify checksum + if (version != 'nightly') { + const checkSum = await calculateChecksum(juliaDownloadPath) + if (fileInfo.sha256 != checkSum) { + throw new Error(`Checksum of downloaded file does not match the expected checksum from versions.json.\nExpected: ${fileInfo.sha256}\nGot: ${checkSum}`) + } + core.debug(`Checksum of downloaded file matches expected checksum: ${checkSum}`) + } else { + core.debug('Skipping checksum check for nightly binaries.') + } + const tempInstallDir = fs.mkdtempSync(`julia-${arch}-${version}-`) // Install it