2013-10-22 09:29:57 +02:00
|
|
|
var URL = require('url')
|
|
|
|
, request = require('request')
|
2013-12-11 23:00:26 +01:00
|
|
|
, stream = require('stream')
|
2013-10-22 09:29:57 +02:00
|
|
|
, UError = require('./error').UserError
|
|
|
|
, mystreams = require('./streams')
|
|
|
|
, Logger = require('./logger')
|
|
|
|
, utils = require('./utils')
|
2013-06-08 03:16:28 +02:00
|
|
|
|
2013-09-25 11:12:33 +02:00
|
|
|
//
|
|
|
|
// Implements Storage interface
|
|
|
|
// (same for storage.js, local-storage.js, up-storage.js)
|
|
|
|
//
|
2013-06-19 18:58:16 +02:00
|
|
|
function Storage(config, mainconfig) {
|
2013-10-26 14:18:36 +02:00
|
|
|
if (!(this instanceof Storage)) return new Storage(config)
|
|
|
|
this.config = config
|
2013-12-11 23:00:26 +01:00
|
|
|
this.is_alive = true
|
2013-10-26 14:18:36 +02:00
|
|
|
this.userAgent = mainconfig.user_agent
|
|
|
|
this.ca = config.ca
|
|
|
|
this.logger = Logger.logger.child({sub: 'out'})
|
2013-12-09 04:59:31 +01:00
|
|
|
this.server_id = mainconfig.server_id
|
2013-06-08 03:16:28 +02:00
|
|
|
|
2013-10-26 14:18:36 +02:00
|
|
|
this.url = URL.parse(this.config.url)
|
2013-06-19 18:58:16 +02:00
|
|
|
if (this.url.hostname === 'registry.npmjs.org') {
|
2013-10-26 14:18:36 +02:00
|
|
|
this.ca = this.ca || require('./npmsslkeys')
|
2013-06-20 15:41:07 +02:00
|
|
|
|
|
|
|
// npm registry is too slow working with ssl :(
|
|
|
|
/*if (this.config._autogenerated) {
|
2013-06-19 18:58:16 +02:00
|
|
|
// encrypt all the things!
|
2013-10-26 14:18:36 +02:00
|
|
|
this.url.protocol = 'https'
|
|
|
|
this.config.url = URL.format(this.url)
|
2013-06-20 15:41:07 +02:00
|
|
|
}*/
|
2013-06-08 03:16:28 +02:00
|
|
|
}
|
2013-06-19 18:58:16 +02:00
|
|
|
|
2013-11-24 18:07:18 +01:00
|
|
|
_setupProxy.call(this, this.url.hostname, config, mainconfig, this.url.protocol === 'https:')
|
|
|
|
|
2013-10-26 14:18:36 +02:00
|
|
|
this.config.url = this.config.url.replace(/\/$/, '')
|
2013-12-04 20:39:29 +01:00
|
|
|
if (isNaN(parseFloat(this.config.timeout)) || !isFinite(this.config.timeout)) {
|
2013-12-07 23:37:27 +01:00
|
|
|
this.config.timeout = 30000
|
2013-12-04 20:39:29 +01:00
|
|
|
}
|
2013-10-26 14:18:36 +02:00
|
|
|
return this
|
2013-06-08 03:16:28 +02:00
|
|
|
}
|
|
|
|
|
2013-11-24 18:07:18 +01: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) {
|
|
|
|
this.logger.debug({url: this.url.href, rule: no_proxy_item}, 'not using proxy for @{url}, excluded by @{rule} rule')
|
|
|
|
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 {
|
|
|
|
this.logger.debug({url: this.url.href, proxy: this.proxy}, 'using proxy @{proxy} for @{url}')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-09-28 19:31:58 +02:00
|
|
|
Storage.prototype.request = function(options, cb) {
|
2013-12-11 23:00:26 +01:00
|
|
|
if (!this.status_check()) {
|
|
|
|
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 11:31:48 +02:00
|
|
|
var self = this
|
2013-10-26 14:18:36 +02:00
|
|
|
, headers = options.headers || {}
|
2013-12-09 05:00:16 +01:00
|
|
|
headers.Accept = headers.Accept || 'application/json'
|
|
|
|
headers['User-Agent'] = headers['User-Agent'] || this.userAgent
|
2013-10-11 07:32:59 +02:00
|
|
|
|
2013-10-22 11:31:48 +02:00
|
|
|
var method = options.method || 'GET'
|
2013-10-26 14:18:36 +02:00
|
|
|
, uri = options.uri_full || (this.config.url + options.uri)
|
2013-10-11 07:32:59 +02:00
|
|
|
self.logger.info({
|
|
|
|
method: method,
|
|
|
|
headers: headers,
|
|
|
|
uri: uri,
|
2013-10-26 14:18:36 +02:00
|
|
|
}, "making request: '@{method} @{uri}'")
|
2013-10-11 07:32:59 +02:00
|
|
|
|
2013-10-22 09:29:57 +02:00
|
|
|
if (utils.is_object(options.json)) {
|
2013-10-26 14:18:36 +02:00
|
|
|
var json = JSON.stringify(options.json)
|
2013-12-09 05:00:16 +01:00
|
|
|
headers['Content-Type'] = headers['Content-Type'] || 'application/json'
|
2013-10-11 07:32:59 +02:00
|
|
|
}
|
|
|
|
|
2013-09-28 19:31:58 +02:00
|
|
|
var req = request({
|
2013-10-11 07:32:59 +02:00
|
|
|
url: uri,
|
|
|
|
method: method,
|
2013-09-28 19:31:58 +02:00
|
|
|
headers: headers,
|
2013-10-11 07:32:59 +02:00
|
|
|
body: json,
|
2013-09-28 19:31:58 +02:00
|
|
|
ca: this.ca,
|
2013-11-24 18:07:18 +01:00
|
|
|
proxy: this.proxy,
|
2013-12-04 20:39:29 +01:00
|
|
|
timeout: this.config.timeout
|
2013-10-11 07:32:59 +02:00
|
|
|
}, function(err, res, body) {
|
2013-10-22 11:31:48 +02:00
|
|
|
var error
|
2013-10-11 07:46:37 +02:00
|
|
|
if (!err) {
|
2013-10-26 14:18:36 +02:00
|
|
|
var res_length = body.length
|
2013-10-11 07:46:37 +02:00
|
|
|
|
2013-10-22 11:31:48 +02:00
|
|
|
if (options.json && res.statusCode < 300) {
|
2013-10-11 07:46:37 +02:00
|
|
|
try {
|
2013-10-26 14:18:36 +02:00
|
|
|
body = JSON.parse(body)
|
2013-10-22 11:31:48 +02:00
|
|
|
} catch(_err) {
|
|
|
|
body = {}
|
|
|
|
err = _err
|
|
|
|
error = err.message
|
2013-10-11 07:46:37 +02:00
|
|
|
}
|
2013-10-11 07:32:59 +02:00
|
|
|
}
|
|
|
|
|
2013-10-22 11:31:48 +02:00
|
|
|
if (!err && utils.is_object(body)) {
|
2013-10-11 07:46:37 +02:00
|
|
|
if (body.error) {
|
2013-10-26 14:18:36 +02:00
|
|
|
error = body.error
|
2013-10-11 07:46:37 +02:00
|
|
|
}
|
2013-10-11 07:32:59 +02:00
|
|
|
}
|
2013-10-11 07:46:37 +02:00
|
|
|
} else {
|
2013-10-26 14:18:36 +02:00
|
|
|
error = err.message
|
2013-10-11 07:32:59 +02:00
|
|
|
}
|
|
|
|
|
2013-10-26 14:18:36 +02:00
|
|
|
var msg = '@{!status}, req: \'@{request.method} @{request.url}\''
|
2013-10-11 07:32:59 +02:00
|
|
|
if (error) {
|
2013-10-26 14:18:36 +02:00
|
|
|
msg += ', error: @{!error}'
|
2013-10-11 07:32:59 +02:00
|
|
|
} else {
|
2013-10-26 14:18:36 +02:00
|
|
|
msg += ', bytes: @{bytes.in}/@{bytes.out}'
|
2013-10-11 07:32:59 +02:00
|
|
|
}
|
|
|
|
self.logger.warn({
|
2013-10-11 07:46:37 +02:00
|
|
|
err: err,
|
2013-10-11 07:32:59 +02:00
|
|
|
request: {method: method, url: uri},
|
|
|
|
level: 35, // http
|
2013-10-11 07:46:37 +02:00
|
|
|
status: res != null ? res.statusCode : 'ERR',
|
2013-10-11 07:32:59 +02:00
|
|
|
error: error,
|
|
|
|
bytes: {
|
|
|
|
in: json ? json.length : 0,
|
2013-10-11 07:46:37 +02:00
|
|
|
out: res_length || 0,
|
2013-10-11 07:32:59 +02:00
|
|
|
}
|
2013-10-26 14:18:36 +02:00
|
|
|
}, msg)
|
|
|
|
if (cb) cb.apply(self, arguments)
|
|
|
|
})
|
2013-10-11 07:32:59 +02:00
|
|
|
req.on('response', function(res) {
|
2013-12-11 23:00:26 +01:00
|
|
|
if (!req._sinopia_aborted) self.status_check(true)
|
2013-10-26 14:18:36 +02:00
|
|
|
})
|
2013-09-28 19:31:58 +02:00
|
|
|
req.on('error', function() {
|
2013-12-11 23:00:26 +01:00
|
|
|
if (!req._sinopia_aborted) self.status_check(false)
|
2013-10-26 14:18:36 +02:00
|
|
|
})
|
|
|
|
return req
|
2013-09-28 19:31:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Storage.prototype.status_check = function(alive) {
|
|
|
|
if (arguments.length === 0) {
|
2013-12-15 01:05:58 +01:00
|
|
|
return true // hold off this feature until v0.6.0
|
|
|
|
|
2013-12-11 23:00:26 +01:00
|
|
|
if (!this.is_alive && Math.abs(Date.now() - this.is_alive_time) < 2*60*1000) {
|
2013-10-26 14:18:36 +02:00
|
|
|
return false
|
2013-09-28 19:31:58 +02:00
|
|
|
} else {
|
2013-10-26 14:18:36 +02:00
|
|
|
return true
|
2013-09-28 19:31:58 +02:00
|
|
|
}
|
|
|
|
} else {
|
2013-12-11 23:00:26 +01:00
|
|
|
if (this.is_alive && !alive) {
|
|
|
|
this.logger.warn({host: this.url.host}, 'host @{host} is now offline')
|
|
|
|
} else if (!this.is_alive && alive) {
|
|
|
|
this.logger.info({host: this.url.host}, 'host @{host} is back online')
|
|
|
|
}
|
|
|
|
|
2013-10-26 14:18:36 +02:00
|
|
|
this.is_alive = alive
|
|
|
|
this.is_alive_time = Date.now()
|
2013-09-28 19:31:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-06-19 18:58:16 +02:00
|
|
|
Storage.prototype.can_fetch_url = function(url) {
|
2013-10-26 14:18:36 +02:00
|
|
|
url = URL.parse(url)
|
2013-06-19 18:58:16 +02:00
|
|
|
|
|
|
|
return url.protocol === this.url.protocol
|
|
|
|
&& url.host === this.url.host
|
2013-06-20 15:41:07 +02:00
|
|
|
&& url.path.indexOf(this.url.path) === 0
|
2013-06-19 18:58:16 +02:00
|
|
|
}
|
|
|
|
|
2013-12-09 04:58:25 +01:00
|
|
|
Storage.prototype.add_package = function(name, metadata, options, callback) {
|
|
|
|
if (typeof(options) === 'function') callback = options, options = {}
|
|
|
|
|
2013-09-28 19:31:58 +02:00
|
|
|
this.request({
|
|
|
|
uri: '/' + escape(name),
|
2013-09-28 12:59:05 +02:00
|
|
|
method: 'PUT',
|
|
|
|
json: metadata,
|
|
|
|
}, function(err, res, body) {
|
2013-10-26 14:18:36 +02:00
|
|
|
if (err) return callback(err)
|
2013-09-28 12:59:05 +02:00
|
|
|
if (!(res.statusCode >= 200 && res.statusCode < 300)) {
|
2013-10-26 14:18:36 +02:00
|
|
|
return callback(new Error('bad status code: ' + res.statusCode))
|
2013-09-28 12:59:05 +02:00
|
|
|
}
|
2013-10-26 14:18:36 +02:00
|
|
|
callback(null, body)
|
|
|
|
})
|
2013-09-25 11:18:38 +02:00
|
|
|
}
|
|
|
|
|
2013-12-09 04:58:25 +01:00
|
|
|
Storage.prototype.add_version = function(name, version, metadata, tag, options, callback) {
|
|
|
|
if (typeof(options) === 'function') callback = options, options = {}
|
|
|
|
|
2013-09-28 19:31:58 +02:00
|
|
|
this.request({
|
|
|
|
uri: '/' + escape(name) + '/' + escape(version) + '/-tag/' + escape(tag),
|
2013-09-28 13:08:38 +02:00
|
|
|
method: 'PUT',
|
|
|
|
json: metadata,
|
|
|
|
}, function(err, res, body) {
|
2013-10-26 14:18:36 +02:00
|
|
|
if (err) return callback(err)
|
2013-09-28 13:08:38 +02:00
|
|
|
if (!(res.statusCode >= 200 && res.statusCode < 300)) {
|
2013-10-26 14:18:36 +02:00
|
|
|
return callback(new Error('bad status code: ' + res.statusCode))
|
2013-09-28 13:08:38 +02:00
|
|
|
}
|
2013-10-26 14:18:36 +02:00
|
|
|
callback(null, body)
|
|
|
|
})
|
2013-09-25 11:18:38 +02:00
|
|
|
}
|
|
|
|
|
2013-12-09 04:58:25 +01:00
|
|
|
Storage.prototype.add_tarball = function(name, filename, options) {
|
|
|
|
if (!options) options = {}
|
|
|
|
|
2013-10-26 14:18:36 +02:00
|
|
|
var stream = new mystreams.UploadTarballStream()
|
|
|
|
, self = this
|
2013-09-28 14:19:40 +02:00
|
|
|
|
2013-09-28 19:31:58 +02:00
|
|
|
var wstream = this.request({
|
|
|
|
uri: '/' + escape(name) + '/-/' + escape(filename) + '/whatever',
|
2013-09-28 14:19:40 +02:00
|
|
|
method: 'PUT',
|
|
|
|
headers: {
|
2013-12-09 05:00:16 +01:00
|
|
|
'Content-Type': 'application/octet-stream'
|
2013-09-28 14:19:40 +02:00
|
|
|
},
|
2013-10-26 14:18:36 +02:00
|
|
|
})
|
2013-09-28 14:19:40 +02:00
|
|
|
|
|
|
|
wstream.on('response', function(res) {
|
|
|
|
if (!(res.statusCode >= 200 && res.statusCode < 300)) {
|
|
|
|
return stream.emit('error', new UError({
|
|
|
|
msg: 'bad uplink status code: ' + res.statusCode,
|
|
|
|
status: 500,
|
2013-10-26 14:18:36 +02:00
|
|
|
}))
|
2013-09-28 14:19:40 +02:00
|
|
|
}
|
2013-10-26 14:18:36 +02:00
|
|
|
stream.emit('success')
|
|
|
|
})
|
2013-09-28 14:19:40 +02:00
|
|
|
|
|
|
|
wstream.on('error', function(err) {
|
2013-10-26 14:18:36 +02:00
|
|
|
stream.emit('error', err)
|
|
|
|
})
|
2013-09-28 14:19:40 +02:00
|
|
|
|
|
|
|
stream.abort = function() {
|
2013-09-28 14:37:24 +02:00
|
|
|
process.nextTick(function() {
|
|
|
|
if (wstream.req) {
|
2013-12-11 23:00:26 +01:00
|
|
|
wstream._sinopia_aborted = true
|
2013-10-26 14:18:36 +02:00
|
|
|
wstream.req.abort()
|
2013-09-28 14:37:24 +02:00
|
|
|
}
|
2013-10-26 14:18:36 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
stream.done = function() {}
|
|
|
|
stream.pipe(wstream)
|
2013-09-28 14:19:40 +02:00
|
|
|
|
2013-10-26 14:18:36 +02:00
|
|
|
return stream
|
2013-09-25 11:18:38 +02:00
|
|
|
}
|
|
|
|
|
2013-12-09 04:58:25 +01: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-10-22 11:31:48 +02:00
|
|
|
}
|
2013-12-09 04:58:25 +01:00
|
|
|
this._add_proxy_headers(options.req, headers)
|
|
|
|
|
2013-09-28 19:31:58 +02:00
|
|
|
this.request({
|
|
|
|
uri: '/' + escape(name),
|
2013-06-08 03:16:28 +02:00
|
|
|
json: true,
|
2013-10-22 11:31:48 +02:00
|
|
|
headers: headers,
|
2013-06-08 03:16:28 +02:00
|
|
|
}, function(err, res, body) {
|
2013-10-26 14:18:36 +02:00
|
|
|
if (err) return callback(err)
|
2013-06-14 10:34:29 +02:00
|
|
|
if (res.statusCode === 404) {
|
|
|
|
return callback(new UError({
|
|
|
|
msg: 'package doesn\'t exist on uplink',
|
|
|
|
status: 404,
|
2013-10-26 14:18:36 +02:00
|
|
|
}))
|
2013-06-14 10:34:29 +02:00
|
|
|
}
|
2013-06-14 09:56:02 +02:00
|
|
|
if (!(res.statusCode >= 200 && res.statusCode < 300)) {
|
2013-10-26 14:18:36 +02:00
|
|
|
return callback(new Error('bad status code: ' + res.statusCode))
|
2013-06-14 09:56:02 +02:00
|
|
|
}
|
2013-10-26 14:18:36 +02:00
|
|
|
callback(null, body, res.headers.etag)
|
|
|
|
})
|
2013-06-08 03:16:28 +02:00
|
|
|
}
|
|
|
|
|
2013-12-09 04:58:25 +01:00
|
|
|
Storage.prototype.get_tarball = function(name, options, filename) {
|
|
|
|
if (!options) options = {}
|
2013-10-26 14:18:36 +02:00
|
|
|
return this.get_url(this.config.url + '/' + name + '/-/' + filename)
|
2013-06-20 15:41:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Storage.prototype.get_url = function(url) {
|
2013-10-26 14:18:36 +02:00
|
|
|
var stream = new mystreams.ReadTarballStream()
|
|
|
|
stream.abort = function() {}
|
2013-06-20 15:41:07 +02:00
|
|
|
|
2013-09-28 19:31:58 +02:00
|
|
|
var rstream = this.request({
|
2013-10-02 20:48:32 +02:00
|
|
|
uri_full: url,
|
2013-06-19 18:58:16 +02:00
|
|
|
encoding: null,
|
2013-10-26 14:18:36 +02:00
|
|
|
})
|
|
|
|
|
2013-06-20 15:41:07 +02:00
|
|
|
rstream.on('response', function(res) {
|
2013-06-19 18:58:16 +02:00
|
|
|
if (res.statusCode === 404) {
|
2013-06-20 15:41:07 +02:00
|
|
|
return stream.emit('error', new UError({
|
2013-06-19 18:58:16 +02:00
|
|
|
msg: 'file doesn\'t exist on uplink',
|
|
|
|
status: 404,
|
2013-10-26 14:18:36 +02:00
|
|
|
}))
|
2013-06-19 18:58:16 +02:00
|
|
|
}
|
|
|
|
if (!(res.statusCode >= 200 && res.statusCode < 300)) {
|
2013-06-20 15:41:07 +02:00
|
|
|
return stream.emit('error', new UError({
|
|
|
|
msg: 'bad uplink status code: ' + res.statusCode,
|
|
|
|
status: 500,
|
2013-10-26 14:18:36 +02:00
|
|
|
}))
|
2013-06-19 18:58:16 +02:00
|
|
|
}
|
2013-06-20 15:41:07 +02:00
|
|
|
|
2013-10-26 14:18:36 +02:00
|
|
|
rstream.pipe(stream)
|
|
|
|
})
|
2013-06-20 15:41:07 +02:00
|
|
|
|
|
|
|
rstream.on('error', function(err) {
|
2013-10-26 14:18:36 +02:00
|
|
|
stream.emit('error', err)
|
|
|
|
})
|
|
|
|
return stream
|
2013-06-19 18:58:16 +02:00
|
|
|
}
|
|
|
|
|
2013-12-09 04:58:25 +01: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 14:18:36 +02:00
|
|
|
module.exports = Storage
|
2013-06-08 03:16:28 +02:00
|
|
|
|