index.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  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 (
  56. body && !bodyIsStream && !bodyIsPromise && typeof body !== 'string' && !Buffer.isBuffer(body)
  57. ) {
  58. headers['content-type'] = headers['content-type'] || 'application/json'
  59. body = JSON.stringify(body)
  60. } else if (body && !headers['content-type']) {
  61. headers['content-type'] = 'application/octet-stream'
  62. }
  63. if (opts.gzip) {
  64. headers['content-encoding'] = 'gzip'
  65. if (bodyIsStream) {
  66. const gz = new zlib.Gzip()
  67. body.on('error', /* istanbul ignore next: unlikely and hard to test */
  68. err => gz.emit('error', err))
  69. body = body.pipe(gz)
  70. } else if (!bodyIsPromise) {
  71. body = new zlib.Gzip().end(body).concat()
  72. }
  73. }
  74. const parsed = new url.URL(uri)
  75. if (opts.query) {
  76. const q = typeof opts.query === 'string' ? qs.parse(opts.query)
  77. : opts.query
  78. Object.keys(q).forEach(key => {
  79. if (q[key] !== undefined) {
  80. parsed.searchParams.set(key, q[key])
  81. }
  82. })
  83. uri = url.format(parsed)
  84. }
  85. if (parsed.searchParams.get('write') === 'true' && method === 'GET') {
  86. // do not cache, because this GET is fetching a rev that will be
  87. // used for a subsequent PUT or DELETE, so we need to conditionally
  88. // update cache.
  89. opts.offline = false
  90. opts.preferOffline = false
  91. opts.preferOnline = true
  92. }
  93. const doFetch = async fetchBody => {
  94. const p = fetch(uri, {
  95. agent: opts.agent,
  96. algorithms: opts.algorithms,
  97. body: fetchBody,
  98. cache: getCacheMode(opts),
  99. cachePath: opts.cache,
  100. ca: opts.ca,
  101. cert: auth.cert || opts.cert,
  102. headers,
  103. integrity: opts.integrity,
  104. key: auth.key || opts.key,
  105. localAddress: opts.localAddress,
  106. maxSockets: opts.maxSockets,
  107. memoize: opts.memoize,
  108. method: method,
  109. noProxy: opts.noProxy,
  110. proxy: opts.httpsProxy || opts.proxy,
  111. retry: opts.retry ? opts.retry : {
  112. retries: opts.fetchRetries,
  113. factor: opts.fetchRetryFactor,
  114. minTimeout: opts.fetchRetryMintimeout,
  115. maxTimeout: opts.fetchRetryMaxtimeout,
  116. },
  117. strictSSL: opts.strictSSL,
  118. timeout: opts.timeout || 30 * 1000,
  119. }).then(res => checkResponse({
  120. method,
  121. uri,
  122. res,
  123. registry,
  124. startTime,
  125. auth,
  126. opts,
  127. }))
  128. if (typeof opts.otpPrompt === 'function') {
  129. return p.catch(async er => {
  130. if (er instanceof HttpErrorAuthOTP) {
  131. let otp
  132. // if otp fails to complete, we fail with that failure
  133. try {
  134. otp = await opts.otpPrompt()
  135. } catch (_) {
  136. // ignore this error
  137. }
  138. // if no otp provided, or otpPrompt errored, throw the original HTTP error
  139. if (!otp) {
  140. throw er
  141. }
  142. return regFetch(uri, { ...opts, otp })
  143. }
  144. throw er
  145. })
  146. } else {
  147. return p
  148. }
  149. }
  150. return Promise.resolve(body).then(doFetch)
  151. }
  152. module.exports.json = fetchJSON
  153. function fetchJSON (uri, opts) {
  154. return regFetch(uri, opts).then(res => res.json())
  155. }
  156. module.exports.json.stream = fetchJSONStream
  157. function fetchJSONStream (uri, jsonPath,
  158. /* istanbul ignore next */ opts_ = {}) {
  159. const opts = { ...defaultOpts, ...opts_ }
  160. const parser = JSONStream.parse(jsonPath, opts.mapJSON)
  161. regFetch(uri, opts).then(res =>
  162. res.body.on('error',
  163. /* istanbul ignore next: unlikely and difficult to test */
  164. er => parser.emit('error', er)).pipe(parser)
  165. ).catch(er => parser.emit('error', er))
  166. return parser
  167. }
  168. module.exports.pickRegistry = pickRegistry
  169. function pickRegistry (spec, opts = {}) {
  170. spec = npa(spec)
  171. let registry = spec.scope &&
  172. opts[spec.scope.replace(/^@?/, '@') + ':registry']
  173. if (!registry && opts.scope) {
  174. registry = opts[opts.scope.replace(/^@?/, '@') + ':registry']
  175. }
  176. if (!registry) {
  177. registry = opts.registry || defaultOpts.registry
  178. }
  179. return registry
  180. }
  181. function getCacheMode (opts) {
  182. return opts.offline ? 'only-if-cached'
  183. : opts.preferOffline ? 'force-cache'
  184. : opts.preferOnline ? 'no-cache'
  185. : 'default'
  186. }
  187. function getHeaders (uri, auth, opts) {
  188. const headers = Object.assign({
  189. 'user-agent': opts.userAgent,
  190. }, opts.headers || {})
  191. if (opts.authType) {
  192. headers['npm-auth-type'] = opts.authType
  193. }
  194. if (opts.scope) {
  195. headers['npm-scope'] = opts.scope
  196. }
  197. if (opts.npmSession) {
  198. headers['npm-session'] = opts.npmSession
  199. }
  200. if (opts.npmCommand) {
  201. headers['npm-command'] = opts.npmCommand
  202. }
  203. // If a tarball is hosted on a different place than the manifest, only send
  204. // credentials on `alwaysAuth`
  205. if (auth.token) {
  206. headers.authorization = `Bearer ${auth.token}`
  207. } else if (auth.auth) {
  208. headers.authorization = `Basic ${auth.auth}`
  209. }
  210. if (opts.otp) {
  211. headers['npm-otp'] = opts.otp
  212. }
  213. return headers
  214. }
  215. module.exports.cleanUrl = require('./clean-url.js')