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