From f04b157a0162114de7252b682ecf4b66895d7e87 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Sat, 15 Oct 2016 15:19:49 +0100 Subject: [PATCH 1/8] respect indent of manifest if detectable when modifying - fixes #1091 (#1098) --- package.json | 1 + src/cli/commands/add.js | 6 ++-- src/cli/commands/init.js | 8 ++--- src/cli/commands/install.js | 65 ++-------------------------------- src/cli/commands/remove.js | 5 ++- src/cli/commands/version.js | 16 ++++++--- src/config.js | 67 +++++++++++++++++++++++++++++++++++- src/constants.js | 1 + src/fetchers/base-fetcher.js | 5 ++- src/util/fs.js | 12 ++++++- src/util/misc.js | 4 --- yarn.lock | 6 ++++ 12 files changed, 109 insertions(+), 87 deletions(-) diff --git a/package.json b/package.json index 2ca3786d93..806611b035 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "death": "^1.0.0", "debug": "^2.2.0", "defaults": "^1.0.3", + "detect-indent": "^4.0.0", "diff": "^2.2.1", "eslint-plugin-react": "5.2.2", "github": "2.5.1", diff --git a/src/cli/commands/add.js b/src/cli/commands/add.js index e792d3c22c..41d4136c1a 100644 --- a/src/cli/commands/add.js +++ b/src/cli/commands/add.js @@ -92,7 +92,7 @@ export class Add extends Install { const {dev, exact, tilde, optional, peer} = this.flags; // get all the different registry manifests in this folder - const jsons = await this.getRootManifests(); + const manifests = await this.config.getRootManifests(); // add new patterns to their appropriate registry manifest for (const pattern of this.resolver.dedupePatterns(this.args)) { @@ -134,7 +134,7 @@ export class Add extends Install { } // add it to manifest - const object = jsons[ref.registry].object; + const object = manifests[ref.registry].object; for (const key of targetKeys) { const target = object[key] = object[key] || {}; target[pkg.name] = version; @@ -149,7 +149,7 @@ export class Add extends Install { this.resolver.removePattern(pattern); } - await this.saveRootManifests(jsons); + await this.config.saveRootManifests(manifests); } } diff --git a/src/cli/commands/init.js b/src/cli/commands/init.js index b56dc7f597..dfef22183b 100644 --- a/src/cli/commands/init.js +++ b/src/cli/commands/init.js @@ -4,8 +4,6 @@ import type {Reporter} from '../../reporters/index.js'; import type Config from '../../config.js'; import {stringifyPerson} from '../../util/normalize-manifest/util.js'; import {registryNames} from '../../registries/index.js'; -import Lockfile from '../../lockfile/wrapper.js'; -import {Install} from './install.js'; import * as child from '../../util/child.js'; import * as fs from '../../util/fs.js'; @@ -22,9 +20,7 @@ export async function run( flags: Object, args: Array, ): Promise { - const lockfile = new Lockfile(); - const install = new Install(flags, config, reporter, lockfile); - const manifests = await install.getRootManifests(); + const manifests = await config.getRootManifests(); let gitUrl; const author = { @@ -137,5 +133,5 @@ export async function run( reporter.success(`Saved ${path.basename(targetManifest.loc)}`); } - await install.saveRootManifests(manifests); + await config.saveRootManifests(manifests); } diff --git a/src/cli/commands/install.js b/src/cli/commands/install.js index 6f95e318a5..2e2ecc5404 100644 --- a/src/cli/commands/install.js +++ b/src/cli/commands/install.js @@ -6,7 +6,6 @@ import type {Manifest, DependencyRequestPatterns} from '../../types.js'; import type Config from '../../config.js'; import type {RegistryNames} from '../../registries/index.js'; import normalizeManifest from '../../util/normalize-manifest/index.js'; -import {stringify} from '../../util/misc.js'; import {registryNames} from '../../registries/index.js'; import {MessageError} from '../../errors.js'; import Lockfile from '../../lockfile/wrapper.js'; @@ -42,14 +41,6 @@ export type InstallCwdRequest = [ Object ]; -type RootManifests = { - [registryName: RegistryNames]: { - loc: string, - object: Object, - exists: boolean, - } -}; - type IntegrityMatch = { actual: string, expected: string, @@ -129,14 +120,6 @@ function normalizeFlags(config: Config, rawFlags: Object): Flags { return flags; } -const sortObject = (object) => { - const sortedObject = {}; - Object.keys(object).sort().forEach((item) => { - sortedObject[item] = object[item]; - }); - return sortedObject; -}; - export class Install { constructor( flags: Object, @@ -445,7 +428,7 @@ export class Install { // save resolutions to their appropriate root manifest if (Object.keys(this.resolutions).length) { - const jsons = await this.getRootManifests(); + const manifests = await this.config.getRootManifests(); for (const name in this.resolutions) { const version = this.resolutions[name]; @@ -467,59 +450,17 @@ export class Install { const ref = manifest._reference; invariant(ref, 'expected reference'); - const object = jsons[ref.registry].object; + const object = manifests[ref.registry].object; object.resolutions = object.resolutions || {}; object.resolutions[name] = version; } - await this.saveRootManifests(jsons); + await this.config.saveRootManifests(manifests); } return flattenedPatterns; } - /** - * Get root manifests. - */ - - async getRootManifests(): Promise { - const manifests: RootManifests = {}; - for (const registryName of registryNames) { - const registry = registries[registryName]; - const jsonLoc = path.join(this.config.cwd, registry.filename); - - let object = {}; - let exists = false; - if (await fs.exists(jsonLoc)) { - exists = true; - object = await fs.readJson(jsonLoc); - } - manifests[registryName] = {loc: jsonLoc, object, exists}; - } - return manifests; - } - - /** - * Save root manifests. - */ - - async saveRootManifests(manifests: RootManifests): Promise { - for (const registryName of registryNames) { - const {loc, object, exists} = manifests[registryName]; - if (!exists && !Object.keys(object).length) { - continue; - } - - for (const field of constants.DEPENDENCY_TYPES) { - if (object[field]) { - object[field] = sortObject(object[field]); - } - } - - await fs.writeFile(loc, stringify(object) + '\n'); - } - } - /** * Save updated integrity and lockfiles. */ diff --git a/src/cli/commands/remove.js b/src/cli/commands/remove.js index df02b2f2a6..a021c8fc5a 100644 --- a/src/cli/commands/remove.js +++ b/src/cli/commands/remove.js @@ -30,8 +30,7 @@ export async function run( // load manifests const lockfile = await Lockfile.fromDirectory(config.cwd); - const install = new Install(flags, config, new NoopReporter(), lockfile); - const rootManifests = await install.getRootManifests(); + const rootManifests = await config.getRootManifests(); const manifests = []; for (const name of args) { @@ -66,7 +65,7 @@ export async function run( } // save manifests - await install.saveRootManifests(rootManifests); + await config.saveRootManifests(rootManifests); // run hooks - npm runs these one after another for (const action of ['preuninstall', 'uninstall', 'postuninstall']) { diff --git a/src/cli/commands/version.js b/src/cli/commands/version.js index 3636d7691c..467fb6afe7 100644 --- a/src/cli/commands/version.js +++ b/src/cli/commands/version.js @@ -2,9 +2,9 @@ import type {Reporter} from '../../reporters/index.js'; import type Config from '../../config.js'; +import {registryNames} from '../../registries/index.js'; import executeLifecycleScript from './_execute-lifecycle-script.js'; import {MessageError} from '../../errors.js'; -import {stringify} from '../../util/misc.js'; import {spawn} from '../../util/child.js'; import * as fs from '../../util/fs.js'; @@ -81,9 +81,17 @@ export async function setVersion( // update version reporter.info(`${reporter.lang('newVersion')}: ${newVersion}`); - const json = await fs.readJson(pkgLoc); - pkg.version = json.version = newVersion; - await fs.writeFile(pkgLoc, `${stringify(json)}\n`); + pkg.version = newVersion; + + // update versions in manifests + const manifests = await config.getRootManifests(); + for (const registryName of registryNames) { + const manifest = manifests[registryName]; + if (manifest.exists) { + manifest.object.version = newVersion; + } + } + await config.saveRootManifests(manifests); return async function(): Promise { invariant(newVersion, 'expected version'); diff --git a/src/config.js b/src/config.js index f6e4373b2c..34bd646d56 100644 --- a/src/config.js +++ b/src/config.js @@ -9,9 +9,10 @@ import * as fs from './util/fs.js'; import * as constants from './constants.js'; import ConstraintResolver from './package-constraint-resolver.js'; import RequestManager from './util/request-manager.js'; -import {registries} from './registries/index.js'; +import {registries, registryNames} from './registries/index.js'; import map from './util/map.js'; +const detectIndent = require('detect-indent'); const invariant = require('invariant'); const path = require('path'); const url = require('url'); @@ -40,6 +41,24 @@ type PackageMetadata = { package: Manifest }; + +type RootManifests = { + [registryName: RegistryNames]: { + loc: string, + indent: ?string, + object: Object, + exists: boolean, + } +}; + +function sortObject(object: Object): Object { + const sortedObject = {}; + Object.keys(object).sort().forEach((item) => { + sortedObject[item] = object[item]; + }); + return sortedObject; +} + export type ConfigRegistries = { [name: RegistryNames]: Registry }; @@ -372,4 +391,50 @@ export default class Config { } return this.registries[registryName].folder; } + + /** + * Get root manifests. + */ + + async getRootManifests(): Promise { + const manifests: RootManifests = {}; + for (const registryName of registryNames) { + const registry = registries[registryName]; + const jsonLoc = path.join(this.cwd, registry.filename); + + let object = {}; + let exists = false; + let indent; + if (await fs.exists(jsonLoc)) { + exists = true; + + const info = await fs.readJsonAndFile(jsonLoc); + object = info.object; + indent = detectIndent(info.content).indent || undefined; + } + manifests[registryName] = {loc: jsonLoc, object, exists, indent}; + } + return manifests; + } + + /** + * Save root manifests. + */ + + async saveRootManifests(manifests: RootManifests): Promise { + for (const registryName of registryNames) { + const {loc, object, exists, indent} = manifests[registryName]; + if (!exists && !Object.keys(object).length) { + continue; + } + + for (const field of constants.DEPENDENCY_TYPES) { + if (object[field]) { + object[field] = sortObject(object[field]); + } + } + + await fs.writeFile(loc, JSON.stringify(object, null, indent || constants.DEFAULT_INDENT) + '\n'); + } + } } diff --git a/src/constants.js b/src/constants.js index d64f59e9f0..9fbf91ce5e 100644 --- a/src/constants.js +++ b/src/constants.js @@ -45,6 +45,7 @@ export const METADATA_FILENAME = '.yarn-metadata.json'; export const TARBALL_FILENAME = '.yarn-tarball.tgz'; export const CLEAN_FILENAME = '.yarnclean'; +export const DEFAULT_INDENT = ' '; export const SINGLE_INSTANCE_PORT = 31997; export const SINGLE_INSTANCE_FILENAME = '.yarn-single-instance'; diff --git a/src/fetchers/base-fetcher.js b/src/fetchers/base-fetcher.js index 9bfbbe3a0d..ddee7fab8b 100644 --- a/src/fetchers/base-fetcher.js +++ b/src/fetchers/base-fetcher.js @@ -5,7 +5,6 @@ import type {PackageRemote, FetchedMetadata, FetchedOverride} from '../types.js' import type {RegistryNames} from '../registries/index.js'; import type Config from '../config.js'; import * as constants from '../constants.js'; -import * as util from '../util/misc.js'; import * as fs from '../util/fs.js'; const path = require('path'); @@ -49,11 +48,11 @@ export default class BaseFetcher { // load the new normalized manifest const pkg = await this.config.readManifest(dest, this.registry); - await fs.writeFile(path.join(dest, constants.METADATA_FILENAME), util.stringify({ + await fs.writeFile(path.join(dest, constants.METADATA_FILENAME), JSON.stringify({ remote: this.remote, registry: this.registry, hash, - })); + }, null, ' ')); return { resolved, diff --git a/src/util/fs.js b/src/util/fs.js index 441f06cdd3..0943b8500e 100644 --- a/src/util/fs.js +++ b/src/util/fs.js @@ -315,9 +315,19 @@ export async function readFileAny(files: Array): Promise { } export async function readJson(loc: string): Promise { + return (await readJsonAndFile(loc)).object; +} + +export async function readJsonAndFile(loc: string): Promise<{ + object: Object, + content: string, +}> { const file = await readFile(loc); try { - return map(JSON.parse(stripBOM(file))); + return { + object: map(JSON.parse(stripBOM(file))), + content: file, + }; } catch (err) { err.message = `${loc}: ${err.message}`; throw err; diff --git a/src/util/misc.js b/src/util/misc.js index ef7aa4c166..a92a57c106 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -30,7 +30,3 @@ export function removeSuffix(pattern: string, suffix: string): string { return pattern; } - -export function stringify(obj: Object): string { - return JSON.stringify(obj, null, ' '); -} diff --git a/yarn.lock b/yarn.lock index 4bd048ee9a..e7f968e68f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1653,6 +1653,12 @@ detect-file@^0.1.0: dependencies: fs-exists-sync "^0.1.0" +detect-indent: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" + detect-indent@^3.0.0, detect-indent@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-3.0.1.tgz#9dc5e5ddbceef8325764b9451b02bc6d54084f75" From 8ebc3c2ea8aa26a41b6aaa0be9b0469eb3aaa04b Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Sat, 15 Oct 2016 15:39:17 +0100 Subject: [PATCH 2/8] Nicer unexpected error handling (#1097) * don't leak stack traces into output, write yarn-error.log on unexpected errors - fixes #948 * fix lint * fix lint * fix lint * fix test --- .../throw name non-string/expected.json | 2 +- src/cli/commands/pack.js | 1 + src/cli/index.js | 68 ++++++++++++------- src/config.js | 3 +- src/errors.js | 6 +- src/package-fetcher.js | 3 +- src/package-request.js | 2 +- src/reporters/lang/en.js | 1 + src/util/normalize-manifest/fix.js | 25 +------ src/util/normalize-manifest/validate.js | 9 +-- 10 files changed, 61 insertions(+), 59 deletions(-) diff --git a/__tests__/fixtures/normalize-manifest/throw name non-string/expected.json b/__tests__/fixtures/normalize-manifest/throw name non-string/expected.json index 6f065d868e..2dee571a7c 100644 --- a/__tests__/fixtures/normalize-manifest/throw name non-string/expected.json +++ b/__tests__/fixtures/normalize-manifest/throw name non-string/expected.json @@ -1,3 +1,3 @@ { - "_error": "Fatal problem with name in manifest: must be a string" + "_error": "\"name\" is not a string" } diff --git a/src/cli/commands/pack.js b/src/cli/commands/pack.js index 49a2f338bd..c1e8de6cd3 100644 --- a/src/cli/commands/pack.js +++ b/src/cli/commands/pack.js @@ -32,6 +32,7 @@ const DEFAULT_IGNORE = ignoreLinesToRegex([ '*.swp', '._*', 'npm-debug.log', + 'yarn-error.log', '.npmrc', '.yarnrc', '.npmignore', diff --git a/src/cli/index.js b/src/cli/index.js index 141b8f48a3..9d359e5832 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -2,6 +2,7 @@ import {ConsoleReporter, JSONReporter} from '../reporters/index.js'; import {sortAlpha} from '../util/misc.js'; +import {registries, registryNames} from '../registries/index.js'; import * as commands from './commands/index.js'; import * as constants from '../constants.js'; import * as network from '../util/network.js'; @@ -171,10 +172,7 @@ if (typeof command.hasWrapper === 'function') { outputWrapper = command.hasWrapper(commander, commander.args); } if (outputWrapper) { - reporter.header(commandName, { - name: 'yarn', - version: pkg.version, - }); + reporter.header(commandName, pkg); } if (command.noArguments && args.length) { @@ -283,6 +281,38 @@ const runEventuallyWithNetwork = (mutexPort: ?string): Promise => { }); }; +function onUnexpectedError(err: Error) { + function indent(str: string): string { + return '\n ' + str.trim().split('\n').join('\n '); + } + + const log = []; + log.push(`Arguments: ${indent(process.argv.join(' '))}`); + log.push(`PATH: ${indent(process.env.PATH || 'undefined')}`); + log.push(`Yarn version: ${indent(pkg.version)}`); + log.push(`Node version: ${indent(process.versions.node)}`); + log.push(`Platform: ${indent(process.platform + ' ' + process.arch)}`); + + // add manifests + for (const registryName of registryNames) { + const possibleLoc = path.join(config.cwd, registries[registryName].filename); + const manifest = fs.existsSync(possibleLoc) ? fs.readFileSync(possibleLoc, 'utf8') : 'No manifest'; + log.push(`${registryName} manifest: ${indent(manifest)}`); + } + + // lockfile + const lockLoc = path.join(config.cwd, constants.LOCKFILE_FILENAME); + const lockfile = fs.existsSync(lockLoc) ? fs.readFileSync(lockLoc, 'utf8') : 'No lockfile'; + log.push(`Lockfile: ${indent(lockfile)}`); + + log.push(`Trace: ${indent(err.stack)}`); + + const errorLoc = path.join(config.cwd, 'yarn-error.log'); + fs.writeFileSync(errorLoc, log.join('\n\n') + '\n'); + + reporter.error(reporter.lang('unexpectedError', errorLoc)); +} + // config.init({ modulesFolder: commander.modulesFolder, @@ -310,33 +340,21 @@ config.init({ } else if (mutexType === 'network') { return runEventuallyWithNetwork(mutexSpecifier).then(exit); } else { - throw new Error(`Unknown single instance type ${mutexType}`); + throw new MessageError(`Unknown single instance type ${mutexType}`); } } else { return run().then(exit); } -}).catch((errs: ?(Array | Error)) => { - function logError(err) { - if (err instanceof MessageError) { - reporter.error(err.message); - } else { - reporter.error(err.stack.replace(/^Error: /, '')); - } +}).catch((err: Error) => { + if (err instanceof MessageError) { + reporter.error(err.message); + } else { + onUnexpectedError(err); } - if (errs) { - if (Array.isArray(errs)) { - for (const err of errs) { - logError(err); - } - } else { - logError(errs); - } - - const actualCommandForHelp = commands[commandName] ? commandName : aliases[commandName]; - if (actualCommandForHelp) { - reporter.info(getDocsInfo(actualCommandForHelp)); - } + const actualCommandForHelp = commands[commandName] ? commandName : aliases[commandName]; + if (actualCommandForHelp) { + reporter.info(getDocsInfo(actualCommandForHelp)); } process.exit(1); diff --git a/src/config.js b/src/config.js index 34bd646d56..a5ec06a610 100644 --- a/src/config.js +++ b/src/config.js @@ -5,6 +5,7 @@ import type {Reporter} from './reporters/index.js'; import type Registry from './registries/base-registry.js'; import type {Manifest, PackageRemote} from './types.js'; import normalizeManifest from './util/normalize-manifest/index.js'; +import {MessageError} from './errors.js'; import * as fs from './util/fs.js'; import * as constants from './constants.js'; import ConstraintResolver from './package-constraint-resolver.js'; @@ -349,7 +350,7 @@ export default class Config { } } - throw new Error(`Couldn't find a package.json (or bower.json) file in ${dir}`); + throw new MessageError(`Couldn't find a package.json (or bower.json) file in ${dir}`); }); } diff --git a/src/errors.js b/src/errors.js index 2e98691c15..809ffe2884 100644 --- a/src/errors.js +++ b/src/errors.js @@ -1,7 +1,5 @@ /* @flow */ -export class SecurityError extends Error {} - export class MessageError extends Error { constructor(msg: string, code?: string) { super(msg); @@ -11,6 +9,8 @@ export class MessageError extends Error { code: ?string; } -export class SpawnError extends Error { +export class SecurityError extends MessageError {} + +export class SpawnError extends MessageError { EXIT_CODE: number; } diff --git a/src/package-fetcher.js b/src/package-fetcher.js index 1c40d773be..828fdf1299 100644 --- a/src/package-fetcher.js +++ b/src/package-fetcher.js @@ -6,6 +6,7 @@ import type {Fetchers} from './fetchers/index.js'; import type {Reporter} from './reporters/index.js'; import type PackageReference from './package-reference.js'; import type Config from './config.js'; +import {MessageError} from './errors.js'; import * as fetchers from './fetchers/index.js'; import * as fs from './util/fs.js'; import * as promise from './util/promise.js'; @@ -37,7 +38,7 @@ export default class PackageFetcher { const remote = ref.remote; const Fetcher = fetchers[remote.type]; if (!Fetcher) { - throw new Error(`Unknown fetcher for ${remote.type}`); + throw new MessageError(`Unknown fetcher for ${remote.type}`); } const fetcher = new Fetcher(dest, remote, this.config); diff --git a/src/package-request.js b/src/package-request.js index b83798856c..47c2dad0f4 100644 --- a/src/package-request.js +++ b/src/package-request.js @@ -132,7 +132,7 @@ export default class PackageRequest { if (Resolver) { return Resolver; } else { - throw new Error(`Unknown registry resolver ${this.registry}`); + throw new MessageError(`Unknown registry resolver ${this.registry}`); } } diff --git a/src/reporters/lang/en.js b/src/reporters/lang/en.js index b059a278d7..f3aa2c6d3e 100644 --- a/src/reporters/lang/en.js +++ b/src/reporters/lang/en.js @@ -69,6 +69,7 @@ const messages = { noVersion: `Package doesn't have a version.`, answerRequired: 'An answer is required.', missingWhyDependency: 'Missing package name, folder or path to file to identify why a package has been installed', + unexpectedError: 'An unexpected error occured, please open a bug report with the information provided in $0.', tooManyArguments: 'Too many arguments, maximum of $0.', tooFewArguments: 'Not enough arguments, expected at least $0.', diff --git a/src/util/normalize-manifest/fix.js b/src/util/normalize-manifest/fix.js index c8ea5bbf42..7212291ebf 100644 --- a/src/util/normalize-manifest/fix.js +++ b/src/util/normalize-manifest/fix.js @@ -20,21 +20,6 @@ type Dict = { [key: string]: T; }; -class ManifestError extends Error { - key: string; - problem: string; - - constructor(key: string, problem: string) { - super(); - this.key = key; - this.problem = problem; - } - - get message(): string { - return `Fatal problem with ${this.key} in manifest: ${this.problem}`; - } -} - export default async function ( info: Dict, moduleLoc: string, @@ -147,17 +132,11 @@ export default async function ( info.homepage = url.format(parts); } - const name = info.name; - - if (typeof name !== 'string') { - throw new ManifestError('name', 'must be a string'); - } - // if the `bin` field is as string then expand it to an object with a single property // based on the original `bin` field and `name field` // { name: "foo", bin: "cli.js" } -> { name: "foo", bin: { foo: "cli.js" } } - if (typeof info.bin === 'string') { - info.bin = {[name]: info.bin}; + if (typeof info.name === 'string' && typeof info.bin === 'string') { + info.bin = {[info.name]: info.bin}; } // bundleDependencies is an alias for bundledDependencies diff --git a/src/util/normalize-manifest/validate.js b/src/util/normalize-manifest/validate.js index a0facae85b..855d20f285 100644 --- a/src/util/normalize-manifest/validate.js +++ b/src/util/normalize-manifest/validate.js @@ -1,6 +1,7 @@ /* @flow */ import type {Reporter} from '../../reporters/index.js'; +import {MessageError} from '../../errors.js'; import {isValidLicense} from './util.js'; import typos from './typos.js'; @@ -61,18 +62,18 @@ export default function(info: Object, isRoot: boolean, reporter: Reporter, warn: // cannot start with a dot if (name[0] === '.') { - throw new TypeError(reporter.lang('manifestNameDot')); + throw new MessageError(reporter.lang('manifestNameDot')); } // cannot contain the following characters if (!isValidPackageName(name)) { - throw new TypeError(reporter.lang('manifestNameIllegalChars')); + throw new MessageError(reporter.lang('manifestNameIllegalChars')); } // cannot equal node_modules or favicon.ico const lower = name.toLowerCase(); if (lower === 'node_modules' || lower === 'favicon.ico') { - throw new TypeError(reporter.lang('manifestNameBlacklisted')); + throw new MessageError(reporter.lang('manifestNameBlacklisted')); } } @@ -92,7 +93,7 @@ export default function(info: Object, isRoot: boolean, reporter: Reporter, warn: for (const key of strings) { const val = info[key]; if (val && typeof val !== 'string') { - throw new TypeError(reporter.lang('manifestStringExpected', key)); + throw new MessageError(reporter.lang('manifestStringExpected', key)); } } From 965811cd1b4357a04fe33607aa21d5e64cccca53 Mon Sep 17 00:00:00 2001 From: Troy DeMonbreun Date: Sat, 15 Oct 2016 12:00:03 -0500 Subject: [PATCH 3/8] Preserve any subsequent '/' after user/repo split (#1023) --- src/resolvers/exotics/hosted-git-resolver.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resolvers/exotics/hosted-git-resolver.js b/src/resolvers/exotics/hosted-git-resolver.js index 1b425a44de..35e3175e25 100644 --- a/src/resolvers/exotics/hosted-git-resolver.js +++ b/src/resolvers/exotics/hosted-git-resolver.js @@ -22,9 +22,9 @@ export function explodeHostedGitFragment(fragment: string, reporter: Reporter): const userParts = fragment.split('/'); - if (userParts.length === 2) { + if (userParts.length >= 2) { const user = userParts.shift(); - const repoParts = userParts.shift().split('#'); + const repoParts = userParts.join('/').split('#'); if (repoParts.length <= 2) { return { From 73251d4ffd983e6001a58c69fa988bc15690c023 Mon Sep 17 00:00:00 2001 From: Tommy Graves Date: Sat, 15 Oct 2016 13:08:30 -0400 Subject: [PATCH 4/8] Fixed config resolution (#1024) --- src/registries/yarn-registry.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/registries/yarn-registry.js b/src/registries/yarn-registry.js index 4747500895..110391e781 100644 --- a/src/registries/yarn-registry.js +++ b/src/registries/yarn-registry.js @@ -56,14 +56,22 @@ export default class YarnRegistry extends NpmRegistry { homeConfig: Object; getOption(key: string): mixed { - let val = this.config[key] || this.registries.npm.getOption(npmMap[key]); + let val = this.config[key]; - // if we have no yarn option for this or have used a default then use the npm - // value if it exists - if (!val || val === DEFAULTS[key]) { - val = this.registries.npm.getOption(key) || val; + // if this isn't set in a yarn config, then use npm + if (typeof val === 'undefined') { + val = this.registries.npm.getOption(npmMap[key]); } + if (typeof val === 'undefined') { + val = this.registries.npm.getOption(key); + } + + // if this isn't set in a yarn config or npm config, then use the default (or undefined) + if (typeof val === 'undefined') { + val = DEFAULTS[key]; + } + return val; } From b0611a6ee5220b0b1e955b271b6140640158f96c Mon Sep 17 00:00:00 2001 From: Tommy Graves Date: Sat, 15 Oct 2016 13:09:08 -0400 Subject: [PATCH 5/8] Add support for strict-ssl config (#1025) * Fixed config resolution * Added support for disabling strict ssl * Fixed flow errors --- src/config.js | 1 + src/registries/yarn-registry.js | 1 + src/util/request-manager.js | 11 ++++++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/config.js b/src/config.js index a5ec06a610..43cb91346d 100644 --- a/src/config.js +++ b/src/config.js @@ -183,6 +183,7 @@ export default class Config { userAgent: String(this.getOption('user-agent')), httpProxy: String(this.getOption('proxy') || ''), httpsProxy: String(this.getOption('https-proxy') || ''), + strictSSL: Boolean(this.getOption('strict-ssl')), }); } diff --git a/src/registries/yarn-registry.js b/src/registries/yarn-registry.js index 110391e781..95492c5cf9 100644 --- a/src/registries/yarn-registry.js +++ b/src/registries/yarn-registry.js @@ -26,6 +26,7 @@ export const DEFAULTS = { 'ignore-scripts': false, 'ignore-optional': false, registry: YARN_REGISTRY, + 'strict-ssl': true, 'user-agent': [ `yarn/${pkg.version}`, 'npm/?', diff --git a/src/util/request-manager.js b/src/util/request-manager.js index c8939662ef..0e66a11e87 100644 --- a/src/util/request-manager.js +++ b/src/util/request-manager.js @@ -40,6 +40,7 @@ type RequestParams = { proxy?: string, encoding?: ?string, forever?: boolean, + strictSSL?: boolean, headers?: { [name: string]: string }, @@ -67,6 +68,7 @@ export default class RequestManager { this.captureHar = false; this.httpsProxy = null; this.httpProxy = null; + this.strictSSL = true; this.userAgent = ''; this.reporter = reporter; this.running = 0; @@ -82,6 +84,7 @@ export default class RequestManager { running: number; httpsProxy: ?string; httpProxy: ?string; + strictSSL: boolean; offlineQueue: Array; queue: Array; max: number; @@ -98,6 +101,7 @@ export default class RequestManager { captureHar?: boolean, httpProxy?: string, httpsProxy?: string, + strictSSL?: boolean, }) { if (opts.userAgent != null) { this.userAgent = opts.userAgent; @@ -118,6 +122,10 @@ export default class RequestManager { if (opts.httpsProxy != null) { this.httpsProxy = opts.httpsProxy; } + + if (opts.strictSSL !== null && typeof opts.strictSSL !== 'undefined') { + this.strictSSL = opts.strictSSL; + } } /** @@ -155,7 +163,8 @@ export default class RequestManager { params.method = params.method || 'GET'; params.forever = true; params.retryAttempts = 0; - + params.strictSSL = this.strictSSL; + params.headers = Object.assign({ 'User-Agent': this.userAgent, }, params.headers); From a0c50f5d24aac8bd63f1564ae69b795c03bc3467 Mon Sep 17 00:00:00 2001 From: Jake Basile Date: Sat, 15 Oct 2016 12:37:12 -0500 Subject: [PATCH 6/8] add option to disable emoji (#922) --- src/cli/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cli/index.js b/src/cli/index.js index 9d359e5832..6b13a508f2 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -58,6 +58,10 @@ commander.option( '--mutex [:specifier]', 'use a mutex to ensure only one yarn instance is executing', ); +commander.option( + '--no-emoji', + 'disable emoji in output', +); // get command name let commandName: string = args.shift() || ''; @@ -159,7 +163,7 @@ if (commander.json) { Reporter = JSONReporter; } const reporter = new Reporter({ - emoji: process.stdout.isTTY && process.platform === 'darwin', + emoji: commander.emoji && process.stdout.isTTY && process.platform === 'darwin', }); reporter.initPeakMemoryCounter(); From a3eaa55e4b39813c83f3cf86f3611438b59e922f Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 15 Oct 2016 10:54:39 -0700 Subject: [PATCH 7/8] Support npmrc for private registries and auth (#839) * Get npm registry and auth from .npmrc file * Fetch tarball through registry Needed if the registry requires auth to get tarballs * Support .npmrc auth for write commands * Support basic auth * Attempt to fix flow * Implement default request method for BaseRegistry Needed for tarball fetching now that it is going through the registry * Fix flow bug * Clean up * Fix requestUrl * Broken by #712, which changed from url.resolve to url.format. We need resolve in the case where the pathname is really a full url (when requesting tarballs). So only add the trailing / if it is not there. * Only add authorization if the request is going to the registry (not random tarballs). * Check scoped always-auth option * Remove unused function (bad rebase) --- src/cli/commands/login.js | 15 +++++-- src/cli/commands/owner.js | 4 +- src/cli/commands/publish.js | 2 +- src/cli/commands/tag.js | 9 ++-- src/config.js | 8 +--- src/fetchers/tarball-fetcher.js | 4 +- src/registries/base-registry.js | 12 +++-- src/registries/index.js | 5 +++ src/registries/npm-registry.js | 78 ++++++++++++++++++++++++++++----- src/registries/yarn-registry.js | 2 +- src/reporters/lang/en.js | 1 + src/util/misc.js | 8 ++++ 12 files changed, 115 insertions(+), 33 deletions(-) diff --git a/src/cli/commands/login.js b/src/cli/commands/login.js index 1d4edb086e..b36017a691 100644 --- a/src/cli/commands/login.js +++ b/src/cli/commands/login.js @@ -33,12 +33,21 @@ async function getCredentials(config: Config, reporter: Reporter): Promise Promise > { + const auth = config.registries.npm.getAuth(name); + if (auth) { + config.registries.npm.setToken(auth); + return function revoke(): Promise { + reporter.info(reporter.lang('notRevokingConfigToken')); + return Promise.resolve(); + }; + } + const env = process.env.YARN_AUTH_TOKEN || process.env.KPM_AUTH_TOKEN || process.env.NPM_AUTH_TOKEN; if (env) { - config.registries.npm.setToken(env); + config.registries.npm.setToken(`Bearer ${env}`); return function revoke(): Promise { reporter.info(reporter.lang('notRevokingEnvToken')); return Promise.resolve(); @@ -80,7 +89,7 @@ export async function getToken(config: Config, reporter: Reporter): Promise< reporter.success(reporter.lang('loggedIn')); const token = res.token; - config.registries.npm.setToken(token); + config.registries.npm.setToken(`Bearer ${token}`); return async function revoke(): Promise { reporter.success(reporter.lang('revokedToken')); diff --git a/src/cli/commands/owner.js b/src/cli/commands/owner.js index 5c0b2569c3..07d0f58d1d 100644 --- a/src/cli/commands/owner.js +++ b/src/cli/commands/owner.js @@ -34,7 +34,7 @@ export async function mutate( const msgs = buildMessages(username, name); reporter.step(1, 3, reporter.lang('loggingIn')); - const revoke = await getToken(config, reporter); + const revoke = await getToken(config, reporter, name); reporter.step(2, 3, msgs.info); const user = await config.registries.npm.request(`-/user/org.couchdb.user:${username}`); @@ -160,7 +160,7 @@ export const {run, setFlags} = buildSubCommands('owner', { const name = await getName(args, config); reporter.step(1, 3, reporter.lang('loggingIn')); - const revoke = await getToken(config, reporter); + const revoke = await getToken(config, reporter, name); reporter.step(2, 3, reporter.lang('ownerGetting', name)); const pkg = await config.registries.npm.request(name); diff --git a/src/cli/commands/publish.js b/src/cli/commands/publish.js index 22807cc9e6..f0a67d7996 100644 --- a/src/cli/commands/publish.js +++ b/src/cli/commands/publish.js @@ -138,7 +138,7 @@ export async function run( // reporter.step(2, 4, reporter.lang('loggingIn')); - const revoke = await getToken(config, reporter); + const revoke = await getToken(config, reporter, pkg.name); // reporter.step(3, 4, reporter.lang('publishing')); diff --git a/src/cli/commands/tag.js b/src/cli/commands/tag.js index 9be0a9ff45..8e66021212 100644 --- a/src/cli/commands/tag.js +++ b/src/cli/commands/tag.js @@ -50,7 +50,7 @@ export const {run, setFlags, examples} = buildSubCommands('tag', { const tag = args.shift(); reporter.step(1, 3, reporter.lang('loggingIn')); - const revoke = await getToken(config, reporter); + const revoke = await getToken(config, reporter, name); reporter.step(2, 3, reporter.lang('creatingTag', tag, range)); const result = await config.registries.npm.request( @@ -91,7 +91,7 @@ export const {run, setFlags, examples} = buildSubCommands('tag', { const tag = args.shift(); reporter.step(1, 3, reporter.lang('loggingIn')); - const revoke = await getToken(config, reporter); + const revoke = await getToken(config, reporter, name); reporter.step(2, 3, reporter.lang('deletingTags')); const result = await config.registries.npm.request(`-/package/${name}/dist-tags/${encodeURI(tag)}`, { @@ -120,11 +120,12 @@ export const {run, setFlags, examples} = buildSubCommands('tag', { flags: Object, args: Array, ): Promise { + const name = await getName(args, config); + reporter.step(1, 3, reporter.lang('loggingIn')); - const revoke = await getToken(config, reporter); + const revoke = await getToken(config, reporter, name); reporter.step(2, 3, reporter.lang('gettingTags')); - const name = await getName(args, config); const tags = await config.registries.npm.request(`-/package/${name}/dist-tags`); if (tags) { diff --git a/src/config.js b/src/config.js index 43cb91346d..2b6ec61b4d 100644 --- a/src/config.js +++ b/src/config.js @@ -1,8 +1,7 @@ /* @flow */ -import type {RegistryNames} from './registries/index.js'; +import type {RegistryNames, ConfigRegistries} from './registries/index.js'; import type {Reporter} from './reporters/index.js'; -import type Registry from './registries/base-registry.js'; import type {Manifest, PackageRemote} from './types.js'; import normalizeManifest from './util/normalize-manifest/index.js'; import {MessageError} from './errors.js'; @@ -42,7 +41,6 @@ type PackageMetadata = { package: Manifest }; - type RootManifests = { [registryName: RegistryNames]: { loc: string, @@ -60,10 +58,6 @@ function sortObject(object: Object): Object { return sortedObject; } -export type ConfigRegistries = { - [name: RegistryNames]: Registry -}; - export default class Config { constructor(reporter: Reporter) { this.constraintResolver = new ConstraintResolver(this, reporter); diff --git a/src/fetchers/tarball-fetcher.js b/src/fetchers/tarball-fetcher.js index 460bc7d14b..557382d849 100644 --- a/src/fetchers/tarball-fetcher.js +++ b/src/fetchers/tarball-fetcher.js @@ -130,9 +130,9 @@ export default class TarballFetcher extends BaseFetcher { fetchFromExternal(): Promise { const {reference: ref} = this; + const registry = this.config.registries[this.registry]; - return this.config.requestManager.request({ - url: ref, + return registry.request(ref, { headers: { 'Accept-Encoding': 'gzip', 'Accept': 'application/octet-stream', diff --git a/src/registries/base-registry.js b/src/registries/base-registry.js index 4b89e31c97..e784889e0b 100644 --- a/src/registries/base-registry.js +++ b/src/registries/base-registry.js @@ -1,7 +1,8 @@ /* @flow */ import type RequestManager, {RequestMethods} from '../util/request-manager.js'; -import type Config, {ConfigRegistries} from '../config.js'; +import type Config from '../config.js'; +import type {ConfigRegistries} from './index.js'; import {removePrefix} from '../util/misc.js'; const objectPath = require('object-path'); @@ -11,6 +12,8 @@ export type RegistryRequestOptions = { method?: RequestMethods, auth?: Object, body?: mixed, + buffer?: bool, + process?: Function }; export type CheckOutdatedReturn = Promise<{ @@ -73,8 +76,11 @@ export default class BaseRegistry { return Promise.reject(new Error('unimplemented')); } - request(pathname: string, opts?: RegistryRequestOptions = {}): Promise { - return Promise.reject(new Error('unimplemented')); + request(pathname: string, opts?: RegistryRequestOptions = {}): Promise<*> { + return this.requestManager.request({ + url: pathname, + ...opts, + }); } async init(): Promise { diff --git a/src/registries/index.js b/src/registries/index.js index 83e907aa61..9956d2276e 100644 --- a/src/registries/index.js +++ b/src/registries/index.js @@ -13,3 +13,8 @@ export const registries = { export const registryNames = Object.keys(registries); export type RegistryNames = $Keys; +export type ConfigRegistries = { + npm: NpmRegistry, + yarn: YarnRegistry, + bower: BowerRegistry +}; diff --git a/src/registries/npm-registry.js b/src/registries/npm-registry.js index 872efd2138..d76c4b894d 100644 --- a/src/registries/npm-registry.js +++ b/src/registries/npm-registry.js @@ -2,11 +2,12 @@ import type RequestManager from '../util/request-manager.js'; import type {RegistryRequestOptions, CheckOutdatedReturn} from './base-registry.js'; -import type Config, {ConfigRegistries} from '../config.js'; +import type Config from '../config.js'; +import type {ConfigRegistries} from './index.js'; import * as fs from '../util/fs.js'; import NpmResolver from '../resolvers/registries/npm-resolver.js'; import Registry from './base-registry.js'; -import {removeSuffix} from '../util/misc.js'; +import {addSuffix} from '../util/misc'; const defaults = require('defaults'); const userHome = require('user-home'); @@ -14,6 +15,8 @@ const path = require('path'); const url = require('url'); const ini = require('ini'); +const DEFAULT_REGISTRY = 'https://registry.npmjs.org/'; + function getGlobalPrefix(): string { if (process.env.PREFIX) { return process.env.PREFIX; @@ -46,24 +49,26 @@ export default class NpmRegistry extends Registry { return name.replace('/', '%2f'); } - request(pathname: string, opts?: RegistryRequestOptions = {}): Promise { - const registry = removeSuffix(String(this.registries.yarn.getOption('registry')), '/'); + request(pathname: string, opts?: RegistryRequestOptions = {}): Promise<*> { + const registry = addSuffix(this.getRegistry(pathname), '/'); + const requestUrl = url.resolve(registry, pathname); + const alwaysAuth = this.getScopedOption(registry.replace(/^https?:/, ''), 'always-auth') + || this.getOption('always-auth'); const headers = {}; - if (this.token) { - headers.authorization = `Bearer ${this.token}`; + if (this.token || (alwaysAuth && requestUrl.startsWith(registry))) { + headers.authorization = this.getAuth(pathname); } - // $FlowFixMe : https://github.com/facebook/flow/issues/908 - const requestUrl = url.format(`${registry}/${pathname}`); - return this.requestManager.request({ url: requestUrl, method: opts.method, body: opts.body, auth: opts.auth, headers, - json: true, + json: !opts.buffer, + buffer: opts.buffer, + process: opts.process, gzip: true, }); } @@ -129,4 +134,57 @@ export default class NpmRegistry extends Registry { defaults(this.config, config); } } + + getScope(packageName: string): string { + return !packageName || packageName[0] !== '@' ? '' : packageName.split(/\/|%2f/)[0]; + } + + getRegistry(packageName: string): string { + // Try scoped registry, and default registry + for (const scope of [this.getScope(packageName), '']) { + const registry = this.getScopedOption(scope, 'registry') + || this.registries.yarn.getScopedOption(scope, 'registry'); + if (registry) { + return String(registry); + } + } + + return DEFAULT_REGISTRY; + } + + getAuth(packageName: string): string { + if (this.token) { + return this.token; + } + + for (let registry of [this.getRegistry(packageName), '', DEFAULT_REGISTRY]) { + registry = registry.replace(/^https?:/, ''); + + // Check for bearer token. + let auth = this.getScopedOption(registry, '_authToken'); + if (auth) { + return `Bearer ${String(auth)}`; + } + + // Check for basic auth token. + auth = this.getScopedOption(registry, '_auth'); + if (auth) { + return `Basic ${String(auth)}`; + } + + // Check for basic username/password auth. + const username = this.getScopedOption(registry, 'username'); + const password = this.getScopedOption(registry, '_password'); + if (username && password) { + const pw = new Buffer(String(password), 'base64').toString(); + return 'Basic ' + new Buffer(String(username) + ':' + pw).toString('base64'); + } + } + + return ''; + } + + getScopedOption(scope: string, option: string): mixed { + return this.getOption(scope + (scope ? ':' : '') + option); + } } diff --git a/src/registries/yarn-registry.js b/src/registries/yarn-registry.js index 95492c5cf9..56ecb07303 100644 --- a/src/registries/yarn-registry.js +++ b/src/registries/yarn-registry.js @@ -1,7 +1,7 @@ /* @flow */ import type RequestManager from '../util/request-manager.js'; -import type {ConfigRegistries} from '../config.js'; +import type {ConfigRegistries} from './index.js'; import {YARN_REGISTRY} from '../constants.js'; import NpmRegistry from './npm-registry.js'; import stringify from '../lockfile/stringify.js'; diff --git a/src/reporters/lang/en.js b/src/reporters/lang/en.js index f3aa2c6d3e..85f0f12059 100644 --- a/src/reporters/lang/en.js +++ b/src/reporters/lang/en.js @@ -182,6 +182,7 @@ const messages = { loggingIn: 'Logging in', loggedIn: 'Logged in.', notRevokingEnvToken: 'Not revoking login token, specified via environment variable.', + notRevokingConfigToken: 'Not revoking login token, specified via config file.', noTokenToRevoke: 'No login token to revoke.', revokingToken: 'Revoking token', revokedToken: 'Revoked login token.', diff --git a/src/util/misc.js b/src/util/misc.js index a92a57c106..af102cf675 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -30,3 +30,11 @@ export function removeSuffix(pattern: string, suffix: string): string { return pattern; } + +export function addSuffix(pattern: string, suffix: string): string { + if (!pattern.endsWith(suffix)) { + return pattern + suffix; + } + + return pattern; +} From 7a18523163867b29a0014c2073aae0087201931f Mon Sep 17 00:00:00 2001 From: Carl Henrik Lunde Date: Sat, 15 Oct 2016 19:59:38 +0200 Subject: [PATCH 8/8] Custom CA trust store with config option 'cafile' (#736) Add support for local registries with TLS/SSL certificates issued by private CAs certificates or self-signed certificates. References #606, #631 --- __tests__/__mocks__/request.js | 6 +- __tests__/fixtures/certificates/cacerts.pem | 57 +++++++++++++++++++ .../fixtures/certificates/server-cert.pem | 21 +++++++ .../fixtures/certificates/server-key.pem | 28 +++++++++ __tests__/util/request-manager.js | 36 ++++++++++++ src/config.js | 4 +- src/util/request-manager.js | 22 +++++++ 7 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 __tests__/fixtures/certificates/cacerts.pem create mode 100644 __tests__/fixtures/certificates/server-cert.pem create mode 100644 __tests__/fixtures/certificates/server-key.pem create mode 100644 __tests__/util/request-manager.js diff --git a/__tests__/__mocks__/request.js b/__tests__/__mocks__/request.js index 23909bd5f1..7acf72febc 100644 --- a/__tests__/__mocks__/request.js +++ b/__tests__/__mocks__/request.js @@ -49,11 +49,13 @@ const httpMock = { request(options: Object, callback?: ?Function): ClientRequest { const alias = getRequestAlias(options); const loc = path.join(CACHE_DIR, `${alias}.bin`); + // allow the client to bypass the local fs fixture cache by adding nocache to the query string + const allowCache = options.uri.href.indexOf('nocache') == -1; // TODO better way to do this - const httpModule = options.port === 443 ? https : http; + const httpModule = options.uri.href.startsWith('https:') ? https : http; - if (fs.existsSync(loc)) { + if (allowCache && fs.existsSync(loc)) { // cached options.agent = null; options.socketPath = null; diff --git a/__tests__/fixtures/certificates/cacerts.pem b/__tests__/fixtures/certificates/cacerts.pem new file mode 100644 index 0000000000..9c0798c3d9 --- /dev/null +++ b/__tests__/fixtures/certificates/cacerts.pem @@ -0,0 +1,57 @@ +the first CA is a random CA not used in this test, to verify +that multiple CAs work +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIRANOxciY0IzLc9AUoUSrsnGowDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTYxMDA2MTU0MzU1 +WhcNMjExMDA2MTU0MzU1WjBKMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDEjMCEGA1UEAxMaTGV0J3MgRW5jcnlwdCBBdXRob3JpdHkgWDMwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc0wzwWuUuR7dyXTeDs2hjMOrX +NSYZJeG9vjXxcJIvt7hLQQWrqZ41CFjssSrEaIcLo+N15Obzp2JxunmBYB/XkZqf +89B4Z3HIaQ6Vkc/+5pnpYDxIzH7KTXcSJJ1HG1rrueweNwAcnKx7pwXqzkrrvUHl +Npi5y/1tPJZo3yMqQpAMhnRnyH+lmrhSYRQTP2XpgofL2/oOVvaGifOFP5eGr7Dc +Gu9rDZUWfcQroGWymQQ2dYBrrErzG5BJeC+ilk8qICUpBMZ0wNAxzY8xOJUWuqgz +uEPxsR/DMH+ieTETPS02+OP88jNquTkxxa/EjQ0dZBYzqvqEKbbUC8DYfcOTAgMB +AAGjggFnMIIBYzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADBU +BgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEBATAwMC4GCCsGAQUFBwIB +FiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3JnMB0GA1UdDgQWBBSo +SmpjBH3duubRObemRWXv86jsoTAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3Js +LnJvb3QteDEubGV0c2VuY3J5cHQub3JnMHIGCCsGAQUFBwEBBGYwZDAwBggrBgEF +BQcwAYYkaHR0cDovL29jc3Aucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcvMDAGCCsG +AQUFBzAChiRodHRwOi8vY2VydC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZy8wHwYD +VR0jBBgwFoAUebRZ5nu25eQBc4AIiMgaWPbpm24wDQYJKoZIhvcNAQELBQADggIB +ABnPdSA0LTqmRf/Q1eaM2jLonG4bQdEnqOJQ8nCqxOeTRrToEKtwT++36gTSlBGx +A/5dut82jJQ2jxN8RI8L9QFXrWi4xXnA2EqA10yjHiR6H9cj6MFiOnb5In1eWsRM +UM2v3e9tNsCAgBukPHAg1lQh07rvFKm/Bz9BCjaxorALINUfZ9DD64j2igLIxle2 +DPxW8dI/F2loHMjXZjqG8RkqZUdoxtID5+90FgsGIfkMpqgRS05f4zPbCEHqCXl1 +eO5HyELTgcVlLXXQDgAWnRzut1hFJeczY1tjQQno6f6s+nMydLN26WuU4s3UYvOu +OsUxRlJu7TSRHqDC3lSE5XggVkzdaPkuKGQbGpny+01/47hfXXNB7HntWNZ6N2Vw +p7G6OfY+YQrZwIaQmhrIqJZuigsrbe3W+gdn5ykE9+Ky0VgVUsfxo52mwFYs1JKY +2PGDuWx8M6DlS6qQkvHaRUo0FMd8TsSlbF0/v965qGFKhSDeQoMpYnwcmQilRh/0 +ayLThlHLN81gSkJjVrPI0Y8xCVPB4twb1PFUd2fPM3sA1tJ83sZ5v8vgFv2yofKR +PB0t6JzUA81mSqM3kxl5e+IZwhYAyO0OTg3/fs8HqGTNKd9BqoUwSRBzp06JMg5b +rUCGwbCUDI0mxadJ3Bz4WxR6fyNpBK2yAinWEsikxqEt +-----END CERTIFICATE----- +comments should be stripped +-----BEGIN CERTIFICATE----- +MIIDfzCCAmegAwIBAgIJAM4nWEf/MeHVMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV +BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg +Q29tcGFueSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNjEwMTIwNTQ3NDla +Fw0yNjEwMTAwNTQ3NDlaMFYxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0 +IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQxEjAQBgNVBAMMCWxv +Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALVCI5Ma6AR0 +oDv/OqactDMR4pA6CZJnNDYbjRIkjsXi3pXuAXbQ8J/lD7EKNhu8wxrM0PZvdE1s +ERjgFmZlYYTJkoDQUr9HagwAhAHrUuAq6FsmogLOl1L4QmeddCxExLdTePJAvVxc ++fr3mk7I9Kt5FV1kDOZMyGqkLwKvcGXjx4ue+ZMgQZjWoU4Om7ktA57siAUkrxQg +SonEnlSAlEQy6/wRSR2XQ85e+o1JHMIti4+h/Soo4BmHetec7zKvHjsL1kuT9s43 +YiVlj660cc/Rmqt61OqPrumeKeVLRLmOD71l0yjU8QEwvAvvzFwdTZQSPUVGb4Je +9Apxn7MNnKMCAwEAAaNQME4wHQYDVR0OBBYEFFzPIkvacjl8MFC6eRCwSTyW5z9c +MB8GA1UdIwQYMBaAFFzPIkvacjl8MFC6eRCwSTyW5z9cMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQELBQADggEBAKN9RXWgMuEwhLYzG///duWTcIC7UfamGDVlezxa +BkR+VGkRSlo4v+MaZKscG2D/NGh4PP5PQZr7okLQY6MIKmcFkIN1BDziEVfKICFs +AIrcajDLyHaLcAmZv2VJe1sz12pmGZG7uTOncMAngZoNDNI0f4djzvmRd9nn4yGo +o8vhLMzgdXhp3T7yaCjpbpZUd1bnggJXz9MO76EBcCkS3+HcRRr/0KMD/tk5tZUx +s35hnRrJi9HvhFjZbJA/KGG8DJp4oyzEfbufmUJ7OdbMn++W3NtG1BrRhbKmH5og +tpfAr88iJ6BFaXV/4JIxc4Fga8dvjJR3ueh5pT2om/rUzkQ= +-----END CERTIFICATE----- +another comment diff --git a/__tests__/fixtures/certificates/server-cert.pem b/__tests__/fixtures/certificates/server-cert.pem new file mode 100644 index 0000000000..3b3ce804b0 --- /dev/null +++ b/__tests__/fixtures/certificates/server-cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDfzCCAmegAwIBAgIJAM4nWEf/MeHVMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV +BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg +Q29tcGFueSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNjEwMTIwNTQ3NDla +Fw0yNjEwMTAwNTQ3NDlaMFYxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0 +IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQxEjAQBgNVBAMMCWxv +Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALVCI5Ma6AR0 +oDv/OqactDMR4pA6CZJnNDYbjRIkjsXi3pXuAXbQ8J/lD7EKNhu8wxrM0PZvdE1s +ERjgFmZlYYTJkoDQUr9HagwAhAHrUuAq6FsmogLOl1L4QmeddCxExLdTePJAvVxc ++fr3mk7I9Kt5FV1kDOZMyGqkLwKvcGXjx4ue+ZMgQZjWoU4Om7ktA57siAUkrxQg +SonEnlSAlEQy6/wRSR2XQ85e+o1JHMIti4+h/Soo4BmHetec7zKvHjsL1kuT9s43 +YiVlj660cc/Rmqt61OqPrumeKeVLRLmOD71l0yjU8QEwvAvvzFwdTZQSPUVGb4Je +9Apxn7MNnKMCAwEAAaNQME4wHQYDVR0OBBYEFFzPIkvacjl8MFC6eRCwSTyW5z9c +MB8GA1UdIwQYMBaAFFzPIkvacjl8MFC6eRCwSTyW5z9cMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQELBQADggEBAKN9RXWgMuEwhLYzG///duWTcIC7UfamGDVlezxa +BkR+VGkRSlo4v+MaZKscG2D/NGh4PP5PQZr7okLQY6MIKmcFkIN1BDziEVfKICFs +AIrcajDLyHaLcAmZv2VJe1sz12pmGZG7uTOncMAngZoNDNI0f4djzvmRd9nn4yGo +o8vhLMzgdXhp3T7yaCjpbpZUd1bnggJXz9MO76EBcCkS3+HcRRr/0KMD/tk5tZUx +s35hnRrJi9HvhFjZbJA/KGG8DJp4oyzEfbufmUJ7OdbMn++W3NtG1BrRhbKmH5og +tpfAr88iJ6BFaXV/4JIxc4Fga8dvjJR3ueh5pT2om/rUzkQ= +-----END CERTIFICATE----- diff --git a/__tests__/fixtures/certificates/server-key.pem b/__tests__/fixtures/certificates/server-key.pem new file mode 100644 index 0000000000..adaa006d23 --- /dev/null +++ b/__tests__/fixtures/certificates/server-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1QiOTGugEdKA7 +/zqmnLQzEeKQOgmSZzQ2G40SJI7F4t6V7gF20PCf5Q+xCjYbvMMazND2b3RNbBEY +4BZmZWGEyZKA0FK/R2oMAIQB61LgKuhbJqICzpdS+EJnnXQsRMS3U3jyQL1cXPn6 +95pOyPSreRVdZAzmTMhqpC8Cr3Bl48eLnvmTIEGY1qFODpu5LQOe7IgFJK8UIEqJ +xJ5UgJREMuv8EUkdl0POXvqNSRzCLYuPof0qKOAZh3rXnO8yrx47C9ZLk/bON2Il +ZY+utHHP0ZqretTqj67pninlS0S5jg+9ZdMo1PEBMLwL78xcHU2UEj1FRm+CXvQK +cZ+zDZyjAgMBAAECggEBAKRSKlgRG2f2ptDdaDlldMOboi6oPsc3wpCO14wsEjb5 +nlqDo1YowwvhqCESpcztil7AcWwHzILnxnQrqoL3w7mS17rpoSqBPnVU/leTE9Xf +cDg6RMOQsITqRaETkB8V1NRx2wKbiE+0hndrgruL2KufIKxCqKMb1tE+uNORYq8q +kFIBvUnhjLFzepZ749wYTx2Tkdrfe3Y3sW2a17LRr43s0Z41skX/9iz7GdIW8OLD +Edfw7COvlxZQjtmLqwmoFd5YjOo8u6C6kr74H8OLplGN35cPc+L57kFqKvxV42Lq +y+EirIZ3b8uItsuOPIf92snpfK9CmdwTCx5lrMmpqjkCgYEA8QKr8xmI3fhUxPD8 +9PYQlIa+pKMtX1sKgS3sA2ZD9bwRC9HmkXhYTYsrakbX2VaGe1dmc/7KMaNaVkrT ++BfMk9SACnoul7dXzV6amzM7AdrZYkJgejbmSJtYGz3YnHQTz4ipaffHfsKoWdvs +LdvPgBBdfbYsvGxB5ya9kvcsLyUCgYEAwIgawo1mU5KC8BXOvzKSKJ6vTULtnF6d +zf1OGv6atrS36Sg3ycqPL+kUy44bl1ckjh+VMp6rty/5Z/K1PKkyNniznxj9Epwu +4O/aI/Q7SrKZmjH64gXWBmyj8doNHM3nFF9mSIjRromovuUeOcpFlATBCe0Pxpxt +sGlhjnrMVicCgYBvIwlBv9uiaBpG+s3a9AEvTHdrGigZGbVdXlzAMI9UKNY/ehp1 +qGYn0+5AQszUVxcKl4ISKUL54tcMhdL7S5Y18T7eFfuYUJ53gJGQ0e366/1kVzGA +CgLlJmVZoopZkxlzkRR2XiErbf4N+eEOQJeN+X3zM2ert8woGHBA7iP81QKBgBi0 +3oo8zvbGhFr+0WsjuDHSOzi07/zy/1khulYoef4cLsWSzaXtgnZpeKuubsf6/Mvo +LaMzTWHSnDTEppFEPRdUYeh2snMi67kdzmZyvvEU/jUVWNaMXSyx4E/25Vve6Fpq +65s/Q3kcXTUx/bD4zfjyqzr02uNny4Op4kUAaRxdAoGBAK0t4FVXTOWIf6xxgGMZ +BBkNu6N1n/Io3pliCpYpZrBE/6NEOaEeniyaM0BbAatR+SemaYICnEj31ueonlIs +PLUi9H177Cnu2DZwBMaWx78r260RIxF+bcdTBjQ51fXt7/44okFjHmCcRVA1q/Rc +WdI83+niMywS/XVfLAMhnFWR +-----END PRIVATE KEY----- diff --git a/__tests__/util/request-manager.js b/__tests__/util/request-manager.js new file mode 100644 index 0000000000..340fcc9f18 --- /dev/null +++ b/__tests__/util/request-manager.js @@ -0,0 +1,36 @@ +/* @flow */ +/* eslint max-len: 0 */ + +import {NoopReporter} from '../../src/reporters/index.js'; +import Config from '../../src/config.js'; +import type {ConfigOptions} from '../../src/config.js'; +import * as fs from '../../src/util/fs.js'; + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; + +const https = require('https'); +const path = require('path'); + +async function createConfig(opts: ConfigOptions = {}): Promise { + const config = new Config(new NoopReporter()); + await config.init(opts); + return config; +} + +test('RequestManager.request with cafile', async () => { + let body; + const options = { + key: await fs.readFile(path.join(__dirname, '..', 'fixtures', 'certificates', 'server-key.pem')), + cert: await fs.readFile(path.join(__dirname, '..', 'fixtures', 'certificates', 'server-cert.pem')), + }; + const server = https.createServer(options, (req, res) => { res.end('ok'); }); + try { + server.listen(0); + const config = await createConfig({'cafile': path.join(__dirname, '..', 'fixtures', 'certificates', 'cacerts.pem')}); + const port = server.address().port; + body = await config.requestManager.request({url: `https://localhost:${port}/?nocache`, headers: {Connection: 'close'}}); + } finally { + server.close(); + } + expect(body).toBe('ok'); +}); diff --git a/src/config.js b/src/config.js index 2b6ec61b4d..8eb9d75762 100644 --- a/src/config.js +++ b/src/config.js @@ -17,7 +17,7 @@ const invariant = require('invariant'); const path = require('path'); const url = require('url'); -type ConfigOptions = { +export type ConfigOptions = { cwd?: ?string, cacheFolder?: ?string, tempFolder?: ?string, @@ -29,6 +29,7 @@ type ConfigOptions = { captureHar?: boolean, ignorePlatform?: boolean, ignoreEngines?: boolean, + cafile?: ?string, // Loosely compare semver for invalid cases like "0.01.0" looseSemver?: ?boolean, @@ -178,6 +179,7 @@ export default class Config { httpProxy: String(this.getOption('proxy') || ''), httpsProxy: String(this.getOption('https-proxy') || ''), strictSSL: Boolean(this.getOption('strict-ssl')), + cafile: String(opts.cafile || this.getOption('cafile') || ''), }); } diff --git a/src/util/request-manager.js b/src/util/request-manager.js index 0e66a11e87..662882d863 100644 --- a/src/util/request-manager.js +++ b/src/util/request-manager.js @@ -13,6 +13,7 @@ import type RequestT from 'request'; const RequestCaptureHar = require('request-capture-har'); const invariant = require('invariant'); const url = require('url'); +const fs = require('fs'); const successHosts = map(); const controlOffline = network.isOffline(); @@ -39,6 +40,7 @@ type RequestParams = { body?: mixed, proxy?: string, encoding?: ?string, + ca?: Array, forever?: boolean, strictSSL?: boolean, headers?: { @@ -67,6 +69,7 @@ export default class RequestManager { this.offlineQueue = []; this.captureHar = false; this.httpsProxy = null; + this.ca = null; this.httpProxy = null; this.strictSSL = true; this.userAgent = ''; @@ -85,6 +88,7 @@ export default class RequestManager { httpsProxy: ?string; httpProxy: ?string; strictSSL: boolean; + ca: ?Array; offlineQueue: Array; queue: Array; max: number; @@ -102,6 +106,7 @@ export default class RequestManager { httpProxy?: string, httpsProxy?: string, strictSSL?: boolean, + cafile?: string, }) { if (opts.userAgent != null) { this.userAgent = opts.userAgent; @@ -126,6 +131,19 @@ export default class RequestManager { if (opts.strictSSL !== null && typeof opts.strictSSL !== 'undefined') { this.strictSSL = opts.strictSSL; } + + if (opts.cafile != null && opts.cafile != '') { + // The CA bundle file can contain one or more certificates with comments/text between each PEM block. + // tls.connect wants an array of certificates without any comments/text, so we need to split the string + // and strip out any text in between the certificates + try { + const bundle = fs.readFileSync(opts.cafile).toString(); + const hasPemPrefix = (block) => block.startsWith('-----BEGIN '); + this.ca = bundle.split(/(-----BEGIN .*\r?\n[^-]+\r?\n--.*)/).filter(hasPemPrefix); + } catch (err) { + this.reporter.error(`Could not open cafile: ${err.message}`); + } + } } /** @@ -340,6 +358,10 @@ export default class RequestManager { params.proxy = proxy; } + if (this.ca != null) { + params.ca = this.ca; + } + const request = this._getRequestModule(); const req = request(params);