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