123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- const Fetcher = require('./fetcher.js')
- const RemoteFetcher = require('./remote.js')
- const _tarballFromResolved = Symbol.for('pacote.Fetcher._tarballFromResolved')
- const pacoteVersion = require('../package.json').version
- const npa = require('npm-package-arg')
- const rpj = require('read-package-json-fast')
- const pickManifest = require('npm-pick-manifest')
- const ssri = require('ssri')
- const Minipass = require('minipass')
- // Corgis are cute. 🐕🐶
- const corgiDoc = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'
- const fullDoc = 'application/json'
- const fetch = require('npm-registry-fetch')
- // TODO: memoize reg requests, so we don't even have to check cache
- const _headers = Symbol('_headers')
- class RegistryFetcher extends Fetcher {
- constructor (spec, opts) {
- super(spec, opts)
- // you usually don't want to fetch the same packument multiple times in
- // the span of a given script or command, no matter how many pacote calls
- // are made, so this lets us avoid doing that. It's only relevant for
- // registry fetchers, because other types simulate their packument from
- // the manifest, which they memoize on this.package, so it's very cheap
- // already.
- this.packumentCache = this.opts.packumentCache || null
- // handle case when npm-package-arg guesses wrong.
- if (this.spec.type === 'tag' &&
- this.spec.rawSpec === '' &&
- this.defaultTag !== 'latest')
- this.spec = npa(`${this.spec.name}@${this.defaultTag}`)
- this.registry = fetch.pickRegistry(spec, opts)
- this.packumentUrl = this.registry.replace(/\/*$/, '/') +
- this.spec.escapedName
- // XXX pacote <=9 has some logic to ignore opts.resolved if
- // the resolved URL doesn't go to the same registry.
- // Consider reproducing that here, to throw away this.resolved
- // in that case.
- }
- resolve () {
- if (this.resolved)
- return Promise.resolve(this.resolved)
- // fetching the manifest sets resolved and (usually) integrity
- return this.manifest().then(() => {
- if (this.resolved)
- return this.resolved
- throw Object.assign(
- new Error('Invalid package manifest: no `dist.tarball` field'),
- { package: this.spec.toString() }
- )
- })
- }
- [_headers] () {
- return {
- // npm will override UA, but ensure that we always send *something*
- 'user-agent': this.opts.userAgent ||
- `pacote/${pacoteVersion} node/${process.version}`,
- ...(this.opts.headers || {}),
- 'pacote-version': pacoteVersion,
- 'pacote-req-type': 'packument',
- 'pacote-pkg-id': `registry:${this.spec.name}`,
- accept: this.fullMetadata ? fullDoc : corgiDoc,
- }
- }
- async packument () {
- // note this might be either an in-flight promise for a request,
- // or the actual packument, but we never want to make more than
- // one request at a time for the same thing regardless.
- if (this.packumentCache && this.packumentCache.has(this.packumentUrl))
- return this.packumentCache.get(this.packumentUrl)
- // npm-registry-fetch the packument
- // set the appropriate header for corgis if fullMetadata isn't set
- // return the res.json() promise
- const p = fetch(this.packumentUrl, {
- ...this.opts,
- headers: this[_headers](),
- spec: this.spec,
- // never check integrity for packuments themselves
- integrity: null,
- }).then(res => res.json().then(packument => {
- packument._cached = res.headers.has('x-local-cache')
- packument._contentLength = +res.headers.get('content-length')
- if (this.packumentCache)
- this.packumentCache.set(this.packumentUrl, packument)
- return packument
- })).catch(er => {
- if (this.packumentCache)
- this.packumentCache.delete(this.packumentUrl)
- if (er.code === 'E404' && !this.fullMetadata) {
- // possible that corgis are not supported by this registry
- this.fullMetadata = true
- return this.packument()
- }
- throw er
- })
- if (this.packumentCache)
- this.packumentCache.set(this.packumentUrl, p)
- return p
- }
- manifest () {
- if (this.package)
- return Promise.resolve(this.package)
- return this.packument()
- .then(packument => pickManifest(packument, this.spec.fetchSpec, {
- ...this.opts,
- defaultTag: this.defaultTag,
- before: this.before,
- }) /* XXX add ETARGET and E403 revalidation of cached packuments here */)
- .then(mani => {
- // add _resolved and _integrity from dist object
- const { dist } = mani
- if (dist) {
- this.resolved = mani._resolved = dist.tarball
- mani._from = this.from
- const distIntegrity = dist.integrity ? ssri.parse(dist.integrity)
- : dist.shasum ? ssri.fromHex(dist.shasum, 'sha1', {...this.opts})
- : null
- if (distIntegrity) {
- if (!this.integrity)
- this.integrity = distIntegrity
- else if (!this.integrity.match(distIntegrity)) {
- // only bork if they have algos in common.
- // otherwise we end up breaking if we have saved a sha512
- // previously for the tarball, but the manifest only
- // provides a sha1, which is possible for older publishes.
- // Otherwise, this is almost certainly a case of holding it
- // wrong, and will result in weird or insecure behavior
- // later on when building package tree.
- for (const algo of Object.keys(this.integrity)) {
- if (distIntegrity[algo]) {
- throw Object.assign(new Error(
- `Integrity checksum failed when using ${algo}: `+
- `wanted ${this.integrity} but got ${distIntegrity}.`
- ), { code: 'EINTEGRITY' })
- }
- }
- // made it this far, the integrity is worthwhile. accept it.
- // the setter here will take care of merging it into what we
- // already had.
- this.integrity = distIntegrity
- }
- }
- }
- if (this.integrity)
- mani._integrity = String(this.integrity)
- this.package = rpj.normalize(mani)
- return this.package
- })
- }
- [_tarballFromResolved] () {
- // we use a RemoteFetcher to get the actual tarball stream
- return new RemoteFetcher(this.resolved, {
- ...this.opts,
- resolved: this.resolved,
- pkgid: `registry:${this.spec.name}@${this.resolved}`,
- })[_tarballFromResolved]()
- }
- get types () {
- return [
- 'tag',
- 'version',
- 'range',
- ]
- }
- }
- module.exports = RegistryFetcher
|