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