index.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. 'use strict'
  2. const { HttpErrorAuthOTP } = require('./errors.js')
  3. const checkResponse = require('./check-response.js')
  4. const getAuth = require('./auth.js')
  5. const fetch = require('make-fetch-happen')
  6. const JSONStream = require('minipass-json-stream')
  7. const npa = require('npm-package-arg')
  8. const qs = require('querystring')
  9. const url = require('url')
  10. const zlib = require('minizlib')
  11. const Minipass = require('minipass')
  12. const defaultOpts = require('./default-opts.js')
  13. // WhatWG URL throws if it's not fully resolved
  14. const urlIsValid = u => {
  15. try {
  16. return !!new url.URL(u)
  17. } catch (_) {
  18. return false
  19. }
  20. }
  21. module.exports = regFetch
  22. function regFetch (uri, /* istanbul ignore next */ opts_ = {}) {
  23. const opts = {
  24. ...defaultOpts,
  25. ...opts_,
  26. }
  27. // if we did not get a fully qualified URI, then we look at the registry
  28. // config or relevant scope to resolve it.
  29. const uriValid = urlIsValid(uri)
  30. let registry = opts.registry || defaultOpts.registry
  31. if (!uriValid) {
  32. registry = opts.registry = (
  33. (opts.spec && pickRegistry(opts.spec, opts)) ||
  34. opts.registry ||
  35. registry
  36. )
  37. uri = `${
  38. registry.trim().replace(/\/?$/g, '')
  39. }/${
  40. uri.trim().replace(/^\//, '')
  41. }`
  42. // asserts that this is now valid
  43. new url.URL(uri)
  44. }
  45. const method = opts.method || 'GET'
  46. // through that takes into account the scope, the prefix of `uri`, etc
  47. const startTime = Date.now()
  48. const auth = getAuth(uri, opts)
  49. const headers = getHeaders(uri, auth, opts)
  50. let body = opts.body
  51. const bodyIsStream = Minipass.isStream(body)
  52. const bodyIsPromise = body &&
  53. typeof body === 'object' &&
  54. typeof body.then === 'function'
  55. if (body && !bodyIsStream && !bodyIsPromise && typeof body !== 'string' && !Buffer.isBuffer(body)) {
  56. headers['content-type'] = headers['content-type'] || 'application/json'
  57. body = JSON.stringify(body)
  58. } else if (body && !headers['content-type'])
  59. headers['content-type'] = 'application/octet-stream'
  60. if (opts.gzip) {
  61. headers['content-encoding'] = 'gzip'
  62. if (bodyIsStream) {
  63. const gz = new zlib.Gzip()
  64. body.on('error', /* istanbul ignore next: unlikely and hard to test */
  65. err => gz.emit('error', err))
  66. body = body.pipe(gz)
  67. } else if (!bodyIsPromise)
  68. body = new zlib.Gzip().end(body).concat()
  69. }
  70. const parsed = new url.URL(uri)
  71. if (opts.query) {
  72. const q = typeof opts.query === 'string' ? qs.parse(opts.query)
  73. : opts.query
  74. Object.keys(q).forEach(key => {
  75. if (q[key] !== undefined)
  76. parsed.searchParams.set(key, q[key])
  77. })
  78. uri = url.format(parsed)
  79. }
  80. if (parsed.searchParams.get('write') === 'true' && method === 'GET') {
  81. // do not cache, because this GET is fetching a rev that will be
  82. // used for a subsequent PUT or DELETE, so we need to conditionally
  83. // update cache.
  84. opts.offline = false
  85. opts.preferOffline = false
  86. opts.preferOnline = true
  87. }
  88. const doFetch = async body => {
  89. const p = fetch(uri, {
  90. agent: opts.agent,
  91. algorithms: opts.algorithms,
  92. body,
  93. cache: getCacheMode(opts),
  94. cacheManager: opts.cache,
  95. ca: opts.ca,
  96. cert: opts.cert,
  97. headers,
  98. integrity: opts.integrity,
  99. key: opts.key,
  100. localAddress: opts.localAddress,
  101. maxSockets: opts.maxSockets,
  102. memoize: opts.memoize,
  103. method: method,
  104. noProxy: opts.noProxy,
  105. proxy: opts.httpsProxy || opts.proxy,
  106. retry: opts.retry ? opts.retry : {
  107. retries: opts.fetchRetries,
  108. factor: opts.fetchRetryFactor,
  109. minTimeout: opts.fetchRetryMintimeout,
  110. maxTimeout: opts.fetchRetryMaxtimeout,
  111. },
  112. strictSSL: opts.strictSSL,
  113. timeout: opts.timeout || 30 * 1000,
  114. }).then(res => checkResponse({
  115. method,
  116. uri,
  117. res,
  118. registry,
  119. startTime,
  120. auth,
  121. opts,
  122. }))
  123. if (typeof opts.otpPrompt === 'function') {
  124. return p.catch(async er => {
  125. if (er instanceof HttpErrorAuthOTP) {
  126. // if otp fails to complete, we fail with that failure
  127. const otp = await opts.otpPrompt()
  128. // if no otp provided, throw the original HTTP error
  129. if (!otp)
  130. throw er
  131. return regFetch(uri, { ...opts, otp })
  132. }
  133. throw er
  134. })
  135. } else
  136. return p
  137. }
  138. return Promise.resolve(body).then(doFetch)
  139. }
  140. module.exports.json = fetchJSON
  141. function fetchJSON (uri, opts) {
  142. return regFetch(uri, opts).then(res => res.json())
  143. }
  144. module.exports.json.stream = fetchJSONStream
  145. function fetchJSONStream (uri, jsonPath,
  146. /* istanbul ignore next */ opts_ = {}) {
  147. const opts = { ...defaultOpts, ...opts_ }
  148. const parser = JSONStream.parse(jsonPath, opts.mapJSON)
  149. regFetch(uri, opts).then(res =>
  150. res.body.on('error',
  151. /* istanbul ignore next: unlikely and difficult to test */
  152. er => parser.emit('error', er)).pipe(parser)
  153. ).catch(er => parser.emit('error', er))
  154. return parser
  155. }
  156. module.exports.pickRegistry = pickRegistry
  157. function pickRegistry (spec, opts = {}) {
  158. spec = npa(spec)
  159. let registry = spec.scope &&
  160. opts[spec.scope.replace(/^@?/, '@') + ':registry']
  161. if (!registry && opts.scope)
  162. registry = opts[opts.scope.replace(/^@?/, '@') + ':registry']
  163. if (!registry)
  164. registry = opts.registry || defaultOpts.registry
  165. return registry
  166. }
  167. function getCacheMode (opts) {
  168. return opts.offline ? 'only-if-cached'
  169. : opts.preferOffline ? 'force-cache'
  170. : opts.preferOnline ? 'no-cache'
  171. : 'default'
  172. }
  173. function getHeaders (uri, auth, opts) {
  174. const headers = Object.assign({
  175. 'user-agent': opts.userAgent,
  176. }, opts.headers || {})
  177. if (opts.projectScope)
  178. headers['npm-scope'] = opts.projectScope
  179. if (opts.npmSession)
  180. headers['npm-session'] = opts.npmSession
  181. if (opts.npmCommand)
  182. headers['npm-command'] = opts.npmCommand
  183. // If a tarball is hosted on a different place than the manifest, only send
  184. // credentials on `alwaysAuth`
  185. if (auth.token)
  186. headers.authorization = `Bearer ${auth.token}`
  187. else if (auth.auth)
  188. headers.authorization = `Basic ${auth.auth}`
  189. if (opts.otp)
  190. headers['npm-otp'] = opts.otp
  191. return headers
  192. }