2014-11-13 12:13:37 -05:00
var Cookies = require ( 'cookies' )
var express = require ( 'express' )
var expressJson5 = require ( 'express-json5' )
var Error = require ( 'http-errors' )
2014-11-16 12:44:46 -05:00
var Path = require ( 'path' )
2014-11-13 12:13:37 -05:00
var Middleware = require ( './middleware' )
var Utils = require ( './utils' )
var expect _json = Middleware . expect _json
var match = Middleware . match
var media = Middleware . media
var validate _name = Middleware . validate _name
2014-11-16 12:44:46 -05:00
var validate _pkg = Middleware . validate _package
2014-11-13 12:13:37 -05:00
2015-04-08 15:54:59 -05:00
module . exports = function ( config , auth , storage ) {
2014-11-13 12:13:37 -05:00
var app = express . Router ( )
2015-04-08 15:54:59 -05:00
var can = Middleware . allow ( auth )
2014-11-13 12:13:37 -05:00
// validate all of these params as a package name
// this might be too harsh, so ask if it causes trouble
2014-11-16 12:44:46 -05:00
app . param ( 'package' , validate _pkg )
2014-11-13 12:13:37 -05:00
app . param ( 'filename' , validate _name )
app . param ( 'tag' , validate _name )
app . param ( 'version' , validate _name )
app . param ( 'revision' , validate _name )
// these can't be safely put into express url for some reason
app . param ( '_rev' , match ( /^-rev$/ ) )
app . param ( 'org_couchdb_user' , match ( /^org\.couchdb\.user:/ ) )
app . param ( 'anything' , match ( /.*/ ) )
2014-11-16 07:37:50 -05:00
app . use ( auth . basic _middleware ( ) )
2014-11-24 14:46:37 -05:00
//app.use(auth.bearer_middleware())
2014-11-13 12:13:37 -05:00
app . use ( expressJson5 ( { strict : false , limit : config . max _body _size || '10mb' } ) )
app . use ( Middleware . anti _loop ( config ) )
2015-06-28 20:10:54 -05:00
// encode / in a scoped package name to be matched as a single parameter in routes
app . use ( function ( req , res , next ) {
if ( req . url . indexOf ( '@' ) != - 1 ) {
// e.g.: /@org/pkg/1.2.3 -> /@org%2Fpkg/1.2.3, /@org%2Fpkg/1.2.3 -> /@org%2Fpkg/1.2.3
req . url = req . url . replace ( /^(\/@[^\/%]+)\/(?!$)/ , '$1%2F' )
}
next ( )
} )
2014-11-16 07:37:50 -05:00
// for "npm whoami"
app . get ( '/whoami' , function ( req , res , next ) {
if ( req . headers . referer === 'whoami' ) {
next ( { username : req . remote _user . name } )
} else {
next ( 'route' )
}
} )
2014-12-04 20:53:37 -05:00
app . get ( '/-/whoami' , function ( req , res , next ) {
next ( { username : req . remote _user . name } )
} )
2014-11-16 07:37:50 -05:00
2014-11-13 12:13:37 -05:00
// TODO: anonymous user?
app . get ( '/:package/:version?' , can ( 'access' ) , function ( req , res , next ) {
storage . get _package ( req . params . package , { req : req } , function ( err , info ) {
if ( err ) return next ( err )
info = Utils . filter _tarball _urls ( info , req , config )
var version = req . params . version
2014-11-13 13:32:31 -05:00
if ( ! version ) return next ( info )
2014-11-13 12:13:37 -05:00
var t = Utils . get _version ( info , version )
2014-11-13 13:32:31 -05:00
if ( t != null ) return next ( t )
2014-11-13 12:13:37 -05:00
if ( info [ 'dist-tags' ] != null ) {
if ( info [ 'dist-tags' ] [ version ] != null ) {
version = info [ 'dist-tags' ] [ version ]
2014-11-13 13:32:31 -05:00
t = Utils . get _version ( info , version )
if ( t != null ) return next ( t )
2014-11-13 12:13:37 -05:00
}
}
return next ( Error [ 404 ] ( 'version not found: ' + req . params . version ) )
} )
} )
app . get ( '/:package/-/:filename' , can ( 'access' ) , function ( req , res , next ) {
var stream = storage . get _tarball ( req . params . package , req . params . filename )
stream . on ( 'content-length' , function ( v ) {
res . header ( 'Content-Length' , v )
} )
stream . on ( 'error' , function ( err ) {
return res . report _error ( err )
} )
res . header ( 'Content-Type' , 'application/octet-stream' )
stream . pipe ( res )
} )
// searching packages
app . get ( '/-/all/:anything?' , function ( req , res , next ) {
2015-05-16 17:29:16 -05:00
var received _end = false
var response _finished = false
var processing _pkgs = 0
res . status ( 200 )
res . write ( '{"_updated":' + Date . now ( ) ) ;
var stream = storage . search ( req . param . startkey || 0 , { req : req } )
stream . on ( 'data' , function each ( pkg ) {
processing _pkgs ++
auth . allow _access ( pkg . name , req . remote _user , function ( err , allowed ) {
processing _pkgs --
if ( err ) {
if ( err . status && String ( err . status ) . match ( /^4\d\d$/ ) ) {
// auth plugin returns 4xx user error,
// that's equivalent of !allowed basically
allowed = false
} else {
stream . abort ( err )
2015-04-08 15:54:59 -05:00
}
2015-05-16 17:29:16 -05:00
}
if ( allowed ) {
res . write ( ',\n' + JSON . stringify ( pkg . name ) + ':' + JSON . stringify ( pkg ) )
}
2015-04-08 15:54:59 -05:00
2015-05-16 17:29:16 -05:00
check _finish ( )
2015-02-24 22:21:57 -05:00
} )
2014-11-13 12:13:37 -05:00
} )
2015-05-16 17:29:16 -05:00
stream . on ( 'error' , function ( _err ) {
res . socket . destroy ( )
} )
stream . on ( 'end' , function ( ) {
received _end = true
check _finish ( )
} )
function check _finish ( ) {
if ( ! received _end ) return
if ( processing _pkgs ) return
if ( response _finished ) return
response _finished = true
res . end ( '}\n' )
}
} )
2014-11-13 12:13:37 -05:00
// placeholder 'cause npm require to be authenticated to publish
// we do not do any real authentication yet
2015-02-06 13:59:18 -05:00
app . post ( '/_session' , Cookies . express ( ) , function ( req , res , next ) {
2014-11-13 12:13:37 -05:00
res . cookies . set ( 'AuthSession' , String ( Math . random ( ) ) , {
// npmjs.org sets 10h expire
expires : new Date ( Date . now ( ) + 10 * 60 * 60 * 1000 )
} )
2014-11-13 13:32:31 -05:00
next ( { ok : true , name : 'somebody' , roles : [ ] } )
2014-11-13 12:13:37 -05:00
} )
app . get ( '/-/user/:org_couchdb_user' , function ( req , res , next ) {
res . status ( 200 )
2014-11-13 13:32:31 -05:00
next ( {
2014-11-13 12:13:37 -05:00
ok : 'you are authenticated as "' + req . remote _user . name + '"' ,
} )
} )
app . put ( '/-/user/:org_couchdb_user/:_rev?/:revision?' , function ( req , res , next ) {
2014-11-24 14:46:37 -05:00
var token = ( req . body . name && req . body . password )
? auth . aes _encrypt ( req . body . name + ':' + req . body . password ) . toString ( 'base64' )
: undefined
2014-11-13 12:13:37 -05:00
if ( req . remote _user . name != null ) {
res . status ( 201 )
2014-11-13 13:32:31 -05:00
return next ( {
2014-11-16 07:37:50 -05:00
ok : "you are authenticated as '" + req . remote _user . name + "'" ,
2014-11-24 14:46:37 -05:00
//token: auth.issue_token(req.remote_user),
token : token ,
2014-11-13 12:13:37 -05:00
} )
} else {
if ( typeof ( req . body . name ) !== 'string' || typeof ( req . body . password ) !== 'string' ) {
if ( typeof ( req . body . password _sha ) ) {
2014-11-13 13:32:31 -05:00
return next ( Error [ 422 ] ( 'your npm version is outdated\nPlease update to npm@1.4.5 or greater.\nSee https://github.com/rlidwka/sinopia/issues/93 for details.' ) )
2014-11-13 12:13:37 -05:00
} else {
return next ( Error [ 422 ] ( 'user/password is not found in request (npm issue?)' ) )
}
}
2014-11-16 07:37:50 -05:00
auth . add _user ( req . body . name , req . body . password , function ( err , user ) {
2014-11-13 12:13:37 -05:00
if ( err ) {
2015-02-12 06:18:47 -05:00
if ( err . status >= 400 && err . status < 500 ) {
// With npm registering is the same as logging in,
// and npm accepts only an 409 error.
// So, changing status code here.
return next ( Error [ 409 ] ( err . message ) )
2014-11-13 12:13:37 -05:00
}
return next ( err )
}
2014-11-16 07:37:50 -05:00
req . remote _user = user
2014-11-13 12:13:37 -05:00
res . status ( 201 )
2014-11-16 07:37:50 -05:00
return next ( {
ok : "user '" + req . body . name + "' created" ,
2014-11-24 14:46:37 -05:00
//token: auth.issue_token(req.remote_user),
token : token ,
2014-11-16 07:37:50 -05:00
} )
2014-11-13 12:13:37 -05:00
} )
}
} )
2015-05-10 10:23:31 -05:00
function tag _package _version ( req , res , next ) {
2014-11-13 12:13:37 -05:00
if ( typeof ( req . body ) !== 'string' ) return next ( 'route' )
var tags = { }
tags [ req . params . tag ] = req . body
2015-05-10 10:23:31 -05:00
storage . merge _tags ( req . params . package , tags , function ( err ) {
2014-11-13 12:13:37 -05:00
if ( err ) return next ( err )
res . status ( 201 )
2014-11-13 13:32:31 -05:00
return next ( { ok : 'package tagged' } )
2014-11-13 12:13:37 -05:00
} )
2015-05-10 10:23:31 -05:00
}
// tagging a package
app . put ( '/:package/:tag' ,
can ( 'publish' ) , media ( 'application/json' ) , tag _package _version )
app . post ( '/-/package/:package/dist-tags/:tag' ,
can ( 'publish' ) , media ( 'application/json' ) , tag _package _version )
app . put ( '/-/package/:package/dist-tags/:tag' ,
can ( 'publish' ) , media ( 'application/json' ) , tag _package _version )
app . delete ( '/-/package/:package/dist-tags/:tag' , can ( 'publish' ) , function ( req , res , next ) {
var tags = { }
tags [ req . params . tag ] = null
storage . merge _tags ( req . params . package , tags , function ( err ) {
if ( err ) return next ( err )
res . status ( 201 )
return next ( { ok : 'tag removed' } )
} )
} )
app . get ( '/-/package/:package/dist-tags' , can ( 'access' ) , function ( req , res , next ) {
storage . get _package ( req . params . package , { req : req } , function ( err , info ) {
if ( err ) return next ( err )
next ( info [ 'dist-tags' ] )
} )
} )
app . post ( '/-/package/:package/dist-tags' ,
can ( 'publish' ) , media ( 'application/json' ) , expect _json ,
function ( req , res , next ) {
storage . merge _tags ( req . params . package , req . body , function ( err ) {
if ( err ) return next ( err )
res . status ( 201 )
return next ( { ok : 'tags updated' } )
} )
} )
app . put ( '/-/package/:package/dist-tags' ,
can ( 'publish' ) , media ( 'application/json' ) , expect _json ,
function ( req , res , next ) {
storage . replace _tags ( req . params . package , req . body , function ( err ) {
if ( err ) return next ( err )
res . status ( 201 )
return next ( { ok : 'tags updated' } )
} )
} )
app . delete ( '/-/package/:package/dist-tags' ,
can ( 'publish' ) , media ( 'application/json' ) ,
function ( req , res , next ) {
storage . replace _tags ( req . params . package , { } , function ( err ) {
if ( err ) return next ( err )
res . status ( 201 )
return next ( { ok : 'tags removed' } )
} )
2014-11-13 12:13:37 -05:00
} )
// publishing a package
app . put ( '/:package/:_rev?/:revision?' , can ( 'publish' ) , media ( 'application/json' ) , expect _json , function ( req , res , next ) {
var name = req . params . package
if ( Object . keys ( req . body ) . length == 1 && Utils . is _object ( req . body . users ) ) {
// 501 status is more meaningful, but npm doesn't show error message for 5xx
return next ( Error [ 404 ] ( 'npm star|unstar calls are not implemented' ) )
}
try {
var metadata = Utils . validate _metadata ( req . body , name )
} catch ( err ) {
return next ( Error [ 422 ] ( 'bad incoming package data' ) )
}
if ( req . params . _rev ) {
storage . change _package ( name , metadata , req . params . revision , function ( err ) {
after _change ( err , 'package changed' )
} )
} else {
storage . add _package ( name , metadata , function ( err ) {
after _change ( err , 'created new package' )
} )
}
function after _change ( err , ok _message ) {
// old npm behaviour
if ( metadata . _attachments == null ) {
if ( err ) return next ( err )
res . status ( 201 )
2014-11-13 13:32:31 -05:00
return next ( { ok : ok _message } )
2014-11-13 12:13:37 -05:00
}
// npm-registry-client 0.3+ embeds tarball into the json upload
// https://github.com/isaacs/npm-registry-client/commit/e9fbeb8b67f249394f735c74ef11fe4720d46ca0
// issue #31, dealing with it here:
if ( typeof ( metadata . _attachments ) !== 'object'
|| Object . keys ( metadata . _attachments ) . length !== 1
|| typeof ( metadata . versions ) !== 'object'
|| Object . keys ( metadata . versions ) . length !== 1 ) {
// npm is doing something strange again
// if this happens in normal circumstances, report it as a bug
return next ( Error [ 400 ] ( 'unsupported registry call' ) )
}
if ( err && err . status != 409 ) return next ( err )
// at this point document is either created or existed before
var t1 = Object . keys ( metadata . _attachments ) [ 0 ]
2014-11-16 12:44:46 -05:00
create _tarball ( Path . basename ( t1 ) , metadata . _attachments [ t1 ] , function ( err ) {
2014-11-13 12:13:37 -05:00
if ( err ) return next ( err )
var t2 = Object . keys ( metadata . versions ) [ 0 ]
metadata . versions [ t2 ] . readme = metadata . readme != null ? String ( metadata . readme ) : ''
create _version ( t2 , metadata . versions [ t2 ] , function ( err ) {
if ( err ) return next ( err )
add _tags ( metadata [ 'dist-tags' ] , function ( err ) {
if ( err ) return next ( err )
res . status ( 201 )
2014-11-13 13:32:31 -05:00
return next ( { ok : ok _message } )
2014-11-13 12:13:37 -05:00
} )
} )
} )
}
function create _tarball ( filename , data , cb ) {
var stream = storage . add _tarball ( name , filename )
stream . on ( 'error' , function ( err ) {
cb ( err )
} )
stream . on ( 'success' , function ( ) {
cb ( )
} )
// this is dumb and memory-consuming, but what choices do we have?
stream . end ( Buffer ( data . data , 'base64' ) )
stream . done ( )
}
function create _version ( version , data , cb ) {
storage . add _version ( name , version , data , null , cb )
}
function add _tags ( tags , cb ) {
2015-05-10 10:23:31 -05:00
storage . merge _tags ( name , tags , cb )
2014-11-13 12:13:37 -05:00
}
} )
// unpublishing an entire package
app . delete ( '/:package/-rev/*' , can ( 'publish' ) , function ( req , res , next ) {
storage . remove _package ( req . params . package , function ( err ) {
if ( err ) return next ( err )
res . status ( 201 )
2014-11-13 13:32:31 -05:00
return next ( { ok : 'package removed' } )
2014-11-13 12:13:37 -05:00
} )
} )
// removing a tarball
app . delete ( '/:package/-/:filename/-rev/:revision' , can ( 'publish' ) , function ( req , res , next ) {
storage . remove _tarball ( req . params . package , req . params . filename , req . params . revision , function ( err ) {
if ( err ) return next ( err )
res . status ( 201 )
2014-11-13 13:32:31 -05:00
return next ( { ok : 'tarball removed' } )
2014-11-13 12:13:37 -05:00
} )
} )
// uploading package tarball
app . put ( '/:package/-/:filename/*' , can ( 'publish' ) , media ( 'application/octet-stream' ) , function ( req , res , next ) {
var name = req . params . package
var stream = storage . add _tarball ( name , req . params . filename )
req . pipe ( stream )
// checking if end event came before closing
var complete = false
req . on ( 'end' , function ( ) {
complete = true
stream . done ( )
} )
req . on ( 'close' , function ( ) {
if ( ! complete ) {
stream . abort ( )
}
} )
stream . on ( 'error' , function ( err ) {
return res . report _error ( err )
} )
stream . on ( 'success' , function ( ) {
res . status ( 201 )
2014-11-13 13:32:31 -05:00
return next ( {
2014-11-13 12:13:37 -05:00
ok : 'tarball uploaded successfully'
} )
} )
} )
// adding a version
app . put ( '/:package/:version/-tag/:tag' , can ( 'publish' ) , media ( 'application/json' ) , expect _json , function ( req , res , next ) {
var name = req . params . package
var version = req . params . version
var tag = req . params . tag
storage . add _version ( name , version , req . body , tag , function ( err ) {
if ( err ) return next ( err )
res . status ( 201 )
2014-11-13 13:32:31 -05:00
return next ( { ok : 'package published' } )
2014-11-13 12:13:37 -05:00
} )
} )
return app
}