mirror of
https://github.com/nodejs/node.git
synced 2025-05-17 00:33:26 +00:00

PR-URL: https://github.com/nodejs/node/pull/14235 Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com> Reviewed-By: Refael Ackermann <refack@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Yuta Hiroto <hello@about-hiroppy.com> Reviewed-By: Gibson Fahnestock <gibfahn@gmail.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: James M Snell <jasnell@gmail.com>
322 lines
9.4 KiB
JavaScript
322 lines
9.4 KiB
JavaScript
module.exports = regRequest
|
||
|
||
// npm: means
|
||
// 1. https
|
||
// 2. send authorization
|
||
// 3. content-type is 'application/json' -- metadata
|
||
//
|
||
var assert = require('assert')
|
||
var url = require('url')
|
||
var zlib = require('zlib')
|
||
var Stream = require('stream').Stream
|
||
var STATUS_CODES = require('http').STATUS_CODES
|
||
|
||
var request = require('request')
|
||
var once = require('once')
|
||
|
||
function regRequest (uri, params, cb_) {
|
||
assert(typeof uri === 'string', 'must pass uri to request')
|
||
assert(params && typeof params === 'object', 'must pass params to request')
|
||
assert(typeof cb_ === 'function', 'must pass callback to request')
|
||
|
||
params.method = params.method || 'GET'
|
||
this.log.verbose('request', 'uri', uri)
|
||
|
||
// Since there are multiple places where an error could occur,
|
||
// don't let the cb be called more than once.
|
||
var cb = once(cb_)
|
||
|
||
if (uri.match(/^\/?favicon.ico/)) {
|
||
return cb(new Error("favicon.ico isn't a package, it's a picture."))
|
||
}
|
||
|
||
var adduserChange = /\/?-\/user\/org\.couchdb\.user:([^/]+)\/-rev/
|
||
var isUserChange = uri.match(adduserChange)
|
||
var adduserNew = /\/?-\/user\/org\.couchdb\.user:([^/?]+)$/
|
||
var isNewUser = uri.match(adduserNew)
|
||
var alwaysAuth = params.auth && params.auth.alwaysAuth
|
||
var isDelete = params.method === 'DELETE'
|
||
var isWrite = params.body || isDelete
|
||
|
||
if (isUserChange && !isWrite) {
|
||
return cb(new Error('trying to change user document without writing(?!)'))
|
||
}
|
||
|
||
if (params.authed == null) {
|
||
// new users can *not* use auth, because they don't *have* auth yet
|
||
if (isUserChange) {
|
||
this.log.verbose('request', 'updating existing user; sending authorization')
|
||
params.authed = true
|
||
} else if (isNewUser) {
|
||
this.log.verbose('request', "new user, so can't send auth")
|
||
params.authed = false
|
||
} else if (alwaysAuth) {
|
||
this.log.verbose('request', 'always-auth set; sending authorization')
|
||
params.authed = true
|
||
} else if (isWrite) {
|
||
this.log.verbose('request', 'sending authorization for write operation')
|
||
params.authed = true
|
||
} else {
|
||
// most of the time we don't want to auth
|
||
this.log.verbose('request', 'no auth needed')
|
||
params.authed = false
|
||
}
|
||
}
|
||
|
||
var self = this
|
||
this.attempt(function (operation) {
|
||
makeRequest.call(self, uri, params, function (er, parsed, raw, response) {
|
||
if (response) {
|
||
self.log.verbose('headers', response.headers)
|
||
if (response.headers['npm-notice']) {
|
||
self.log.warn('notice', response.headers['npm-notice'])
|
||
}
|
||
}
|
||
|
||
if (!er || (er.message && er.message.match(/^SSL Error/))) {
|
||
if (er) er.code = 'ESSL'
|
||
return cb(er, parsed, raw, response)
|
||
}
|
||
|
||
// Only retry on 408, 5xx or no `response`.
|
||
var statusCode = response && response.statusCode
|
||
|
||
var timeout = statusCode === 408
|
||
var serverError = statusCode >= 500
|
||
var statusRetry = !statusCode || timeout || serverError
|
||
if (er && statusRetry && operation.retry(er)) {
|
||
self.log.info('retry', 'will retry, error on last attempt: ' + er)
|
||
return undefined
|
||
}
|
||
cb.apply(null, arguments)
|
||
})
|
||
})
|
||
}
|
||
|
||
function makeRequest (uri, params, cb_) {
|
||
var socket
|
||
var cb = once(function (er, parsed, raw, response) {
|
||
if (socket) {
|
||
// The socket might be returned to a pool for re-use, so don’t keep
|
||
// the 'error' listener from here attached.
|
||
socket.removeListener('error', cb)
|
||
}
|
||
|
||
return cb_(er, parsed, raw, response)
|
||
})
|
||
|
||
var parsed = url.parse(uri)
|
||
var headers = {}
|
||
|
||
// metadata should be compressed
|
||
headers['accept-encoding'] = 'gzip'
|
||
|
||
// metadata should be minified, if the registry supports it
|
||
|
||
var er = this.authify(params.authed, parsed, headers, params.auth)
|
||
if (er) return cb_(er)
|
||
|
||
var useCorgi = params.fullMetadata == null ? false : !params.fullMetadata
|
||
|
||
var opts = this.initialize(
|
||
parsed,
|
||
params.method,
|
||
useCorgi ? 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*' : 'application/json',
|
||
headers
|
||
)
|
||
|
||
opts.followRedirect = (typeof params.follow === 'boolean' ? params.follow : true)
|
||
opts.encoding = null // tell request let body be Buffer instance
|
||
|
||
if (params.etag) {
|
||
this.log.verbose('etag', params.etag)
|
||
headers[params.method === 'GET' ? 'if-none-match' : 'if-match'] = params.etag
|
||
}
|
||
|
||
if (params.lastModified && params.method === 'GET') {
|
||
this.log.verbose('lastModified', params.lastModified)
|
||
headers['if-modified-since'] = params.lastModified
|
||
}
|
||
|
||
// figure out wth body is
|
||
if (params.body) {
|
||
if (Buffer.isBuffer(params.body)) {
|
||
opts.body = params.body
|
||
headers['content-type'] = 'application/json'
|
||
headers['content-length'] = params.body.length
|
||
} else if (typeof params.body === 'string') {
|
||
opts.body = params.body
|
||
headers['content-type'] = 'application/json'
|
||
headers['content-length'] = Buffer.byteLength(params.body)
|
||
} else if (params.body instanceof Stream) {
|
||
headers['content-type'] = 'application/octet-stream'
|
||
if (params.body.size) headers['content-length'] = params.body.size
|
||
} else {
|
||
delete params.body._etag
|
||
delete params.body._lastModified
|
||
opts.json = params.body
|
||
}
|
||
}
|
||
|
||
this.log.http('request', params.method, parsed.href || '/')
|
||
|
||
var done = requestDone.call(this, params.method, uri, cb)
|
||
var req = request(opts, params.streaming ? undefined : decodeResponseBody(done))
|
||
|
||
req.on('error', cb)
|
||
|
||
// This should not be necessary, as the HTTP implementation in Node
|
||
// passes errors occurring on the socket to the request itself. Being overly
|
||
// cautious comes at a low cost, though.
|
||
req.on('socket', function (s) {
|
||
socket = s
|
||
socket.on('error', cb)
|
||
})
|
||
|
||
if (params.streaming) {
|
||
req.on('response', function (response) {
|
||
if (response.statusCode >= 400) {
|
||
var parts = []
|
||
response.on('data', function (data) {
|
||
parts.push(data)
|
||
})
|
||
response.on('end', function () {
|
||
decodeResponseBody(done)(null, response, Buffer.concat(parts))
|
||
})
|
||
} else {
|
||
response.on('end', function () {
|
||
// don't ever re-use connections that had server errors.
|
||
// those sockets connect to the Bad Place!
|
||
if (response.socket && response.statusCode > 500) {
|
||
response.socket.destroy()
|
||
}
|
||
})
|
||
|
||
return cb(null, response)
|
||
}
|
||
})
|
||
}
|
||
|
||
if (params.body && (params.body instanceof Stream)) {
|
||
params.body.pipe(req)
|
||
}
|
||
}
|
||
|
||
function decodeResponseBody (cb) {
|
||
return function (er, response, data) {
|
||
if (er) return cb(er, response, data)
|
||
|
||
// don't ever re-use connections that had server errors.
|
||
// those sockets connect to the Bad Place!
|
||
if (response.socket && response.statusCode > 500) {
|
||
response.socket.destroy()
|
||
}
|
||
|
||
if (response.headers['content-encoding'] !== 'gzip') {
|
||
return cb(er, response, data)
|
||
}
|
||
|
||
zlib.gunzip(data, function (er, buf) {
|
||
if (er) return cb(er, response, data)
|
||
|
||
cb(null, response, buf)
|
||
})
|
||
}
|
||
}
|
||
|
||
// cb(er, parsed, raw, response)
|
||
function requestDone (method, where, cb) {
|
||
return function (er, response, data) {
|
||
if (er) return cb(er)
|
||
|
||
var urlObj = url.parse(where)
|
||
if (urlObj.auth) urlObj.auth = '***'
|
||
this.log.http(response.statusCode, url.format(urlObj))
|
||
|
||
if (Buffer.isBuffer(data)) {
|
||
data = data.toString()
|
||
}
|
||
|
||
var parsed
|
||
if (data && typeof data === 'string' && response.statusCode !== 304) {
|
||
try {
|
||
parsed = JSON.parse(data)
|
||
} catch (ex) {
|
||
ex.message += '\n' + data
|
||
this.log.verbose('bad json', data)
|
||
this.log.error('registry', 'error parsing json')
|
||
return cb(ex, null, data, response)
|
||
}
|
||
} else if (data) {
|
||
parsed = data
|
||
data = JSON.stringify(parsed)
|
||
}
|
||
|
||
// expect data with any error codes
|
||
if (!data && response.statusCode >= 400) {
|
||
var code = response.statusCode
|
||
return cb(
|
||
makeError(code + ' ' + STATUS_CODES[code], null, code),
|
||
null,
|
||
data,
|
||
response
|
||
)
|
||
}
|
||
|
||
er = null
|
||
if (parsed && response.headers.etag) {
|
||
parsed._etag = response.headers.etag
|
||
}
|
||
|
||
if (parsed && response.headers['last-modified']) {
|
||
parsed._lastModified = response.headers['last-modified']
|
||
}
|
||
|
||
// for the search endpoint, the 'error' property can be an object
|
||
if ((parsed && parsed.error && typeof parsed.error !== 'object') ||
|
||
response.statusCode >= 400) {
|
||
var w = url.parse(where).pathname.substr(1)
|
||
var name
|
||
if (!w.match(/^-/)) {
|
||
w = w.split('/')
|
||
var index = w.indexOf('_rewrite')
|
||
if (index === -1) {
|
||
index = w.length - 1
|
||
} else {
|
||
index++
|
||
}
|
||
name = decodeURIComponent(w[index])
|
||
}
|
||
|
||
if (!parsed.error) {
|
||
er = makeError(
|
||
'Registry returned ' + response.statusCode +
|
||
' for ' + method +
|
||
' on ' + where,
|
||
name,
|
||
response.statusCode
|
||
)
|
||
} else if (name && parsed.error === 'not_found') {
|
||
er = makeError('404 Not Found: ' + name, name, response.statusCode)
|
||
} else {
|
||
er = makeError(
|
||
parsed.error + ' ' + (parsed.reason || '') + ': ' + (name || w),
|
||
name,
|
||
response.statusCode
|
||
)
|
||
}
|
||
}
|
||
return cb(er, parsed, data, response)
|
||
}.bind(this)
|
||
}
|
||
|
||
function makeError (message, name, code) {
|
||
var er = new Error(message)
|
||
if (name) er.pkgid = name
|
||
if (code) {
|
||
er.statusCode = code
|
||
er.code = 'E' + code
|
||
}
|
||
return er
|
||
}
|