")
61 | .html(html)
62 | .appendTo($entry)
63 |
64 | $entry.height(height + $readme.outerHeight())
65 |
66 | transitionComplete(function() {
67 | $entry.css('height', 'auto')
68 | })
69 | }
70 | })
71 | }
72 | })
73 | })
74 |
--------------------------------------------------------------------------------
/lib/GUI/js/main.js:
--------------------------------------------------------------------------------
1 | // twitter bootstrap stuff;
2 | // not in static 'cause I want it to be bundled with the rest of javascripts
3 | require('./bootstrap-modal')
4 |
5 | // our own files
6 | require('./search')
7 | require('./entry')
8 |
9 | var $ = require('unopinionate').selector
10 | $(document).on('click', '.js-userLogoutBtn', function() {
11 | $('#userLogoutForm').submit()
12 | return false
13 | })
14 |
--------------------------------------------------------------------------------
/lib/GUI/js/search.js:
--------------------------------------------------------------------------------
1 | var $ = require('unopinionate').selector
2 | var template = require('../entry.hbs')
3 |
4 | $(function() {
5 | ;(function(window, document) {
6 | var $form = $('#search-form')
7 | var $input = $form.find('input')
8 | var $searchResults = $('#search-results')
9 | var $pkgListing = $('#all-packages')
10 | var $searchBtn = $('.js-search-btn')
11 | var request
12 | var currentResults
13 |
14 | var toggle = function(validQuery) {
15 | $searchResults.toggleClass('show', validQuery)
16 | $pkgListing.toggleClass('hide', validQuery)
17 |
18 | $searchBtn.find('i').toggleClass('icon-cancel', validQuery)
19 | $searchBtn.find('i').toggleClass('icon-search', !validQuery)
20 | }
21 |
22 | $form.bind('submit keyup', function(e) {
23 | var q, qBool
24 |
25 | e.preventDefault()
26 |
27 | q = $input.val()
28 | qBool = (q !== '')
29 |
30 | toggle(qBool)
31 |
32 | if (!qBool) {
33 | if (request && typeof request.abort === 'function') {
34 | request.abort()
35 | }
36 |
37 | currentResults = null
38 | $searchResults.html('')
39 | return
40 | }
41 |
42 | if (request && typeof request.abort === 'function') {
43 | request.abort()
44 | }
45 |
46 | if (!currentResults) {
47 | $searchResults.html(
48 | "

")
49 | }
50 |
51 | request = $.getJSON('-/search/' + q, function( results ) {
52 | currentResults = results
53 |
54 | if (results.length > 0) {
55 | var html = ''
56 |
57 | $.each(results, function(i, entry) {
58 | html += template(entry)
59 | })
60 |
61 | $searchResults.html(html)
62 | } else {
63 | $searchResults.html(
64 | "
No Results
")
65 | }
66 | })
67 | })
68 |
69 | $(document).on('click', '.icon-cancel', function(e) {
70 | e.preventDefault()
71 | $input.val('')
72 | $form.keyup()
73 | })
74 |
75 | })(window, window.document)
76 | })
77 |
--------------------------------------------------------------------------------
/lib/auth.js:
--------------------------------------------------------------------------------
1 | var Crypto = require('crypto')
2 | var jju = require('jju')
3 | var Error = require('http-errors')
4 | var Logger = require('./logger')
5 | var load_plugins = require('./plugin-loader').load_plugins
6 |
7 | module.exports = Auth
8 |
9 | function Auth(config) {
10 | var self = Object.create(Auth.prototype)
11 | self.config = config
12 | self.logger = Logger.logger.child({ sub: 'auth' })
13 | self.secret = config.secret
14 |
15 | var plugin_params = {
16 | config: config,
17 | logger: self.logger
18 | }
19 |
20 | if (config.users_file) {
21 | if (!config.auth || !config.auth.htpasswd) {
22 | // b/w compat
23 | config.auth = config.auth || {}
24 | config.auth.htpasswd = { file: config.users_file }
25 | }
26 | }
27 |
28 | self.plugins = load_plugins(config, config.auth, plugin_params, function (p) {
29 | return p.authenticate || p.allow_access || p.allow_publish
30 | })
31 |
32 | self.plugins.unshift({
33 | sinopia_version: '1.1.0',
34 |
35 | authenticate: function(user, password, cb) {
36 | if (config.users != null
37 | && config.users[user] != null
38 | && (Crypto.createHash('sha1').update(password).digest('hex')
39 | === config.users[user].password)
40 | ) {
41 | return cb(null, [ user ])
42 | }
43 |
44 | return cb()
45 | },
46 |
47 | adduser: function(user, password, cb) {
48 | if (config.users && config.users[user])
49 | return cb( Error[403]('this user already exists') )
50 |
51 | return cb()
52 | },
53 | })
54 |
55 | function allow_action(action) {
56 | return function(user, package, cb) {
57 | var ok = package[action].reduce(function(prev, curr) {
58 | if (user.groups.indexOf(curr) !== -1) return true
59 | return prev
60 | }, false)
61 |
62 | if (ok) return cb(null, true)
63 |
64 | if (user.name) {
65 | cb( Error[403]('user ' + user.name + ' is not allowed to ' + action + ' package ' + package.name) )
66 | } else {
67 | cb( Error[403]('unregistered users are not allowed to ' + action + ' package ' + package.name) )
68 | }
69 | }
70 | }
71 |
72 | self.plugins.push({
73 | authenticate: function(user, password, cb) {
74 | return cb( Error[403]('bad username/password, access denied') )
75 | },
76 |
77 | add_user: function(user, password, cb) {
78 | return cb( Error[409]('registration is disabled') )
79 | },
80 |
81 | allow_access: allow_action('access'),
82 | allow_publish: allow_action('publish'),
83 | })
84 |
85 | return self
86 | }
87 |
88 | Auth.prototype.authenticate = function(user, password, cb) {
89 | var plugins = this.plugins.slice(0)
90 |
91 | ;(function next() {
92 | var p = plugins.shift()
93 |
94 | if (typeof(p.authenticate) !== 'function') {
95 | return next()
96 | }
97 |
98 | p.authenticate(user, password, function(err, groups) {
99 | if (err) return cb(err)
100 | if (groups != null && groups != false)
101 | return cb(err, AuthenticatedUser(user, groups))
102 | next()
103 | })
104 | })()
105 | }
106 |
107 | Auth.prototype.add_user = function(user, password, cb) {
108 | var self = this
109 | var plugins = this.plugins.slice(0)
110 |
111 | ;(function next() {
112 | var p = plugins.shift()
113 | var n = 'adduser'
114 | if (typeof(p[n]) !== 'function') {
115 | n = 'add_user'
116 | }
117 | if (typeof(p[n]) !== 'function') {
118 | next()
119 | } else {
120 | p[n](user, password, function(err, ok) {
121 | if (err) return cb(err)
122 | if (ok) return self.authenticate(user, password, cb)
123 | next()
124 | })
125 | }
126 | })()
127 | }
128 |
129 | Auth.prototype.allow_access = function(package_name, user, callback) {
130 | var plugins = this.plugins.slice(0)
131 | var package = Object.assign({ name: package_name },
132 | this.config.get_package_spec(package_name))
133 |
134 | ;(function next() {
135 | var p = plugins.shift()
136 |
137 | if (typeof(p.allow_access) !== 'function') {
138 | return next()
139 | }
140 |
141 | p.allow_access(user, package, function(err, ok) {
142 | if (err) return callback(err)
143 | if (ok) return callback(null, ok)
144 | next() // cb(null, false) causes next plugin to roll
145 | })
146 | })()
147 | }
148 |
149 | Auth.prototype.allow_publish = function(package_name, user, callback) {
150 | var plugins = this.plugins.slice(0)
151 | var package = Object.assign({ name: package_name },
152 | this.config.get_package_spec(package_name))
153 |
154 | ;(function next() {
155 | var p = plugins.shift()
156 |
157 | if (typeof(p.allow_publish) !== 'function') {
158 | return next()
159 | }
160 |
161 | p.allow_publish(user, package, function(err, ok) {
162 | if (err) return callback(err)
163 | if (ok) return callback(null, ok)
164 | next() // cb(null, false) causes next plugin to roll
165 | })
166 | })()
167 | }
168 |
169 | Auth.prototype.basic_middleware = function() {
170 | var self = this
171 | return function(req, res, _next) {
172 | req.pause()
173 | function next(err) {
174 | req.resume()
175 | // uncomment this to reject users with bad auth headers
176 | //return _next.apply(null, arguments)
177 |
178 | // swallow error, user remains unauthorized
179 | // set remoteUserError to indicate that user was attempting authentication
180 | if (err) req.remote_user.error = err.message
181 | return _next()
182 | }
183 |
184 | if (req.remote_user != null && req.remote_user.name !== undefined)
185 | return next()
186 | req.remote_user = AnonymousUser()
187 |
188 | var authorization = req.headers.authorization
189 | if (authorization == null) return next()
190 |
191 | var parts = authorization.split(' ')
192 |
193 | if (parts.length !== 2)
194 | return next( Error[400]('bad authorization header') )
195 |
196 | var scheme = parts[0]
197 | if (scheme === 'Basic') {
198 | var credentials = Buffer(parts[1], 'base64').toString()
199 | } else if (scheme === 'Bearer') {
200 | var credentials = self.aes_decrypt(Buffer(parts[1], 'base64')).toString('utf8')
201 | if (!credentials) return next()
202 | } else {
203 | return next()
204 | }
205 |
206 | var index = credentials.indexOf(':')
207 | if (index < 0) return next()
208 |
209 | var user = credentials.slice(0, index)
210 | var pass = credentials.slice(index + 1)
211 |
212 | self.authenticate(user, pass, function(err, user) {
213 | if (!err) {
214 | req.remote_user = user
215 | next()
216 | } else {
217 | req.remote_user = AnonymousUser()
218 | next(err)
219 | }
220 | })
221 | }
222 | }
223 |
224 | Auth.prototype.bearer_middleware = function() {
225 | var self = this
226 | return function(req, res, _next) {
227 | req.pause()
228 | function next(_err) {
229 | req.resume()
230 | return _next.apply(null, arguments)
231 | }
232 |
233 | if (req.remote_user != null && req.remote_user.name !== undefined)
234 | return next()
235 | req.remote_user = AnonymousUser()
236 |
237 | var authorization = req.headers.authorization
238 | if (authorization == null) return next()
239 |
240 | var parts = authorization.split(' ')
241 |
242 | if (parts.length !== 2)
243 | return next( Error[400]('bad authorization header') )
244 |
245 | var scheme = parts[0]
246 | var token = parts[1]
247 |
248 | if (scheme !== 'Bearer')
249 | return next()
250 |
251 | try {
252 | var user = self.decode_token(token)
253 | } catch(err) {
254 | return next(err)
255 | }
256 |
257 | req.remote_user = AuthenticatedUser(user.u, user.g)
258 | req.remote_user.token = token
259 | next()
260 | }
261 | }
262 |
263 | Auth.prototype.cookie_middleware = function() {
264 | var self = this
265 | return function(req, res, _next) {
266 | req.pause()
267 | function next(_err) {
268 | req.resume()
269 | return _next()
270 | }
271 |
272 | if (req.remote_user != null && req.remote_user.name !== undefined)
273 | return next()
274 |
275 | req.remote_user = AnonymousUser()
276 |
277 | var token = req.cookies.get('token')
278 | if (token == null) return next()
279 |
280 | /*try {
281 | var user = self.decode_token(token, 60*60)
282 | } catch(err) {
283 | return next()
284 | }
285 |
286 | req.remote_user = AuthenticatedUser(user.u, user.g)
287 | req.remote_user.token = token
288 | next()*/
289 | var credentials = self.aes_decrypt(Buffer(token, 'base64')).toString('utf8')
290 | if (!credentials) return next()
291 |
292 | var index = credentials.indexOf(':')
293 | if (index < 0) return next()
294 |
295 | var user = credentials.slice(0, index)
296 | var pass = credentials.slice(index + 1)
297 |
298 | self.authenticate(user, pass, function(err, user) {
299 | if (!err) {
300 | req.remote_user = user
301 | next()
302 | } else {
303 | req.remote_user = AnonymousUser()
304 | next(err)
305 | }
306 | })
307 | }
308 | }
309 |
310 | Auth.prototype.issue_token = function(user) {
311 | var data = jju.stringify({
312 | u: user.name,
313 | g: user.real_groups && user.real_groups.length ? user.real_groups : undefined,
314 | t: ~~(Date.now()/1000),
315 | }, { indent: false })
316 |
317 | data = Buffer(data, 'utf8')
318 | var mac = Crypto.createHmac('sha256', this.secret).update(data).digest()
319 | return Buffer.concat([ data, mac ]).toString('base64')
320 | }
321 |
322 | Auth.prototype.decode_token = function(str, expire_time) {
323 | var buf = Buffer(str, 'base64')
324 | if (buf.length <= 32) throw Error[401]('invalid token')
325 |
326 | var data = buf.slice(0, buf.length - 32)
327 | var their_mac = buf.slice(buf.length - 32)
328 | var good_mac = Crypto.createHmac('sha256', this.secret).update(data).digest()
329 |
330 | their_mac = Crypto.createHash('sha512').update(their_mac).digest('hex')
331 | good_mac = Crypto.createHash('sha512').update(good_mac).digest('hex')
332 | if (their_mac !== good_mac) throw Error[401]('bad signature')
333 |
334 | // make token expire in 24 hours
335 | // TODO: make configurable?
336 | expire_time = expire_time || 24*60*60
337 |
338 | data = jju.parse(data.toString('utf8'))
339 | if (Math.abs(data.t - ~~(Date.now()/1000)) > expire_time)
340 | throw Error[401]('token expired')
341 |
342 | return data
343 | }
344 |
345 | Auth.prototype.aes_encrypt = function(buf) {
346 | var c = Crypto.createCipher('aes192', this.secret)
347 | var b1 = c.update(buf)
348 | var b2 = c.final()
349 | return Buffer.concat([ b1, b2 ])
350 | }
351 |
352 | Auth.prototype.aes_decrypt = function(buf) {
353 | try {
354 | var c = Crypto.createDecipher('aes192', this.secret)
355 | var b1 = c.update(buf)
356 | var b2 = c.final()
357 | } catch(_) {
358 | return Buffer(0)
359 | }
360 | return Buffer.concat([ b1, b2 ])
361 | }
362 |
363 | function AnonymousUser() {
364 | return {
365 | name: undefined,
366 | // groups without '$' are going to be deprecated eventually
367 | groups: [ '$all', '$anonymous', '@all', '@anonymous', 'all', 'undefined', 'anonymous' ],
368 | real_groups: [],
369 | }
370 | }
371 |
372 | function AuthenticatedUser(name, groups) {
373 | var _groups = (groups || []).concat([ '$all', '$authenticated', '@all', '@authenticated', 'all' ])
374 | return {
375 | name: name,
376 | groups: _groups,
377 | real_groups: groups,
378 | }
379 | }
380 |
381 |
--------------------------------------------------------------------------------
/lib/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /*eslint no-sync:0*/
4 |
5 | if (process.getuid && process.getuid() === 0) {
6 | global.console.error("Sinopia doesn't need superuser privileges. Don't run it under root.")
7 | }
8 |
9 | process.title = 'sinopia'
10 | require('es6-shim')
11 |
12 | try {
13 | // for debugging memory leaks
14 | // totally optional
15 | require('heapdump')
16 | } catch(err) {}
17 |
18 | var logger = require('./logger')
19 | logger.setup() // default setup
20 |
21 | var commander = require('commander')
22 | var constants = require('constants')
23 | var fs = require('fs')
24 | var http = require('http')
25 | var https = require('https')
26 | var YAML = require('js-yaml')
27 | var Path = require('path')
28 | var URL = require('url')
29 | var server = require('./index')
30 | var Utils = require('./utils')
31 | var pkg_file = '../package.yaml'
32 | var pkg = YAML.safeLoad(fs.readFileSync(__dirname+'/'+ pkg_file, 'utf8'))
33 |
34 | commander
35 | .option('-l, --listen <[host:]port>', 'host:port number to listen on (default: localhost:4873)')
36 | .option('-c, --config
', 'use this configuration file (default: ./config.yaml)')
37 | .version(pkg.version)
38 | .parse(process.argv)
39 |
40 | if (commander.args.length == 1 && !commander.config) {
41 | // handling "sinopia [config]" case if "-c" is missing in commandline
42 | commander.config = commander.args.pop()
43 | }
44 |
45 | if (commander.args.length != 0) {
46 | commander.help()
47 | }
48 |
49 | var config, config_path
50 | try {
51 | if (commander.config) {
52 | config_path = Path.resolve(commander.config)
53 | } else {
54 | config_path = require('./config-path')()
55 | }
56 | config = YAML.safeLoad(fs.readFileSync(config_path, 'utf8'))
57 | logger.logger.warn({ file: config_path }, 'config file - @{file}')
58 | } catch (err) {
59 | logger.logger.fatal({ file: config_path, err: err }, 'cannot open config file @{file}: @{!err.message}')
60 | process.exit(1)
61 | }
62 |
63 | afterConfigLoad()
64 |
65 | function get_listen_addresses() {
66 | // command line || config file || default
67 | var addresses
68 |
69 | if (commander.listen) {
70 | addresses = [ commander.listen ]
71 | } else if (Array.isArray(config.listen)) {
72 | addresses = config.listen
73 | } else if (config.listen) {
74 | addresses = [ config.listen ]
75 | } else {
76 | addresses = [ '4873' ]
77 | }
78 |
79 | addresses = addresses.map(function(addr) {
80 | var parsed_addr = Utils.parse_address(addr)
81 |
82 | if (!parsed_addr) {
83 | logger.logger.warn({ addr: addr },
84 | 'invalid address - @{addr}, we expect a port (e.g. "4873"),'
85 | + ' host:port (e.g. "localhost:4873") or full url'
86 | + ' (e.g. "http://localhost:4873/")')
87 | }
88 |
89 | return parsed_addr
90 |
91 | }).filter(Boolean)
92 |
93 | return addresses
94 | }
95 |
96 | function afterConfigLoad() {
97 | if (!config.self_path) config.self_path = Path.resolve(config_path)
98 | if (!config.https) config.https = { enable: false };
99 |
100 | var app = server(config)
101 |
102 | get_listen_addresses().forEach(function(addr) {
103 | var webServer
104 |
105 | if (addr.proto === 'https') { // https
106 | if (!config.https || !config.https.key || !config.https.cert) {
107 | var conf_path = function(file) {
108 | if (!file) return config_path
109 | return Path.resolve(Path.dirname(config_path), file)
110 | }
111 |
112 | logger.logger.fatal([
113 | 'You need to specify "https.key" and "https.cert" to run https server',
114 | '',
115 | // commands are borrowed from node.js docs
116 | 'To quickly create self-signed certificate, use:',
117 | ' $ openssl genrsa -out ' + conf_path('sinopia-key.pem') + ' 2048',
118 | ' $ openssl req -new -sha256 -key ' + conf_path('sinopia-key.pem') + ' -out ' + conf_path('sinopia-csr.pem'),
119 | ' $ openssl x509 -req -in ' + conf_path('sinopia-csr.pem') + ' -signkey ' + conf_path('sinopia-key.pem') + ' -out ' + conf_path('sinopia-cert.pem'),
120 | '',
121 | 'And then add to config file (' + conf_path() + '):',
122 | ' https:',
123 | ' key: sinopia-key.pem',
124 | ' cert: sinopia-cert.pem',
125 | ].join('\n'))
126 | process.exit(2)
127 | }
128 |
129 | try {
130 | webServer = https.createServer({
131 | secureProtocol: 'SSLv23_method', // disable insecure SSLv2 and SSLv3
132 | secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3,
133 | key: fs.readFileSync(config.https.key),
134 | cert: fs.readFileSync(config.https.cert)
135 | }, app)
136 | } catch (err) { // catch errors related to certificate loading
137 | logger.logger.fatal({ err: err }, 'cannot create server: @{err.message}')
138 | process.exit(2)
139 | }
140 | } else { // http
141 | webServer = http.createServer(app);
142 | }
143 |
144 | webServer
145 | .listen(addr.port || addr.path, addr.host)
146 | .on('error', function(err) {
147 | logger.logger.fatal({ err: err }, 'cannot create server: @{err.message}')
148 | process.exit(2)
149 | })
150 |
151 | logger.logger.warn({
152 | addr: ( addr.path
153 | ? URL.format({
154 | protocol: 'unix',
155 | pathname: addr.path,
156 | })
157 | : URL.format({
158 | protocol: addr.proto,
159 | hostname: addr.host,
160 | port: addr.port,
161 | pathname: '/',
162 | })
163 | ),
164 | version: 'Sinopia/'+pkg.version,
165 | }, 'http address - @{addr}')
166 | })
167 |
168 | // undocumented stuff for tests
169 | if (typeof(process.send) === 'function') {
170 | process.send({ sinopia_started: true })
171 | }
172 | }
173 |
174 | process.on('uncaughtException', function(err) {
175 | logger.logger.fatal( { err: err }
176 | , 'uncaught exception, please report this\n@{err.stack}' )
177 | process.exit(255)
178 | })
179 |
180 |
--------------------------------------------------------------------------------
/lib/config-path.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs')
2 | var Path = require('path')
3 | var logger = require('./logger')
4 |
5 | module.exports = find_config_file
6 |
7 | function find_config_file() {
8 | var paths = get_paths()
9 |
10 | for (var i=0; i [a, b, c, d]
14 | function flatten(array) {
15 | var result = []
16 | for (var i=0; i= last_suffix
190 | || (m[4] === '' && last_suffix !== Infinity)) {
191 | throw Error('invalid interval: ' + interval)
192 | }
193 | last_suffix = parse_interval_table[m[4]]
194 | result += Number(m[1]) * parse_interval_table[m[4]]
195 | })
196 | return result
197 | }
198 |
199 |
--------------------------------------------------------------------------------
/lib/index-web.js:
--------------------------------------------------------------------------------
1 | var async = require('async')
2 | var bodyParser = require('body-parser')
3 | var Cookies = require('cookies')
4 | var express = require('express')
5 | var fs = require('fs')
6 | var Handlebars = require('handlebars')
7 | var renderReadme = require('render-readme')
8 | var Search = require('./search')
9 | var Middleware = require('./middleware')
10 | var match = Middleware.match
11 | var validate_name = Middleware.validate_name
12 | var validate_pkg = Middleware.validate_package
13 |
14 | module.exports = function(config, auth, storage) {
15 | var app = express.Router()
16 | var can = Middleware.allow(auth)
17 |
18 | // validate all of these params as a package name
19 | // this might be too harsh, so ask if it causes trouble
20 | app.param('package', validate_pkg)
21 | app.param('filename', validate_name)
22 | app.param('version', validate_name)
23 | app.param('anything', match(/.*/))
24 |
25 | app.use(Cookies.express())
26 | app.use(bodyParser.urlencoded({ extended: false }))
27 | app.use(auth.cookie_middleware())
28 | app.use(function(req, res, next) {
29 | // disable loading in frames (clickjacking, etc.)
30 | res.header('X-Frame-Options', 'deny')
31 | next()
32 | })
33 |
34 | Search.configureStorage(storage)
35 |
36 | if(config.web && config.web.template) {
37 | var template = Handlebars.compile(fs.readFileSync(config.web.template, 'utf8'));
38 | }
39 | else {
40 | Handlebars.registerPartial('entry', fs.readFileSync(require.resolve('./GUI/entry.hbs'), 'utf8'))
41 | var template = Handlebars.compile(fs.readFileSync(require.resolve('./GUI/index.hbs'), 'utf8'))
42 | }
43 | app.get('/', function(req, res, next) {
44 | var base = config.url_prefix
45 | ? config.url_prefix.replace(/\/$/, '')
46 | : req.protocol + '://' + req.get('host')
47 | res.setHeader('Content-Type', 'text/html')
48 |
49 | storage.get_local(function(err, packages) {
50 | if (err) throw err // that function shouldn't produce any
51 | async.filterSeries(packages, function(package, cb) {
52 | auth.allow_access(package.name, req.remote_user, function(err, allowed) {
53 | setImmediate(function () {
54 | cb(!err && allowed)
55 | })
56 | })
57 | }, function(packages) {
58 | packages.sort(function(p1, p2) {
59 | if (p1.name < p2.name) {
60 | return -1;
61 | }
62 | else {
63 | return 1;
64 | }
65 | });
66 |
67 | next(template({
68 | name: config.web && config.web.title ? config.web.title : 'Sinopia',
69 | packages: packages,
70 | baseUrl: base,
71 | username: req.remote_user.name,
72 | }))
73 | })
74 | })
75 | })
76 |
77 | // Static
78 | app.get('/-/static/:filename', function(req, res, next) {
79 | var file = __dirname + '/static/' + req.params.filename
80 | res.sendFile(file, function(err) {
81 | if (!err) return
82 | if (err.status === 404) {
83 | next()
84 | } else {
85 | next(err)
86 | }
87 | })
88 | })
89 |
90 | app.get('/-/logo', function(req, res, next) {
91 | res.sendFile( config.web && config.web.logo
92 | ? config.web.logo
93 | : __dirname + '/static/logo-sm.png' )
94 | })
95 |
96 | app.post('/-/login', function(req, res, next) {
97 | auth.authenticate(req.body.user, req.body.pass, function(err, user) {
98 | if (!err) {
99 | req.remote_user = user
100 | //res.cookies.set('token', auth.issue_token(req.remote_user))
101 |
102 | var str = req.body.user + ':' + req.body.pass
103 | res.cookies.set('token', auth.aes_encrypt(str).toString('base64'))
104 | }
105 |
106 | var base = config.url_prefix
107 | ? config.url_prefix.replace(/\/$/, '')
108 | : req.protocol + '://' + req.get('host')
109 | res.redirect(base)
110 | })
111 | })
112 |
113 | app.post('/-/logout', function(req, res, next) {
114 | var base = config.url_prefix
115 | ? config.url_prefix.replace(/\/$/, '')
116 | : req.protocol + '://' + req.get('host')
117 | res.cookies.set('token', '')
118 | res.redirect(base)
119 | })
120 |
121 | // Search
122 | app.get('/-/search/:anything', function(req, res, next) {
123 | var results = Search.query(req.params.anything)
124 | var packages = []
125 |
126 | var getData = function(i) {
127 | storage.get_package(results[i].ref, function(err, entry) {
128 | if (!err && entry) {
129 | packages.push(entry.versions[entry['dist-tags'].latest])
130 | }
131 |
132 | if (i >= results.length - 1) {
133 | next(packages)
134 | } else {
135 | getData(i + 1)
136 | }
137 | })
138 | }
139 |
140 | if (results.length) {
141 | getData(0)
142 | } else {
143 | next([])
144 | }
145 | })
146 |
147 | app.get('/-/readme/:package/:version?', can('access'), function(req, res, next) {
148 | storage.get_package(req.params.package, {req: req}, function(err, info) {
149 | if (err) return next(err)
150 | next( renderReadme(info.readme || 'ERROR: No README data found!') )
151 | })
152 | })
153 | return app
154 | }
155 |
156 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | var express = require('express')
2 | var Error = require('http-errors')
3 | var compression = require('compression')
4 | var Auth = require('./auth')
5 | var Logger = require('./logger')
6 | var Config = require('./config')
7 | var Middleware = require('./middleware')
8 | var Cats = require('./status-cats')
9 | var Storage = require('./storage')
10 |
11 | module.exports = function(config_hash) {
12 | Logger.setup(config_hash.logs)
13 |
14 | var config = Config(config_hash)
15 | var storage = Storage(config)
16 | var auth = Auth(config)
17 | var app = express()
18 |
19 | // run in production mode by default, just in case
20 | // it shouldn't make any difference anyway
21 | app.set('env', process.env.NODE_ENV || 'production')
22 |
23 | function error_reporting_middleware(req, res, next) {
24 | res.report_error = res.report_error || function(err) {
25 | if (err.status && err.status >= 400 && err.status < 600) {
26 | if (!res.headersSent) {
27 | res.status(err.status)
28 | next({ error: err.message || 'unknown error' })
29 | }
30 | } else {
31 | Logger.logger.error( { err: err }
32 | , 'unexpected error: @{!err.message}\n@{err.stack}')
33 | if (!res.status || !res.send) {
34 | Logger.logger.error('this is an error in express.js, please report this')
35 | res.destroy()
36 | } else if (!res.headersSent) {
37 | res.status(500)
38 | next({ error: 'internal server error' })
39 | } else {
40 | // socket should be already closed
41 | }
42 | }
43 | }
44 | next()
45 | }
46 |
47 | app.use(Middleware.log)
48 | app.use(error_reporting_middleware)
49 | app.use(function(req, res, next) {
50 | res.setHeader('X-Powered-By', config.user_agent)
51 | next()
52 | })
53 | app.use(Cats.middleware)
54 | app.use(compression())
55 |
56 | app.get('/favicon.ico', function(req, res, next) {
57 | req.url = '/-/static/favicon.png'
58 | next()
59 | })
60 |
61 | // hook for tests only
62 | if (config._debug) {
63 | app.get('/-/_debug', function(req, res, next) {
64 | var do_gc = typeof(global.gc) !== 'undefined'
65 | if (do_gc) global.gc()
66 | next({
67 | pid : process.pid,
68 | main : process.mainModule.filename,
69 | conf : config.self_path,
70 | mem : process.memoryUsage(),
71 | gc : do_gc,
72 | })
73 | })
74 | }
75 |
76 | app.use(require('./index-api')(config, auth, storage))
77 |
78 | if (config.web && config.web.enable === false) {
79 | app.get('/', function(req, res, next) {
80 | next( Error[404]('web interface is disabled in the config file') )
81 | })
82 | } else {
83 | app.use(require('./index-web')(config, auth, storage))
84 | }
85 |
86 | app.get('/*', function(req, res, next) {
87 | next( Error[404]('file not found') )
88 | })
89 |
90 | app.use(function(err, req, res, next) {
91 | if (Object.prototype.toString.call(err) !== '[object Error]') return next(err)
92 | if (err.code === 'ECONNABORT' && res.statusCode === 304) return next()
93 | if (typeof(res.report_error) !== 'function') {
94 | // in case of very early error this middleware may not be loaded before error is generated
95 | // fixing that
96 | error_reporting_middleware(req, res, function(){})
97 | }
98 | res.report_error(err)
99 | })
100 |
101 | app.use(Middleware.final)
102 |
103 | return app
104 | }
105 |
106 |
--------------------------------------------------------------------------------
/lib/local-data.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs')
2 | var Path = require('path')
3 |
4 | module.exports = LocalData
5 |
6 | function LocalData(path) {
7 | var self = Object.create(LocalData.prototype)
8 | self.path = path
9 | try {
10 | self.data = JSON.parse(fs.readFileSync(self.path, 'utf8'))
11 | } catch(_) {
12 | self.data = { list: [] }
13 | }
14 | return self
15 | }
16 |
17 | LocalData.prototype.add = function(name) {
18 | if (this.data.list.indexOf(name) === -1) {
19 | this.data.list.push(name)
20 | this.sync()
21 | }
22 | }
23 |
24 | LocalData.prototype.remove = function(name) {
25 | var i = this.data.list.indexOf(name)
26 | if (i !== -1) {
27 | this.data.list.splice(i, 1)
28 | }
29 |
30 | this.sync()
31 | }
32 |
33 | LocalData.prototype.get = function() {
34 | return this.data.list
35 | }
36 |
37 | LocalData.prototype.sync = function() {
38 | // Uses sync to prevent ugly race condition
39 | try {
40 | require('mkdirp').sync(Path.dirname(this.path))
41 | } catch(err) {}
42 | fs.writeFileSync(this.path, JSON.stringify(this.data))
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/lib/local-fs.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs')
2 | var Error = require('http-errors')
3 | var mkdirp = require('mkdirp')
4 | var Path = require('path')
5 | var MyStreams = require('./streams')
6 |
7 | function FSError(code) {
8 | var err = Error(code)
9 | err.code = code
10 | return err
11 | }
12 |
13 | try {
14 | var fsExt = require('fs-ext')
15 | } catch (e) {
16 | fsExt = {
17 | flock: function() {
18 | arguments[arguments.length-1]()
19 | }
20 | }
21 | }
22 |
23 | function tempFile(str) {
24 | return str + '.tmp' + String(Math.random()).substr(2)
25 | }
26 |
27 | function renameTmp(src, dst, _cb) {
28 | function cb(err) {
29 | if (err) fs.unlink(src)
30 | _cb(err)
31 | }
32 |
33 | if (process.platform !== 'win32') {
34 | return fs.rename(src, dst, cb)
35 | }
36 |
37 | // windows can't remove opened file,
38 | // but it seem to be able to rename it
39 | var tmp = tempFile(dst)
40 | fs.rename(dst, tmp, function(err) {
41 | fs.rename(src, dst, cb)
42 | if (!err) fs.unlink(tmp)
43 | })
44 | }
45 |
46 | function write(dest, data, cb) {
47 | var safe_write = function(cb) {
48 | var tmpname = tempFile(dest)
49 | fs.writeFile(tmpname, data, function(err) {
50 | if (err) return cb(err)
51 | renameTmp(tmpname, dest, cb)
52 | })
53 | }
54 |
55 | safe_write(function(err) {
56 | if (err && err.code === 'ENOENT') {
57 | mkdirp(Path.dirname(dest), function(err) {
58 | if (err) return cb(err)
59 | safe_write(cb)
60 | })
61 | } else {
62 | cb(err)
63 | }
64 | })
65 | }
66 |
67 | function write_stream(name) {
68 | var stream = MyStreams.UploadTarballStream()
69 |
70 | var _ended = 0
71 | stream.on('end', function() {
72 | _ended = 1
73 | })
74 |
75 | fs.exists(name, function(exists) {
76 | if (exists) return stream.emit('error', FSError('EEXISTS'))
77 |
78 | var tmpname = name + '.tmp-'+String(Math.random()).replace(/^0\./, '')
79 | var file = fs.createWriteStream(tmpname)
80 | var opened = false
81 | stream.pipe(file)
82 |
83 | stream.done = function() {
84 | function onend() {
85 | file.on('close', function() {
86 | renameTmp(tmpname, name, function(err) {
87 | if (err) {
88 | stream.emit('error', err)
89 | } else {
90 | stream.emit('success')
91 | }
92 | })
93 | })
94 | file.destroySoon()
95 | }
96 | if (_ended) {
97 | onend()
98 | } else {
99 | stream.on('end', onend)
100 | }
101 | }
102 | stream.abort = function() {
103 | if (opened) {
104 | opened = false
105 | file.on('close', function() {
106 | fs.unlink(tmpname, function(){})
107 | })
108 | }
109 | file.destroySoon()
110 | }
111 | file.on('open', function() {
112 | opened = true
113 | // re-emitting open because it's handled in storage.js
114 | stream.emit('open')
115 | })
116 | file.on('error', function(err) {
117 | stream.emit('error', err)
118 | })
119 | })
120 | return stream
121 | }
122 |
123 | function read_stream(name, stream, callback) {
124 | var rstream = fs.createReadStream(name)
125 | rstream.on('error', function(err) {
126 | stream.emit('error', err)
127 | })
128 | rstream.on('open', function(fd) {
129 | fs.fstat(fd, function(err, stats) {
130 | if (err) return stream.emit('error', err)
131 | stream.emit('content-length', stats.size)
132 | stream.emit('open')
133 | rstream.pipe(stream)
134 | })
135 | })
136 |
137 | var stream = MyStreams.ReadTarballStream()
138 | stream.abort = function() {
139 | rstream.close()
140 | }
141 | return stream
142 | }
143 |
144 | function create(name, contents, callback) {
145 | fs.exists(name, function(exists) {
146 | if (exists) return callback( FSError('EEXISTS') )
147 | write(name, contents, callback)
148 | })
149 | }
150 |
151 | function update(name, contents, callback) {
152 | fs.exists(name, function(exists) {
153 | if (!exists) return callback( FSError('ENOENT') )
154 | write(name, contents, callback)
155 | })
156 | }
157 |
158 | function read(name, callback) {
159 | fs.readFile(name, callback)
160 | }
161 |
162 | // open and flock with exponential backoff
163 | function open_flock(name, opmod, flmod, tries, backoff, cb) {
164 | fs.open(name, opmod, function(err, fd) {
165 | if (err) return cb(err, fd)
166 |
167 | fsExt.flock(fd, flmod, function(err) {
168 | if (err) {
169 | if (!tries) {
170 | fs.close(fd, function() {
171 | cb(err)
172 | })
173 | } else {
174 | fs.close(fd, function() {
175 | setTimeout(function() {
176 | open_flock(name, opmod, flmod, tries-1, backoff*2, cb)
177 | }, backoff)
178 | })
179 | }
180 | } else {
181 | cb(null, fd)
182 | }
183 | })
184 | })
185 | }
186 |
187 | // this function neither unlocks file nor closes it
188 | // it'll have to be done manually later
189 | function lock_and_read(name, _callback) {
190 | open_flock(name, 'r', 'exnb', 4, 10, function(err, fd) {
191 | function callback(err) {
192 | if (err && fd) {
193 | fs.close(fd, function(err2) {
194 | _callback(err)
195 | })
196 | } else {
197 | _callback.apply(null, arguments)
198 | }
199 | }
200 |
201 | if (err) return callback(err, fd)
202 |
203 | fs.fstat(fd, function(err, st) {
204 | if (err) return callback(err, fd)
205 |
206 | var buffer = Buffer(st.size)
207 | if (st.size === 0) return onRead(null, 0, buffer)
208 | fs.read(fd, buffer, 0, st.size, null, onRead)
209 |
210 | function onRead(err, bytesRead, buffer) {
211 | if (err) return callback(err, fd)
212 | if (bytesRead != st.size) return callback(Error('st.size != bytesRead'), fd)
213 |
214 | callback(null, fd, buffer)
215 | }
216 | })
217 | })
218 | }
219 |
220 | module.exports.read = read
221 |
222 | module.exports.read_json = function(name, cb) {
223 | read(name, function(err, res) {
224 | if (err) return cb(err)
225 |
226 | var args = []
227 | try {
228 | args = [ null, JSON.parse(res.toString('utf8')) ]
229 | } catch(err) {
230 | args = [ err ]
231 | }
232 | cb.apply(null, args)
233 | })
234 | }
235 |
236 | module.exports.lock_and_read = lock_and_read
237 |
238 | module.exports.lock_and_read_json = function(name, cb) {
239 | lock_and_read(name, function(err, fd, res) {
240 | if (err) return cb(err, fd)
241 |
242 | var args = []
243 | try {
244 | args = [ null, fd, JSON.parse(res.toString('utf8')) ]
245 | } catch(err) {
246 | args = [ err, fd ]
247 | }
248 | cb.apply(null, args)
249 | })
250 | }
251 |
252 | module.exports.create = create
253 |
254 | module.exports.create_json = function(name, value, cb) {
255 | create(name, JSON.stringify(value, null, '\t'), cb)
256 | }
257 |
258 | module.exports.update = update
259 |
260 | module.exports.update_json = function(name, value, cb) {
261 | update(name, JSON.stringify(value, null, '\t'), cb)
262 | }
263 |
264 | module.exports.write = write
265 |
266 | module.exports.write_json = function(name, value, cb) {
267 | write(name, JSON.stringify(value, null, '\t'), cb)
268 | }
269 |
270 | module.exports.write_stream = write_stream
271 |
272 | module.exports.read_stream = read_stream
273 |
274 | module.exports.unlink = fs.unlink
275 |
276 | module.exports.rmdir = fs.rmdir
277 |
278 |
--------------------------------------------------------------------------------
/lib/logger.js:
--------------------------------------------------------------------------------
1 | var Logger = require('bunyan')
2 | var Error = require('http-errors')
3 | var Stream = require('stream')
4 | var Utils = require('./utils')
5 |
6 | function getlvl(x) {
7 | switch(true) {
8 | case x < 15 : return 'trace'
9 | case x < 25 : return 'debug'
10 | case x < 35 : return 'info'
11 | case x == 35 : return 'http'
12 | case x < 45 : return 'warn'
13 | case x < 55 : return 'error'
14 | default : return 'fatal'
15 | }
16 | }
17 |
18 | module.exports.setup = function(logs) {
19 | var streams = []
20 | if (logs == null) logs = [{ type: 'stdout', format: 'pretty', level: 'http' }]
21 |
22 | logs.forEach(function(target) {
23 | var stream = new Stream()
24 | stream.writable = true
25 |
26 | if (target.type === 'stdout' || target.type === 'stderr') {
27 | // destination stream
28 | var dest = target.type === 'stdout' ? process.stdout : process.stderr
29 |
30 | if (target.format === 'pretty') {
31 | // making fake stream for prettypritting
32 | stream.write = function(obj) {
33 | dest.write(print(obj.level, obj.msg, obj, dest.isTTY) + '\n')
34 | }
35 | } else {
36 | stream.write = function(obj) {
37 | dest.write(JSON.stringify(obj, Logger.safeCycles()) + '\n')
38 | }
39 | }
40 | } else if (target.type === 'file') {
41 | var dest = require('fs').createWriteStream(target.path, {flags: 'a', encoding: 'utf8'})
42 | dest.on('error', function (err) {
43 | Logger.emit('error', err)
44 | })
45 | stream.write = function(obj) {
46 | if (target.format === 'pretty') {
47 | dest.write(print(obj.level, obj.msg, obj, false) + '\n')
48 | } else {
49 | dest.write(JSON.stringify(obj, Logger.safeCycles()) + '\n')
50 | }
51 | }
52 | } else {
53 | throw Error('wrong target type for a log')
54 | }
55 |
56 | if (target.level === 'http') target.level = 35
57 | streams.push({
58 | type: 'raw',
59 | level: target.level || 35,
60 | stream: stream,
61 | })
62 | })
63 |
64 | var logger = new Logger({
65 | name: 'sinopia',
66 | streams: streams,
67 | serializers: {
68 | err: Logger.stdSerializers.err,
69 | req: Logger.stdSerializers.req,
70 | res: Logger.stdSerializers.res,
71 | },
72 | })
73 |
74 | module.exports.logger = logger
75 | }
76 |
77 | // adopted from socket.io
78 | // this part was converted to coffee-script and back again over the years,
79 | // so it might look weird
80 |
81 | // level to color
82 | var levels = {
83 | fatal : 31,
84 | error : 31,
85 | warn : 33,
86 | http : 35,
87 | info : 36,
88 | debug : 90,
89 | trace : 90,
90 | }
91 |
92 | var max = 0
93 | for (var l in levels) {
94 | max = Math.max(max, l.length)
95 | }
96 |
97 | function pad(str) {
98 | if (str.length < max) return str + ' '.repeat(max - str.length)
99 | return str
100 | }
101 |
102 | var subsystems = [{
103 | in : '\033[32m<--\033[39m',
104 | out : '\033[33m-->\033[39m',
105 | fs : '\033[90m-=-\033[39m',
106 | default : '\033[34m---\033[39m',
107 | }, {
108 | in : '<--',
109 | out : '-->',
110 | fs : '-=-',
111 | default : '---',
112 | }]
113 |
114 | function print(type, msg, obj, colors) {
115 | if (typeof type === 'number') type = getlvl(type)
116 | var finalmsg = msg.replace(/@{(!?[$A-Za-z_][$0-9A-Za-z\._]*)}/g, function(_, name) {
117 | var str = obj, is_error
118 | if (name[0] === '!') {
119 | name = name.substr(1)
120 | is_error = true
121 | }
122 |
123 | var _ref = name.split('.')
124 | for (var _i = 0; _i < _ref.length; _i++) {
125 | var id = _ref[_i]
126 | if (Utils.is_object(str) || Array.isArray(str)) {
127 | str = str[id]
128 | } else {
129 | str = undefined
130 | }
131 | }
132 |
133 | if (typeof(str) === 'string') {
134 | if (!colors || str.includes('\n')) {
135 | return str
136 | } else if (is_error) {
137 | return '\033[31m' + str + '\033[39m'
138 | } else {
139 | return '\033[32m' + str + '\033[39m'
140 | }
141 | } else {
142 | return require('util').inspect(str, null, null, colors)
143 | }
144 | })
145 |
146 | var sub = subsystems[colors ? 0 : 1][obj.sub] || subsystems[+!colors].default
147 | if (colors) {
148 | return ' \033[' + levels[type] + 'm' + (pad(type)) + '\033[39m ' + sub + ' ' + finalmsg
149 | } else {
150 | return ' ' + (pad(type)) + ' ' + sub + ' ' + finalmsg
151 | }
152 | }
153 |
154 |
--------------------------------------------------------------------------------
/lib/middleware.js:
--------------------------------------------------------------------------------
1 | var crypto = require('crypto')
2 | var Error = require('http-errors')
3 | var utils = require('./utils')
4 | var Logger = require('./logger')
5 |
6 | module.exports.match = function match(regexp) {
7 | return function(req, res, next, value, name) {
8 | if (regexp.exec(value)) {
9 | next()
10 | } else {
11 | next('route')
12 | }
13 | }
14 | }
15 |
16 | module.exports.validate_name = function validate_name(req, res, next, value, name) {
17 | if (value.charAt(0) === '-') {
18 | // special case in couchdb usually
19 | next('route')
20 | } else if (utils.validate_name(value)) {
21 | next()
22 | } else {
23 | next( Error[403]('invalid ' + name) )
24 | }
25 | }
26 |
27 | module.exports.validate_package = function validate_package(req, res, next, value, name) {
28 | if (value.charAt(0) === '-') {
29 | // special case in couchdb usually
30 | next('route')
31 | } else if (utils.validate_package(value)) {
32 | next()
33 | } else {
34 | next( Error[403]('invalid ' + name) )
35 | }
36 | }
37 |
38 | module.exports.media = function media(expect) {
39 | return function(req, res, next) {
40 | if (req.headers['content-type'] !== expect) {
41 | next( Error[415]('wrong content-type, expect: ' + expect
42 | + ', got: '+req.headers['content-type']) )
43 | } else {
44 | next()
45 | }
46 | }
47 | }
48 |
49 | module.exports.expect_json = function expect_json(req, res, next) {
50 | if (!utils.is_object(req.body)) {
51 | return next( Error[400]("can't parse incoming json") )
52 | }
53 | next()
54 | }
55 |
56 | module.exports.anti_loop = function(config) {
57 | return function(req, res, next) {
58 | if (req.headers.via != null) {
59 | var arr = req.headers.via.split(',')
60 |
61 | for (var i=0; i= 200 && res.statusCode < 300)) {
120 | res.header('ETag', '"' + md5sum(body) + '"')
121 | }
122 | } else {
123 | // send(null), send(204), etc.
124 | }
125 | } catch(err) {
126 | // if sinopia sends headers first, and then calls res.send()
127 | // as an error handler, we can't report error properly,
128 | // and should just close socket
129 | if (err.message.match(/set headers after they are sent/)) {
130 | if (res.socket != null) res.socket.destroy()
131 | return
132 | } else {
133 | throw err
134 | }
135 | }
136 |
137 | res.send(body)
138 | }
139 |
140 | module.exports.log = function(req, res, next) {
141 | // logger
142 | req.log = Logger.logger.child({ sub: 'in' })
143 |
144 | var _auth = req.headers.authorization
145 | if (_auth != null) req.headers.authorization = ''
146 | var _cookie = req.headers.cookie
147 | if (_cookie != null) req.headers.cookie = ''
148 |
149 | req.url = req.originalUrl
150 | req.log.info( { req: req, ip: req.ip }
151 | , '@{ip} requested \'@{req.method} @{req.url}\'' )
152 | req.originalUrl = req.url
153 |
154 | if (_auth != null) req.headers.authorization = _auth
155 | if (_cookie != null) req.headers.cookie = _cookie
156 |
157 | var bytesin = 0
158 | req.on('data', function(chunk) {
159 | bytesin += chunk.length
160 | })
161 |
162 | var bytesout = 0
163 | var _write = res.write
164 | res.write = function(buf) {
165 | bytesout += buf.length
166 | _write.apply(res, arguments)
167 | }
168 |
169 | function log() {
170 | var message = "@{status}, user: @{user}, req: '@{request.method} @{request.url}'"
171 | if (res._sinopia_error) {
172 | message += ', error: @{!error}'
173 | } else {
174 | message += ', bytes: @{bytes.in}/@{bytes.out}'
175 | }
176 |
177 | req.url = req.originalUrl
178 | req.log.warn({
179 | request : { method: req.method, url: req.url },
180 | level : 35, // http
181 | user : req.remote_user && req.remote_user.name,
182 | status : res.statusCode,
183 | error : res._sinopia_error,
184 | bytes : {
185 | in : bytesin,
186 | out : bytesout,
187 | }
188 | }, message)
189 | req.originalUrl = req.url
190 | }
191 |
192 | req.on('close', function() {
193 | log(true)
194 | })
195 |
196 | var _end = res.end
197 | res.end = function(buf) {
198 | if (buf) bytesout += buf.length
199 | _end.apply(res, arguments)
200 | log()
201 | }
202 | next()
203 | }
204 |
205 |
--------------------------------------------------------------------------------
/lib/plugin-loader.js:
--------------------------------------------------------------------------------
1 | var Path = require('path')
2 |
3 | function try_load(path) {
4 | try {
5 | return require(path)
6 | } catch(err) {
7 | if (err.code === 'MODULE_NOT_FOUND') {
8 | return null
9 | }
10 | throw err
11 | }
12 | }
13 |
14 | function load_plugins(config, plugin_configs, params, sanity_check) {
15 | var plugins = Object.keys(plugin_configs || {}).map(function(p) {
16 | var plugin
17 |
18 | // npm package
19 | if (plugin == null && p.match(/^[^\.\/]/)) {
20 | plugin = try_load('sinopia-' + p)
21 | }
22 |
23 | if (plugin == null) {
24 | plugin = try_load(p)
25 | }
26 |
27 | // relative to config path
28 | if (plugin == null && p.match(/^\.\.?($|\/)/)) {
29 | plugin = try_load(Path.resolve(Path.dirname(config.self_path), p))
30 | }
31 |
32 | if (plugin == null) {
33 | throw Error('"' + p + '" plugin not found\n'
34 | + 'try "npm install sinopia-' + p + '"')
35 | }
36 |
37 | if (typeof(plugin) !== 'function')
38 | throw Error('"' + p + '" doesn\'t look like a valid plugin')
39 |
40 | plugin = plugin(plugin_configs[p], params)
41 |
42 | if (plugin == null || !sanity_check(plugin))
43 | throw Error('"' + p + '" doesn\'t look like a valid plugin')
44 |
45 | return plugin
46 | })
47 |
48 | return plugins
49 | }
50 |
51 | exports.load_plugins = load_plugins;
52 |
--------------------------------------------------------------------------------
/lib/search.js:
--------------------------------------------------------------------------------
1 | var lunr = require('lunr')
2 |
3 | function Search() {
4 | var self = Object.create(Search.prototype)
5 | self.index = lunr(function() {
6 | this.field('name' , { boost: 10 })
7 | this.field('description' , { boost: 4 })
8 | this.field('author' , { boost: 6 })
9 | this.field('readme')
10 | })
11 | return self
12 | }
13 |
14 | Search.prototype.query = function(q) {
15 | return this.index.search(q)
16 | }
17 |
18 | Search.prototype.add = function(package) {
19 | this.index.add({
20 | id: package.name,
21 | name: package.name,
22 | description: package.description,
23 | author: package._npmUser ? package._npmUser.name : '???',
24 | })
25 | },
26 |
27 | Search.prototype.remove = function(name) {
28 | this.index.remove({ id: name })
29 | }
30 |
31 | Search.prototype.reindex = function() {
32 | var self = this
33 | this.storage.get_local(function(err, packages) {
34 | if (err) throw err // that function shouldn't produce any
35 | var i = packages.length
36 | while (i--) {
37 | self.add(packages[i])
38 | }
39 | })
40 | }
41 |
42 | Search.prototype.configureStorage = function(storage) {
43 | this.storage = storage
44 | this.reindex()
45 | }
46 |
47 | module.exports = Search()
48 |
49 |
--------------------------------------------------------------------------------
/lib/static/ajax.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlidwka/sinopia/3f55fb4c0c6685e8b22796cce7b523bdbfb4019e/lib/static/ajax.gif
--------------------------------------------------------------------------------
/lib/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlidwka/sinopia/3f55fb4c0c6685e8b22796cce7b523bdbfb4019e/lib/static/favicon.ico
--------------------------------------------------------------------------------
/lib/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlidwka/sinopia/3f55fb4c0c6685e8b22796cce7b523bdbfb4019e/lib/static/favicon.png
--------------------------------------------------------------------------------
/lib/static/fontello.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlidwka/sinopia/3f55fb4c0c6685e8b22796cce7b523bdbfb4019e/lib/static/fontello.eot
--------------------------------------------------------------------------------
/lib/static/fontello.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/lib/static/fontello.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlidwka/sinopia/3f55fb4c0c6685e8b22796cce7b523bdbfb4019e/lib/static/fontello.ttf
--------------------------------------------------------------------------------
/lib/static/fontello.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlidwka/sinopia/3f55fb4c0c6685e8b22796cce7b523bdbfb4019e/lib/static/fontello.woff
--------------------------------------------------------------------------------
/lib/static/logo-sm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlidwka/sinopia/3f55fb4c0c6685e8b22796cce7b523bdbfb4019e/lib/static/logo-sm.png
--------------------------------------------------------------------------------
/lib/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlidwka/sinopia/3f55fb4c0c6685e8b22796cce7b523bdbfb4019e/lib/static/logo.png
--------------------------------------------------------------------------------
/lib/status-cats.js:
--------------------------------------------------------------------------------
1 |
2 | // see https://secure.flickr.com/photos/girliemac/sets/72157628409467125
3 |
4 | var images = {
5 | 100: 'aVvDhR', // '6512768893', // 100 - Continue
6 | 101: 'aXXExP', // '6540479029', // 101 - Switching Protocols
7 | 200: 'aVuVsF', // '6512628175', // 200 - OK
8 | 201: 'aXWm1Z', // '6540221577', // 201 - Created
9 | 202: 'aXXEyF', // '6540479079', // 202 - Accepted
10 | 204: 'aYyJ7B', // '6547319943', // 204 - No Content
11 | 206: 'aVEnUP', // '6514473163', // 206 - Partial Content
12 | 207: 'aVEnRD', // '6514472979', // 207 - Multi-Status
13 | 300: 'aW7mac', // '6519540181', // 300 - Multiple Choices
14 | 301: 'aW7mb4', // '6519540231', // 301 - Moved Permanently
15 | 302: 'aV6jKp', // '6508023829', // 302 - Found
16 | 303: 'aVxtaK', // '6513125065', // 303 - See Other
17 | 304: 'aXY3dH', // '6540551929', // 304 - Not Modified
18 | 305: 'aXX5LK', // '6540365403', // 305 - Use Proxy
19 | 307: 'aVwQnk', // '6513001269', // 307 - Temporary Redirect
20 | 400: 'aXYDeT', // '6540669737', // 400 - Bad Request
21 | 401: 'aV6jwe', // '6508023065', // 401 - Unauthorized
22 | 402: 'aVwQoe', // '6513001321', // 402 - Payment Required
23 | 403: 'aV6jFK', // '6508023617', // 403 - Forbidden
24 | 404: 'aV6juR', // '6508022985', // 404 - Not Found
25 | 405: 'aV6jE8', // '6508023523', // 405 - Method Not Allowed
26 | 406: 'aV6jxa', // '6508023119', // 406 - Not Acceptable
27 | 408: 'aV6jyc', // '6508023179', // 408 - Request Timeout
28 | 409: 'aV6jzz', // '6508023259', // 409 - Conflict
29 | 410: 'aVES2H', // '6514567755', // 410 - Gone
30 | 411: 'aXYVpT', // '6540724141', // 411 - Length Required
31 | 413: 'aV6jHZ', // '6508023747', // 413 - Request Entity Too Large
32 | 414: 'aV6jBa', // '6508023351', // 414 - Request-URI Too Long
33 | 416: 'aVxQvr', // '6513196851', // 416 - Requested Range Not Satisfiable
34 | 417: 'aV6jGP', // '6508023679', // 417 - Expectation Failed
35 | 418: 'aV6J7c', // '6508102407', // 418 - I'm a teapot
36 | 422: 'aVEnTt', // '6514473085', // 422 - Unprocessable Entity
37 | 423: 'aVEyVZ', // '6514510235', // 423 - Locked
38 | 424: 'aVEWZ6', // '6514584423', // 424 - Failed Dependency
39 | 425: 'aXYdzH', // '6540586787', // 425 - Unordered Collection
40 | 426: 'aVdo4M', // '6509400771', // 426 - Upgrade Required
41 | 429: 'aVdo8F', // '6509400997', // 429 - Too Many Requests
42 | 431: 'aVdo3n', // '6509400689', // 431 - Request Header Fields Too Large
43 | 444: 'aVdo1P', // '6509400599', // 444 - No Response
44 | 450: 'aVxtbK', // '6513125123', // 450 - Blocked by Windows Parental Controls
45 | 451: 'eTiGQd', // '9113233540', // 451 - Unavailable for Legal Reasons
46 | 500: 'aVdo6e', // '6509400855', // 500 - Internal Server Error
47 | 502: 'aV6jCv', // '6508023429', // 502 - Bad Gateway
48 | 503: 'aXYvop', // '6540643319', // 503 - Service Unavailable
49 | 506: 'aXYvnH', // '6540643279', // 506 - Variant Also Negotiates
50 | 507: 'aVdnZa', // '6509400503', // 507 - Insufficient Storage
51 | 508: 'aVdnYa', // '6509400445', // 508 - Loop Detected
52 | 509: 'aXXg1V', // '6540399865', // 509 - Bandwidth Limit Exceeded
53 | 599: 'aVdo7v', // '6509400929', // 599 - Network connect timeout error
54 | }
55 |
56 | module.exports.get_image = function(status) {
57 | if (status in images) {
58 | return 'http://flic.kr/p/' + images[status]
59 | //return 'https://secure.flickr.com/photos/girliemac/'+images[status]+'/in/set-72157628409467125/lightbox/'
60 | }
61 | }
62 |
63 | module.exports.middleware = function(req, res, next) {
64 | var _writeHead = res.writeHead
65 | res.writeHead = function(status) {
66 | if (status in images) {
67 | res.setHeader('X-Status-Cat', module.exports.get_image(status))
68 | }
69 | _writeHead.apply(res, arguments)
70 | }
71 |
72 | next()
73 | }
74 |
75 |
--------------------------------------------------------------------------------
/lib/streams.js:
--------------------------------------------------------------------------------
1 | var Stream = require('stream')
2 | var Util = require('util')
3 |
4 | module.exports.ReadTarballStream = ReadTarball
5 | module.exports.UploadTarballStream = UploadTarball
6 |
7 | //
8 | // This stream is used to read tarballs from repository
9 | //
10 | function ReadTarball(options) {
11 | var self = new Stream.PassThrough(options)
12 | Object.setPrototypeOf(self, ReadTarball.prototype)
13 |
14 | // called when data is not needed anymore
15 | add_abstract_method(self, 'abort')
16 |
17 | return self
18 | }
19 |
20 | Util.inherits(ReadTarball, Stream.PassThrough)
21 |
22 | //
23 | // This stream is used to upload tarballs to a repository
24 | //
25 | function UploadTarball(options) {
26 | var self = new Stream.PassThrough(options)
27 | Object.setPrototypeOf(self, UploadTarball.prototype)
28 |
29 | // called when user closes connection before upload finishes
30 | add_abstract_method(self, 'abort')
31 |
32 | // called when upload finishes successfully
33 | add_abstract_method(self, 'done')
34 |
35 | return self
36 | }
37 |
38 | Util.inherits(UploadTarball, Stream.PassThrough)
39 |
40 | //
41 | // This function intercepts abstract calls and replays them allowing
42 | // us to attach those functions after we are ready to do so
43 | //
44 | function add_abstract_method(self, name) {
45 | self._called_methods = self._called_methods || {}
46 | self.__defineGetter__(name, function() {
47 | return function() {
48 | self._called_methods[name] = true
49 | }
50 | })
51 | self.__defineSetter__(name, function(fn) {
52 | delete self[name]
53 | self[name] = fn
54 | if (self._called_methods && self._called_methods[name]) {
55 | delete self._called_methods[name]
56 | self[name]()
57 | }
58 | })
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var Semver = require('semver')
3 | var URL = require('url')
4 | var Logger = require('./logger')
5 |
6 | module.exports.validate_package = function(name) {
7 | name = name.split('/', 2)
8 | if (name.length === 1) {
9 | // normal package
10 | return module.exports.validate_name(name[0])
11 | } else {
12 | // scoped package
13 | return name[0][0] === '@'
14 | && module.exports.validate_name(name[0].slice(1))
15 | && module.exports.validate_name(name[1])
16 | }
17 | }
18 |
19 | // from normalize-package-data/lib/fixer.js
20 | module.exports.validate_name = function(name) {
21 | if (typeof(name) !== 'string') return false
22 | name = name.toLowerCase()
23 |
24 | // all URL-safe characters and "@" for issue #75
25 | if (!name.match(/^[-a-zA-Z0-9_.!~*'()@]+$/)
26 | || name.charAt(0) === '.' // ".bin", etc.
27 | || name.charAt(0) === '-' // "-" is reserved by couchdb
28 | || name === 'node_modules'
29 | || name === '__proto__'
30 | || name === 'package.json'
31 | || name === 'favicon.ico'
32 | ) {
33 | return false
34 | } else {
35 | return true
36 | }
37 | }
38 |
39 | module.exports.is_object = function(obj) {
40 | return typeof(obj) === 'object' && obj !== null && !Array.isArray(obj)
41 | }
42 |
43 | module.exports.validate_metadata = function(object, name) {
44 | assert(module.exports.is_object(object), 'not a json object')
45 | assert.equal(object.name, name)
46 |
47 | if (!module.exports.is_object(object['dist-tags'])) {
48 | object['dist-tags'] = {}
49 | }
50 |
51 | if (!module.exports.is_object(object['versions'])) {
52 | object['versions'] = {}
53 | }
54 |
55 | return object
56 | }
57 |
58 | module.exports.filter_tarball_urls = function(pkg, req, config) {
59 | function filter(_url) {
60 | if (!req.headers.host) return _url
61 |
62 | var filename = URL.parse(_url).pathname.replace(/^.*\//, '')
63 |
64 | if (config.url_prefix != null) {
65 | var result = config.url_prefix.replace(/\/$/, '')
66 | } else {
67 | var result = req.protocol + '://' + req.headers.host
68 | }
69 |
70 | return result + '/' + pkg.name.replace(/\//g, '%2f') + '/-/' + filename
71 | }
72 |
73 | for (var ver in pkg.versions) {
74 | var dist = pkg.versions[ver].dist
75 | if (dist != null && dist.tarball != null) {
76 | //dist.__sinopia_orig_tarball = dist.tarball
77 | dist.tarball = filter(dist.tarball)
78 | }
79 | }
80 | return pkg
81 | }
82 |
83 | function can_add_tag(tag, config) {
84 | if (!tag) return false
85 | if (tag === 'latest' && config.ignore_latest_tag) return false
86 | return true
87 | }
88 |
89 | module.exports.tag_version = function(data, version, tag, config) {
90 | if (!can_add_tag(tag, config)) return false
91 |
92 | switch (typeof(data['dist-tags'][tag])) {
93 | case 'string':
94 | data['dist-tags'][tag] = [ data['dist-tags'][tag] ]
95 | break
96 | case 'object': // array
97 | break
98 | default:
99 | data['dist-tags'][tag] = []
100 | }
101 | if (data['dist-tags'][tag].indexOf(version) === -1) {
102 | data['dist-tags'][tag].push(version)
103 | data['dist-tags'][tag] = module.exports.semver_sort(data['dist-tags'][tag])
104 | return data['dist-tags'][tag][data['dist-tags'][tag].length - 1] === version
105 | }
106 | return false
107 | }
108 |
109 | // gets version from a package object taking into account semver weirdness
110 | module.exports.get_version = function(object, version) {
111 | if (object.versions[version] != null) return object.versions[version]
112 |
113 | try {
114 | version = Semver.parse(version, true)
115 | for (var k in object.versions) {
116 | if (version.compare(Semver.parse(k, true)) === 0) {
117 | return object.versions[k]
118 | }
119 | }
120 | } catch (err) {
121 | return undefined
122 | }
123 | }
124 |
125 | module.exports.parse_address = function parse_address(addr) {
126 | //
127 | // Allow:
128 | //
129 | // - https:localhost:1234 - protocol + host + port
130 | // - localhost:1234 - host + port
131 | // - 1234 - port
132 | // - http::1234 - protocol + port
133 | // - https://localhost:443/ - full url + https
134 | // - http://[::1]:443/ - ipv6
135 | // - unix:/tmp/http.sock - unix sockets
136 | // - https://unix:/tmp/http.sock - unix sockets (https)
137 | //
138 | // TODO: refactor it to something more reasonable?
139 | //
140 | // protocol : // ( host )|( ipv6 ): port /
141 | var m = /^((https?):(\/\/)?)?((([^\/:]*)|\[([^\[\]]+)\]):)?(\d+)\/?$/.exec(addr)
142 |
143 | if (m) return {
144 | proto: m[2] || 'http',
145 | host: m[6] || m[7] || 'localhost',
146 | port: m[8] || '4873',
147 | }
148 |
149 | var m = /^((https?):(\/\/)?)?unix:(.*)$/.exec(addr)
150 |
151 | if (m) return {
152 | proto: m[2] || 'http',
153 | path: m[4],
154 | }
155 |
156 | return null
157 | }
158 |
159 | // function filters out bad semver versions and sorts the array
160 | module.exports.semver_sort = function semver_sort(array) {
161 | return array
162 | .filter(function(x) {
163 | if (!Semver.parse(x, true)) {
164 | Logger.logger.warn( {ver: x}, 'ignoring bad version @{ver}' )
165 | return false
166 | }
167 | return true
168 | })
169 | .sort(Semver.compareLoose)
170 | .map(String)
171 | }
172 |
173 |
--------------------------------------------------------------------------------
/package.yaml:
--------------------------------------------------------------------------------
1 | # use "yapm install ." if you're installing this from git repository
2 |
3 | name: sinopia
4 | version: 1.4.0
5 | description: Private npm repository server
6 |
7 | author:
8 | name: Alex Kocharin
9 | email: alex@kocharin.ru
10 |
11 | repository:
12 | type: git
13 | url: git://github.com/rlidwka/sinopia
14 |
15 | main: index.js
16 |
17 | bin:
18 | sinopia: ./bin/sinopia
19 |
20 | dependencies:
21 | express: '>=5.0.0-0 <6.0.0-0'
22 |
23 | # express middlewares
24 | express-json5: '>=0.1.0 <1.0.0-0'
25 | body-parser: '>=1.9.2 <2.0.0-0'
26 | compression: '>=1.2.0 <2.0.0-0'
27 |
28 | commander: '>=2.3.0 <3.0.0-0'
29 | js-yaml: '>=3.0.1 <4.0.0-0'
30 | cookies: '>=0.5.0 <1.0.0-0'
31 | request: '>=2.31.0 <3.0.0-0'
32 | async: '>=0.9.0 <1.0.0-0'
33 | es6-shim: '0.21.x'
34 | semver: '>=2.2.1 <5.0.0-0'
35 | minimatch: '>=0.2.14 <2.0.0-0'
36 | bunyan: '>=0.22.1 <2.0.0-0'
37 | handlebars: '2.x'
38 | highlight.js: '8.x'
39 | lunr: '>=0.5.2 <1.0.0-0'
40 | render-readme: '>=0.2.1'
41 | jju: '1.x'
42 | JSONStream: '1.x'
43 |
44 | mkdirp: '>=0.3.5 <1.0.0-0'
45 | sinopia-htpasswd: '>= 0.4.3'
46 | http-errors: '>=1.2.0'
47 |
48 | # node 0.10 compatibility, should go away soon
49 | readable-stream: '~1.1.0'
50 |
51 | optionalDependencies:
52 | # those are native modules that could fail to compile
53 | # and unavailable on windows
54 | fs-ext: '>=0.4.1 <1.0.0-0'
55 | crypt3: '>=0.1.6 <1.0.0-0' # for sinopia-htpasswd
56 |
57 | devDependencies:
58 | #
59 | # Tools required for testing
60 | #
61 | rimraf: '>=2.2.5 <3.0.0-0'
62 | bluebird: '2 >=2.9'
63 |
64 | mocha: '2 >=2.2.3'
65 |
66 | #
67 | # Linting tools
68 | #
69 | eslint: '1 >=1.1.0'
70 |
71 | # for debugging memory leaks, it'll be require()'d if
72 | # installed, but I don't want it to be installed everytime
73 | #heapdump: '*'
74 |
75 | #
76 | # Tools required to build static files
77 | #
78 | browserify: '7.x'
79 | browserify-handlebars: '1.x'
80 | grunt: '>=0.4.4 <1.0.0-0'
81 | grunt-cli: '*'
82 | grunt-browserify: '>=2.0.8 <3.0.0-0'
83 | grunt-contrib-less: '>=0.11.0 <1.0.0-0'
84 | grunt-contrib-watch: '>=0.6.1 <1.0.0-0'
85 |
86 | # for building static/main.js,
87 | # not required in runtime
88 | unopinionate: '>=0.0.4 <1.0.0-0'
89 | onclick: '>=0.1.0 <1.0.0-0'
90 | transition-complete: '>=0.0.2 <1.0.0-0'
91 |
92 | keywords:
93 | - private
94 | - package
95 | - repository
96 | - registry
97 | - modules
98 | - proxy
99 | - server
100 |
101 | scripts:
102 | test: eslint . && mocha ./test/functional ./test/unit
103 | test-travis: eslint . && mocha -R spec ./test/functional ./test/unit
104 | test-only: mocha ./test/functional ./test/unit
105 | lint: eslint .
106 | prepublish: js-yaml package.yaml > package.json
107 | clean-shrinkwrap: |
108 | node -e '
109 | function clean(j) {
110 | if (!j) return
111 | for (var k in j) {
112 | delete j[k].from
113 | delete j[k].resolved
114 | if (j[k].dependencies) clean(j[k].dependencies)
115 | }
116 | }
117 | x = JSON.parse(require("fs").readFileSync("./npm-shrinkwrap.json"))
118 | clean(x.dependencies)
119 | x = JSON.stringify(x, null, " ")
120 | require("fs").writeFileSync("./npm-shrinkwrap.json", x + "\n")
121 | '
122 |
123 | # we depend on streams2 stuff
124 | # it can be replaced with isaacs/readable-stream, ask if you need to use 0.8
125 | engines:
126 | node: '>=0.10'
127 |
128 | preferGlobal: true
129 |
130 | publishConfig:
131 | registry: https://registry.npmjs.org/
132 |
133 | license:
134 | type: WTFPL
135 | url: http://www.wtfpl.net/txt/copying/
136 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 |
2 | env:
3 | node: true
4 | mocha: true
5 |
6 |
--------------------------------------------------------------------------------
/test/README.md:
--------------------------------------------------------------------------------
1 | All tests are split in three folders:
2 |
3 | - `unit` - Tests that cover functions that transform data in an non-trivial way. These tests simply `require()` a few files and run code in there, so they are very fast.
4 | - `functional` - Tests that launch a sinopia instance and perform a series of requests to it over http. They are slower than unit tests.
5 | - `integration` - Tests that launch a sinopia instance and do requests to it using npm. They are really slow and can hit a real npm registry.
6 |
7 | Unit and functional tests are executed automatically by running `npm test` from the project's root directory. Integration tests are supposed to be executed manually from time to time.
8 |
--------------------------------------------------------------------------------
/test/functional/access.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = function () {
3 | describe('access control', function () {
4 | var server = process.server
5 | var oldauth
6 |
7 | before(function () {
8 | oldauth = server.authstr
9 | })
10 |
11 | after(function () {
12 | server.authstr = oldauth
13 | })
14 |
15 | function check_access(auth, pkg, ok) {
16 | it((ok ? 'allows' : 'forbids') +' access ' + auth + ' to ' + pkg, function () {
17 | server.authstr = auth
18 | ? 'Basic '+(new Buffer(auth).toString('base64'))
19 | : undefined
20 |
21 | var req = server.get_package(pkg)
22 |
23 | if (ok) {
24 | return req.status(404)
25 | .body_error(/no such package available/)
26 | } else {
27 | return req.status(403)
28 | .body_error(/not allowed to access package/)
29 | }
30 | })
31 | }
32 |
33 | function check_publish(auth, pkg, ok) {
34 | it((ok ? 'allows' : 'forbids') + ' publish ' + auth + ' to ' + pkg, function () {
35 | server.authstr = auth
36 | ? 'Basic '+(new Buffer(auth).toString('base64'))
37 | : undefined
38 |
39 | var req = server.put_package(pkg, require('./lib/package')(pkg))
40 |
41 | if (ok) {
42 | return req.status(404)
43 | .body_error(/this package cannot be added/)
44 | } else {
45 | return req.status(403)
46 | .body_error(/not allowed to publish package/)
47 | }
48 | })
49 | }
50 |
51 | check_access('test:test', 'test-access-only', true)
52 | check_access(undefined, 'test-access-only', true)
53 | check_access('test:badpass', 'test-access-only', true)
54 | check_publish('test:test', 'test-access-only', false)
55 | check_publish(undefined, 'test-access-only', false)
56 | check_publish('test:badpass', 'test-access-only', false)
57 |
58 | check_access('test:test', 'test-publish-only', false)
59 | check_access(undefined, 'test-publish-only', false)
60 | check_access('test:badpass', 'test-publish-only', false)
61 | check_publish('test:test', 'test-publish-only', true)
62 | check_publish(undefined, 'test-publish-only', true)
63 | check_publish('test:badpass', 'test-publish-only', true)
64 |
65 | check_access('test:test', 'test-only-test', true)
66 | check_access(undefined, 'test-only-test', false)
67 | check_access('test:badpass', 'test-only-test', false)
68 | check_publish('test:test', 'test-only-test', true)
69 | check_publish(undefined, 'test-only-test', false)
70 | check_publish('test:badpass', 'test-only-test', false)
71 |
72 | check_access('test:test', 'test-only-auth', true)
73 | check_access(undefined, 'test-only-auth', false)
74 | check_access('test:badpass', 'test-only-auth', false)
75 | check_publish('test:test', 'test-only-auth', true)
76 | check_publish(undefined, 'test-only-auth', false)
77 | check_publish('test:badpass', 'test-only-auth', false)
78 | })
79 | }
80 |
81 |
--------------------------------------------------------------------------------
/test/functional/addtag.js:
--------------------------------------------------------------------------------
1 |
2 | function readfile(x) {
3 | return require('fs').readFileSync(__dirname + '/' + x)
4 | }
5 |
6 | module.exports = function () {
7 | var server = process.server
8 |
9 | it('add tag - 404', function () {
10 | return server.add_tag('testpkg-tag', 'tagtagtag', '0.0.1')
11 | .status(404)
12 | .body_error(/no such package/)
13 | })
14 |
15 | describe('addtag', function() {
16 | before(function () {
17 | return server.put_package('testpkg-tag', eval(
18 | '(' + readfile('fixtures/publish.json5')
19 | .toString('utf8')
20 | .replace(/__NAME__/g, 'testpkg-tag')
21 | .replace(/__VERSION__/g, '0.0.1')
22 | + ')'
23 | )).status(201)
24 | })
25 |
26 | it('add testpkg-tag', function(){})
27 |
28 | it('add tag - bad ver', function () {
29 | return server.add_tag('testpkg-tag', 'tagtagtag', '0.0.1-x')
30 | .status(404)
31 | .body_error(/version doesn't exist/)
32 | })
33 |
34 | it('add tag - bad tag', function () {
35 | return server.add_tag('testpkg-tag', 'tag/tag/tag', '0.0.1-x')
36 | .status(403)
37 | .body_error(/invalid tag/)
38 | })
39 |
40 | it('add tag - good', function () {
41 | return server.add_tag('testpkg-tag', 'tagtagtag', '0.0.1')
42 | .status(201)
43 | .body_ok(/tagged/)
44 | })
45 | })
46 | }
47 |
--------------------------------------------------------------------------------
/test/functional/adduser.js:
--------------------------------------------------------------------------------
1 | var Server = require('./lib/server')
2 |
3 | module.exports = function() {
4 | var server = new Server('http://localhost:55551/')
5 |
6 | describe('adduser', function() {
7 | var user = String(Math.random())
8 | var pass = String(Math.random())
9 | before(function () {
10 | return server.auth(user, pass)
11 | .status(201)
12 | .body_ok(/user .* created/)
13 | })
14 |
15 | it('creating new user', function(){})
16 |
17 | it('should log in', function () {
18 | return server.auth(user, pass)
19 | .status(201)
20 | .body_ok(/you are authenticated as/)
21 | })
22 |
23 | it('should not register more users', function () {
24 | return server.auth(String(Math.random()), String(Math.random()))
25 | .status(409)
26 | .body_error(/maximum amount of users reached/)
27 | })
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/test/functional/basic.js:
--------------------------------------------------------------------------------
1 | require('./lib/startup')
2 |
3 | var assert = require('assert')
4 | var crypto = require('crypto')
5 |
6 | function readfile(x) {
7 | return require('fs').readFileSync(__dirname + '/' + x)
8 | }
9 |
10 | module.exports = function () {
11 | var server = process.server
12 | var server2 = process.server2
13 |
14 | it('trying to fetch non-existent package', function () {
15 | return server.get_package('testpkg').status(404).body_error(/no such package/)
16 | })
17 |
18 | describe('testpkg', function () {
19 | before(function () {
20 | return server.add_package('testpkg')
21 | })
22 |
23 | it('creating new package', function (){/* test for before() */})
24 |
25 | it('downloading non-existent tarball', function () {
26 | return server.get_tarball('testpkg', 'blahblah').status(404).body_error(/no such file/)
27 | })
28 |
29 | it('uploading incomplete tarball', function () {
30 | return server.put_tarball_incomplete('testpkg', 'blahblah1', readfile('fixtures/binary'), 3000)
31 | })
32 |
33 | describe('tarball', function () {
34 | before(function () {
35 | return server.put_tarball('testpkg', 'blahblah', readfile('fixtures/binary'))
36 | .status(201)
37 | .body_ok(/.*/)
38 | })
39 |
40 | it('uploading new tarball', function (){/* test for before() */})
41 |
42 | it('downloading newly created tarball', function () {
43 | return server.get_tarball('testpkg', 'blahblah')
44 | .status(200)
45 | .then(function (body) {
46 | assert.deepEqual(body, readfile('fixtures/binary').toString('utf8'))
47 | })
48 | })
49 |
50 | it('uploading new package version (bad sha)', function () {
51 | var pkg = require('./lib/package')('testpkg')
52 | pkg.dist.shasum = crypto.createHash('sha1').update('fake').digest('hex')
53 |
54 | return server.put_version('testpkg', '0.0.1', pkg)
55 | .status(400)
56 | .body_error(/shasum error/)
57 | })
58 |
59 | describe('version', function () {
60 | before(function () {
61 | var pkg = require('./lib/package')('testpkg')
62 | pkg.dist.shasum = crypto.createHash('sha1').update(readfile('fixtures/binary')).digest('hex')
63 | return server.put_version('testpkg', '0.0.1', pkg)
64 | .status(201)
65 | .body_ok(/published/)
66 | })
67 |
68 | it('uploading new package version', function (){/* test for before() */})
69 |
70 | it('downloading newly created package', function () {
71 | return server.get_package('testpkg')
72 | .status(200)
73 | .then(function (body) {
74 | assert.equal(body.name, 'testpkg')
75 | assert.equal(body.versions['0.0.1'].name, 'testpkg')
76 | assert.equal(body.versions['0.0.1'].dist.tarball, 'http://localhost:55551/testpkg/-/blahblah')
77 | assert.deepEqual(body['dist-tags'], {latest: '0.0.1'})
78 | })
79 | })
80 |
81 | it('downloading package via server2', function () {
82 | return server2.get_package('testpkg')
83 | .status(200)
84 | .then(function (body) {
85 | assert.equal(body.name, 'testpkg')
86 | assert.equal(body.versions['0.0.1'].name, 'testpkg')
87 | assert.equal(body.versions['0.0.1'].dist.tarball, 'http://localhost:55552/testpkg/-/blahblah')
88 | assert.deepEqual(body['dist-tags'], {latest: '0.0.1'})
89 | })
90 | })
91 | })
92 | })
93 | })
94 |
95 | it('uploading new package version for bad pkg', function () {
96 | return server.put_version('testpxg', '0.0.1', require('./lib/package')('testpxg'))
97 | .status(404)
98 | .body_error(/no such package/)
99 | })
100 |
101 | it('doubleerr test', function () {
102 | return server.put_tarball('testfwd2', 'blahblah', readfile('fixtures/binary'))
103 | .status(404)
104 | .body_error(/no such/)
105 | })
106 |
107 | it('publishing package / bad ro uplink', function () {
108 | return server.put_package('baduplink', require('./lib/package')('baduplink'))
109 | .status(503)
110 | .body_error(/one of the uplinks is down, refuse to publish/)
111 | })
112 |
113 | it('who am I?', function () {
114 | return server.whoami().then(function (username) {
115 | assert.equal(username, 'test')
116 | })
117 | })
118 | }
119 |
120 |
--------------------------------------------------------------------------------
/test/functional/config-1.yaml:
--------------------------------------------------------------------------------
1 | storage: ./test-storage
2 |
3 | users:
4 | test:
5 | password: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3
6 |
7 | users_file: ./test-storage/.htpasswd
8 | max_users: 1
9 |
10 | web:
11 | enable: true
12 |
13 | uplinks:
14 | express:
15 | url: http://localhost:55550/
16 | timeout: 100ms
17 | server2:
18 | url: http://localhost:55552/
19 | baduplink:
20 | url: http://localhost:55666/
21 |
22 | logs:
23 | - {type: stdout, format: pretty, level: trace}
24 |
25 | packages:
26 | '@test/*':
27 | allow_access: all
28 | allow_publish: all
29 | proxy: server2
30 |
31 | 'testfwd*':
32 | allow_access: all
33 | allow_publish: all
34 | proxy_access: server2
35 | proxy_publish: server2
36 |
37 | 'testloop':
38 | allow_access: all
39 | allow_publish: all
40 | proxy_access: server2
41 | proxy_publish: server2
42 |
43 | 'testexp*':
44 | allow_access: all
45 | allow_publish: all
46 | proxy_access: express
47 |
48 | 'test-nullstorage*':
49 | allow_access: all
50 | allow_publish: all
51 | proxy_access: server2
52 | storage: false
53 |
54 | 'baduplink':
55 | allow_access: all
56 | allow_publish: all
57 | proxy_access: baduplink
58 |
59 | 'test-access-only':
60 | allow_access: $all
61 | allow_publish: nobody
62 | storage: false
63 |
64 | 'test-publish-only':
65 | allow_access: nobody
66 | allow_publish: $all
67 | storage: false
68 |
69 | 'test-only-test':
70 | allow_access: test
71 | allow_publish: test
72 | storage: false
73 |
74 | 'test-only-auth':
75 | allow_access: $authenticated
76 | allow_publish: $authenticated
77 | storage: false
78 |
79 | '*':
80 | allow_access: test undefined
81 | allow_publish: test undefined
82 |
83 | # this should not matter
84 | testpkg:
85 | allow_access: none
86 |
87 | listen: 55551
88 |
89 | # expose internal methods
90 | _debug: true
91 |
--------------------------------------------------------------------------------
/test/functional/config-2.yaml:
--------------------------------------------------------------------------------
1 | storage: ./test-storage2
2 |
3 | users:
4 | test:
5 | password: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3
6 | authtest:
7 | password: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3
8 |
9 | uplinks:
10 | server1:
11 | url: http://localhost:55551/
12 | maxage: 0
13 |
14 | web:
15 | enable: true
16 |
17 | auth:
18 | ./plugins/authenticate:
19 | accept_user: authtest2
20 | with_password: blahblah
21 |
22 | ./plugins/authorize:
23 | allow_user: authtest
24 | to_access: test-auth-allow
25 |
26 | logs:
27 | - {type: stdout, format: pretty, level: trace}
28 |
29 | packages:
30 | '@test/*':
31 | allow_access: all
32 | allow_publish: all
33 | proxy: server1
34 |
35 | 'testfwd':
36 | allow_access: all
37 | allow_publish: all
38 |
39 | 'testloop':
40 | allow_access: all
41 | allow_publish: all
42 | proxy_access: server1
43 | proxy_publish: server1
44 |
45 | 'testpkg*':
46 | allow_access: test anonymous
47 | allow_publish: test anonymous
48 | proxy_access: server1
49 |
50 | 'test-nullstorage*':
51 | allow_access: all
52 | allow_publish: all
53 |
54 | 'test-auth-regular':
55 | allow_access: $authenticated
56 |
57 | 'test-auth-*':
58 | handled_by_auth_plugin: true
59 |
60 | '*':
61 | allow_access: test anonymous
62 | allow_publish: test anonymous
63 |
64 | listen: 55552
65 |
66 | # expose internal methods
67 | _debug: true
68 |
--------------------------------------------------------------------------------
/test/functional/fixtures/binary:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlidwka/sinopia/3f55fb4c0c6685e8b22796cce7b523bdbfb4019e/test/functional/fixtures/binary
--------------------------------------------------------------------------------
/test/functional/fixtures/newnpmreg.json:
--------------------------------------------------------------------------------
1 | {"_id":"testpkg-newnpmreg","name":"testpkg-newnpmreg","description":"","dist-tags":{"foo":"0.0.0","latest":"0.0.0"},"versions":{"0.0.0":{"name":"testpkg-newnpmreg","version":"0.0.0","description":"","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" && exit 1"},"repository":{"type":"git","url":""},"author":"","license":"ISC","_id":"testpkg-newnpmreg@0.0.0","dist":{"shasum":"8ee7331cbc641581b1a8cecd9d38d744a8feb863","tarball":"http://localhost:1234/testpkg-newnpmreg/-/testpkg-newnpmreg-0.0.0.tgz"},"_from":".","_npmVersion":"1.3.1","_npmUser":{"name":"alex","email":"alex@kocharin.ru"},"maintainers":[{"name":"alex","email":"alex@kocharin.ru"}]}},"readme":"blah blah blah","maintainers":[{"name":"alex","email":"alex@kocharin.ru"}],"_attachments":{"testpkg-newnpmreg-0.0.0.tgz":{"content_type":"application/octet-stream","data":"H4sIAAAAAAAAA+2TsW7CMBCGM/spTh6YKHUSIJLXqkPnrixWcIMLsS3btCDEu/fs0Ba1SFVVVISUP8Odzqf/zlY+K+qlaOSt7eLo2RudnVmMsel4DBjzasKOY1JZlJDlRVkU5aSspnnG8pIVOZ6fe5FTWvsgHK7yV5/uLvARr0Q7qkUrKadB+mCXzY2Wr9q2TjZ0SF+k88poPGUj/LAyl752yoauioVWqJgpPZcb/Hmw0jV4ynfJEw9lvTAwo/fOGcdBG4h18FbW6knJ+YzCYAByowLkdD+kTlrjVTBumzy2Nq7XqIDea7eKY7FJrMPCuG6Hlaql9rHr4fGO7i/9pFcl+4X/rWhX557xA/9FVZ3gv+j5/w9F+jl8g58c0OeQyCdH3HOglETsObxTTw7McwLJClt+wzz5JD45IPEcEHjMEfg0r8M9pQfaOSDs5NLP16tXr15XqzeJD6m5AAwAAA==","length":352}}}
2 |
--------------------------------------------------------------------------------
/test/functional/fixtures/publish.json5:
--------------------------------------------------------------------------------
1 | { _id: '__NAME__',
2 | name: '__NAME__',
3 | description: '',
4 | 'dist-tags': { latest: '__VERSION__' },
5 | versions:
6 | { '__VERSION__':
7 | { name: '__NAME__',
8 | version: '__VERSION__',
9 | description: '',
10 | main: 'index.js',
11 | scripts: { test: 'echo "Error: no test specified" && exit 1' },
12 | author: '',
13 | license: 'ISC',
14 | readme: 'ERROR: No README data found!',
15 | _id: '__NAME__@__VERSION__',
16 | dist:
17 | { shasum: '071c8dd9fd775bf3ebc0d5108431110f5f857ce3',
18 | tarball: 'http://localhost:4873/__NAME__/-/__NAME__-__VERSION__.tgz' },
19 | _from: '.',
20 | _npmVersion: '1.3.21',
21 | _npmUser: { name: 'rlidwka', email: 'alex@kocharin.ru' },
22 | maintainers: [ { name: 'rlidwka', email: 'alex@kocharin.ru' } ] } },
23 | readme: 'ERROR: No README data found!',
24 | maintainers: [ { name: 'rlidwka', email: 'alex@kocharin.ru' } ],
25 | _attachments:
26 | { '__NAME__-__VERSION__.tgz':
27 | { content_type: 'application/octet-stream',
28 | data: 'H4sIAAAAAAAAA+2SP2vDMBDFPftTHDdkah3Zim3IGjJ0zppFyNdE+SMJSSmBkO8eWS6mQ7cGSsC/5cG9p7uTkBXyKHY0t4MWB2909mQYY81iAVHLtmY/NcGrCrKyquq25Q1vm4yVnPEm+s9e5DcuPggXV/lrn+EuMOqLcMsBUIsz4RIwkA/v9rjDt1iN4Bc5r4zuPVawok4GduSlUzZ8O2P6LFQqKN3RNf6kIT1kfTRuKZem9DGSewNbXDtn3BK0gd4Ab0mqT0XdFmE2A7qqACXGk/fUTVzC3rhxLJ6UJO3T9h+bFeb3/L9fdGJiYuI1eACk8AYWAAgAAA==',
29 | length: 250 } } }
30 |
--------------------------------------------------------------------------------
/test/functional/fixtures/scoped.json:
--------------------------------------------------------------------------------
1 | {"_id":"@test/scoped","name":"@test/scoped","description":"test... test... test...","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@test/scoped","version":"1.0.0","description":"test... test... test...","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" && exit 1"},"author":"","license":"ISC","readme":"ERROR: No README data found!","_id":"@test/scoped@1.0.0","_shasum":"6e67b14e2c0e450b942e2bc8086b49e90f594790","_from":".","_npmVersion":"2.0.1","_nodeVersion":"0.10.25","_npmUser":{},"dist":{"shasum":"6e67b14e2c0e450b942e2bc8086b49e90f594790","tarball":"http://localhost:4873/@test/scoped/-/@test/scoped-1.0.0.tgz"}}},"readme":"ERROR: No README data found!","_attachments":{"@test/scoped-1.0.0.tgz":{"content_type":"application/octet-stream","data":"H4sIAAAAAAAAAytITM5OTE/VL4DQelnF+XkMVAYGBgZmZiYKQNrQ3NQAmQYDYwMg29DIxMTY2Mzc1NSYwcDQ2MDIDChPbYdgA6XFJYlFQKdQag7ELwpweoiAaqW8xNxUJSslh5LU4hL94uT8gtQUJR2lstSi4sz8PKCEoZ6BngFQJCW1OLkos6AEIgpSraenp4BGA9XlJmaCFGTmpaRWAJMTUASir1jJqhqsDSiZmpyRrxCj5FpUlF9kpZCXD9auUFyQmpyZlpmaEqOkoKamkFqRWaJgqFSro5RYWpKRXwTUBzQsJzM5Na8Y5GLPYGel2oEOv6EOCtDyf2Vibg617SCQ/41MzZHyv+Fo/qcnAOV+KwXU3M8FzfxWCuC8z4WU863QMzyM5gJleysFWK7nguZ5Ky4FsAqgFaTkeS5IjgfqUuKCZngrBWB+5xro4Bp2AJb/QZGhC4l/XXCs65WkV1HJDgL539QAOf8bmwHzv4khWD2V7McLRnj+l+/mgDCY307enXXYQKTN+LUmn5QRq/u+5mVOLy/szBZTXN1764bRpKAgp3t7j08XuS7itTLT4+P+P49iligvXC/2ydVmZendyg9vfLbOiOjZqOPNYHsm2OxLmOHhUglVT5n0Sql0brFjOqcM7b8qxGe+37PB4lT+95fvmOTrVK0ueU3pKqp6PPVztrrvWq5di9afssrV8mlh5JZw43q65OrW94t8SwVYDIrWaLfmcZWErmCuU+8pqe37lHy7zVN1O5vZl3NRyZYhy3LZw7VXym/VMhOZ5h3A/lZxyXJR0er9pmK/CzbPnbaq6OyR7/zbv5S8/L677Kryv/suO2f/6sn/0X+p5kC9RPmfdOP/9Qvb6vjmv1S3/SMT9e1kQ40d2783Sw7OOzyz6pLxec4tohVH/Geoy3684erJb8P+ZG7Mr51pZ2eZvr7/QpbVdU4yA8/ARuEoGAWjYBSQBQDM0BedABAAAA==","length":736}}}
2 |
--------------------------------------------------------------------------------
/test/functional/fixtures/tags.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "__NAME__",
3 | "versions": {
4 | "0.1.0": {
5 | "name": "__NAME__",
6 | "version": "0.1.0",
7 | "dist": {
8 | "shasum": "fake",
9 | "tarball": "http://localhost:55551/__NAME__/-/blahblah"
10 | }
11 | },
12 | "0.1.1alpha": {
13 | "name": "__NAME__",
14 | "version": "0.1.1alpha",
15 | "dist": {
16 | "shasum": "fake",
17 | "tarball": "http://localhost:55551/__NAME__/-/blahblah"
18 | }
19 | },
20 | "0.1.2": {
21 | "name": "__NAME__",
22 | "version": "0.1.2",
23 | "dist": {
24 | "shasum": "fake",
25 | "tarball": "http://localhost:55551/__NAME__/-/blahblah"
26 | }
27 | },
28 | "0.1.3alpha": {
29 | "name": "__NAME__",
30 | "version": "0.1.3alpha",
31 | "dist": {
32 | "shasum": "fake",
33 | "tarball": "http://localhost:55551/__NAME__/-/blahblah"
34 | }
35 | },
36 | "1.1": {
37 | "name": "__NAME__",
38 | "version": "1.1",
39 | "dist": {
40 | "shasum": "fake",
41 | "tarball": "http://localhost:55551/__NAME__/-/blahblah"
42 | }
43 | }
44 | },
45 | "dist-tags": {
46 | "something": "0.1.1alpha",
47 | "bad": "1.1"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/test/functional/gh29.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var crypto = require('crypto')
3 |
4 | function readfile(x) {
5 | return require('fs').readFileSync(__dirname + '/' + x)
6 | }
7 |
8 | module.exports = function() {
9 | var server = process.server
10 | var server2 = process.server2
11 |
12 | it('downloading non-existent tarball #1 / srv2', function () {
13 | return server2.get_tarball('testpkg-gh29', 'blahblah')
14 | .status(404)
15 | .body_error(/no such package/)
16 | })
17 |
18 | describe('pkg-gh29', function() {
19 | before(function () {
20 | return server.put_package('testpkg-gh29', require('./lib/package')('testpkg-gh29'))
21 | .status(201)
22 | .body_ok(/created new package/)
23 | })
24 |
25 | it('creating new package / srv1', function(){})
26 |
27 | it('downloading non-existent tarball #2 / srv2', function () {
28 | return server2.get_tarball('testpkg-gh29', 'blahblah')
29 | .status(404)
30 | .body_error(/no such file/)
31 | })
32 |
33 | describe('tarball', function() {
34 | before(function () {
35 | return server.put_tarball('testpkg-gh29', 'blahblah', readfile('fixtures/binary'))
36 | .status(201)
37 | .body_ok(/.*/)
38 | })
39 |
40 | it('uploading new tarball / srv1', function(){})
41 |
42 | describe('pkg version', function() {
43 | before(function () {
44 | var pkg = require('./lib/package')('testpkg-gh29')
45 | pkg.dist.shasum = crypto.createHash('sha1').update(readfile('fixtures/binary')).digest('hex')
46 | return server.put_version('testpkg-gh29', '0.0.1', pkg)
47 | .status(201)
48 | .body_ok(/published/)
49 | })
50 |
51 | it('uploading new package version / srv1', function(){})
52 |
53 | it('downloading newly created tarball / srv2', function () {
54 | return server2.get_tarball('testpkg-gh29', 'blahblah')
55 | .status(200)
56 | .then(function (body) {
57 | assert.deepEqual(body, readfile('fixtures/binary').toString('utf8'))
58 | })
59 | })
60 | })
61 | })
62 | })
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/test/functional/gzip.js:
--------------------------------------------------------------------------------
1 | require('./lib/startup')
2 |
3 | var assert = require('assert')
4 | var Promise = require('bluebird')
5 |
6 | function readfile(x) {
7 | return require('fs').readFileSync(__dirname + '/' + x)
8 | }
9 |
10 | module.exports = function() {
11 | var server = process.server
12 | var express = process.express
13 |
14 | describe('testexp_gzip', function() {
15 | before(function() {
16 | express.get('/testexp_gzip', function(req, res) {
17 | var x = eval(
18 | '(' + readfile('fixtures/publish.json5')
19 | .toString('utf8')
20 | .replace(/__NAME__/g, 'testexp_gzip')
21 | .replace(/__VERSION__/g, '0.0.1')
22 | + ')'
23 | )
24 |
25 | // overcoming compress threshold
26 | x.versions['0.0.2'] = x.versions['0.0.1']
27 | x.versions['0.0.3'] = x.versions['0.0.1']
28 | x.versions['0.0.4'] = x.versions['0.0.1']
29 | x.versions['0.0.5'] = x.versions['0.0.1']
30 | x.versions['0.0.6'] = x.versions['0.0.1']
31 | x.versions['0.0.7'] = x.versions['0.0.1']
32 | x.versions['0.0.8'] = x.versions['0.0.1']
33 | x.versions['0.0.9'] = x.versions['0.0.1']
34 |
35 | require('zlib').gzip(JSON.stringify(x), function(err, buf) {
36 | assert.equal(err, null)
37 | assert.equal(req.headers['accept-encoding'], 'gzip')
38 | res.header('content-encoding', 'gzip')
39 | res.send(buf)
40 | })
41 | })
42 |
43 | express.get('/testexp_baddata', function(req, res) {
44 | assert.equal(req.headers['accept-encoding'], 'gzip')
45 | res.header('content-encoding', 'gzip')
46 | res.send(new Buffer([1,2,3,4,5,6,7,7,6,5,4,3,2,1]))
47 | })
48 | })
49 |
50 | it('should not fail on bad gzip', function () {
51 | return server.get_package('testexp_baddata')
52 | .status(404)
53 | })
54 |
55 | it('should understand gzipped data from uplink', function () {
56 | return server.get_package('testexp_gzip')
57 | .status(200)
58 | .response(function (res) {
59 | assert.equal(res.headers['content-encoding'], undefined)
60 | })
61 | .then(function (body) {
62 | assert.equal(body.name, 'testexp_gzip')
63 | assert.equal(Object.keys(body.versions).length, 9)
64 | })
65 | })
66 |
67 | it('should serve gzipped data', function () {
68 | return server.request({
69 | uri: '/testexp_gzip',
70 | encoding: null,
71 | headers: {
72 | 'Accept-encoding': 'gzip',
73 | },
74 | json: false,
75 | }).status(200)
76 | .response(function (res) {
77 | assert.equal(res.headers['content-encoding'], 'gzip')
78 | })
79 | .then(function (body) {
80 | assert.throws(function() {
81 | JSON.parse(body.toString('utf8'))
82 | })
83 |
84 | return new Promise(function (resolve) {
85 | require('zlib').gunzip(body, function(err, buf) {
86 | assert.equal(err, null)
87 | body = JSON.parse(buf)
88 | assert.equal(body.name, 'testexp_gzip')
89 | assert.equal(Object.keys(body.versions).length, 9)
90 | resolve()
91 | })
92 | })
93 | })
94 | })
95 | })
96 | }
97 |
98 |
--------------------------------------------------------------------------------
/test/functional/incomplete.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 |
3 | module.exports = function() {
4 | var server = process.server
5 | var express = process.express
6 |
7 | describe('Incomplete', function() {
8 | before(function() {
9 | express.get('/testexp-incomplete', function(_, res) {
10 | res.send({
11 | "name": "testexp-incomplete",
12 | "versions": {
13 | "0.1.0": {
14 | "name": "testexp_tags",
15 | "version": "0.1.0",
16 | "dist": {
17 | "shasum": "fake",
18 | "tarball": "http://localhost:55550/testexp-incomplete/-/content-length.tar.gz"
19 | }
20 | },
21 | "0.1.1": {
22 | "name": "testexp_tags",
23 | "version": "0.1.1",
24 | "dist": {
25 | "shasum": "fake",
26 | "tarball": "http://localhost:55550/testexp-incomplete/-/chunked.tar.gz"
27 | }
28 | }
29 | }
30 | })
31 | })
32 | })
33 |
34 | ;[ 'content-length', 'chunked' ].forEach(function(type) {
35 | it('should not store tarballs / ' + type, function(_cb) {
36 | var called
37 | express.get('/testexp-incomplete/-/'+type+'.tar.gz', function(_, res) {
38 | if (called) return res.socket.destroy()
39 | called = true
40 | if (type !== 'chunked') res.header('content-length', 1e6)
41 | res.write('test test test\n')
42 | setTimeout(function() {
43 | res.socket.write('200\nsss\n')
44 | res.socket.destroy()
45 | cb()
46 | }, 10)
47 | })
48 |
49 | server.request({ uri: '/testexp-incomplete/-/'+type+'.tar.gz' })
50 | .status(200)
51 | .response(function (res) {
52 | if (type !== 'chunked') assert.equal(res.headers['content-length'], 1e6)
53 | })
54 | .then(function (body) {
55 | assert(body.match(/test test test/))
56 | })
57 |
58 | function cb() {
59 | server.request({ uri: '/testexp-incomplete/-/'+type+'.tar.gz' })
60 | .body_error('internal server error')
61 | .then(function () { _cb() })
62 | }
63 | })
64 | })
65 | })
66 | }
67 |
68 |
--------------------------------------------------------------------------------
/test/functional/index.js:
--------------------------------------------------------------------------------
1 | //require('es6-shim')
2 | require('./lib/startup')
3 | var Promise = require('bluebird')
4 |
5 | var assert = require('assert')
6 | var async = require('async')
7 | var exec = require('child_process').exec
8 |
9 | describe('Func', function() {
10 | var server = process.server
11 | var server2 = process.server2
12 |
13 | before(function (cb) {
14 | async.parallel([
15 | function (cb) {
16 | require('./lib/startup').start('./test-storage', './config-1.yaml', cb)
17 | },
18 | function (cb) {
19 | require('./lib/startup').start('./test-storage2', './config-2.yaml', cb)
20 | },
21 | ], cb)
22 | })
23 |
24 | before(function() {
25 | return Promise.all([ server, server2 ].map(function(server) {
26 | return server.debug().status(200).then(function (body) {
27 | server.pid = body.pid
28 |
29 | return new Promise(function (resolve, reject) {
30 | exec('lsof -p ' + Number(server.pid), function(err, result) {
31 | assert.equal(err, null)
32 | server.fdlist = result.replace(/ +/g, ' ')
33 | resolve()
34 | })
35 | })
36 | })
37 | }))
38 | })
39 |
40 | before(function auth() {
41 | return Promise.all([ server, server2 ].map(function(server, cb) {
42 | return server.auth('test', 'test').status(201).body_ok(/'test'/)
43 | }))
44 | })
45 |
46 | it('authenticate', function(){/* test for before() */})
47 |
48 | require('./access')()
49 | require('./basic')()
50 | require('./gh29')()
51 | require('./tags')()
52 | require('./gzip')()
53 | require('./incomplete')()
54 | require('./mirror')()
55 | require('./newnpmreg')()
56 | require('./nullstorage')()
57 | require('./race')()
58 | require('./racycrash')()
59 | require('./scoped')()
60 | require('./security')()
61 | require('./adduser')()
62 | require('./addtag')()
63 | require('./plugins')()
64 |
65 | after(function (cb) {
66 | async.map([ server, server2 ], function(server, cb) {
67 | exec('lsof -p ' + Number(server.pid), function(err, result) {
68 | assert.equal(err, null)
69 | result = result.split('\n').filter(function(q) {
70 | if (q.match(/TCP .*->.* \(ESTABLISHED\)/)) return false
71 | if (q.match(/\/libcrypt-[^\/]+\.so/)) return false
72 | if (q.match(/\/node_modules\/crypt3\/build\/Release/)) return false
73 | return true
74 | }).join('\n').replace(/ +/g, ' ')
75 |
76 | assert.equal(server.fdlist, result)
77 | cb()
78 | })
79 | }, cb)
80 | })
81 | })
82 |
83 | process.on('unhandledRejection', function (err) {
84 | process.nextTick(function () {
85 | throw err
86 | })
87 | })
88 |
89 |
--------------------------------------------------------------------------------
/test/functional/lib/package.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = function(name, version) {
3 | return {
4 | "name": name,
5 | "version": version || "0.0.0",
6 | "dist": {
7 | "shasum": "fake",
8 | "tarball": "http://localhost:55551/"+encodeURIComponent(name)+"/-/blahblah"
9 | }
10 | }
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/test/functional/lib/server.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var request = require('./smart_request')
3 | var Promise = require('bluebird')
4 |
5 | function Server(url) {
6 | var self = Object.create(Server.prototype)
7 | self.url = url.replace(/\/$/, '')
8 | self.userAgent = 'node/v0.10.8 linux x64'
9 | self.authstr = 'Basic '+(new Buffer('test:test')).toString('base64')
10 | return self
11 | }
12 |
13 | Server.prototype.request = function(options) {
14 | assert(options.uri)
15 | var headers = options.headers || {}
16 | headers.accept = headers.accept || 'application/json'
17 | headers['user-agent'] = headers['user-agent'] || this.userAgent
18 | headers.authorization = headers.authorization || this.authstr
19 |
20 | return request({
21 | url: this.url + options.uri,
22 | method: options.method || 'GET',
23 | headers: headers,
24 | encoding: options.encoding,
25 | json: options.json != null ? options.json : true,
26 | })
27 | }
28 |
29 | Server.prototype.auth = function(user, pass) {
30 | this.authstr = 'Basic '+(Buffer(user+':'+pass)).toString('base64')
31 | return this.request({
32 | uri: '/-/user/org.couchdb.user:'+encodeURIComponent(user)+'/-rev/undefined',
33 | method: 'PUT',
34 | json: {
35 | name: user,
36 | password: pass,
37 | email: 'test@example.com',
38 | _id: 'org.couchdb.user:' + user,
39 | type: 'user',
40 | roles: [],
41 | date: new Date(),
42 | }
43 | })
44 | }
45 |
46 | Server.prototype.get_package = function(name) {
47 | return this.request({
48 | uri: '/'+encodeURIComponent(name),
49 | method: 'GET',
50 | })
51 | }
52 |
53 | Server.prototype.put_package = function(name, data) {
54 | if (typeof(data) === 'object' && !Buffer.isBuffer(data)) data = JSON.stringify(data)
55 | return this.request({
56 | uri: '/'+encodeURIComponent(name),
57 | method: 'PUT',
58 | headers: {
59 | 'content-type': 'application/json'
60 | },
61 | }).send(data)
62 | }
63 |
64 | Server.prototype.put_version = function(name, version, data) {
65 | if (typeof(data) === 'object' && !Buffer.isBuffer(data)) data = JSON.stringify(data)
66 | return this.request({
67 | uri: '/'+encodeURIComponent(name)+'/'+encodeURIComponent(version)+'/-tag/latest',
68 | method: 'PUT',
69 | headers: {
70 | 'content-type': 'application/json'
71 | },
72 | }).send(data)
73 | }
74 |
75 | Server.prototype.get_tarball = function(name, filename) {
76 | return this.request({
77 | uri: '/'+encodeURIComponent(name)+'/-/'+encodeURIComponent(filename),
78 | method: 'GET',
79 | })
80 | }
81 |
82 | Server.prototype.put_tarball = function(name, filename, data) {
83 | return this.request({
84 | uri: '/'+encodeURIComponent(name)+'/-/'+encodeURIComponent(filename)+'/whatever',
85 | method: 'PUT',
86 | headers: {
87 | 'content-type': 'application/octet-stream'
88 | },
89 | }).send(data)
90 | }
91 |
92 | Server.prototype.add_tag = function(name, tag, version) {
93 | return this.request({
94 | uri: '/'+encodeURIComponent(name)+'/'+encodeURIComponent(tag),
95 | method: 'PUT',
96 | headers: {
97 | 'content-type': 'application/json'
98 | },
99 | }).send(JSON.stringify(version))
100 | }
101 |
102 | Server.prototype.put_tarball_incomplete = function(name, filename, data, size, cb) {
103 | var promise = this.request({
104 | uri: '/'+encodeURIComponent(name)+'/-/'+encodeURIComponent(filename)+'/whatever',
105 | method: 'PUT',
106 | headers: {
107 | 'content-type': 'application/octet-stream',
108 | 'content-length': size,
109 | },
110 | timeout: 1000,
111 | })
112 |
113 | promise.request(function (req) {
114 | req.write(data)
115 | setTimeout(function() {
116 | req.req.abort()
117 | }, 20)
118 | })
119 |
120 | return new Promise(function (resolve, reject) {
121 | promise
122 | .then(function() {
123 | reject(Error('no error'))
124 | })
125 | .catch(function(err) {
126 | if (err.code === 'ECONNRESET') {
127 | resolve()
128 | } else {
129 | reject(err)
130 | }
131 | })
132 | })
133 | }
134 |
135 | Server.prototype.add_package = function(name) {
136 | return this.put_package(name, require('./package')(name))
137 | .status(201)
138 | .body_ok('created new package')
139 | }
140 |
141 | Server.prototype.whoami = function() {
142 | return this.request({ uri:'/-/whoami' })
143 | .status(200)
144 | .then(function(x) { return x.username })
145 | }
146 |
147 | Server.prototype.debug = function() {
148 | return this.request({
149 | uri: '/-/_debug',
150 | method: 'GET',
151 | headers: {
152 | 'content-type': 'application/json'
153 | },
154 | })
155 | }
156 |
157 | module.exports = Server
158 |
159 |
--------------------------------------------------------------------------------
/test/functional/lib/smart_request.js:
--------------------------------------------------------------------------------
1 |
2 | var assert = require('assert')
3 | var request = require('request')
4 | var Promise = require('bluebird')
5 | var sym = typeof Symbol !== 'undefined'
6 | ? Symbol('smart_request_data')
7 | : '__why_does_node_0.10_support_suck_so_much'
8 |
9 | function smart_request(options) {
10 | var self = {}
11 | self[sym] = {}
12 | self[sym].error = Error()
13 | Error.captureStackTrace(self[sym].error, smart_request)
14 |
15 | var result = new Promise(function (resolve, reject) {
16 | self[sym].request = request(options, function (err, res, body) {
17 | if (err) return reject(err)
18 | self[sym].response = res
19 | resolve(body)
20 | })
21 | })
22 |
23 | return extend(self, result)
24 | }
25 |
26 | function extend(self, promise) {
27 | promise[sym] = self[sym]
28 | Object.setPrototypeOf(promise, extensions)
29 | return promise
30 | }
31 |
32 | var extensions = Object.create(Promise.prototype)
33 |
34 | extensions.status = function (expected) {
35 | var self_data = this[sym]
36 |
37 | return extend(this, this.then(function (body) {
38 | try {
39 | assert.equal(self_data.response.statusCode, expected)
40 | } catch(err) {
41 | self_data.error.message = err.message
42 | throw self_data.error
43 | }
44 | return body
45 | }))
46 | }
47 |
48 | extensions.body_ok = function (expected) {
49 | var self_data = this[sym]
50 |
51 | return extend(this, this.then(function (body) {
52 | try {
53 | if (Object.prototype.toString.call(expected) === '[object RegExp]') {
54 | assert(body.ok.match(expected), "'" + body.ok + "' doesn't match " + expected)
55 | } else {
56 | assert.equal(body.ok, expected)
57 | }
58 | assert.equal(body.error, null)
59 | } catch(err) {
60 | self_data.error.message = err.message
61 | throw self_data.error
62 | }
63 | return body
64 | }))
65 | }
66 |
67 | extensions.body_error = function (expected) {
68 | var self_data = this[sym]
69 |
70 | return extend(this, this.then(function (body) {
71 | try {
72 | if (Object.prototype.toString.call(expected) === '[object RegExp]') {
73 | assert(body.error.match(expected), body.error + " doesn't match " + expected)
74 | } else {
75 | assert.equal(body.error, expected)
76 | }
77 | assert.equal(body.ok, null)
78 | } catch(err) {
79 | self_data.error.message = err.message
80 | throw self_data.error
81 | }
82 | return body
83 | }))
84 | }
85 |
86 | extensions.request = function (cb) {
87 | cb(this[sym].request)
88 | return this
89 | }
90 |
91 | extensions.response = function (cb) {
92 | var self_data = this[sym]
93 |
94 | return extend(this, this.then(function (body) {
95 | cb(self_data.response)
96 | return body
97 | }))
98 | }
99 |
100 | extensions.send = function (data) {
101 | this[sym].request.end(data)
102 | return this
103 | }
104 |
105 | module.exports = smart_request
106 |
107 |
--------------------------------------------------------------------------------
/test/functional/lib/startup.js:
--------------------------------------------------------------------------------
1 | var fork = require('child_process').fork
2 | var express = require('express')
3 | var rimraf = require('rimraf')
4 | var Server = require('./server')
5 |
6 | var forks = process.forks = []
7 | process.server = Server('http://localhost:55551/')
8 | process.server2 = Server('http://localhost:55552/')
9 | process.express = express()
10 | process.express.listen(55550)
11 |
12 | module.exports.start = function start(dir, conf, cb) {
13 | rimraf(__dirname + '/../' + dir, function() {
14 | // filter out --debug-brk
15 | var oldArgv = process.execArgv
16 | process.execArgv = process.execArgv.filter(function(x) {
17 | return x !== '--debug-brk'
18 | })
19 |
20 | var f = fork(__dirname + '/../../../bin/sinopia'
21 | , ['-c', __dirname + '/../' + conf]
22 | , {silent: !process.env.TRAVIS}
23 | )
24 | forks.push(f)
25 | f.on('message', function(msg) {
26 | if ('sinopia_started' in msg) {
27 | cb(), cb = function(){}
28 | }
29 | })
30 | f.on('error', function(err) {
31 | throw err
32 | })
33 | process.execArgv = oldArgv
34 | })
35 | }
36 |
37 | process.on('exit', function() {
38 | if (forks[0]) forks[0].kill()
39 | if (forks[1]) forks[1].kill()
40 | })
41 |
42 |
--------------------------------------------------------------------------------
/test/functional/mirror.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 |
3 | function readfile(x) {
4 | return require('fs').readFileSync(__dirname + '/' + x)
5 | }
6 |
7 | module.exports = function() {
8 | var server = process.server
9 | var server2 = process.server2
10 |
11 | it('testing anti-loop', function () {
12 | return server2.get_package('testloop')
13 | .status(404)
14 | .body_error(/no such package/)
15 | })
16 |
17 | ;['fwd', /*'loop'*/].forEach(function(pkg) {
18 | var prefix = pkg + ': '
19 | pkg = 'test' + pkg
20 |
21 | describe(pkg, function() {
22 | before(function () {
23 | return server.put_package(pkg, require('./lib/package')(pkg))
24 | .status(201)
25 | .body_ok(/created new package/)
26 | })
27 |
28 | it(prefix+'creating new package', function(){})
29 |
30 | describe(pkg, function() {
31 | before(function () {
32 | return server.put_version(pkg, '0.1.1', require('./lib/package')(pkg))
33 | .status(201)
34 | .body_ok(/published/)
35 | })
36 |
37 | it(prefix+'uploading new package version', function(){})
38 |
39 | it(prefix+'uploading incomplete tarball', function () {
40 | return server.put_tarball_incomplete(pkg, pkg+'.bad', readfile('fixtures/binary'), 3000)
41 | })
42 |
43 | describe('tarball', function() {
44 | before(function () {
45 | return server.put_tarball(pkg, pkg+'.file', readfile('fixtures/binary'))
46 | .status(201)
47 | .body_ok(/.*/)
48 | })
49 |
50 | it(prefix+'uploading new tarball', function(){})
51 |
52 | it(prefix+'downloading tarball from server1', function () {
53 | return server.get_tarball(pkg, pkg+'.file')
54 | .status(200)
55 | .then(function (body) {
56 | assert.deepEqual(body, readfile('fixtures/binary').toString('utf8'))
57 | })
58 | })
59 | })
60 | })
61 | })
62 | })
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/test/functional/newnpmreg.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 |
3 | function readfile(x) {
4 | return require('fs').readFileSync(__dirname + '/' + x)
5 | }
6 |
7 | function sha(x) {
8 | return require('crypto').createHash('sha1', 'binary').update(x).digest('hex')
9 | }
10 |
11 | module.exports = function() {
12 | var server = process.server
13 | var server2 = process.server2
14 | var express = process.express
15 |
16 | describe('newnpmreg', function() {
17 | before(function () {
18 | return server.request({
19 | uri: '/testpkg-newnpmreg',
20 | headers: {
21 | 'content-type': 'application/json',
22 | },
23 | method: 'PUT',
24 | json: JSON.parse(readfile('fixtures/newnpmreg.json')),
25 | }).status(201)
26 | })
27 |
28 | it('add pkg', function () {})
29 |
30 | it('server1 - tarball', function () {
31 | return server.get_tarball('testpkg-newnpmreg', 'testpkg-newnpmreg-0.0.0.tgz')
32 | .status(200)
33 | .then(function (body) {
34 | // not real sha due to utf8 conversion
35 | assert.strictEqual(sha(body), '789ca61e3426ce55c4983451b58e62b04abceaf6')
36 | })
37 | })
38 |
39 | it('server2 - tarball', function () {
40 | return server2.get_tarball('testpkg-newnpmreg', 'testpkg-newnpmreg-0.0.0.tgz')
41 | .status(200)
42 | .then(function (body) {
43 | // not real sha due to utf8 conversion
44 | assert.strictEqual(sha(body), '789ca61e3426ce55c4983451b58e62b04abceaf6')
45 | })
46 | })
47 |
48 | it('server1 - package', function () {
49 | return server.get_package('testpkg-newnpmreg')
50 | .status(200)
51 | .then(function (body) {
52 | assert.equal(body.name, 'testpkg-newnpmreg')
53 | assert.equal(body.versions['0.0.0'].name, 'testpkg-newnpmreg')
54 | assert.equal(body.versions['0.0.0'].dist.tarball, 'http://localhost:55551/testpkg-newnpmreg/-/testpkg-newnpmreg-0.0.0.tgz')
55 | assert.deepEqual(body['dist-tags'], {foo: '0.0.0', latest: '0.0.0'})
56 | })
57 | })
58 |
59 | it('server2 - package', function () {
60 | return server2.get_package('testpkg-newnpmreg')
61 | .status(200)
62 | .then(function (body) {
63 | assert.equal(body.name, 'testpkg-newnpmreg')
64 | assert.equal(body.versions['0.0.0'].name, 'testpkg-newnpmreg')
65 | assert.equal(body.versions['0.0.0'].dist.tarball, 'http://localhost:55552/testpkg-newnpmreg/-/testpkg-newnpmreg-0.0.0.tgz')
66 | assert.deepEqual(body['dist-tags'], {foo: '0.0.0', latest: '0.0.0'})
67 | })
68 | })
69 |
70 | it('server1 - readme', function () {
71 | return server.request({ uri: '/-/readme/testpkg-newnpmreg' })
72 | .status(200)
73 | .then(function (body) {
74 | assert.equal(body, 'blah blah blah
\n')
75 | })
76 | })
77 |
78 | it('server2 - readme', function () {
79 | return server2.request({ uri: '/-/readme/testpkg-newnpmreg' })
80 | .status(200)
81 | .then(function (body) {
82 | assert.equal(body, 'blah blah blah
\n')
83 | })
84 | })
85 |
86 | describe('search', function() {
87 | function check(obj) {
88 | obj['testpkg-newnpmreg'].time.modified = '2014-10-02T07:07:51.000Z'
89 | assert.deepEqual(obj['testpkg-newnpmreg'],
90 | { name: 'testpkg-newnpmreg',
91 | description: '',
92 | author: '',
93 | license: 'ISC',
94 | 'dist-tags': { latest: '0.0.0' },
95 | maintainers: [ { name: 'alex', email: 'alex@kocharin.ru' } ],
96 | readmeFilename: '',
97 | time: { modified: '2014-10-02T07:07:51.000Z' },
98 | versions: {},
99 | repository: { type: 'git', url: '' } })
100 | }
101 |
102 | before(function () {
103 | express.get('/-/all', function(req, res) {
104 | res.send({})
105 | })
106 | })
107 |
108 | it('server1 - search', function () {
109 | return server.request({ uri: '/-/all' })
110 | .status(200)
111 | .then(check)
112 | })
113 |
114 | it('server2 - search', function () {
115 | return server2.request({ uri: '/-/all' })
116 | .status(200)
117 | .then(check)
118 | })
119 | })
120 | })
121 | }
122 |
--------------------------------------------------------------------------------
/test/functional/nullstorage.js:
--------------------------------------------------------------------------------
1 | require('./lib/startup')
2 |
3 | var assert = require('assert')
4 | var crypto = require('crypto')
5 |
6 | function readfile(x) {
7 | return require('fs').readFileSync(__dirname + '/' + x)
8 | }
9 |
10 | module.exports = function() {
11 | var server = process.server
12 | var server2 = process.server2
13 |
14 | it('trying to fetch non-existent package / null storage', function () {
15 | return server.get_package('test-nullstorage-nonexist')
16 | .status(404)
17 | .body_error(/no such package/)
18 | })
19 |
20 | describe('test-nullstorage on server2', function() {
21 | before(function () {
22 | return server2.add_package('test-nullstorage2')
23 | })
24 |
25 | it('creating new package - server2', function(){/* test for before() */})
26 |
27 | it('downloading non-existent tarball', function () {
28 | return server.get_tarball('test-nullstorage2', 'blahblah')
29 | .status(404)
30 | .body_error(/no such file/)
31 | })
32 |
33 | describe('tarball', function() {
34 | before(function () {
35 | return server2.put_tarball('test-nullstorage2', 'blahblah', readfile('fixtures/binary'))
36 | .status(201)
37 | .body_ok(/.*/)
38 | })
39 |
40 | before(function () {
41 | var pkg = require('./lib/package')('test-nullstorage2')
42 | pkg.dist.shasum = crypto.createHash('sha1').update(readfile('fixtures/binary')).digest('hex')
43 | return server2.put_version('test-nullstorage2', '0.0.1', pkg)
44 | .status(201)
45 | .body_ok(/published/)
46 | })
47 |
48 | it('uploading new tarball', function () {/* test for before() */})
49 |
50 | it('downloading newly created tarball', function () {
51 | return server.get_tarball('test-nullstorage2', 'blahblah')
52 | .status(200)
53 | .then(function (body) {
54 | assert.deepEqual(body, readfile('fixtures/binary').toString('utf8'))
55 | })
56 | })
57 |
58 | it('downloading newly created package', function () {
59 | return server.get_package('test-nullstorage2')
60 | .status(200)
61 | .then(function (body) {
62 | assert.equal(body.name, 'test-nullstorage2')
63 | assert.equal(body.versions['0.0.1'].name, 'test-nullstorage2')
64 | assert.equal(body.versions['0.0.1'].dist.tarball, 'http://localhost:55551/test-nullstorage2/-/blahblah')
65 | assert.deepEqual(body['dist-tags'], {latest: '0.0.1'})
66 | })
67 | })
68 | })
69 | })
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/test/functional/plugins.js:
--------------------------------------------------------------------------------
1 | require('./lib/startup')
2 |
3 | var assert = require('assert')
4 |
5 | module.exports = function() {
6 | var server2 = process.server2
7 |
8 | describe('authentication', function() {
9 | var authstr
10 |
11 | before(function() {
12 | authstr = server2.authstr
13 | })
14 |
15 | it('should not authenticate with wrong password', function () {
16 | return server2.auth('authtest', 'wrongpass')
17 | .status(409)
18 | .body_error('this user already exists')
19 | .then(function () {
20 | return server2.whoami()
21 | })
22 | .then(function (username) {
23 | assert.equal(username, null)
24 | })
25 | })
26 |
27 | it('wrong password handled by plugin', function () {
28 | return server2.auth('authtest2', 'wrongpass')
29 | .status(409)
30 | .body_error('registration is disabled')
31 | .then(function () {
32 | return server2.whoami()
33 | })
34 | .then(function (username) {
35 | assert.equal(username, null)
36 | })
37 | })
38 |
39 | it('right password handled by plugin', function () {
40 | return server2.auth('authtest2', 'blahblah')
41 | .status(201)
42 | .body_ok(/'authtest2'/)
43 | .then(function () {
44 | return server2.whoami()
45 | })
46 | .then(function (username) {
47 | assert.equal(username, 'authtest2')
48 | })
49 | })
50 |
51 | after(function() {
52 | server2.authstr = authstr
53 | })
54 | })
55 |
56 | describe('authorization', function() {
57 | var authstr
58 |
59 | before(function() {
60 | authstr = server2.authstr
61 | })
62 |
63 | describe('authtest', function() {
64 | before(function () {
65 | return server2.auth('authtest', 'test')
66 | .status(201)
67 | .body_ok(/'authtest'/)
68 | })
69 |
70 | it('access test-auth-allow', function () {
71 | return server2.get_package('test-auth-allow')
72 | .status(404)
73 | .body_error('no such package available')
74 | })
75 |
76 | it('access test-auth-deny', function () {
77 | return server2.get_package('test-auth-deny')
78 | .status(403)
79 | .body_error("you're not allowed here")
80 | })
81 |
82 | it('access test-auth-regular', function () {
83 | return server2.get_package('test-auth-regular')
84 | .status(404)
85 | .body_error('no such package available')
86 | })
87 | })
88 |
89 | describe('authtest2', function() {
90 | before(function () {
91 | return server2.auth('authtest2', 'blahblah')
92 | .status(201)
93 | .body_ok(/'authtest2'/)
94 | })
95 |
96 | it('access test-auth-allow', function () {
97 | return server2.get_package('test-auth-allow')
98 | .status(403)
99 | .body_error("i don't know anything about you")
100 | })
101 |
102 | it('access test-auth-deny', function () {
103 | return server2.get_package('test-auth-deny')
104 | .status(403)
105 | .body_error("i don't know anything about you")
106 | })
107 |
108 | it('access test-auth-regular', function () {
109 | return server2.get_package('test-auth-regular')
110 | .status(404)
111 | .body_error('no such package available')
112 | })
113 | })
114 |
115 | after(function() {
116 | server2.authstr = authstr
117 | })
118 | })
119 | }
120 |
121 |
--------------------------------------------------------------------------------
/test/functional/plugins/authenticate.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = Plugin
3 |
4 | function Plugin(config, stuff) {
5 | var self = Object.create(Plugin.prototype)
6 | self._config = config
7 | return self
8 | }
9 |
10 | // plugin is expected to be compatible with...
11 | Plugin.prototype.sinopia_version = '1.1.0'
12 |
13 | Plugin.prototype.authenticate = function(user, password, cb) {
14 | var self = this
15 | if (user !== self._config.accept_user) {
16 | // delegate to next plugin
17 | return cb(null, false)
18 | }
19 | if (password !== self._config.with_password) {
20 | var err = Error("i don't like your password")
21 | err.status = 403
22 | return cb(err)
23 | }
24 | return cb(null, [ user ])
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/test/functional/plugins/authorize.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = Plugin
3 |
4 | function Plugin(config, stuff) {
5 | var self = Object.create(Plugin.prototype)
6 | self._config = config
7 | return self
8 | }
9 |
10 | // plugin is expected to be compatible with...
11 | Plugin.prototype.sinopia_version = '1.1.0'
12 |
13 | Plugin.prototype.allow_access = function(user, package, cb) {
14 | var self = this
15 | if (!package.handled_by_auth_plugin) {
16 | // delegate to next plugin
17 | return cb(null, false)
18 | }
19 | if (user.name !== self._config.allow_user) {
20 | var err = Error("i don't know anything about you")
21 | err.status = 403
22 | return cb(err)
23 | }
24 | if (package.name !== self._config.to_access) {
25 | var err = Error("you're not allowed here")
26 | err.status = 403
27 | return cb(err)
28 | }
29 | return cb(null, true)
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/test/functional/race.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var async = require('async')
3 | var _oksum = 0
4 |
5 | module.exports = function() {
6 | var server = process.server
7 |
8 | describe('race', function() {
9 | before(function () {
10 | return server.put_package('race', require('./lib/package')('race'))
11 | .status(201)
12 | .body_ok(/created new package/)
13 | })
14 |
15 | it('creating new package', function(){})
16 |
17 | it('uploading 10 same versions', function (callback) {
18 | var fns = []
19 | for (var i=0; i<10; i++) {
20 | fns.push(function(cb_) {
21 | var data = require('./lib/package')('race')
22 | data.rand = Math.random()
23 |
24 | var _res
25 | server.put_version('race', '0.0.1', data)
26 | .response(function (res) { _res = res })
27 | .then(function (body) {
28 | cb_(null, [ _res, body ])
29 | })
30 | })
31 | }
32 |
33 | async.parallel(fns, function(err, res) {
34 | var okcount = 0
35 | var failcount = 0
36 |
37 | assert.equal(err, null)
38 |
39 | res.forEach(function(arr) {
40 | var resp = arr[0]
41 | var body = arr[1]
42 |
43 | if (resp.statusCode === 201 && ~body.ok.indexOf('published')) okcount++
44 | if (resp.statusCode === 409 && ~body.error.indexOf('already present')) failcount++
45 | if (resp.statusCode === 503 && ~body.error.indexOf('unavailable')) failcount++
46 | })
47 | assert.equal(okcount + failcount, 10)
48 | assert.equal(okcount, 1)
49 | _oksum += okcount
50 |
51 | callback()
52 | })
53 | })
54 |
55 | it('uploading 10 diff versions', function (callback) {
56 | var fns = []
57 | for (var i=0; i<10; i++) {
58 | ;(function(i) {
59 | fns.push(function(cb_) {
60 | var _res
61 | server.put_version('race', '0.1.'+String(i), require('./lib/package')('race'))
62 | .response(function (res) { _res = res })
63 | .then(function (body) {
64 | cb_(null, [ _res, body ])
65 | })
66 | })
67 | })(i)
68 | }
69 |
70 | async.parallel(fns, function(err, res) {
71 | var okcount = 0
72 | var failcount = 0
73 |
74 | assert.equal(err, null)
75 | res.forEach(function(arr) {
76 | var resp = arr[0]
77 | var body = arr[1]
78 | if (resp.statusCode === 201 && ~body.ok.indexOf('published')) okcount++
79 | if (resp.statusCode === 409 && ~body.error.indexOf('already present')) failcount++
80 | if (resp.statusCode === 503 && ~body.error.indexOf('unavailable')) failcount++
81 | })
82 | assert.equal(okcount + failcount, 10)
83 | assert.notEqual(okcount, 1)
84 | _oksum += okcount
85 |
86 | callback()
87 | })
88 | })
89 |
90 | after('downloading package', function () {
91 | return server.get_package('race')
92 | .status(200)
93 | .then(function (body) {
94 | assert.equal(Object.keys(body.versions).length, _oksum)
95 | })
96 | })
97 | })
98 | }
99 |
100 |
--------------------------------------------------------------------------------
/test/functional/racycrash.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 |
3 | module.exports = function() {
4 | var server = process.server
5 | var express = process.express
6 |
7 | describe('Racy', function() {
8 | var on_tarball
9 |
10 | before(function() {
11 | express.get('/testexp-racycrash', function(_, res) {
12 | res.send({
13 | "name": "testexp-racycrash",
14 | "versions": {
15 | "0.1.0": {
16 | "name": "testexp_tags",
17 | "version": "0.1.0",
18 | "dist": {
19 | "shasum": "fake",
20 | "tarball": "http://localhost:55550/testexp-racycrash/-/test.tar.gz"
21 | }
22 | }
23 | }
24 | })
25 | })
26 |
27 | express.get('/testexp-racycrash/-/test.tar.gz', function(_, res) {
28 | on_tarball(res)
29 | })
30 | })
31 |
32 | it('should not crash on error if client disconnects', function(_cb) {
33 | on_tarball = function(res) {
34 | res.header('content-length', 1e6)
35 | res.write('test test test\n')
36 | setTimeout(function() {
37 | res.write('test test test\n')
38 | res.socket.destroy()
39 | cb()
40 | }, 200)
41 | }
42 |
43 | server.request({ uri: '/testexp-racycrash/-/test.tar.gz' })
44 | .then(function (body) {
45 | assert.equal(body, 'test test test\n')
46 | })
47 |
48 | function cb() {
49 | // test for NOT crashing
50 | server.request({ uri: '/testexp-racycrash' })
51 | .status(200)
52 | .then(function () { _cb() })
53 | }
54 | })
55 |
56 | it('should not store tarball', function () {
57 | on_tarball = function(res) {
58 | res.socket.destroy()
59 | }
60 |
61 | return server.request({ uri: '/testexp-racycrash/-/test.tar.gz' })
62 | .body_error('internal server error')
63 | })
64 | })
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/test/functional/scoped.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 |
3 | function readfile(x) {
4 | return require('fs').readFileSync(__dirname + '/' + x)
5 | }
6 |
7 | function sha(x) {
8 | return require('crypto').createHash('sha1', 'binary').update(x).digest('hex')
9 | }
10 |
11 | module.exports = function() {
12 | var server = process.server
13 | var server2 = process.server2
14 |
15 | describe('test-scoped', function() {
16 | before(function () {
17 | return server.request({
18 | uri: '/@test%2fscoped',
19 | headers: {
20 | 'content-type': 'application/json',
21 | },
22 | method: 'PUT',
23 | json: JSON.parse(readfile('fixtures/scoped.json')),
24 | }).status(201)
25 | })
26 |
27 | it('add pkg', function () {})
28 |
29 | it('server1 - tarball', function () {
30 | return server.get_tarball('@test/scoped', 'scoped-1.0.0.tgz')
31 | .status(200)
32 | .then(function (body) {
33 | // not real sha due to utf8 conversion
34 | assert.strictEqual(sha(body), 'c59298948907d077c3b42f091554bdeea9208964')
35 | })
36 | })
37 |
38 | it('server2 - tarball', function () {
39 | return server2.get_tarball('@test/scoped', 'scoped-1.0.0.tgz')
40 | .status(200)
41 | .then(function (body) {
42 | // not real sha due to utf8 conversion
43 | assert.strictEqual(sha(body), 'c59298948907d077c3b42f091554bdeea9208964')
44 | })
45 | })
46 |
47 | it('server1 - package', function () {
48 | return server.get_package('@test/scoped')
49 | .status(200)
50 | .then(function (body) {
51 | assert.equal(body.name, '@test/scoped')
52 | assert.equal(body.versions['1.0.0'].name, '@test/scoped')
53 | assert.equal(body.versions['1.0.0'].dist.tarball, 'http://localhost:55551/@test%2fscoped/-/scoped-1.0.0.tgz')
54 | assert.deepEqual(body['dist-tags'], {latest: '1.0.0'})
55 | })
56 | })
57 |
58 | it('server2 - package', function () {
59 | return server2.get_package('@test/scoped')
60 | .status(200)
61 | .then(function (body) {
62 | assert.equal(body.name, '@test/scoped')
63 | assert.equal(body.versions['1.0.0'].name, '@test/scoped')
64 | assert.equal(body.versions['1.0.0'].dist.tarball, 'http://localhost:55552/@test%2fscoped/-/scoped-1.0.0.tgz')
65 | assert.deepEqual(body['dist-tags'], {latest: '1.0.0'})
66 | })
67 | })
68 |
69 | it('server2 - nginx workaround', function () {
70 | return server2.request({ uri: '/@test/scoped/1.0.0' })
71 | .status(200)
72 | .then(function (body) {
73 | assert.equal(body.name, '@test/scoped')
74 | assert.equal(body.dist.tarball, 'http://localhost:55552/@test%2fscoped/-/scoped-1.0.0.tgz')
75 | })
76 | })
77 | })
78 | }
79 |
--------------------------------------------------------------------------------
/test/functional/security.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 |
3 | module.exports = function() {
4 | var server = process.server
5 |
6 | describe('Security', function() {
7 | before(function() {
8 | return server.add_package('testpkg-sec')
9 | })
10 |
11 | it('bad pkg #1', function () {
12 | return server.get_package('package.json')
13 | .status(403)
14 | .body_error(/invalid package/)
15 | })
16 |
17 | it('bad pkg #2', function () {
18 | return server.get_package('__proto__')
19 | .status(403)
20 | .body_error(/invalid package/)
21 | })
22 |
23 | it('__proto__, connect stuff', function () {
24 | return server.request({ uri: '/testpkg-sec?__proto__=1' })
25 | .then(function (body) {
26 | // test for NOT outputting stack trace
27 | assert(!body || typeof(body) === 'object' || body.indexOf('node_modules') === -1)
28 |
29 | // test for NOT crashing
30 | return server.request({ uri: '/testpkg-sec' }).status(200)
31 | })
32 | })
33 |
34 | it('do not return package.json as an attachment', function () {
35 | return server.request({ uri: '/testpkg-sec/-/package.json' })
36 | .status(403)
37 | .body_error(/invalid filename/)
38 | })
39 |
40 | it('silly things - reading #1', function () {
41 | return server.request({ uri: '/testpkg-sec/-/../../../../../../../../etc/passwd' })
42 | .status(404)
43 | })
44 |
45 | it('silly things - reading #2', function () {
46 | return server.request({ uri: '/testpkg-sec/-/%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd' })
47 | .status(403)
48 | .body_error(/invalid filename/)
49 | })
50 |
51 | it('silly things - writing #1', function () {
52 | return server.put_tarball('testpkg-sec', 'package.json', '{}')
53 | .status(403)
54 | .body_error(/invalid filename/)
55 | })
56 |
57 | it('silly things - writing #3', function () {
58 | return server.put_tarball('testpkg-sec', 'node_modules', '{}')
59 | .status(403)
60 | .body_error(/invalid filename/)
61 | })
62 |
63 | it('silly things - writing #4', function () {
64 | return server.put_tarball('testpkg-sec', '../testpkg.tgz', '{}')
65 | .status(403)
66 | .body_error(/invalid filename/)
67 | })
68 | })
69 | }
70 |
71 |
--------------------------------------------------------------------------------
/test/functional/tags.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 |
3 | function readfile(x) {
4 | return require('fs').readFileSync(__dirname + '/' + x)
5 | }
6 |
7 | module.exports = function() {
8 | var server = process.server
9 | var express = process.express
10 |
11 | it('tags - testing for 404', function () {
12 | return server.get_package('testexp_tags')
13 | // shouldn't exist yet
14 | .status(404)
15 | .body_error(/no such package/)
16 | })
17 |
18 | describe('tags', function() {
19 | before(function () {
20 | express.get('/testexp_tags', function(req, res) {
21 | var f = readfile('fixtures/tags.json').toString().replace(/__NAME__/g, 'testexp_tags')
22 | res.send(JSON.parse(f))
23 | })
24 | })
25 |
26 | it('fetching package again', function () {
27 | return server.get_package('testexp_tags')
28 | .status(200)
29 | .then(function (body) {
30 | assert.equal(typeof(body.versions['1.1']), 'object')
31 | assert.equal(body['dist-tags'].something, '0.1.1alpha')
32 | // note: 5.4.3 is invalid tag, 0.1.3alpha is highest semver
33 | assert.equal(body['dist-tags'].latest, '0.1.3alpha')
34 | assert.equal(body['dist-tags'].bad, null)
35 | })
36 | })
37 |
38 | ;['0.1.1alpha', '0.1.1-alpha', '0000.00001.001-alpha'].forEach(function(ver) {
39 | it('fetching '+ver, function () {
40 | return server.request({uri:'/testexp_tags/'+ver})
41 | .status(200)
42 | .then(function (body) {
43 | assert.equal(body.version, '0.1.1alpha')
44 | })
45 | })
46 | })
47 | })
48 |
49 | describe('dist-tags methods', function() {
50 | before(function () {
51 | express.get('/testexp_tags2', function(req, res) {
52 | var f = readfile('fixtures/tags.json').toString().replace(/__NAME__/g, 'testexp_tags2')
53 | res.send(JSON.parse(f))
54 | })
55 | })
56 |
57 | // populate cache
58 | before(function () {
59 | return server.get_package('testexp_tags2')
60 | .status(200)
61 | })
62 |
63 | beforeEach(function () {
64 | return server.request({
65 | method: 'PUT',
66 | uri: '/-/package/testexp_tags2/dist-tags',
67 | json: {
68 | foo: '0.1.0',
69 | bar: '0.1.1alpha',
70 | baz: '0.1.2',
71 | },
72 | }).status(201).body_ok(/tags updated/)
73 | })
74 |
75 | it('fetching tags', function () {
76 | return server.request({
77 | method: 'GET',
78 | uri: '/-/package/testexp_tags2/dist-tags',
79 | }).status(200).then(function (body) {
80 | assert.deepEqual(body,
81 | { foo: '0.1.0',
82 | bar: '0.1.1alpha',
83 | baz: '0.1.2',
84 | latest: '0.1.3alpha' })
85 | })
86 | })
87 |
88 | it('merging tags', function () {
89 | return server.request({
90 | method: 'POST',
91 | uri: '/-/package/testexp_tags2/dist-tags',
92 | json: {
93 | foo: '0.1.2',
94 | quux: '0.1.0',
95 | },
96 | }).status(201).body_ok(/updated/).then(function () {
97 | return server.request({
98 | method: 'GET',
99 | uri: '/-/package/testexp_tags2/dist-tags',
100 | }).status(200).then(function (body) {
101 | assert.deepEqual(body,
102 | { foo: '0.1.2',
103 | bar: '0.1.1alpha',
104 | baz: '0.1.2',
105 | quux: '0.1.0',
106 | latest: '0.1.3alpha' })
107 | })
108 | })
109 | })
110 |
111 | it('replacing tags', function () {
112 | return server.request({
113 | method: 'PUT',
114 | uri: '/-/package/testexp_tags2/dist-tags',
115 | json: {
116 | foo: '0.1.2',
117 | quux: '0.1.0',
118 | },
119 | }).status(201).body_ok(/updated/).then(function () {
120 | return server.request({
121 | method: 'GET',
122 | uri: '/-/package/testexp_tags2/dist-tags',
123 | }).status(200).then(function (body) {
124 | assert.deepEqual(body,
125 | { foo: '0.1.2',
126 | quux: '0.1.0',
127 | latest: '0.1.3alpha' })
128 | })
129 | })
130 | })
131 |
132 | it('adding a tag', function () {
133 | return server.request({
134 | method: 'PUT',
135 | uri: '/-/package/testexp_tags2/dist-tags/foo',
136 | json: '0.1.3alpha',
137 | }).status(201).body_ok(/tagged/).then(function () {
138 | return server.request({
139 | method: 'GET',
140 | uri: '/-/package/testexp_tags2/dist-tags',
141 | }).status(200).then(function (body) {
142 | assert.deepEqual(body,
143 | { foo: '0.1.3alpha',
144 | bar: '0.1.1alpha',
145 | baz: '0.1.2',
146 | latest: '0.1.3alpha' })
147 | })
148 | })
149 | })
150 |
151 | it('removing a tag', function () {
152 | return server.request({
153 | method: 'DELETE',
154 | uri: '/-/package/testexp_tags2/dist-tags/foo',
155 | }).status(201).body_ok(/removed/).then(function () {
156 | return server.request({
157 | method: 'GET',
158 | uri: '/-/package/testexp_tags2/dist-tags',
159 | }).status(200).then(function (body) {
160 | assert.deepEqual(body,
161 | { bar: '0.1.1alpha',
162 | baz: '0.1.2',
163 | latest: '0.1.3alpha' })
164 | })
165 | })
166 | })
167 | })
168 | }
169 |
--------------------------------------------------------------------------------
/test/integration/config.yaml:
--------------------------------------------------------------------------------
1 | storage: ./.sinopia_test_env/test-storage
2 |
3 | users:
4 | test:
5 | password: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3
6 |
7 | uplinks:
8 | npmjs:
9 | url: https://registry.npmjs.org/
10 |
11 | logs:
12 | - {type: stdout, format: pretty, level: trace}
13 |
14 | packages:
15 | jju:
16 | allow_access: all
17 | allow_publish: all
18 | proxy_access: npmjs
19 |
20 | '*':
21 | allow_access: all
22 | allow_publish: all
23 |
24 | listen: 55501
25 |
26 |
--------------------------------------------------------------------------------
/test/integration/sinopia-test-1.2.3.tgz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rlidwka/sinopia/3f55fb4c0c6685e8b22796cce7b523bdbfb4019e/test/integration/sinopia-test-1.2.3.tgz
--------------------------------------------------------------------------------
/test/integration/test.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl
2 |
3 | # note to readers: in perl it's useful, in javascript it isn't
4 | use strict;
5 |
6 | # setting up working environment && chdir there
7 | use Cwd 'abs_path';
8 | use File::Basename;
9 | $ENV{HOME} = dirname(abs_path( __FILE__ )) . '/.sinopia_test_env';
10 | system('rm -rf .sinopia_test_env ; mkdir .sinopia_test_env') and quit('fail');
11 | chdir $ENV{HOME};
12 |
13 | use Data::Dumper;
14 | my $pid;
15 |
16 | sub quit {
17 | print $_[0]."\n";
18 | exec("kill $pid ; exit 1");
19 | }
20 |
21 | # run sinopia in a child process
22 | if (($pid = fork()) == 0) {
23 | exec "../../../bin/sinopia ../config.yaml";
24 | die "exec failed";
25 | }
26 |
27 | system('mkdir node_modules') and quit('fail');
28 | system('npm set sinopia_test_config 12345') and quit('fail');
29 |
30 | if (`cat .npmrc` !~ /sinopia_test_config/) {
31 | quit "npm is using wrong config";
32 | }
33 |
34 | system('npm set registry http://localhost:55501') and quit('fail');
35 | system(q{/bin/echo -e 'test\ntest\ns@s.s\n' | npm adduser}) and quit('fail');
36 |
37 | system('npm install jju') and quit('fail');
38 | (`node -e 'console.log(require("jju").parse("{qwerty:123}").qwerty+456)'` =~ /579/) or quit('fail');
39 |
40 | system('npm publish ../sinopia-test-1.2.3.tgz') and quit('fail');
41 | system('npm tag sinopia-test@1.2.3 meow') and quit('fail');
42 | system('npm install sinopia-test@meow') and quit('fail');
43 |
44 | (`node -e 'require("sinopia-test")'` =~ /w==w/) or quit('fail');
45 |
46 | quit("
47 | ==================================================================
48 | All tests seem to be executed successfully, nothing is broken yet.
49 | ==================================================================");
50 |
51 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --reporter dot
2 | --timeout 5000
3 | --inline-diffs
4 |
--------------------------------------------------------------------------------
/test/unit/config_def.js:
--------------------------------------------------------------------------------
1 |
2 | describe('config.yaml', function() {
3 | it('should be parseable', function() {
4 | var source = require('fs').readFileSync(__dirname + '/../../conf/default.yaml', 'utf8')
5 | require('js-yaml').safeLoad(source)
6 | })
7 | })
8 |
9 |
--------------------------------------------------------------------------------
/test/unit/listen_addr.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var parse = require('../../lib/utils').parse_address
3 |
4 | describe('Parse address', function() {
5 | function addTest(what, proto, host, port) {
6 | it(what, function() {
7 | if (proto === null) {
8 | assert.strictEqual(parse(what), null)
9 | } else if (port) {
10 | assert.deepEqual(parse(what), {
11 | proto: proto,
12 | host: host,
13 | port: port,
14 | })
15 | } else {
16 | assert.deepEqual(parse(what), {
17 | proto: proto,
18 | path: host,
19 | })
20 | }
21 | })
22 | }
23 |
24 | addTest('4873', 'http', 'localhost', '4873')
25 | addTest(':4873', 'http', 'localhost', '4873')
26 | addTest('blah:4873', 'http', 'blah', '4873')
27 | addTest('http://:4873', 'http', 'localhost', '4873')
28 | addTest('https::4873', 'https', 'localhost', '4873')
29 | addTest('https:blah:4873', 'https', 'blah', '4873')
30 | addTest('https://blah:4873/', 'https', 'blah', '4873')
31 | addTest('[::1]:4873', 'http', '::1', '4873')
32 | addTest('https:[::1]:4873', 'https', '::1', '4873')
33 |
34 | addTest('unix:/tmp/foo.sock', 'http', '/tmp/foo.sock')
35 | addTest('http:unix:foo.sock', 'http', 'foo.sock')
36 | addTest('https://unix:foo.sock', 'https', 'foo.sock')
37 |
38 | addTest('blah', null)
39 | addTest('blah://4873', null)
40 | addTest('https://blah:4873///', null)
41 | addTest('unix:1234', 'http', 'unix', '1234') // not unix socket
42 | })
43 |
--------------------------------------------------------------------------------
/test/unit/mystreams.js:
--------------------------------------------------------------------------------
1 | var ReadTarball = require('../../lib/streams').ReadTarballStream
2 |
3 | describe('mystreams', function() {
4 | it('should delay events', function(cb) {
5 | var test = new ReadTarball()
6 | test.abort()
7 | setTimeout(function() {
8 | test.abort = function() {
9 | cb()
10 | }
11 | test.abort = function() {
12 | throw Error('fail')
13 | }
14 | }, 10)
15 | })
16 | })
17 |
18 |
--------------------------------------------------------------------------------
/test/unit/no_proxy.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var Storage = require('../../lib/up-storage')
3 |
4 | require('../../lib/logger').setup([])
5 |
6 | function setup(host, config, mainconfig) {
7 | config.url = host
8 | return Storage(config, mainconfig)
9 | }
10 |
11 | describe('Use proxy', function() {
12 | it('should work fine without proxy', function() {
13 | var x = setup('http://x/x', {}, {})
14 | assert.equal(x.proxy, null)
15 | })
16 |
17 | it('local config should take priority', function() {
18 | var x = setup('http://x/x', {http_proxy: '123'}, {http_proxy: '456'})
19 | assert.equal(x.proxy, '123')
20 | })
21 |
22 | it('no_proxy is invalid', function() {
23 | var x = setup('http://x/x', {http_proxy: '123', no_proxy: false}, {})
24 | assert.equal(x.proxy, '123')
25 | var x = setup('http://x/x', {http_proxy: '123', no_proxy: null}, {})
26 | assert.equal(x.proxy, '123')
27 | var x = setup('http://x/x', {http_proxy: '123', no_proxy: []}, {})
28 | assert.equal(x.proxy, '123')
29 | var x = setup('http://x/x', {http_proxy: '123', no_proxy: ''}, {})
30 | assert.equal(x.proxy, '123')
31 | })
32 |
33 | it('no_proxy - simple/include', function() {
34 | var x = setup('http://localhost', {http_proxy: '123'}, {no_proxy: 'localhost'})
35 | assert.equal(x.proxy, undefined)
36 | })
37 |
38 | it('no_proxy - simple/not', function() {
39 | var x = setup('http://localhost', {http_proxy: '123'}, {no_proxy: 'blah'})
40 | assert.equal(x.proxy, '123')
41 | })
42 |
43 | it('no_proxy - various, single string', function() {
44 | var x = setup('http://blahblah', {http_proxy: '123'}, {no_proxy: 'blah'})
45 | assert.equal(x.proxy, '123')
46 | var x = setup('http://blah.blah', {}, {http_proxy: '123', no_proxy: 'blah'})
47 | assert.equal(x.proxy, null)
48 | var x = setup('http://blahblah', {}, {http_proxy: '123', no_proxy: '.blah'})
49 | assert.equal(x.proxy, '123')
50 | var x = setup('http://blah.blah', {http_proxy: '123', no_proxy: '.blah'}, {})
51 | assert.equal(x.proxy, null)
52 | var x = setup('http://blah', {http_proxy: '123', no_proxy: '.blah'}, {})
53 | assert.equal(x.proxy, null)
54 | var x = setup('http://blahh', {http_proxy: '123', no_proxy: 'blah'}, {})
55 | assert.equal(x.proxy, '123')
56 | })
57 |
58 | it('no_proxy - various, array', function() {
59 | var x = setup('http://blahblah', {http_proxy: '123'}, {no_proxy: 'foo,bar,blah'})
60 | assert.equal(x.proxy, '123')
61 | var x = setup('http://blah.blah', {http_proxy: '123'}, {no_proxy: 'foo,bar,blah'})
62 | assert.equal(x.proxy, null)
63 | var x = setup('http://blah.foo', {http_proxy: '123'}, {no_proxy: 'foo,bar,blah'})
64 | assert.equal(x.proxy, null)
65 | var x = setup('http://foo.baz', {http_proxy: '123'}, {no_proxy: 'foo,bar,blah'})
66 | assert.equal(x.proxy, '123')
67 | var x = setup('http://blahblah', {http_proxy: '123'}, {no_proxy: ['foo','bar','blah']})
68 | assert.equal(x.proxy, '123')
69 | var x = setup('http://blah.blah', {http_proxy: '123'}, {no_proxy: ['foo','bar','blah']})
70 | assert.equal(x.proxy, null)
71 | })
72 |
73 | it('no_proxy - hostport', function() {
74 | var x = setup('http://localhost:80', {http_proxy: '123'}, {no_proxy: 'localhost'})
75 | assert.equal(x.proxy, null)
76 | var x = setup('http://localhost:8080', {http_proxy: '123'}, {no_proxy: 'localhost'})
77 | assert.equal(x.proxy, null)
78 | })
79 |
80 | it('no_proxy - secure', function() {
81 | var x = setup('https://something', {http_proxy: '123'}, {})
82 | assert.equal(x.proxy, null)
83 | var x = setup('https://something', {https_proxy: '123'}, {})
84 | assert.equal(x.proxy, '123')
85 | var x = setup('https://something', {http_proxy: '456', https_proxy: '123'}, {})
86 | assert.equal(x.proxy, '123')
87 | })
88 | })
89 |
--------------------------------------------------------------------------------
/test/unit/parse_interval.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var parse_interval = require('../../lib/config').parse_interval
3 |
4 | describe('Parse interval', function() {
5 | function add_test(str, res) {
6 | it('parse ' + str, function() {
7 | if (res === null) {
8 | assert.throws(function() {
9 | console.log(parse_interval(str))
10 | })
11 | } else {
12 | assert.strictEqual(parse_interval(str), res)
13 | }
14 | })
15 | }
16 |
17 | add_test(12345, 12345000)
18 | add_test('1000', 1000000)
19 | add_test('1.5s', 1500)
20 | add_test('25ms', 25)
21 | add_test('2m', 2*1000*60)
22 | add_test('3h', 3*1000*60*60)
23 | add_test('0.5d', 0.5*1000*60*60*24)
24 | add_test('0.5w', 0.5*1000*60*60*24*7)
25 | add_test('1M', 1000*60*60*24*30)
26 | add_test('5s 20ms', 5020)
27 | add_test('1y', 1000*60*60*24*365)
28 | add_test('1y 5', null)
29 | add_test('1m 1m', null)
30 | add_test('1m 1y', null)
31 | add_test('1y 1M 1w 1d 1h 1m 1s 1ms', 34822861001)
32 | add_test(' 5s 25ms ', 5025)
33 | })
34 |
35 |
--------------------------------------------------------------------------------
/test/unit/st_merge.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var semver_sort = require('../../lib/utils').semver_sort
3 | var merge = require('../../lib/storage')._merge_versions
4 |
5 | require('../../lib/logger').setup([])
6 |
7 | describe('Merge', function() {
8 | it('simple', function() {
9 | var x = {
10 | versions: {a:1,b:1,c:1},
11 | 'dist-tags': {},
12 | }
13 | merge(x, {versions: {a:2,q:2}})
14 | assert.deepEqual(x, {
15 | versions: {a:1,b:1,c:1,q:2},
16 | 'dist-tags': {},
17 | })
18 | })
19 |
20 | it('dist-tags - compat', function() {
21 | var x = {
22 | versions: {},
23 | 'dist-tags': {q:'1.1.1',w:['2.2.2']},
24 | }
25 | merge(x, {'dist-tags':{q:'2.2.2',w:'3.3.3',t:'4.4.4'}})
26 | assert.deepEqual(x, {
27 | versions: {},
28 | 'dist-tags': {q:['1.1.1','2.2.2'],w:['2.2.2','3.3.3'],t:['4.4.4']},
29 | })
30 | })
31 |
32 | it('dist-tags - sort', function() {
33 | var x = {
34 | versions: {},
35 | 'dist-tags': {w:['2.2.2','1.1.1','12.2.2','2.2.2-rc2']},
36 | }
37 | merge(x, {'dist-tags':{w:'3.3.3'}})
38 | assert.deepEqual(x, {
39 | versions: {},
40 | 'dist-tags': {w:["1.1.1","2.2.2-rc2","2.2.2","3.3.3","12.2.2"]},
41 | })
42 | })
43 |
44 | it('semver_sort', function() {
45 | assert.deepEqual(semver_sort(['1.2.3','1.2','1.2.3a','1.2.3c','1.2.3-b']),
46 | [ '1.2.3a',
47 | '1.2.3-b',
48 | '1.2.3c',
49 | '1.2.3' ]
50 | )
51 | })
52 | })
53 |
54 |
--------------------------------------------------------------------------------
/test/unit/tag_version.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var tag_version = require('../../lib/utils').tag_version
3 |
4 | require('../../lib/logger').setup([])
5 |
6 | describe('tag_version', function() {
7 | it('add new one', function() {
8 | var x = {
9 | versions: {},
10 | 'dist-tags': {},
11 | }
12 | assert(tag_version(x, '1.1.1', 'foo', {}))
13 | assert.deepEqual(x, {
14 | versions: {},
15 | 'dist-tags': {foo: ['1.1.1']},
16 | })
17 | })
18 |
19 | it('add (compat)', function() {
20 | var x = {
21 | versions: {},
22 | 'dist-tags': {foo: '1.1.0'},
23 | }
24 | assert(tag_version(x, '1.1.1', 'foo', {}))
25 | assert.deepEqual(x, {
26 | versions: {},
27 | 'dist-tags': {foo: ['1.1.0', '1.1.1']},
28 | })
29 | })
30 |
31 | it('add fresh tag', function() {
32 | var x = {
33 | versions: {},
34 | 'dist-tags': {foo: ['1.1.0']},
35 | }
36 | assert(tag_version(x, '1.1.1', 'foo', {}))
37 | assert.deepEqual(x, {
38 | versions: {},
39 | 'dist-tags': {foo: ['1.1.0', '1.1.1']},
40 | })
41 | })
42 |
43 | it('add stale tag', function() {
44 | var x = {
45 | versions: {},
46 | 'dist-tags': {foo: ['1.1.2']},
47 | }
48 | assert(!tag_version(x, '1.1.1', 'foo', {}))
49 | assert.deepEqual(x, {
50 | versions: {},
51 | 'dist-tags': {foo: ['1.1.1', '1.1.2']},
52 | })
53 | })
54 | })
55 |
56 |
--------------------------------------------------------------------------------
/test/unit/toplevel.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var express = require('express')
3 | var request = require('request')
4 | var rimraf = require('rimraf')
5 | var sinopia = require('../../')
6 |
7 | var config = {
8 | storage: __dirname + '/test-storage',
9 | packages: {
10 | '*': {
11 | allow_access: '$all',
12 | },
13 | },
14 | logs: [
15 | {type: 'stdout', format: 'pretty', level: 'fatal'}
16 | ],
17 | }
18 |
19 | describe('toplevel', function() {
20 | var port
21 |
22 | before(function(done) {
23 | rimraf(__dirname + '/test-storage', done)
24 | })
25 |
26 | before(function(done) {
27 | var app = express()
28 | app.use(sinopia(config))
29 |
30 | var server = require('http').createServer(app)
31 | server.listen(0, function() {
32 | port = server.address().port
33 | done()
34 | })
35 | })
36 |
37 | it('should respond on /', function(done) {
38 | request({
39 | url: 'http://localhost:' + port + '/',
40 | }, function(err, res, body) {
41 | assert.equal(err, null)
42 | assert(body.match(/Sinopia<\/title>/))
43 | done()
44 | })
45 | })
46 |
47 | it('should respond on /whatever', function(done) {
48 | request({
49 | url: 'http://localhost:' + port + '/whatever',
50 | }, function(err, res, body) {
51 | assert.equal(err, null)
52 | assert(body.match(/no such package available/))
53 | done()
54 | })
55 | })
56 | })
57 |
--------------------------------------------------------------------------------
/test/unit/utils.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var validate = require('../../lib/utils').validate_name
3 |
4 | describe('Validate', function() {
5 | it('good ones', function() {
6 | assert( validate('sinopia') )
7 | assert( validate('some.weird.package-zzz') )
8 | assert( validate('old-package@0.1.2.tgz') )
9 | })
10 |
11 | it('uppercase', function() {
12 | assert( validate('EVE') )
13 | assert( validate('JSONStream') )
14 | })
15 |
16 | it('no package.json', function() {
17 | assert( !validate('package.json') )
18 | })
19 |
20 | it('no path seps', function() {
21 | assert( !validate('some/thing') )
22 | assert( !validate('some\\thing') )
23 | })
24 |
25 | it('no hidden', function() {
26 | assert( !validate('.bin') )
27 | })
28 |
29 | it('no reserved', function() {
30 | assert( !validate('favicon.ico') )
31 | assert( !validate('node_modules') )
32 | assert( !validate('__proto__') )
33 | })
34 |
35 | it('other', function() {
36 | assert( !validate('pk g') )
37 | assert( !validate('pk\tg') )
38 | assert( !validate('pk%20g') )
39 | assert( !validate('pk+g') )
40 | assert( !validate('pk:g') )
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/test/unit/validate_all.js:
--------------------------------------------------------------------------------
1 | // ensure that all arguments are validated
2 |
3 | var assert = require('assert')
4 |
5 | describe('index.js app', test('index.js'))
6 | describe('index-api.js app', test('index-api.js'))
7 | describe('index-web.js app', test('index-web.js'))
8 |
9 | function test(file) {
10 | return function() {
11 | var source = require('fs').readFileSync(__dirname + '/../../lib/' + file, 'utf8')
12 |
13 | var very_scary_regexp = /\n\s*app\.(\w+)\s*\(\s*(("[^"]*")|('[^']*'))\s*,/g
14 | var m
15 | var params = {}
16 |
17 | while ((m = very_scary_regexp.exec(source)) != null) {
18 | if (m[1] === 'set') continue
19 |
20 | var inner = m[2].slice(1, m[2].length-1)
21 | var t
22 |
23 | inner.split('/').forEach(function(x) {
24 | if (m[1] === 'param') {
25 | params[x] = 'ok'
26 | } else if (t = x.match(/^:([^?:]*)\??$/)) {
27 | params[t[1]] = params[t[1]] || m[0].trim()
28 | }
29 | })
30 | }
31 |
32 | Object.keys(params).forEach(function(param) {
33 | it('should validate ":'+param+'"', function() {
34 | assert.equal(params[param], 'ok')
35 | })
36 | })
37 | }
38 | }
39 |
40 |
--------------------------------------------------------------------------------