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('./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.getAuth = getAuth
  153. module.exports.json = fetchJSON
  154. function fetchJSON (uri, opts) {
  155. return regFetch(uri, opts).then(res => res.json())
  156. }
  157. module.exports.json.stream = fetchJSONStream
  158. function fetchJSONStream (uri, jsonPath,
  159. /* istanbul ignore next */ opts_ = {}) {
  160. const opts = { ...defaultOpts, ...opts_ }
  161. const parser = JSONStream.parse(jsonPath, opts.mapJSON)
  162. regFetch(uri, opts).then(res =>
  163. res.body.on('error',
  164. /* istanbul ignore next: unlikely and difficult to test */
  165. er => parser.emit('error', er)).pipe(parser)
  166. ).catch(er => parser.emit('error', er))
  167. return parser
  168. }
  169. module.exports.pickRegistry = pickRegistry
  170. function pickRegistry (spec, opts = {}) {
  171. spec = npa(spec)
  172. let registry = spec.scope &&
  173. opts[spec.scope.replace(/^@?/, '@') + ':registry']
  174. if (!registry && opts.scope) {
  175. registry = opts[opts.scope.replace(/^@?/, '@') + ':registry']
  176. }
  177. if (!registry) {
  178. registry = opts.registry || defaultOpts.registry
  179. }
  180. return registry
  181. }
  182. function getCacheMode (opts) {
  183. return opts.offline ? 'only-if-cached'
  184. : opts.preferOffline ? 'force-cache'
  185. : opts.preferOnline ? 'no-cache'
  186. : 'default'
  187. }
  188. function getHeaders (uri, auth, opts) {
  189. const headers = Object.assign({
  190. 'user-agent': opts.userAgent,
  191. }, opts.headers || {})
  192. if (opts.authType) {
  193. headers['npm-auth-type'] = opts.authType
  194. }
  195. if (opts.scope) {
  196. headers['npm-scope'] = opts.scope
  197. }
  198. if (opts.npmSession) {
  199. headers['npm-session'] = opts.npmSession
  200. }
  201. if (opts.npmCommand) {
  202. headers['npm-command'] = opts.npmCommand
  203. }
  204. // If a tarball is hosted on a different place than the manifest, only send
  205. // credentials on `alwaysAuth`
  206. if (auth.token) {
  207. headers.authorization = `Bearer ${auth.token}`
  208. } else if (auth.auth) {
  209. headers.authorization = `Basic ${auth.auth}`
  210. }
  211. if (opts.otp) {
  212. headers['npm-otp'] = opts.otp
  213. }
  214. return headers
  215. }