2013-10-22 09:29:57 +02:00
var URL = require ( 'url' )
, request = require ( 'request' )
2013-12-23 01:14:57 +01:00
, Stream = require ( 'stream' )
2014-03-30 23:05:42 +02:00
, zlib = require ( 'zlib' )
2014-09-10 19:55:26 +02:00
, Error = require ( 'http-errors' )
2013-10-22 09:29:57 +02:00
, mystreams = require ( './streams' )
, Logger = require ( './logger' )
, utils = require ( './utils' )
2014-03-08 04:49:59 +01:00
, parse _interval = require ( './config' ) . parse _interval
2013-12-23 01:14:57 +01:00
, encode = encodeURIComponent
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
2014-03-08 04:54:28 +01:00
this . failed _requests = 0
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-06-20 15:41:07 +02:00
// npm registry is too slow working with ssl :(
/ * i f ( t h i s . c o n f i g . _ a u t o g e n e r a t e d ) {
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 ( /\/$/ , '' )
2014-03-08 05:37:16 +01:00
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 20:45:47 +01: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 14:18:36 +02:00
return this
2014-03-13 20:45:47 +01: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-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 ) {
2013-12-23 01:14:57 +01:00
this . logger . debug ( { url : this . url . href , rule : no _proxy _item } ,
'not using proxy for @{url}, excluded by @{rule} rule' )
2013-11-24 18:07:18 +01: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-23 01:14:57 +01:00
this . logger . debug ( { url : this . url . href , proxy : this . proxy } ,
'using proxy @{proxy} for @{url}' )
2013-11-24 18:07:18 +01:00
}
}
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 ( ) ) {
2013-12-23 01:14:57 +01:00
var req = new Stream . Readable ( )
2013-12-11 23:00:26 +01:00
process . nextTick ( function ( ) {
2014-09-10 19:55:26 +02:00
if ( typeof ( cb ) === 'function' ) cb ( Error ( 'uplink is offline' ) )
req . emit ( 'error' , Error ( 'uplink is offline' ) )
2013-12-11 23:00:26 +01:00
} )
// 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 || { }
2014-03-30 23:05:42 +02:00
headers [ 'Accept' ] = headers [ 'Accept' ] || 'application/json'
headers [ 'Accept-Encoding' ] = headers [ 'Accept-Encoding' ] || 'gzip'
2013-12-09 05:00:16 +01:00
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 ,
2014-03-30 23:05:42 +02:00
encoding : null ,
timeout : this . timeout ,
2013-10-11 07:32:59 +02:00
} , function ( err , res , body ) {
2013-10-22 11:31:48 +02:00
var error
2014-03-30 23:05:42 +02:00
var res _length = err ? 0 : body . length
2013-10-11 07:46:37 +02:00
2014-03-30 23:05:42 +02: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-14 02:44:17 +02:00
if ( err ) {
error = err . message
return
}
2013-10-22 11:31:48 +02:00
if ( options . json && res . statusCode < 300 ) {
2013-10-11 07:46:37 +02:00
try {
2014-03-30 23:05:42 +02:00
body = JSON . parse ( body . toString ( 'utf8' ) )
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 ) ) {
2014-06-22 16:06:16 +02:00
if ( typeof ( body . error ) === 'string' ) {
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
}
}
2014-03-30 23:05:42 +02:00
function do _log ( ) {
2014-07-22 21:31:01 +02:00
var message = '@{!status}, req: \'@{request.method} @{request.url}\''
2014-03-30 23:05:42 +02:00
if ( error ) {
2014-07-22 21:31:01 +02:00
message += ', error: @{!error}'
2014-03-30 23:05:42 +02:00
} else {
2014-07-22 21:31:01 +02:00
message += ', bytes: @{bytes.in}/@{bytes.out}'
2013-10-11 07:32:59 +02:00
}
2014-03-30 23:05:42 +02: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 ,
}
2014-07-22 21:31:01 +02:00
} , message )
2014-03-30 23:05:42 +02:00
}
2013-10-26 14:18:36 +02:00
} )
2014-03-08 05:00:07 +01:00
var status _called = false
2013-10-11 07:32:59 +02:00
req . on ( 'response' , function ( res ) {
2014-03-08 05:00:07 +01:00
if ( ! req . _sinopia _aborted && ! status _called ) {
status _called = true
self . status _check ( true )
}
2013-10-26 14:18:36 +02:00
} )
2014-04-14 02:44:17 +02:00
req . on ( 'error' , function ( _err ) {
2014-03-08 05:00:07 +01:00
if ( ! req . _sinopia _aborted && ! status _called ) {
status _called = true
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 ) {
2014-03-08 04:54:28 +01:00
if ( this . failed _requests >= this . max _fails && Math . abs ( Date . now ( ) - this . last _request _time ) < this . fail _timeout ) {
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
2014-03-08 04:54:28 +01:00
}
2013-09-28 19:31:58 +02:00
} else {
2014-03-08 04:54:28 +01:00
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' )
}
2013-12-11 23:00:26 +01:00
}
2014-03-08 04:54:28 +01:00
this . last _request _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 . 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 14:06:30 +01:00
headers [ 'Accept' ] = 'application/octet-stream'
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 ( {
2013-12-23 01:14:57 +01:00
uri : '/' + encode ( 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 ) {
2014-09-10 19:55:26 +02:00
return callback ( Error [ 404 ] ( "package doesn't exist on uplink" ) )
2013-06-14 10:34:29 +02:00
}
2013-06-14 09:56:02 +02:00
if ( ! ( res . statusCode >= 200 && res . statusCode < 300 ) ) {
2014-09-10 19:55:26 +02:00
var error = Error ( 'bad status code: ' + res . statusCode )
error . remoteStatus = res . statusCode
2014-03-29 05:32:05 +01:00
return callback ( error )
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 ( ) { }
2014-03-07 20:48:24 +01:00
var current _length = 0 , expected _length
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-12-26 20:25:19 +01:00
headers : {
Accept : 'application/octet-stream' ,
} ,
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 ) {
2014-09-10 19:55:26 +02:00
return stream . emit ( 'error' , Error [ 404 ] ( "file doesn't exist on uplink" ) )
2013-06-19 18:58:16 +02:00
}
if ( ! ( res . statusCode >= 200 && res . statusCode < 300 ) ) {
2014-09-10 19:55:26 +02:00
return stream . emit ( 'error' , Error ( 'bad uplink status code: ' + res . statusCode ) )
2013-06-19 18:58:16 +02:00
}
2014-03-07 20:48:24 +01:00
if ( res . headers [ 'content-length' ] ) {
expected _length = res . headers [ 'content-length' ]
2014-03-07 20:39:20 +01:00
stream . emit ( 'content-length' , res . headers [ 'content-length' ] )
2014-03-07 20:48:24 +01: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 )
} )
2014-03-07 20:48:24 +01: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 )
2014-09-10 19:55:26 +02:00
stream . emit ( 'error' , Error ( 'content length mismatch' ) )
2014-03-07 20:48:24 +01:00
} )
2013-10-26 14:18:36 +02:00
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