• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const Buffer = require('safe-buffer').Buffer
4
5const checkResponse = require('./check-response.js')
6const config = require('./config.js')
7const getAuth = require('./auth.js')
8const fetch = require('make-fetch-happen')
9const JSONStream = require('JSONStream')
10const npa = require('npm-package-arg')
11const {PassThrough} = require('stream')
12const qs = require('querystring')
13const url = require('url')
14const zlib = require('zlib')
15
16module.exports = regFetch
17function regFetch (uri, opts) {
18  opts = config(opts)
19  const registry = (
20    (opts.spec && pickRegistry(opts.spec, opts)) ||
21    opts.registry ||
22    'https://registry.npmjs.org/'
23  )
24  uri = url.parse(uri).protocol
25    ? uri
26    : `${
27      registry.trim().replace(/\/?$/g, '')
28    }/${
29      uri.trim().replace(/^\//, '')
30    }`
31  // through that takes into account the scope, the prefix of `uri`, etc
32  const startTime = Date.now()
33  const headers = getHeaders(registry, uri, opts)
34  let body = opts.body
35  const bodyIsStream = body &&
36    typeof body === 'object' &&
37    typeof body.pipe === 'function'
38  if (body && !bodyIsStream && typeof body !== 'string' && !Buffer.isBuffer(body)) {
39    headers['content-type'] = headers['content-type'] || 'application/json'
40    body = JSON.stringify(body)
41  } else if (body && !headers['content-type']) {
42    headers['content-type'] = 'application/octet-stream'
43  }
44  if (opts.gzip) {
45    headers['content-encoding'] = 'gzip'
46    if (bodyIsStream) {
47      const gz = zlib.createGzip()
48      body.on('error', err => gz.emit('error', err))
49      body = body.pipe(gz)
50    } else {
51      body = new opts.Promise((resolve, reject) => {
52        zlib.gzip(body, (err, gz) => err ? reject(err) : resolve(gz))
53      })
54    }
55  }
56
57  let q = opts.query
58  if (q) {
59    if (typeof q === 'string') {
60      q = qs.parse(q)
61    } else if (typeof q !== 'object') {
62      throw new TypeError('invalid query option, must be string or object')
63    }
64    Object.keys(q).forEach(key => {
65      if (q[key] === undefined) {
66        delete q[key]
67      }
68    })
69  }
70  const parsed = url.parse(uri)
71
72  const query = parsed.query ? Object.assign(qs.parse(parsed.query), q || {})
73    : Object.keys(q || {}).length ? q
74      : null
75
76  if (query) {
77    if (String(query.write) === 'true' && opts.method === 'GET') {
78      opts = opts.concat({
79        offline: false,
80        'prefer-offline': false,
81        'prefer-online': true
82      })
83    }
84    parsed.search = '?' + qs.stringify(query)
85    uri = url.format(parsed)
86  }
87
88  return opts.Promise.resolve(body).then(body => fetch(uri, {
89    agent: opts.agent,
90    algorithms: opts.algorithms,
91    body,
92    cache: getCacheMode(opts),
93    cacheManager: opts.cache,
94    ca: opts.ca,
95    cert: opts.cert,
96    headers,
97    integrity: opts.integrity,
98    key: opts.key,
99    localAddress: opts['local-address'],
100    maxSockets: opts.maxsockets,
101    memoize: opts.memoize,
102    method: opts.method || 'GET',
103    noProxy: opts['no-proxy'] || opts.noproxy,
104    Promise: opts.Promise,
105    proxy: opts['https-proxy'] || opts.proxy,
106    referer: opts.refer,
107    retry: opts.retry != null ? opts.retry : {
108      retries: opts['fetch-retries'],
109      factor: opts['fetch-retry-factor'],
110      minTimeout: opts['fetch-retry-mintimeout'],
111      maxTimeout: opts['fetch-retry-maxtimeout']
112    },
113    strictSSL: !!opts['strict-ssl'],
114    timeout: opts.timeout
115  }).then(res => checkResponse(
116    opts.method || 'GET', res, registry, startTime, opts
117  )))
118}
119
120module.exports.json = fetchJSON
121function fetchJSON (uri, opts) {
122  return regFetch(uri, opts).then(res => res.json())
123}
124
125module.exports.json.stream = fetchJSONStream
126function fetchJSONStream (uri, jsonPath, opts) {
127  opts = config(opts)
128  const parser = JSONStream.parse(jsonPath, opts.mapJson)
129  const pt = parser.pipe(new PassThrough({objectMode: true}))
130  parser.on('error', err => pt.emit('error', err))
131  regFetch(uri, opts).then(res => {
132    res.body.on('error', err => parser.emit('error', err))
133    res.body.pipe(parser)
134  }, err => pt.emit('error', err))
135  return pt
136}
137
138module.exports.pickRegistry = pickRegistry
139function pickRegistry (spec, opts) {
140  spec = npa(spec)
141  opts = config(opts)
142  let registry = spec.scope &&
143    opts[spec.scope.replace(/^@?/, '@') + ':registry']
144
145  if (!registry && opts.scope) {
146    registry = opts[opts.scope.replace(/^@?/, '@') + ':registry']
147  }
148
149  if (!registry) {
150    registry = opts.registry || 'https://registry.npmjs.org/'
151  }
152
153  return registry
154}
155
156function getCacheMode (opts) {
157  return opts.offline
158    ? 'only-if-cached'
159    : opts['prefer-offline']
160      ? 'force-cache'
161      : opts['prefer-online']
162        ? 'no-cache'
163        : 'default'
164}
165
166function getHeaders (registry, uri, opts) {
167  const headers = Object.assign({
168    'npm-in-ci': !!(
169      opts['is-from-ci'] ||
170      process.env['CI'] === 'true' ||
171      process.env['TDDIUM'] ||
172      process.env['JENKINS_URL'] ||
173      process.env['bamboo.buildKey'] ||
174      process.env['GO_PIPELINE_NAME']
175    ),
176    'npm-scope': opts['project-scope'],
177    'npm-session': opts['npm-session'],
178    'user-agent': opts['user-agent'],
179    'referer': opts.refer
180  }, opts.headers)
181
182  const auth = getAuth(registry, opts)
183  // If a tarball is hosted on a different place than the manifest, only send
184  // credentials on `alwaysAuth`
185  const shouldAuth = (
186    auth.alwaysAuth ||
187    url.parse(uri).host === url.parse(registry).host
188  )
189  if (shouldAuth && auth.token) {
190    headers.authorization = `Bearer ${auth.token}`
191  } else if (shouldAuth && auth.username && auth.password) {
192    const encoded = Buffer.from(
193      `${auth.username}:${auth.password}`, 'utf8'
194    ).toString('base64')
195    headers.authorization = `Basic ${encoded}`
196  } else if (shouldAuth && auth._auth) {
197    headers.authorization = `Basic ${auth._auth}`
198  }
199  if (shouldAuth && auth.otp) {
200    headers['npm-otp'] = auth.otp
201  }
202  return headers
203}
204