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