diff --git a/.mocharc.json b/.mocharc.json index efdee32..b2ce982 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -1,5 +1,5 @@ { - "extension": ["ts"], - "spec": "src/**/*.test.ts", - "require": "ts-node/register" - } + "extension": ["ts"], + "spec": "src/**/*.test.ts", + "require": "ts-node/register" +} diff --git a/src/index.test.ts b/src/index.test.ts index 7750cde..bc05feb 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -3,62 +3,56 @@ import * as path from 'path'; import * as fs from 'fs'; import * as tempy from 'tempy'; - import { Release } from './index'; describe('LS installer', () => { - let release: Release; - - before(() => { - release = new Release({ - name: 'terraform-ls', - version: '0.25.2', - shasums: 'terraform-ls_0.25.2_SHA256SUMS', - shasums_signature: 'terraform-ls_0.25.2_SHA256SUMS.sig', - shasums_signatures: [ - 'terraform-ls_0.25.2_SHA256SUMS.72D7468F.sig', - 'terraform-ls_0.25.2_SHA256SUMS.sig', - ], - builds: [ - { - name: 'terraform-ls', - version: '0.25.2', - os: 'darwin', - arch: 'amd64', - filename: 'terraform-ls_0.25.2_darwin_amd64.zip', - url: 'https://releases.hashicorp.com/terraform-ls/0.25.2/terraform-ls_0.25.2_darwin_amd64.zip', - }, - ], - }); - }); - - it('should calculate correct file sha256 sum', async () => { - const expectedSum = "0314c6a66b059bde92c5ed0f11601c144cbd916eff6d1241b5b44e076e5888dc"; - const testPath = path.resolve(__dirname, "..", "testFixture", "shasumtest.txt"); - - const sum = await release.calculateFileSha256Sum(testPath); - assert.strictEqual(sum, expectedSum); - }); - - it('should download the correct sha256 sum', async () => { - const expectedSum = '8629ccc47ee8d4dfe6d23efb93b293948a088a936180d07d3f2ed118f6dd64a5'; - - const remoteSum = await release.downloadSha256Sum( - release.builds[0].filename - ); - assert.strictEqual(remoteSum, expectedSum); - }); - - it('should download the release', async () => { - const build = release.getBuild('darwin', 'amd64'); - const tmpDir = tempy.directory(); - const zipFile = path.resolve(tmpDir, `terraform-ls_v${release.version}.zip`); - - await release.download(build.url, zipFile, 'js-releases/mocha-test'); - await release.verify(zipFile, build.filename); - - fs.rmSync(tmpDir, { - recursive: true - }); - }).timeout(20 * 1000) // increase timeout for file download + let release: Release; + + before(() => { + release = new Release({ + name: 'terraform-ls', + version: '0.25.2', + shasums: 'terraform-ls_0.25.2_SHA256SUMS', + shasums_signature: 'terraform-ls_0.25.2_SHA256SUMS.sig', + shasums_signatures: ['terraform-ls_0.25.2_SHA256SUMS.72D7468F.sig', 'terraform-ls_0.25.2_SHA256SUMS.sig'], + builds: [ + { + name: 'terraform-ls', + version: '0.25.2', + os: 'darwin', + arch: 'amd64', + filename: 'terraform-ls_0.25.2_darwin_amd64.zip', + url: 'https://releases.hashicorp.com/terraform-ls/0.25.2/terraform-ls_0.25.2_darwin_amd64.zip', + }, + ], + }); + }); + + it('should calculate correct file sha256 sum', async () => { + const expectedSum = '0314c6a66b059bde92c5ed0f11601c144cbd916eff6d1241b5b44e076e5888dc'; + const testPath = path.resolve(__dirname, '..', 'testFixture', 'shasumtest.txt'); + + const sum = await release.calculateFileSha256Sum(testPath); + assert.strictEqual(sum, expectedSum); + }); + + it('should download the correct sha256 sum', async () => { + const expectedSum = '8629ccc47ee8d4dfe6d23efb93b293948a088a936180d07d3f2ed118f6dd64a5'; + + const remoteSum = await release.downloadSha256Sum(release.builds[0].filename); + assert.strictEqual(remoteSum, expectedSum); + }); + + it('should download the release', async () => { + const build = release.getBuild('darwin', 'amd64'); + const tmpDir = tempy.directory(); + const zipFile = path.resolve(tmpDir, `terraform-ls_v${release.version}.zip`); + + await release.download(build.url, zipFile, 'js-releases/mocha-test'); + await release.verify(zipFile, build.filename); + + fs.rmSync(tmpDir, { + recursive: true, + }); + }).timeout(20 * 1000); // increase timeout for file download }); diff --git a/src/index.ts b/src/index.ts index 8b1cfdd..32d8b89 100644 --- a/src/index.ts +++ b/src/index.ts @@ -133,60 +133,62 @@ ZF5q4h4I33PSGDdSvGXn9UMY5Isjpg== -----END PGP PUBLIC KEY BLOCK-----`; -const releasesUrl = "https://releases.hashicorp.com"; +const releasesUrl = 'https://releases.hashicorp.com'; interface Build { - url: string, - filename: string + url: string; + filename: string; } export class Release { - public name: string; - public version: string; - public builds?: any[]; - public shasums?: string; - public shasums_signature?: string; - public shasums_signatures?: any[]; + public name: string; + public version: string; + public builds?: any[]; + public shasums?: string; + public shasums_signature?: string; + public shasums_signatures?: any[]; - constructor( - release: any - ) { - this.name = release.name; - this.version = release.version; - this.builds = release.builds; - this.shasums = release.shasums; - if (release.shasums_signatures) { - this.shasums_signature = release.shasums_signatures.find(sig => sig.endsWith(`_SHA256SUMS.${hashiPublicKeyId}.sig`)); - } else { - this.shasums_signature = release.shasums_signature; - } - } + constructor(release: any) { + this.name = release.name; + this.version = release.version; + this.builds = release.builds; + this.shasums = release.shasums; + if (release.shasums_signatures) { + this.shasums_signature = release.shasums_signatures.find((sig) => + sig.endsWith(`_SHA256SUMS.${hashiPublicKeyId}.sig`), + ); + } else { + this.shasums_signature = release.shasums_signature; + } + } - public getBuild(platform: string, arch: string): Build { - return this.builds.find(b => b.os === platform && b.arch === arch); - } + public getBuild(platform: string, arch: string): Build { + return this.builds.find((b) => b.os === platform && b.arch === arch); + } - public async download(downloadUrl: string, installPath: string, identifier: string): Promise { - const headers = { 'User-Agent': identifier }; - const writer = fs.createWriteStream(installPath); + public async download(downloadUrl: string, installPath: string, identifier: string): Promise { + const headers = { 'User-Agent': identifier }; + const writer = fs.createWriteStream(installPath); - const result = await request(downloadUrl, { headers: { ...headers }, responseType: 'stream' }); - result.pipe(writer); - await finished(writer); - } + const result = await request(downloadUrl, { headers: { ...headers }, responseType: 'stream' }); + result.pipe(writer); + await finished(writer); + } - public async verify(pkg: string, buildName: string): Promise { - const [localSum, remoteSum] = await Promise.all([ - this.calculateFileSha256Sum(pkg), - this.downloadSha256Sum(buildName) - ]); - if (remoteSum !== localSum) { - throw new Error(`Install error: SHA sum for ${buildName} does not match.\n` + - `(expected: ${remoteSum} calculated: ${localSum})`); - } - } + public async verify(pkg: string, buildName: string): Promise { + const [localSum, remoteSum] = await Promise.all([ + this.calculateFileSha256Sum(pkg), + this.downloadSha256Sum(buildName), + ]); + if (remoteSum !== localSum) { + throw new Error( + `Install error: SHA sum for ${buildName} does not match.\n` + + `(expected: ${remoteSum} calculated: ${localSum})`, + ); + } + } - calculateFileSha256Sum(path: string): Promise { + calculateFileSha256Sum(path: string): Promise { return new Promise((resolve, reject) => { const hash = crypto.createHash('sha256'); fs.createReadStream(path) @@ -196,91 +198,97 @@ export class Release { }); } - async downloadSha256Sum(buildName: string): Promise { - const [shasumsResponse, shasumsSignature] = await Promise.all([ - request(`${releasesUrl}/${this.name}/${this.version}/${this.shasums}`, { - responseType: 'text' - }), - request(`${releasesUrl}/${this.name}/${this.version}/${this.shasums_signature}`, { - responseType: 'arraybuffer' - }), - ]); - const publicKey = await openpgp.readKey({ armoredKey: hashiPublicKey }); - const signature = await openpgp.readSignature({ binarySignature: Buffer.from(shasumsSignature, 'hex') }); - const message = await openpgp.createMessage({ text: shasumsResponse }); - const verified = await openpgp.verify({ - message: message, - verificationKeys: publicKey, - signature: signature - }); - if (!verified) { - throw new Error('signature could not be verified'); - } - const shasumLine = shasumsResponse.split(`\n`).find(line => line.includes(buildName)); - if (!shasumLine) { - throw new Error(`Install error: no matching SHA sum for ${buildName}`); - } + async downloadSha256Sum(buildName: string): Promise { + const [shasumsResponse, shasumsSignature] = await Promise.all([ + request(`${releasesUrl}/${this.name}/${this.version}/${this.shasums}`, { + responseType: 'text', + }), + request(`${releasesUrl}/${this.name}/${this.version}/${this.shasums_signature}`, { + responseType: 'arraybuffer', + }), + ]); + const publicKey = await openpgp.readKey({ armoredKey: hashiPublicKey }); + const signature = await openpgp.readSignature({ binarySignature: Buffer.from(shasumsSignature, 'hex') }); + const message = await openpgp.createMessage({ text: shasumsResponse }); + const verified = await openpgp.verify({ + message: message, + verificationKeys: publicKey, + signature: signature, + }); + if (!verified) { + throw new Error('signature could not be verified'); + } + const shasumLine = shasumsResponse.split(`\n`).find((line) => line.includes(buildName)); + if (!shasumLine) { + throw new Error(`Install error: no matching SHA sum for ${buildName}`); + } - return shasumLine.split(" ")[0]; - } + return shasumLine.split(' ')[0]; + } - public unpack(directory: string, pkgName: string): Promise { - return new Promise((resolve, reject) => { - let executable: string; - yauzl.open(pkgName, { lazyEntries: true }, (err, zipfile) => { - if (err) { - return reject(err); - } - zipfile.readEntry(); - zipfile.on('entry', (entry) => { - zipfile.openReadStream(entry, (err, readStream) => { - if (err) { - return reject(err); - } - readStream.on('end', () => { - zipfile.readEntry(); // Close it - }); + public unpack(directory: string, pkgName: string): Promise { + return new Promise((resolve, reject) => { + let executable: string; + yauzl.open(pkgName, { lazyEntries: true }, (err, zipfile) => { + if (err) { + return reject(err); + } + zipfile.readEntry(); + zipfile.on('entry', (entry) => { + zipfile.openReadStream(entry, (err, readStream) => { + if (err) { + return reject(err); + } + readStream.on('end', () => { + zipfile.readEntry(); // Close it + }); - executable = `${directory}/${entry.fileName}`; - const destination = fs.createWriteStream(executable); - readStream.pipe(destination); - }); - }); - zipfile.on('close', () => { - fs.chmodSync(executable, '755'); - return resolve(); - }); - }); - }); - } + executable = `${directory}/${entry.fileName}`; + const destination = fs.createWriteStream(executable); + readStream.pipe(destination); + }); + }); + zipfile.on('close', () => { + fs.chmodSync(executable, '755'); + return resolve(); + }); + }); + }); + } } // includePrerelease: Set to suppress the default behavior of excluding prerelease tagged versions // from ranges unless they are explicitly opted into. -export async function getRelease(product: string, version?: string, userAgent?: string, includePrerelease?: boolean): Promise { - const validVersion = semver.validRange(version, { includePrerelease, loose: true }); // "latest" will return invalid but that's ok because we'll select it by default - const indexUrl = `${releasesUrl}/${product}/index.json`; - const headers = userAgent ? { 'User-Agent': userAgent } : null; - const response = await request(indexUrl, { headers }); - let release: Release; - if (!validVersion) { // pick the latest release (prereleases will be skipped for safety, set an explicit version instead) - const releaseVersions = Object.keys(response.versions).filter(v => !semver.prerelease(v)); - version = releaseVersions.sort((a, b) => semver.rcompare(a, b))[0]; - release = new Release(response.versions[version]); - } else { - release = matchVersion(response.versions, validVersion, includePrerelease); - } - return release; +export async function getRelease( + product: string, + version?: string, + userAgent?: string, + includePrerelease?: boolean, +): Promise { + const validVersion = semver.validRange(version, { includePrerelease, loose: true }); // "latest" will return invalid but that's ok because we'll select it by default + const indexUrl = `${releasesUrl}/${product}/index.json`; + const headers = userAgent ? { 'User-Agent': userAgent } : null; + const response = await request(indexUrl, { headers }); + let release: Release; + if (!validVersion) { + // pick the latest release (prereleases will be skipped for safety, set an explicit version instead) + const releaseVersions = Object.keys(response.versions).filter((v) => !semver.prerelease(v)); + version = releaseVersions.sort((a, b) => semver.rcompare(a, b))[0]; + release = new Release(response.versions[version]); + } else { + release = matchVersion(response.versions, validVersion, includePrerelease); + } + return release; } function matchVersion(versions: Release[], range: string, includePrerelease?: boolean): Release { - // If a prerelease version range is given, it will only match in that series (0.14-rc0, 0.14-rc1) - // unless includePrerelease is set to true - // https://www.npmjs.com/package/semver#prerelease-tags - const version = semver.maxSatisfying(Object.keys(versions), range, { includePrerelease }); - if (version) { - return new Release(versions[version]); - } else { - throw new Error("No matching version found"); - } + // If a prerelease version range is given, it will only match in that series (0.14-rc0, 0.14-rc1) + // unless includePrerelease is set to true + // https://www.npmjs.com/package/semver#prerelease-tags + const version = semver.maxSatisfying(Object.keys(versions), range, { includePrerelease }); + if (version) { + return new Release(versions[version]); + } else { + throw new Error('No matching version found'); + } } diff --git a/src/utils.ts b/src/utils.ts index 2f96c18..35338fb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,8 @@ -import axiosBase, { AxiosRequestConfig } from "axios"; -const ProxyAgent = require("proxy-agent"); +import axiosBase, { AxiosRequestConfig } from 'axios'; +const ProxyAgent = require('proxy-agent'); -const httpProxy = process.env["HTTP_PROXY"] || process.env["http_proxy"]; -const httpsProxy = process.env["HTTPS_PROXY"] || process.env["https_proxy"]; +const httpProxy = process.env['HTTP_PROXY'] || process.env['http_proxy']; +const httpsProxy = process.env['HTTPS_PROXY'] || process.env['https_proxy']; let proxyConf = {}; @@ -16,10 +16,7 @@ if (httpProxy || httpsProxy) { const axios = axiosBase.create({ ...proxyConf }); -export async function request( - url: string, - options: AxiosRequestConfig = {} -): Promise { +export async function request(url: string, options: AxiosRequestConfig = {}): Promise { const result = await axios.get(url, { ...options }); return result.data; } diff --git a/tsconfig.json b/tsconfig.json index 5fece29..b7ce31e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,12 @@ { - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "outDir": "out", - "rootDir": "src", - "sourceMap": true, - "declaration": true - }, - "include": [ - "src" - ], - "exclude": [ - "node_modules", - "out" - ] + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "out", + "rootDir": "src", + "sourceMap": true, + "declaration": true + }, + "include": ["src"], + "exclude": ["node_modules", "out"] }