123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- 'use strict'
- const { HttpErrorAuthOTP } = require('./errors.js')
- const checkResponse = require('./check-response.js')
- const getAuth = require('./auth.js')
- const fetch = require('make-fetch-happen')
- const JSONStream = require('minipass-json-stream')
- const npa = require('npm-package-arg')
- const qs = require('querystring')
- const url = require('url')
- const zlib = require('minizlib')
- const Minipass = require('minipass')
- const defaultOpts = require('./default-opts.js')
- // WhatWG URL throws if it's not fully resolved
- const urlIsValid = u => {
- try {
- return !!new url.URL(u)
- } catch (_) {
- return false
- }
- }
- module.exports = regFetch
- function regFetch (uri, /* istanbul ignore next */ opts_ = {}) {
- const opts = {
- ...defaultOpts,
- ...opts_,
- }
- // if we did not get a fully qualified URI, then we look at the registry
- // config or relevant scope to resolve it.
- const uriValid = urlIsValid(uri)
- let registry = opts.registry || defaultOpts.registry
- if (!uriValid) {
- registry = opts.registry = (
- (opts.spec && pickRegistry(opts.spec, opts)) ||
- opts.registry ||
- registry
- )
- uri = `${
- registry.trim().replace(/\/?$/g, '')
- }/${
- uri.trim().replace(/^\//, '')
- }`
- // asserts that this is now valid
- new url.URL(uri)
- }
- const method = opts.method || 'GET'
- // through that takes into account the scope, the prefix of `uri`, etc
- const startTime = Date.now()
- const auth = getAuth(uri, opts)
- const headers = getHeaders(uri, auth, opts)
- let body = opts.body
- const bodyIsStream = Minipass.isStream(body)
- const bodyIsPromise = body &&
- typeof body === 'object' &&
- typeof body.then === 'function'
- if (body && !bodyIsStream && !bodyIsPromise && typeof body !== 'string' && !Buffer.isBuffer(body)) {
- headers['content-type'] = headers['content-type'] || 'application/json'
- body = JSON.stringify(body)
- } else if (body && !headers['content-type'])
- headers['content-type'] = 'application/octet-stream'
- if (opts.gzip) {
- headers['content-encoding'] = 'gzip'
- if (bodyIsStream) {
- const gz = new zlib.Gzip()
- body.on('error', /* istanbul ignore next: unlikely and hard to test */
- err => gz.emit('error', err))
- body = body.pipe(gz)
- } else if (!bodyIsPromise)
- body = new zlib.Gzip().end(body).concat()
- }
- const parsed = new url.URL(uri)
- if (opts.query) {
- const q = typeof opts.query === 'string' ? qs.parse(opts.query)
- : opts.query
- Object.keys(q).forEach(key => {
- if (q[key] !== undefined)
- parsed.searchParams.set(key, q[key])
- })
- uri = url.format(parsed)
- }
- if (parsed.searchParams.get('write') === 'true' && method === 'GET') {
- // do not cache, because this GET is fetching a rev that will be
- // used for a subsequent PUT or DELETE, so we need to conditionally
- // update cache.
- opts.offline = false
- opts.preferOffline = false
- opts.preferOnline = true
- }
- const doFetch = async body => {
- const p = fetch(uri, {
- agent: opts.agent,
- algorithms: opts.algorithms,
- body,
- cache: getCacheMode(opts),
- cacheManager: opts.cache,
- ca: opts.ca,
- cert: opts.cert,
- headers,
- integrity: opts.integrity,
- key: opts.key,
- localAddress: opts.localAddress,
- maxSockets: opts.maxSockets,
- memoize: opts.memoize,
- method: method,
- noProxy: opts.noProxy,
- proxy: opts.httpsProxy || opts.proxy,
- retry: opts.retry ? opts.retry : {
- retries: opts.fetchRetries,
- factor: opts.fetchRetryFactor,
- minTimeout: opts.fetchRetryMintimeout,
- maxTimeout: opts.fetchRetryMaxtimeout,
- },
- strictSSL: opts.strictSSL,
- timeout: opts.timeout || 30 * 1000,
- }).then(res => checkResponse({
- method,
- uri,
- res,
- registry,
- startTime,
- auth,
- opts,
- }))
- if (typeof opts.otpPrompt === 'function') {
- return p.catch(async er => {
- if (er instanceof HttpErrorAuthOTP) {
- // if otp fails to complete, we fail with that failure
- const otp = await opts.otpPrompt()
- // if no otp provided, throw the original HTTP error
- if (!otp)
- throw er
- return regFetch(uri, { ...opts, otp })
- }
- throw er
- })
- } else
- return p
- }
- return Promise.resolve(body).then(doFetch)
- }
- module.exports.json = fetchJSON
- function fetchJSON (uri, opts) {
- return regFetch(uri, opts).then(res => res.json())
- }
- module.exports.json.stream = fetchJSONStream
- function fetchJSONStream (uri, jsonPath,
- /* istanbul ignore next */ opts_ = {}) {
- const opts = { ...defaultOpts, ...opts_ }
- const parser = JSONStream.parse(jsonPath, opts.mapJSON)
- regFetch(uri, opts).then(res =>
- res.body.on('error',
- /* istanbul ignore next: unlikely and difficult to test */
- er => parser.emit('error', er)).pipe(parser)
- ).catch(er => parser.emit('error', er))
- return parser
- }
- module.exports.pickRegistry = pickRegistry
- function pickRegistry (spec, opts = {}) {
- spec = npa(spec)
- let registry = spec.scope &&
- opts[spec.scope.replace(/^@?/, '@') + ':registry']
- if (!registry && opts.scope)
- registry = opts[opts.scope.replace(/^@?/, '@') + ':registry']
- if (!registry)
- registry = opts.registry || defaultOpts.registry
- return registry
- }
- function getCacheMode (opts) {
- return opts.offline ? 'only-if-cached'
- : opts.preferOffline ? 'force-cache'
- : opts.preferOnline ? 'no-cache'
- : 'default'
- }
- function getHeaders (uri, auth, opts) {
- const headers = Object.assign({
- 'user-agent': opts.userAgent,
- }, opts.headers || {})
- if (opts.projectScope)
- headers['npm-scope'] = opts.projectScope
- if (opts.npmSession)
- headers['npm-session'] = opts.npmSession
- if (opts.npmCommand)
- headers['npm-command'] = opts.npmCommand
- // If a tarball is hosted on a different place than the manifest, only send
- // credentials on `alwaysAuth`
- if (auth.token)
- headers.authorization = `Bearer ${auth.token}`
- else if (auth.auth)
- headers.authorization = `Basic ${auth.auth}`
- if (opts.otp)
- headers['npm-otp'] = opts.otp
- return headers
- }
|