From e81602ef55023cc8ffeebc5509a9a8649d60434d Mon Sep 17 00:00:00 2001 From: Matias Lopez Date: Wed, 17 Mar 2021 21:30:04 -0400 Subject: [PATCH] lib: migrate requests to fetch (#2220) PR-URL: https://github.com/nodejs/node-gyp/pull/2220 Reviewed-By: Jiawen Geng --- lib/install.js | 557 ++++++++++++++++++------------------------ lib/proxy.js | 92 ------- package.json | 2 +- test/test-download.js | 317 ++++++++++-------------- test/test-install.js | 54 ++-- 5 files changed, 403 insertions(+), 619 deletions(-) delete mode 100644 lib/proxy.js diff --git a/lib/install.js b/lib/install.js index f9fa2b34bd..99f6d8592a 100644 --- a/lib/install.js +++ b/lib/install.js @@ -4,55 +4,42 @@ const fs = require('graceful-fs') const os = require('os') const tar = require('tar') const path = require('path') +const util = require('util') +const stream = require('stream') const crypto = require('crypto') const log = require('npmlog') const semver = require('semver') -const request = require('request') +const fetch = require('make-fetch-happen') const processRelease = require('./process-release') const win = process.platform === 'win32' -const getProxyFromURI = require('./proxy') +const streamPipeline = util.promisify(stream.pipeline) -function install (fs, gyp, argv, callback) { - var release = processRelease(argv, gyp, process.version, process.release) +/** + * @param {typeof import('graceful-fs')} fs + */ - // ensure no double-callbacks happen - function cb (err) { - if (cb.done) { - return - } - cb.done = true - if (err) { - log.warn('install', 'got an error, rolling back install') - // roll-back the install if anything went wrong - gyp.commands.remove([release.versionDir], function () { - callback(err) - }) - } else { - callback(null, release.version) - } - } +async function install (fs, gyp, argv) { + const release = processRelease(argv, gyp, process.version, process.release) // Determine which node dev files version we are installing log.verbose('install', 'input version string %j', release.version) if (!release.semver) { // could not parse the version string with semver - return callback(new Error('Invalid version number: ' + release.version)) + throw new Error('Invalid version number: ' + release.version) } if (semver.lt(release.version, '0.8.0')) { - return callback(new Error('Minimum target version is `0.8.0` or greater. Got: ' + release.version)) + throw new Error('Minimum target version is `0.8.0` or greater. Got: ' + release.version) } // 0.x.y-pre versions are not published yet and cannot be installed. Bail. if (release.semver.prerelease[0] === 'pre') { log.verbose('detected "pre" node version', release.version) - if (gyp.opts.nodedir) { - log.verbose('--nodedir flag was passed; skipping install', gyp.opts.nodedir) - callback() - } else { - callback(new Error('"pre" versions of node cannot be installed, use the --nodedir flag instead')) + if (!gyp.opts.nodedir) { + throw new Error('"pre" versions of node cannot be installed, use the --nodedir flag instead') } + log.verbose('--nodedir flag was passed; skipping install', gyp.opts.nodedir) return } @@ -60,296 +47,225 @@ function install (fs, gyp, argv, callback) { log.verbose('install', 'installing version: %s', release.versionDir) // the directory where the dev files will be installed - var devDir = path.resolve(gyp.devDir, release.versionDir) + const devDir = path.resolve(gyp.devDir, release.versionDir) // If '--ensure' was passed, then don't *always* install the version; // check if it is already installed, and only install when needed if (gyp.opts.ensure) { log.verbose('install', '--ensure was passed, so won\'t reinstall if already installed') - fs.stat(devDir, function (err) { - if (err) { - if (err.code === 'ENOENT') { - log.verbose('install', 'version not already installed, continuing with install', release.version) - go() - } else if (err.code === 'EACCES') { - eaccesFallback(err) - } else { - cb(err) + try { + await fs.promises.stat(devDir) + } catch (err) { + if (err.code === 'ENOENT') { + log.verbose('install', 'version not already installed, continuing with install', release.version) + try { + return await go() + } catch (err) { + return rollback(err) } - return + } else if (err.code === 'EACCES') { + return eaccesFallback(err) } - log.verbose('install', 'version is already installed, need to check "installVersion"') - var installVersionFile = path.resolve(devDir, 'installVersion') - fs.readFile(installVersionFile, 'ascii', function (err, ver) { - if (err && err.code !== 'ENOENT') { - return cb(err) - } - var installVersion = parseInt(ver, 10) || 0 - log.verbose('got "installVersion"', installVersion) - log.verbose('needs "installVersion"', gyp.package.installVersion) - if (installVersion < gyp.package.installVersion) { - log.verbose('install', 'version is no good; reinstalling') - go() - } else { - log.verbose('install', 'version is good') - cb() - } - }) - }) + throw err + } + log.verbose('install', 'version is already installed, need to check "installVersion"') + const installVersionFile = path.resolve(devDir, 'installVersion') + let installVersion = 0 + try { + const ver = await fs.promises.readFile(installVersionFile, 'ascii') + installVersion = parseInt(ver, 10) || 0 + } catch (err) { + if (err.code !== 'ENOENT') { + throw err + } + } + log.verbose('got "installVersion"', installVersion) + log.verbose('needs "installVersion"', gyp.package.installVersion) + if (installVersion < gyp.package.installVersion) { + log.verbose('install', 'version is no good; reinstalling') + try { + return await go() + } catch (err) { + return rollback(err) + } + } + log.verbose('install', 'version is good') } else { - go() - } - - function getContentSha (res, callback) { - var shasum = crypto.createHash('sha256') - res.on('data', function (chunk) { - shasum.update(chunk) - }).on('end', function () { - callback(null, shasum.digest('hex')) - }) + try { + return await go() + } catch (err) { + return rollback(err) + } } - function go () { + async function go () { log.verbose('ensuring nodedir is created', devDir) // first create the dir for the node dev files - fs.mkdir(devDir, { recursive: true }, function (err, created) { - if (err) { - if (err.code === 'EACCES') { - eaccesFallback(err) - } else { - cb(err) - } - return - } + try { + const created = await fs.promises.mkdir(devDir, { recursive: true }) if (created) { log.verbose('created nodedir', created) } - - // now download the node tarball - var tarPath = gyp.opts.tarball - var badDownload = false - var extractCount = 0 - var contentShasums = {} - var expectShasums = {} - - // checks if a file to be extracted from the tarball is valid. - // only .h header files and the gyp files get extracted - function isValid (path) { - var isValid = valid(path) - if (isValid) { - log.verbose('extracted file from tarball', path) - extractCount++ - } else { - // invalid - log.silly('ignoring from tarball', path) - } - return isValid + } catch (err) { + if (err.code === 'EACCES') { + return eaccesFallback(err) } - // download the tarball and extract! - if (tarPath) { - return tar.extract({ - file: tarPath, - strip: 1, - filter: isValid, - cwd: devDir - }).then(afterTarball, cb) - } + throw err + } - try { - var req = download(gyp, process.env, release.tarballUrl) - } catch (e) { - return cb(e) + // now download the node tarball + const tarPath = gyp.opts.tarball + let extractCount = 0 + const contentShasums = {} + const expectShasums = {} + + // checks if a file to be extracted from the tarball is valid. + // only .h header files and the gyp files get extracted + function isValid (path) { + const isValid = valid(path) + if (isValid) { + log.verbose('extracted file from tarball', path) + extractCount++ + } else { + // invalid + log.silly('ignoring from tarball', path) } + return isValid + } - // something went wrong downloading the tarball? - req.on('error', function (err) { - if (err.code === 'ENOTFOUND') { - return cb(new Error('This is most likely not a problem with node-gyp or the package itself and\n' + - 'is related to network connectivity. In most cases you are behind a proxy or have bad \n' + - 'network settings.')) - } - badDownload = true - cb(err) - }) + // download the tarball and extract! - req.on('close', function () { - if (extractCount === 0) { - cb(new Error('Connection closed while downloading tarball file')) - } + if (tarPath) { + await tar.extract({ + file: tarPath, + strip: 1, + filter: isValid, + cwd: devDir }) + } else { + try { + const res = await download(gyp, release.tarballUrl) - req.on('response', function (res) { - if (res.statusCode !== 200) { - badDownload = true - cb(new Error(res.statusCode + ' response downloading ' + release.tarballUrl)) - return + if (res.status !== 200) { + throw new Error(`${res.status} response downloading ${release.tarballUrl}`) } - // content checksum - getContentSha(res, function (_, checksum) { - var filename = path.basename(release.tarballUrl).trim() - contentShasums[filename] = checksum - log.verbose('content checksum', filename, checksum) - }) - - // start unzipping and untaring - res.pipe(tar.extract({ - strip: 1, - cwd: devDir, - filter: isValid - }).on('close', afterTarball).on('error', cb)) - }) - // invoked after the tarball has finished being extracted - function afterTarball () { - if (badDownload) { - return - } - if (extractCount === 0) { - return cb(new Error('There was a fatal problem while downloading/extracting the tarball')) + await streamPipeline( + res.body, + // content checksum + new ShaSum((_, checksum) => { + const filename = path.basename(release.tarballUrl).trim() + contentShasums[filename] = checksum + log.verbose('content checksum', filename, checksum) + }), + tar.extract({ + strip: 1, + cwd: devDir, + filter: isValid + }) + ) + } catch (err) { + // something went wrong downloading the tarball? + if (err.code === 'ENOTFOUND') { + throw new Error('This is most likely not a problem with node-gyp or the package itself and\n' + + 'is related to network connectivity. In most cases you are behind a proxy or have bad \n' + + 'network settings.') } - log.verbose('tarball', 'done parsing tarball') - var async = 0 + throw err + } + } - if (win) { - // need to download node.lib - async++ - downloadNodeLib(deref) - } + // invoked after the tarball has finished being extracted + if (extractCount === 0) { + throw new Error('There was a fatal problem while downloading/extracting the tarball') + } - // write the "installVersion" file - async++ - var installVersionPath = path.resolve(devDir, 'installVersion') - fs.writeFile(installVersionPath, gyp.package.installVersion + '\n', deref) + log.verbose('tarball', 'done parsing tarball') + + const installVersionPath = path.resolve(devDir, 'installVersion') + await Promise.all([ + // need to download node.lib + ...(win ? downloadNodeLib() : []), + // write the "installVersion" file + fs.promises.writeFile(installVersionPath, gyp.package.installVersion + '\n'), + // Only download SHASUMS.txt if we downloaded something in need of SHA verification + ...(!tarPath || win ? [downloadShasums()] : []) + ]) + + log.verbose('download contents checksum', JSON.stringify(contentShasums)) + // check content shasums + for (const k in contentShasums) { + log.verbose('validating download checksum for ' + k, '(%s == %s)', contentShasums[k], expectShasums[k]) + if (contentShasums[k] !== expectShasums[k]) { + throw new Error(k + ' local checksum ' + contentShasums[k] + ' not match remote ' + expectShasums[k]) + } + } - // Only download SHASUMS.txt if we downloaded something in need of SHA verification - if (!tarPath || win) { - // download SHASUMS.txt - async++ - downloadShasums(deref) - } + async function downloadShasums () { + log.verbose('check download content checksum, need to download `SHASUMS256.txt`...') + log.verbose('checksum url', release.shasumsUrl) - if (async === 0) { - // no async tasks required - cb() - } + const res = await download(gyp, release.shasumsUrl) - function deref (err) { - if (err) { - return cb(err) - } + if (res.status !== 200) { + throw new Error(`${res.status} status code downloading checksum`) + } - async-- - if (!async) { - log.verbose('download contents checksum', JSON.stringify(contentShasums)) - // check content shasums - for (var k in contentShasums) { - log.verbose('validating download checksum for ' + k, '(%s == %s)', contentShasums[k], expectShasums[k]) - if (contentShasums[k] !== expectShasums[k]) { - cb(new Error(k + ' local checksum ' + contentShasums[k] + ' not match remote ' + expectShasums[k])) - return - } - } - cb() - } + for (const line of (await res.text()).trim().split('\n')) { + const items = line.trim().split(/\s+/) + if (items.length !== 2) { + return } + + // 0035d18e2dcf9aad669b1c7c07319e17abfe3762 ./node-v0.11.4.tar.gz + const name = items[1].replace(/^\.\//, '') + expectShasums[name] = items[0] } - function downloadShasums (done) { - log.verbose('check download content checksum, need to download `SHASUMS256.txt`...') - log.verbose('checksum url', release.shasumsUrl) - try { - var req = download(gyp, process.env, release.shasumsUrl) - } catch (e) { - return cb(e) - } + log.verbose('checksum data', JSON.stringify(expectShasums)) + } - req.on('error', done) - req.on('response', function (res) { - if (res.statusCode !== 200) { - done(new Error(res.statusCode + ' status code downloading checksum')) - return + function downloadNodeLib () { + log.verbose('on Windows; need to download `' + release.name + '.lib`...') + const archs = ['ia32', 'x64', 'arm64'] + return archs.map(async (arch) => { + const dir = path.resolve(devDir, arch) + const targetLibPath = path.resolve(dir, release.name + '.lib') + const { libUrl, libPath } = release[arch] + const name = `${arch} ${release.name}.lib` + log.verbose(name, 'dir', dir) + log.verbose(name, 'url', libUrl) + + await fs.promises.mkdir(dir, { recursive: true }) + log.verbose('streaming', name, 'to:', targetLibPath) + + const res = await download(gyp, libUrl) + + if (res.status === 403 || res.status === 404) { + if (arch === 'arm64') { + // Arm64 is a newer platform on Windows and not all node distributions provide it. + log.verbose(`${name} was not found in ${libUrl}`) + } else { + log.warn(`${name} was not found in ${libUrl}`) } + return + } else if (res.status !== 200) { + throw new Error(`${res.status} status code downloading ${name}`) + } - var chunks = [] - res.on('data', function (chunk) { - chunks.push(chunk) - }) - res.on('end', function () { - var lines = Buffer.concat(chunks).toString().trim().split('\n') - lines.forEach(function (line) { - var items = line.trim().split(/\s+/) - if (items.length !== 2) { - return - } - - // 0035d18e2dcf9aad669b1c7c07319e17abfe3762 ./node-v0.11.4.tar.gz - var name = items[1].replace(/^\.\//, '') - expectShasums[name] = items[0] - }) - - log.verbose('checksum data', JSON.stringify(expectShasums)) - done() - }) - }) - } - - function downloadNodeLib (done) { - log.verbose('on Windows; need to download `' + release.name + '.lib`...') - var archs = ['ia32', 'x64', 'arm64'] - var async = archs.length - archs.forEach(function (arch) { - var dir = path.resolve(devDir, arch) - var targetLibPath = path.resolve(dir, release.name + '.lib') - var libUrl = release[arch].libUrl - var libPath = release[arch].libPath - var name = arch + ' ' + release.name + '.lib' - log.verbose(name, 'dir', dir) - log.verbose(name, 'url', libUrl) - - fs.mkdir(dir, { recursive: true }, function (err) { - if (err) { - return done(err) - } - log.verbose('streaming', name, 'to:', targetLibPath) - - try { - var req = download(gyp, process.env, libUrl, cb) - } catch (e) { - return cb(e) - } - - req.on('error', done) - req.on('response', function (res) { - if (res.statusCode === 403 || res.statusCode === 404) { - if (arch === 'arm64') { - // Arm64 is a newer platform on Windows and not all node distributions provide it. - log.verbose(`${name} was not found in ${libUrl}`) - } else { - log.warn(`${name} was not found in ${libUrl}`) - } - return - } else if (res.statusCode !== 200) { - done(new Error(res.statusCode + ' status code downloading ' + name)) - return - } - - getContentSha(res, function (_, checksum) { - contentShasums[libPath] = checksum - log.verbose('content checksum', libPath, checksum) - }) - - var ws = fs.createWriteStream(targetLibPath) - ws.on('error', cb) - req.pipe(ws) - }) - req.on('end', function () { --async || done() }) - }) - }) - } // downloadNodeLib() - }) // mkdir() + return streamPipeline( + res.body, + new ShaSum((_, checksum) => { + contentShasums[libPath] = checksum + log.verbose('content checksum', libPath, checksum) + }), + fs.createWriteStream(targetLibPath) + ) + }) + } // downloadNodeLib() } // go() /** @@ -358,10 +274,17 @@ function install (fs, gyp, argv, callback) { function valid (file) { // header files - var extname = path.extname(file) + const extname = path.extname(file) return extname === '.h' || extname === '.gypi' } + async function rollback (err) { + log.warn('install', 'got an error, rolling back install') + // roll-back the install if anything went wrong + await util.promisify(gyp.commands.remove)([release.versionDir]) + throw err + } + /** * The EACCES fallback is a workaround for npm's `sudo` behavior, where * it drops the permissions before invoking any child processes (like @@ -371,14 +294,14 @@ function install (fs, gyp, argv, callback) { * the compilation will succeed... */ - function eaccesFallback (err) { - var noretry = '--node_gyp_internal_noretry' + async function eaccesFallback (err) { + const noretry = '--node_gyp_internal_noretry' if (argv.indexOf(noretry) !== -1) { - return cb(err) + throw err } - var tmpdir = os.tmpdir() + const tmpdir = os.tmpdir() gyp.devDir = path.resolve(tmpdir, '.node-gyp') - var userString = '' + let userString = '' try { // os.userInfo can fail on some systems, it's not critical here userString = ` ("${os.userInfo().username}")` @@ -389,59 +312,65 @@ function install (fs, gyp, argv, callback) { log.verbose('tmpdir == cwd', 'automatically will remove dev files after to save disk space') gyp.todo.push({ name: 'remove', args: argv }) } - gyp.commands.install([noretry].concat(argv), cb) + return util.promisify(gyp.commands.install)([noretry].concat(argv)) + } +} + +class ShaSum extends stream.Transform { + constructor (callback) { + super() + this._callback = callback + this._digester = crypto.createHash('sha256') + } + + _transform (chunk, _, callback) { + this._digester.update(chunk) + callback(null, chunk) + } + + _flush (callback) { + this._callback(null, this._digester.digest('hex')) + callback() } } -function download (gyp, env, url) { +async function download (gyp, url) { log.http('GET', url) - var requestOpts = { - uri: url, + const requestOpts = { headers: { - 'User-Agent': 'node-gyp v' + gyp.version + ' (node ' + process.version + ')', + 'User-Agent': `node-gyp v${gyp.version} (node ${process.version})`, Connection: 'keep-alive' - } + }, + proxy: gyp.opts.proxy, + noProxy: gyp.opts.noproxy } - var cafile = gyp.opts.cafile + const cafile = gyp.opts.cafile if (cafile) { - requestOpts.ca = readCAFile(cafile) - } - - // basic support for a proxy server - var proxyUrl = getProxyFromURI(gyp, env, url) - if (proxyUrl) { - if (/^https?:\/\//i.test(proxyUrl)) { - log.verbose('download', 'using proxy url: "%s"', proxyUrl) - requestOpts.proxy = proxyUrl - } else { - log.warn('download', 'ignoring invalid "proxy" config setting: "%s"', proxyUrl) - } + requestOpts.ca = await readCAFile(cafile) } - var req = request(requestOpts) - req.on('response', function (res) { - log.http(res.statusCode, url) - }) + const res = await fetch(url, requestOpts) + log.http(res.status, res.url) - return req + return res } -function readCAFile (filename) { +async function readCAFile (filename) { // The CA file can contain multiple certificates so split on certificate // boundaries. [\S\s]*? is used to match everything including newlines. - var ca = fs.readFileSync(filename, 'utf8') - var re = /(-----BEGIN CERTIFICATE-----[\S\s]*?-----END CERTIFICATE-----)/g + const ca = await fs.promises.readFile(filename, 'utf8') + const re = /(-----BEGIN CERTIFICATE-----[\S\s]*?-----END CERTIFICATE-----)/g return ca.match(re) } module.exports = function (gyp, argv, callback) { - return install(fs, gyp, argv, callback) + install(fs, gyp, argv).then(callback.bind(undefined, null), callback) } module.exports.test = { - download: download, - install: install, - readCAFile: readCAFile + download, + install, + readCAFile } module.exports.usage = 'Install node development files for the specified node version.' diff --git a/lib/proxy.js b/lib/proxy.js deleted file mode 100644 index 92d9ed2f7f..0000000000 --- a/lib/proxy.js +++ /dev/null @@ -1,92 +0,0 @@ -'use strict' -// Taken from https://github.com/request/request/blob/212570b/lib/getProxyFromURI.js - -const url = require('url') - -function formatHostname (hostname) { - // canonicalize the hostname, so that 'oogle.com' won't match 'google.com' - return hostname.replace(/^\.*/, '.').toLowerCase() -} - -function parseNoProxyZone (zone) { - zone = zone.trim().toLowerCase() - - var zoneParts = zone.split(':', 2) - var zoneHost = formatHostname(zoneParts[0]) - var zonePort = zoneParts[1] - var hasPort = zone.indexOf(':') > -1 - - return { hostname: zoneHost, port: zonePort, hasPort: hasPort } -} - -function uriInNoProxy (uri, noProxy) { - var port = uri.port || (uri.protocol === 'https:' ? '443' : '80') - var hostname = formatHostname(uri.hostname) - var noProxyList = noProxy.split(',') - - // iterate through the noProxyList until it finds a match. - return noProxyList.map(parseNoProxyZone).some(function (noProxyZone) { - var isMatchedAt = hostname.indexOf(noProxyZone.hostname) - var hostnameMatched = ( - isMatchedAt > -1 && - (isMatchedAt === hostname.length - noProxyZone.hostname.length) - ) - - if (noProxyZone.hasPort) { - return (port === noProxyZone.port) && hostnameMatched - } - - return hostnameMatched - }) -} - -function getProxyFromURI (gyp, env, uri) { - // If a string URI/URL was given, parse it into a URL object - if (typeof uri === 'string') { - // eslint-disable-next-line - uri = url.parse(uri) - } - - // Decide the proper request proxy to use based on the request URI object and the - // environmental variables (NO_PROXY, HTTP_PROXY, etc.) - // respect NO_PROXY environment variables (see: https://lynx.invisible-island.net/lynx2.8.7/breakout/lynx_help/keystrokes/environments.html) - - var noProxy = gyp.opts.noproxy || env.NO_PROXY || env.no_proxy || env.npm_config_noproxy || '' - - // if the noProxy is a wildcard then return null - - if (noProxy === '*') { - return null - } - - // if the noProxy is not empty and the uri is found return null - - if (noProxy !== '' && uriInNoProxy(uri, noProxy)) { - return null - } - - // Check for HTTP or HTTPS Proxy in environment Else default to null - - if (uri.protocol === 'http:') { - return gyp.opts.proxy || - env.HTTP_PROXY || - env.http_proxy || - env.npm_config_proxy || null - } - - if (uri.protocol === 'https:') { - return gyp.opts.proxy || - env.HTTPS_PROXY || - env.https_proxy || - env.HTTP_PROXY || - env.http_proxy || - env.npm_config_proxy || null - } - - // if none of that works, return null - // (What uri protocol are you using then?) - - return null -} - -module.exports = getProxyFromURI diff --git a/package.json b/package.json index 8e256f0170..1a4fdf1845 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,9 @@ "env-paths": "^2.2.0", "glob": "^7.1.4", "graceful-fs": "^4.2.3", + "make-fetch-happen": "^8.0.9", "nopt": "^5.0.0", "npmlog": "^4.1.2", - "request": "^2.88.2", "rimraf": "^3.0.2", "semver": "^7.3.2", "tar": "^6.0.2", diff --git a/test/test-download.js b/test/test-download.js index fe373e3280..71a3c0d092 100644 --- a/test/test-download.js +++ b/test/test-download.js @@ -1,8 +1,9 @@ 'use strict' -const test = require('tap').test +const { test } = require('tap') const fs = require('fs') const path = require('path') +const util = require('util') const http = require('http') const https = require('https') const install = require('../lib/install') @@ -14,191 +15,142 @@ const log = require('npmlog') log.level = 'warn' -test('download over http', function (t) { +test('download over http', async (t) => { t.plan(2) - var server = http.createServer(function (req, res) { - t.strictEqual(req.headers['user-agent'], - 'node-gyp v42 (node ' + process.version + ')') + const server = http.createServer((req, res) => { + t.strictEqual(req.headers['user-agent'], `node-gyp v42 (node ${process.version})`) res.end('ok') - server.close() }) - var host = 'localhost' - server.listen(0, host, function () { - var port = this.address().port - var gyp = { - opts: {}, - version: '42' - } - var url = 'http://' + host + ':' + port - var req = install.test.download(gyp, {}, url) - req.on('response', function (res) { - var body = '' - res.setEncoding('utf8') - res.on('data', function (data) { - body += data - }) - res.on('end', function () { - t.strictEqual(body, 'ok') - }) - }) - }) + t.tearDown(() => new Promise((resolve) => server.close(resolve))) + + const host = 'localhost' + await new Promise((resolve) => server.listen(0, host, resolve)) + const { port } = server.address() + const gyp = { + opts: {}, + version: '42' + } + const url = `http://${host}:${port}` + const res = await install.test.download(gyp, url) + t.strictEqual(await res.text(), 'ok') }) -test('download over https with custom ca', function (t) { +test('download over https with custom ca', async (t) => { t.plan(3) - var cert = fs.readFileSync(path.join(__dirname, 'fixtures/server.crt'), 'utf8') - var key = fs.readFileSync(path.join(__dirname, 'fixtures/server.key'), 'utf8') + const cafile = path.join(__dirname, '/fixtures/ca.crt') + const [cert, key, ca] = await Promise.all([ + fs.promises.readFile(path.join(__dirname, 'fixtures/server.crt'), 'utf8'), + fs.promises.readFile(path.join(__dirname, 'fixtures/server.key'), 'utf8'), + install.test.readCAFile(cafile) + ]) - var cafile = path.join(__dirname, '/fixtures/ca.crt') - var ca = install.test.readCAFile(cafile) t.strictEqual(ca.length, 1) - var options = { ca: ca, cert: cert, key: key } - var server = https.createServer(options, function (req, res) { - t.strictEqual(req.headers['user-agent'], - 'node-gyp v42 (node ' + process.version + ')') + const options = { ca: ca, cert: cert, key: key } + const server = https.createServer(options, (req, res) => { + t.strictEqual(req.headers['user-agent'], `node-gyp v42 (node ${process.version})`) res.end('ok') - server.close() }) - server.on('clientError', function (err) { - throw err - }) + t.tearDown(() => new Promise((resolve) => server.close(resolve))) - var host = 'localhost' - server.listen(8000, host, function () { - var port = this.address().port - var gyp = { - opts: { cafile: cafile }, - version: '42' - } - var url = 'https://' + host + ':' + port - var req = install.test.download(gyp, {}, url) - req.on('response', function (res) { - var body = '' - res.setEncoding('utf8') - res.on('data', function (data) { - body += data - }) - res.on('end', function () { - t.strictEqual(body, 'ok') - }) - }) - }) + server.on('clientError', (err) => { throw err }) + + const host = 'localhost' + await new Promise((resolve) => server.listen(0, host, resolve)) + const { port } = server.address() + const gyp = { + opts: { cafile }, + version: '42' + } + const url = `https://${host}:${port}` + const res = await install.test.download(gyp, url) + t.strictEqual(await res.text(), 'ok') }) -test('download over http with proxy', function (t) { +test('download over http with proxy', async (t) => { t.plan(2) - var server = http.createServer(function (req, res) { - t.strictEqual(req.headers['user-agent'], - 'node-gyp v42 (node ' + process.version + ')') + const server = http.createServer((_, res) => { res.end('ok') - pserver.close(function () { - server.close() - }) }) - var pserver = http.createServer(function (req, res) { - t.strictEqual(req.headers['user-agent'], - 'node-gyp v42 (node ' + process.version + ')') + const pserver = http.createServer((req, res) => { + t.strictEqual(req.headers['user-agent'], `node-gyp v42 (node ${process.version})`) res.end('proxy ok') - server.close(function () { - pserver.close() - }) }) - var host = 'localhost' - server.listen(0, host, function () { - var port = this.address().port - pserver.listen(port + 1, host, function () { - var gyp = { - opts: { - proxy: 'http://' + host + ':' + (port + 1) - }, - version: '42' - } - var url = 'http://' + host + ':' + port - var req = install.test.download(gyp, {}, url) - req.on('response', function (res) { - var body = '' - res.setEncoding('utf8') - res.on('data', function (data) { - body += data - }) - res.on('end', function () { - t.strictEqual(body, 'proxy ok') - }) - }) - }) - }) + t.tearDown(() => Promise.all([ + new Promise((resolve) => server.close(resolve)), + new Promise((resolve) => pserver.close(resolve)) + ])) + + const host = 'localhost' + await new Promise((resolve) => server.listen(0, host, resolve)) + const { port } = server.address() + await new Promise((resolve) => pserver.listen(port + 1, host, resolve)) + const gyp = { + opts: { + proxy: `http://${host}:${port + 1}`, + noproxy: 'bad' + }, + version: '42' + } + const url = `http://${host}:${port}` + const res = await install.test.download(gyp, url) + t.strictEqual(await res.text(), 'proxy ok') }) -test('download over http with noproxy', function (t) { +test('download over http with noproxy', async (t) => { t.plan(2) - var server = http.createServer(function (req, res) { - t.strictEqual(req.headers['user-agent'], - 'node-gyp v42 (node ' + process.version + ')') + const server = http.createServer((req, res) => { + t.strictEqual(req.headers['user-agent'], `node-gyp v42 (node ${process.version})`) res.end('ok') - pserver.close(function () { - server.close() - }) }) - var pserver = http.createServer(function (req, res) { - t.strictEqual(req.headers['user-agent'], - 'node-gyp v42 (node ' + process.version + ')') + const pserver = http.createServer((_, res) => { res.end('proxy ok') - server.close(function () { - pserver.close() - }) }) - var host = 'localhost' - server.listen(0, host, function () { - var port = this.address().port - pserver.listen(port + 1, host, function () { - var gyp = { - opts: { - proxy: 'http://' + host + ':' + (port + 1), - noproxy: 'localhost' - }, - version: '42' - } - var url = 'http://' + host + ':' + port - var req = install.test.download(gyp, {}, url) - req.on('response', function (res) { - var body = '' - res.setEncoding('utf8') - res.on('data', function (data) { - body += data - }) - res.on('end', function () { - t.strictEqual(body, 'ok') - }) - }) - }) - }) + t.tearDown(() => Promise.all([ + new Promise((resolve) => server.close(resolve)), + new Promise((resolve) => pserver.close(resolve)) + ])) + + const host = 'localhost' + await new Promise((resolve) => server.listen(0, host, resolve)) + const { port } = server.address() + await new Promise((resolve) => pserver.listen(port + 1, host, resolve)) + const gyp = { + opts: { + proxy: `http://${host}:${port + 1}`, + noproxy: host + }, + version: '42' + } + const url = `http://${host}:${port}` + const res = await install.test.download(gyp, url) + t.strictEqual(await res.text(), 'ok') }) -test('download with missing cafile', function (t) { +test('download with missing cafile', async (t) => { t.plan(1) - var gyp = { + const gyp = { opts: { cafile: 'no.such.file' } } try { - install.test.download(gyp, {}, 'http://bad/') + await install.test.download(gyp, {}, 'http://bad/') } catch (e) { t.ok(/no.such.file/.test(e.message)) } }) -test('check certificate splitting', function (t) { - var cas = install.test.readCAFile(path.join(__dirname, 'fixtures/ca-bundle.crt')) +test('check certificate splitting', async (t) => { + const cas = await install.test.readCAFile(path.join(__dirname, 'fixtures/ca-bundle.crt')) t.plan(2) t.strictEqual(cas.length, 2) t.notStrictEqual(cas[0], cas[1]) @@ -206,7 +158,7 @@ test('check certificate splitting', function (t) { // only run this test if we are running a version of Node with predictable version path behavior -test('download headers (actual)', function (t) { +test('download headers (actual)', async (t) => { if (process.env.FAST_TEST || process.release.name !== 'node' || semver.prerelease(process.version) !== null || @@ -214,55 +166,42 @@ test('download headers (actual)', function (t) { return t.skip('Skipping actual download of headers due to test environment configuration') } - t.plan(17) + t.plan(12) const expectedDir = path.join(devDir, process.version.replace(/^v/, '')) - rimraf(expectedDir, (err) => { - t.ifError(err) - - const prog = gyp() - prog.parseArgv([]) - prog.devDir = devDir - log.level = 'warn' - install(prog, [], (err) => { - t.ifError(err) - - fs.readFile(path.join(expectedDir, 'installVersion'), 'utf8', (err, data) => { - t.ifError(err) - t.strictEqual(data, '9\n', 'correct installVersion') - }) - - fs.readdir(path.join(expectedDir, 'include/node'), (err, list) => { - t.ifError(err) - - t.ok(list.includes('common.gypi')) - t.ok(list.includes('config.gypi')) - t.ok(list.includes('node.h')) - t.ok(list.includes('node_version.h')) - t.ok(list.includes('openssl')) - t.ok(list.includes('uv')) - t.ok(list.includes('uv.h')) - t.ok(list.includes('v8-platform.h')) - t.ok(list.includes('v8.h')) - t.ok(list.includes('zlib.h')) - }) - - fs.readFile(path.join(expectedDir, 'include/node/node_version.h'), 'utf8', (err, contents) => { - t.ifError(err) - - const lines = contents.split('\n') - - // extract the 3 version parts from the defines to build a valid version string and - // and check them against our current env version - const version = ['major', 'minor', 'patch'].reduce((version, type) => { - const re = new RegExp(`^#define\\sNODE_${type.toUpperCase()}_VERSION`) - const line = lines.find((l) => re.test(l)) - const i = line ? parseInt(line.replace(/^[^0-9]+([0-9]+).*$/, '$1'), 10) : 'ERROR' - return `${version}${type !== 'major' ? '.' : 'v'}${i}` - }, '') - - t.strictEqual(version, process.version) - }) - }) - }) + await util.promisify(rimraf)(expectedDir) + + const prog = gyp() + prog.parseArgv([]) + prog.devDir = devDir + log.level = 'warn' + await util.promisify(install)(prog, []) + + const data = await fs.promises.readFile(path.join(expectedDir, 'installVersion'), 'utf8') + t.strictEqual(data, '9\n', 'correct installVersion') + + const list = await fs.promises.readdir(path.join(expectedDir, 'include/node')) + t.ok(list.includes('common.gypi')) + t.ok(list.includes('config.gypi')) + t.ok(list.includes('node.h')) + t.ok(list.includes('node_version.h')) + t.ok(list.includes('openssl')) + t.ok(list.includes('uv')) + t.ok(list.includes('uv.h')) + t.ok(list.includes('v8-platform.h')) + t.ok(list.includes('v8.h')) + t.ok(list.includes('zlib.h')) + + const lines = (await fs.promises.readFile(path.join(expectedDir, 'include/node/node_version.h'), 'utf8')).split('\n') + + // extract the 3 version parts from the defines to build a valid version string and + // and check them against our current env version + const version = ['major', 'minor', 'patch'].reduce((version, type) => { + const re = new RegExp(`^#define\\sNODE_${type.toUpperCase()}_VERSION`) + const line = lines.find((l) => re.test(l)) + const i = line ? parseInt(line.replace(/^[^0-9]+([0-9]+).*$/, '$1'), 10) : 'ERROR' + return `${version}${type !== 'major' ? '.' : 'v'}${i}` + }, '') + + t.strictEqual(version, process.version) }) diff --git a/test/test-install.js b/test/test-install.js index c3317155e0..5039dc992e 100644 --- a/test/test-install.js +++ b/test/test-install.js @@ -1,38 +1,46 @@ 'use strict' -const test = require('tap').test -const install = require('../lib/install').test.install +const { test } = require('tap') +const { test: { install } } = require('../lib/install') +const log = require('npmlog') -require('npmlog').level = 'error' // we expect a warning +log.level = 'error' // we expect a warning -test('EACCES retry once', function (t) { +test('EACCES retry once', async (t) => { t.plan(3) - var fs = {} - fs.stat = function (path, cb) { - var err = new Error() - err.code = 'EACCES' - cb(err) - t.ok(true) + const fs = { + promises: { + stat (_) { + const err = new Error() + err.code = 'EACCES' + t.ok(true) + throw err + } + } } - var gyp = {} - gyp.devDir = __dirname - gyp.opts = {} - gyp.opts.ensure = true - gyp.commands = {} - gyp.commands.install = function (argv, cb) { - install(fs, gyp, argv, cb) - } - gyp.commands.remove = function (argv, cb) { - cb() + const Gyp = { + devDir: __dirname, + opts: { + ensure: true + }, + commands: { + install (argv, cb) { + install(fs, Gyp, argv).then(cb, cb) + }, + remove (_, cb) { + cb() + } + } } - gyp.commands.install([], function (err) { + try { + await install(fs, Gyp, []) + } catch (err) { t.ok(true) if (/"pre" versions of node cannot be installed/.test(err.message)) { t.ok(true) - t.ok(true) } - }) + } })