0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-23 22:27:34 -05:00
verdaccio/lib/up-storage.js

355 lines
9.4 KiB
JavaScript
Raw Normal View History

var URL = require('url')
, request = require('request')
2013-12-22 19:14:57 -05:00
, Stream = require('stream')
2014-03-30 16:05:42 -05:00
, zlib = require('zlib')
, UError = require('./error').UserError
, mystreams = require('./streams')
, Logger = require('./logger')
, utils = require('./utils')
, parse_interval = require('./config').parse_interval
2013-12-22 19:14:57 -05:00
, encode = encodeURIComponent
2013-06-07 20:16:28 -05:00
2013-09-25 04:12:33 -05:00
//
// Implements Storage interface
// (same for storage.js, local-storage.js, up-storage.js)
//
2013-06-19 11:58:16 -05:00
function Storage(config, mainconfig) {
2013-10-26 07:18:36 -05:00
if (!(this instanceof Storage)) return new Storage(config)
this.config = config
this.failed_requests = 0
2013-10-26 07:18:36 -05:00
this.userAgent = mainconfig.user_agent
this.ca = config.ca
this.logger = Logger.logger.child({sub: 'out'})
2013-12-08 22:59:31 -05:00
this.server_id = mainconfig.server_id
2013-06-07 20:16:28 -05:00
2013-10-26 07:18:36 -05:00
this.url = URL.parse(this.config.url)
2013-06-19 11:58:16 -05:00
if (this.url.hostname === 'registry.npmjs.org') {
2013-06-20 08:41:07 -05:00
// npm registry is too slow working with ssl :(
/*if (this.config._autogenerated) {
2013-06-19 11:58:16 -05:00
// encrypt all the things!
2013-10-26 07:18:36 -05:00
this.url.protocol = 'https'
this.config.url = URL.format(this.url)
2013-06-20 08:41:07 -05:00
}*/
2013-06-07 20:16:28 -05:00
}
2013-06-19 11:58:16 -05:00
2013-11-24 12:07:18 -05:00
_setupProxy.call(this, this.url.hostname, config, mainconfig, this.url.protocol === 'https:')
2013-10-26 07:18:36 -05:00
this.config.url = this.config.url.replace(/\/$/, '')
if (Number(this.config.timeout) >= 1000) {
this.logger.warn('Too big timeout value: ' + this.config.timeout + '\nWe changed time format to nginx-like one\n(see http://wiki.nginx.org/ConfigNotation)\nso please update your config accordingly')
}
2014-03-13 14:45:47 -05:00
// a bunch of different configurable timers
this.maxage = parse_interval(config_get('maxage' , '2m' ))
this.timeout = parse_interval(config_get('timeout' , '30s'))
this.max_fails = Number(config_get('max_fails' , 2 ))
this.fail_timeout = parse_interval(config_get('fail_timeout', '5m' ))
2013-10-26 07:18:36 -05:00
return this
2014-03-13 14:45:47 -05:00
// just a helper (`config[key] || default` doesn't work because of zeroes)
function config_get(key, def) {
return config[key] != null ? config[key] : def
}
2013-06-07 20:16:28 -05:00
}
2013-11-24 12:07:18 -05:00
function _setupProxy(hostname, config, mainconfig, isHTTPS) {
var no_proxy
var proxy_key = isHTTPS ? 'https_proxy' : 'http_proxy'
// get http_proxy and no_proxy configs
if (proxy_key in config) {
this.proxy = config[proxy_key]
} else if (proxy_key in mainconfig) {
this.proxy = mainconfig[proxy_key]
}
if ('no_proxy' in config) {
no_proxy = config.no_proxy
} else if ('no_proxy' in mainconfig) {
no_proxy = mainconfig.no_proxy
}
// use wget-like algorithm to determine if proxy shouldn't be used
if (hostname[0] !== '.') hostname = '.' + hostname
if (typeof(no_proxy) === 'string' && no_proxy.length) {
no_proxy = no_proxy.split(',')
}
if (Array.isArray(no_proxy)) {
for (var i=0; i<no_proxy.length; i++) {
var no_proxy_item = no_proxy[i]
if (no_proxy_item[0] !== '.') no_proxy_item = '.' + no_proxy_item
if (hostname.lastIndexOf(no_proxy_item) === hostname.length - no_proxy_item.length) {
if (this.proxy) {
2013-12-22 19:14:57 -05:00
this.logger.debug({url: this.url.href, rule: no_proxy_item},
'not using proxy for @{url}, excluded by @{rule} rule')
2013-11-24 12:07:18 -05:00
this.proxy = false
}
break
}
}
}
// if it's non-string (i.e. "false"), don't use it
if (typeof(this.proxy) !== 'string') {
delete this.proxy
} else {
2013-12-22 19:14:57 -05:00
this.logger.debug({url: this.url.href, proxy: this.proxy},
'using proxy @{proxy} for @{url}')
2013-11-24 12:07:18 -05:00
}
}
2013-09-28 12:31:58 -05:00
Storage.prototype.request = function(options, cb) {
if (!this.status_check()) {
2013-12-22 19:14:57 -05:00
var req = new Stream.Readable()
process.nextTick(function() {
if (typeof(cb) === 'function') cb(new Error('uplink is offline'))
req.emit('error', new Error('uplink is offline'))
})
// preventing 'Uncaught, unspecified "error" event'
req.on('error', function(){})
return req
}
2013-10-22 04:31:48 -05:00
var self = this
2013-10-26 07:18:36 -05:00
, headers = options.headers || {}
2014-03-30 16:05:42 -05:00
headers['Accept'] = headers['Accept'] || 'application/json'
headers['Accept-Encoding'] = headers['Accept-Encoding'] || 'gzip'
headers['User-Agent'] = headers['User-Agent'] || this.userAgent
2013-10-22 04:31:48 -05:00
var method = options.method || 'GET'
2013-10-26 07:18:36 -05:00
, uri = options.uri_full || (this.config.url + options.uri)
self.logger.info({
method: method,
headers: headers,
uri: uri,
2013-10-26 07:18:36 -05:00
}, "making request: '@{method} @{uri}'")
if (utils.is_object(options.json)) {
2013-10-26 07:18:36 -05:00
var json = JSON.stringify(options.json)
headers['Content-Type'] = headers['Content-Type'] || 'application/json'
}
2013-09-28 12:31:58 -05:00
var req = request({
url: uri,
method: method,
2013-09-28 12:31:58 -05:00
headers: headers,
body: json,
2013-09-28 12:31:58 -05:00
ca: this.ca,
2013-11-24 12:07:18 -05:00
proxy: this.proxy,
2014-03-30 16:05:42 -05:00
encoding: null,
timeout: this.timeout,
}, function(err, res, body) {
2013-10-22 04:31:48 -05:00
var error
2014-03-30 16:05:42 -05:00
var res_length = err ? 0 : body.length
2014-03-30 16:05:42 -05:00
do_gunzip(function() {
do_decode()
do_log()
if (cb) cb(err, res, body)
})
function do_gunzip(cb) {
if (err) return cb()
if (res.headers['content-encoding'] !== 'gzip') return cb()
zlib.gunzip(body, function(er, buf) {
if (er) err = er
body = buf
return cb()
})
}
function do_decode() {
2014-04-13 19:44:17 -05:00
if (err) {
error = err.message
return
}
2013-10-22 04:31:48 -05:00
if (options.json && res.statusCode < 300) {
try {
2014-03-30 16:05:42 -05:00
body = JSON.parse(body.toString('utf8'))
2013-10-22 04:31:48 -05:00
} catch(_err) {
body = {}
err = _err
error = err.message
}
}
2013-10-22 04:31:48 -05:00
if (!err && utils.is_object(body)) {
if (typeof(body.error) === 'string') {
2013-10-26 07:18:36 -05:00
error = body.error
}
}
}
2014-03-30 16:05:42 -05:00
function do_log() {
var message = '@{!status}, req: \'@{request.method} @{request.url}\''
2014-03-30 16:05:42 -05:00
if (error) {
message += ', error: @{!error}'
2014-03-30 16:05:42 -05:00
} else {
message += ', bytes: @{bytes.in}/@{bytes.out}'
}
2014-03-30 16:05:42 -05:00
self.logger.warn({
err: err,
request: {method: method, url: uri},
level: 35, // http
status: res != null ? res.statusCode : 'ERR',
error: error,
bytes: {
in: json ? json.length : 0,
out: res_length || 0,
}
}, message)
2014-03-30 16:05:42 -05:00
}
2013-10-26 07:18:36 -05:00
})
2014-03-07 23:00:07 -05:00
var status_called = false
req.on('response', function(res) {
2014-03-07 23:00:07 -05:00
if (!req._sinopia_aborted && !status_called) {
status_called = true
self.status_check(true)
}
2013-10-26 07:18:36 -05:00
})
2014-04-13 19:44:17 -05:00
req.on('error', function(_err) {
2014-03-07 23:00:07 -05:00
if (!req._sinopia_aborted && !status_called) {
status_called = true
self.status_check(false)
}
2013-10-26 07:18:36 -05:00
})
return req
2013-09-28 12:31:58 -05:00
}
Storage.prototype.status_check = function(alive) {
if (arguments.length === 0) {
if (this.failed_requests >= this.max_fails && Math.abs(Date.now() - this.last_request_time) < this.fail_timeout) {
2013-10-26 07:18:36 -05:00
return false
2013-09-28 12:31:58 -05:00
} else {
2013-10-26 07:18:36 -05:00
return true
}
2013-09-28 12:31:58 -05:00
} else {
if (alive) {
if (this.failed_requests >= this.max_fails) {
this.logger.warn({host: this.url.host}, 'host @{host} is back online')
}
this.failed_requests = 0
} else {
this.failed_requests++
if (this.failed_requests === this.max_fails) {
this.logger.warn({host: this.url.host}, 'host @{host} is now offline')
}
}
this.last_request_time = Date.now()
2013-09-28 12:31:58 -05:00
}
}
2013-06-19 11:58:16 -05:00
Storage.prototype.can_fetch_url = function(url) {
2013-10-26 07:18:36 -05:00
url = URL.parse(url)
2013-06-19 11:58:16 -05:00
return url.protocol === this.url.protocol
&& url.host === this.url.host
2013-06-20 08:41:07 -05:00
&& url.path.indexOf(this.url.path) === 0
2013-06-19 11:58:16 -05:00
}
2013-12-08 22:58:25 -05:00
Storage.prototype.get_package = function(name, options, callback) {
if (typeof(options) === 'function') callback = options, options = {}
var headers = {}
if (options.etag) {
headers['If-None-Match'] = options.etag
2013-12-27 08:06:30 -05:00
headers['Accept'] = 'application/octet-stream'
2013-10-22 04:31:48 -05:00
}
2013-12-08 22:58:25 -05:00
this._add_proxy_headers(options.req, headers)
2013-09-28 12:31:58 -05:00
this.request({
2013-12-22 19:14:57 -05:00
uri: '/' + encode(name),
2013-06-07 20:16:28 -05:00
json: true,
2013-10-22 04:31:48 -05:00
headers: headers,
2013-06-07 20:16:28 -05:00
}, function(err, res, body) {
2013-10-26 07:18:36 -05:00
if (err) return callback(err)
2013-06-14 03:34:29 -05:00
if (res.statusCode === 404) {
return callback(new UError({
message: 'package doesn\'t exist on uplink',
2013-06-14 03:34:29 -05:00
status: 404,
2013-10-26 07:18:36 -05:00
}))
2013-06-14 03:34:29 -05:00
}
2013-06-14 02:56:02 -05:00
if (!(res.statusCode >= 200 && res.statusCode < 300)) {
var error = new Error('bad status code: ' + res.statusCode)
error.status = res.statusCode
return callback(error)
2013-06-14 02:56:02 -05:00
}
2013-10-26 07:18:36 -05:00
callback(null, body, res.headers.etag)
})
2013-06-07 20:16:28 -05:00
}
2013-12-08 22:58:25 -05:00
Storage.prototype.get_tarball = function(name, options, filename) {
if (!options) options = {}
2013-10-26 07:18:36 -05:00
return this.get_url(this.config.url + '/' + name + '/-/' + filename)
2013-06-20 08:41:07 -05:00
}
Storage.prototype.get_url = function(url) {
2013-10-26 07:18:36 -05:00
var stream = new mystreams.ReadTarballStream()
stream.abort = function() {}
2014-03-07 14:48:24 -05:00
var current_length = 0, expected_length
2013-06-20 08:41:07 -05:00
2013-09-28 12:31:58 -05:00
var rstream = this.request({
uri_full: url,
2013-06-19 11:58:16 -05:00
encoding: null,
headers: {
Accept: 'application/octet-stream',
},
2013-10-26 07:18:36 -05:00
})
2013-06-20 08:41:07 -05:00
rstream.on('response', function(res) {
2013-06-19 11:58:16 -05:00
if (res.statusCode === 404) {
2013-06-20 08:41:07 -05:00
return stream.emit('error', new UError({
message: 'file doesn\'t exist on uplink',
2013-06-19 11:58:16 -05:00
status: 404,
2013-10-26 07:18:36 -05:00
}))
2013-06-19 11:58:16 -05:00
}
if (!(res.statusCode >= 200 && res.statusCode < 300)) {
2013-06-20 08:41:07 -05:00
return stream.emit('error', new UError({
message: 'bad uplink status code: ' + res.statusCode,
2013-06-20 08:41:07 -05:00
status: 500,
2013-10-26 07:18:36 -05:00
}))
2013-06-19 11:58:16 -05:00
}
2014-03-07 14:48:24 -05:00
if (res.headers['content-length']) {
expected_length = res.headers['content-length']
stream.emit('content-length', res.headers['content-length'])
2014-03-07 14:48:24 -05:00
}
2013-06-20 08:41:07 -05:00
2013-10-26 07:18:36 -05:00
rstream.pipe(stream)
})
2013-06-20 08:41:07 -05:00
rstream.on('error', function(err) {
2013-10-26 07:18:36 -05:00
stream.emit('error', err)
})
2014-03-07 14:48:24 -05:00
rstream.on('data', function(d) {
current_length += d.length
})
rstream.on('end', function(d) {
if (d) current_length += d.length
if (expected_length && current_length != expected_length)
stream.emit('error', new Error('content length mismatch'))
})
2013-10-26 07:18:36 -05:00
return stream
2013-06-19 11:58:16 -05:00
}
2013-12-08 22:58:25 -05:00
Storage.prototype._add_proxy_headers = function(req, headers) {
if (req) {
headers['X-Forwarded-For'] = (
(req && req.headers['x-forwarded-for']) ?
req.headers['x-forwarded-for'] + ', ' :
''
) + req.connection.remoteAddress
}
// always attach Via header to avoid loops, even if we're not proxying
headers['Via'] =
(req && req.headers['via']) ?
req.headers['via'] + ', ' :
''
headers['Via'] += '1.1 ' + this.server_id + ' (Sinopia)'
}
2013-10-26 07:18:36 -05:00
module.exports = Storage
2013-06-07 20:16:28 -05:00