Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify downloaded test runner zip file 812 #4193

Merged
merged 13 commits into from
Jul 9, 2019
121 changes: 120 additions & 1 deletion cli/lib/tasks/download.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const request = require('request')
const Promise = require('bluebird')
const requestProgress = require('request-progress')
const { stripIndent } = require('common-tags')
const hasha = require('hasha')

const { throwFormErrorText, errors } = require('../errors')
const fs = require('../fs')
Expand Down Expand Up @@ -79,6 +80,99 @@ const prettyDownloadErr = (err, version) => {
return throwFormErrorText(errors.failedDownload)(msg)
}

const getFileSize = (filename) => {
return fs.statAsync(filename).get('size')
}

/**
* Checks checksum and file size for the given file. Allows both
* values or just one of them to be checked.
*/
const verifyDownloadedFile = (filename, expectedSize, expectedChecksum) => {
if (expectedSize && expectedChecksum) {
debug('verifying checksum and file size')

return Promise.join(
hasha.fromFile(filename),
getFileSize(filename)
).spread((checksum, filesize) => {
jennifer-shehane marked this conversation as resolved.
Show resolved Hide resolved
if (checksum === expectedChecksum && filesize === expectedSize) {
debug('downloaded file has the expected checksum and size ✅')

return
}

debug('raising error: checksum or file size mismatch')
const text = stripIndent`
Corrupted download

Expected downloaded file to have checksum: ${expectedChecksum}
Computed checksum: ${checksum}

Expected downloaded file to have size: ${expectedSize}
Computed size: ${filesize}
`

debug(text)

throw new Error(text)
})
}

if (expectedChecksum) {
debug('only checking expected file checksum %d', expectedChecksum)

return hasha.fromFile(filename)
.then((checksum) => {
if (checksum === expectedChecksum) {
debug('downloaded file has the expected checksum ✅')

return
}

debug('raising error: file checksum mismatch')
const text = stripIndent`
Corrupted download

Expected downloaded file to have checksum: ${expectedChecksum}
Computed checksum: ${checksum}
`

throw new Error(text)
})
}

if (expectedSize) {
// maybe we don't have a checksum, but at least CDN returns content length
// which we can check against the file size
debug('only checking expected file size %d', expectedSize)

return getFileSize(filename)
.then((filesize) => {
if (filesize === expectedSize) {
debug('downloaded file has the expected size ✅')

return
}

debug('raising error: file size mismatch')
const text = stripIndent`
Corrupted download

Expected downloaded file to have size: ${expectedSize}
Computed size: ${filesize}
`

throw new Error(text)
})
}

debug('downloaded file lacks checksum or size to verify')

return Promise.resolve()

}

// downloads from given url
// return an object with
// {filename: ..., downloaded: true}
Expand Down Expand Up @@ -109,11 +203,27 @@ const downloadFromUrl = ({ url, downloadDestination, progress }) => {

// closure
let started = null
let expectedSize
let expectedChecksum

requestProgress(req, {
throttle: progress.throttle,
})
.on('response', (response) => {
expectedSize = response.headers['x-amz-meta-size'] ||
response.headers['content-length']
expectedChecksum = response.headers['x-amz-meta-checksum']

if (expectedChecksum) {
debug('expected checksum %s', expectedChecksum)
}

if (expectedSize) {
// convert from string (all Amazon custom headers are strings)
expectedSize = Number(expectedSize)
debug('expected file size %d', expectedSize)
}

// start counting now once we've gotten
// response headers
started = new Date()
Expand Down Expand Up @@ -149,11 +259,19 @@ const downloadFromUrl = ({ url, downloadDestination, progress }) => {
.on('finish', () => {
debug('downloading finished')

resolve(redirectVersion)
verifyDownloadedFile(downloadDestination, expectedSize, expectedChecksum)
.then(() => {
return resolve(redirectVersion)
}, reject)
})
})
}

/**
* Download Cypress.zip from external url to local file.
* @param [string] version Could be "3.3.0" or full URL
* @param [string] downloadDestination Local filename to save as
*/
const start = ({ version, downloadDestination, progress }) => {
if (!downloadDestination) {
la(is.unemptyString(downloadDestination), 'missing download dir', arguments)
Expand All @@ -170,6 +288,7 @@ const start = ({ version, downloadDestination, progress }) => {
progress.throttle = 100

debug('needed Cypress version: %s', version)
debug('source url %s', url)
debug(`downloading cypress.zip to "${downloadDestination}"`)

// ensure download dir exists
Expand Down
1 change: 1 addition & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"fs-extra": "4.0.1",
"getos": "3.1.0",
"glob": "7.1.3",
"hasha": "5.0.0",
"is-ci": "1.2.1",
"is-installed-globally": "0.1.0",
"lazy-ass": "1.6.0",
Expand Down
98 changes: 98 additions & 0 deletions cli/test/lib/tasks/download_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const la = require('lazy-ass')
const is = require('check-more-types')
const path = require('path')
const nock = require('nock')
const hasha = require('hasha')
const snapshot = require('../../support/snapshot')

const fs = require(`${lib}/fs`)
Expand Down Expand Up @@ -135,6 +136,103 @@ describe('lib/tasks/download', function () {
})
})

describe('verify downloaded file', function () {
before(function () {
this.expectedChecksum = hasha.fromFileSync('test/fixture/example.zip')
this.expectedFileSize = fs.statSync('test/fixture/example.zip').size
this.onProgress = sinon.stub().returns(undefined)
})

it('throws if file size is different from expected', function () {
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(200, () => {
return fs.createReadStream('test/fixture/example.zip')
}, {
// definitely incorrect file size
'content-length': '10',
})

return expect(download.start({
downloadDestination: this.options.downloadDestination,
version: this.options.version,
progress: { onProgress: this.onProgress },
})).to.be.rejected
})

it('throws if file size is different from expected x-amz-meta-size', function () {
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(200, () => {
return fs.createReadStream('test/fixture/example.zip')
}, {
// definitely incorrect file size
'x-amz-meta-size': '10',
})

return expect(download.start({
downloadDestination: this.options.downloadDestination,
version: this.options.version,
progress: { onProgress: this.onProgress },
})).to.be.rejected
})

it('throws if checksum is different from expected', function () {
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(200, () => {
return fs.createReadStream('test/fixture/example.zip')
}, {
'x-amz-meta-checksum': 'incorrect-checksum',
})

return expect(download.start({
downloadDestination: this.options.downloadDestination,
version: this.options.version,
progress: { onProgress: this.onProgress },
})).to.be.rejected
})

it('throws if checksum and file size are different from expected', function () {
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(200, () => {
return fs.createReadStream('test/fixture/example.zip')
}, {
'x-amz-meta-checksum': 'incorrect-checksum',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

neat, didn't know you could add headers to s3 resources like this

'x-amz-meta-size': '10',
})

return expect(download.start({
downloadDestination: this.options.downloadDestination,
version: this.options.version,
progress: { onProgress: this.onProgress },
})).to.be.rejected
})

it('passes when checksum and file size match', function () {
nock('https://download.cypress.io')
.get('/desktop/1.2.3')
.query(true)
.reply(200, () => {
return fs.createReadStream('test/fixture/example.zip')
}, {
'x-amz-meta-checksum': this.expectedChecksum,
'x-amz-meta-size': String(this.expectedFileSize),
})

return download.start({
downloadDestination: this.options.downloadDestination,
version: this.options.version,
progress: { onProgress: this.onProgress },
})
})
})

it('resolves with response x-version if present', function () {
nock('https://aws.amazon.com')
.get('/some.zip')
Expand Down