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' )
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
module . exports = function ( config , auth , storage ) {
var app = express . Router ( )
var can = Middleware . allow ( config )
// validate all of these params as a package name
// this might be too harsh, so ask if it causes trouble
app . param ( 'package' , validate _name )
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 ( ) )
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 ) )
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-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 ) {
storage . search ( req . param . startkey || 0 , { req : req } , function ( err , result ) {
if ( err ) return next ( err )
for ( var pkg in result ) {
if ( ! config . allow _access ( pkg , req . remote _user ) ) {
delete result [ pkg ]
}
}
2014-11-13 13:32:31 -05:00
return next ( result )
2014-11-13 12:13:37 -05:00
} )
} )
//app.get('/*', function(req, res) {
// proxy.request(req, res)
//})
// placeholder 'cause npm require to be authenticated to publish
// we do not do any real authentication yet
app . post ( '/_session' , Cookies . express ( ) , function ( req , res ) {
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 ) {
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 + "'" ,
token : auth . issue _token ( req . remote _user ) ,
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 ) {
if ( err . status < 500 && err . message === 'this user already exists' ) {
// with npm registering is the same as logging in
// so we replace message in case of conflict
return next ( Error [ 409 ] ( 'bad username/password, access denied' ) )
}
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" ,
token : auth . issue _token ( req . remote _user ) ,
} )
2014-11-13 12:13:37 -05:00
} )
}
} )
// tagging a package
app . put ( '/:package/:tag' , can ( 'publish' ) , media ( 'application/json' ) , function ( req , res , next ) {
if ( typeof ( req . body ) !== 'string' ) return next ( 'route' )
var tags = { }
tags [ req . params . tag ] = req . body
storage . add _tags ( req . params . package , tags , function ( err ) {
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
} )
} )
// 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 ]
create _tarball ( t1 , metadata . _attachments [ t1 ] , function ( err ) {
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 ) {
storage . add _tags ( name , tags , cb )
}
} )
// 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
}