Refactor api, relocate routes and clean up the code

This commit is contained in:
Juan Picado @jotadeveloper 2017-06-15 15:53:55 +02:00
parent b3a82bc294
commit a6d3745cd4
No known key found for this signature in database
GPG Key ID: 18AC54485952D158
12 changed files with 652 additions and 520 deletions

View File

@ -36,18 +36,14 @@ function validate_name(name) {
name = name.toLowerCase();
// all URL-safe characters and "@" for issue #75
if (!name.match(/^[-a-zA-Z0-9_.!~*'()@]+$/)
return !(!name.match(/^[-a-zA-Z0-9_.!~*'()@]+$/)
|| name.charAt(0) === '.' // ".bin", etc.
|| name.charAt(0) === '-' // "-" is reserved by couchdb
|| name === 'node_modules'
|| name === '__proto__'
|| name === 'package.json'
|| name === 'favicon.ico'
) {
return false;
} else {
return true;
}
);
}
/**

View File

@ -1,24 +1,25 @@
'use strict';
let Cookies = require('cookies');
let express = require('express');
let bodyParser = require('body-parser');
let Error = require('http-errors');
let Path = require('path');
let Middleware = require('../middleware');
let Notify = require('../../notify');
let Utils = require('../../utils');
let expect_json = Middleware.expect_json;
let match = Middleware.match;
let media = Middleware.media;
let validate_name = Middleware.validate_name;
let validate_pkg = Middleware.validate_package;
const express = require('express');
const bodyParser = require('body-parser');
const Middleware = require('../middleware');
const match = Middleware.match;
const validate_name = Middleware.validate_name;
const validate_pkg = Middleware.validate_package;
const encodeScopePackage = Middleware.encodeScopePackage;
const whoami = require('./endpoint/whoami');
const ping = require('./endpoint/ping');
const user = require('./endpoint/user');
const distTags = require('./endpoint/dist-tags');
const publish = require('./endpoint/publish');
const search = require('./endpoint/search');
const pkg = require('./endpoint/package');
module.exports = function(config, auth, storage) {
/* eslint new-cap:off */
const app = express.Router();
const can = Middleware.allow(auth);
const notify = Notify.notify;
/* eslint new-cap:off */
// validate all of these params as a package name
// this might be too harsh, so ask if it causes trouble
@ -40,453 +41,22 @@ module.exports = function(config, auth, storage) {
app.use(Middleware.anti_loop(config));
// encode / in a scoped package name to be matched as a single parameter in routes
app.use(function(req, res, next) {
if (req.url.indexOf('@') != -1) {
// e.g.: /@org/pkg/1.2.3 -> /@org%2Fpkg/1.2.3, /@org%2Fpkg/1.2.3 -> /@org%2Fpkg/1.2.3
req.url = req.url.replace(/^(\/@[^\/%]+)\/(?!$)/, '$1%2F');
}
next();
});
app.use(encodeScopePackage);
// for "npm whoami"
app.get('/whoami', function(req, res, next) {
if (req.headers.referer === 'whoami') {
next({username: req.remote_user.name});
} else {
next('route');
}
});
app.get('/-/whoami', function(req, res, next) {
next({username: req.remote_user.name});
});
whoami(app);
// TODO: anonymous user?
app.get('/:package/:version?', can('access'), function(req, res, next) {
storage.get_package(req.params.package, {req: req}, function(err, info) {
if (err) return next(err);
info = Utils.filter_tarball_urls(info, req, config);
pkg(app, auth, storage, config);
let version = req.params.version;
if (!version) return next(info);
search(app, auth, storage);
let t = Utils.get_version(info, version);
if (t != null) return next(t);
user(app, auth);
if (info['dist-tags'] != null) {
if (info['dist-tags'][version] != null) {
version = info['dist-tags'][version];
t = Utils.get_version(info, version);
if (t != null) return next(t);
}
}
distTags(app, auth, storage);
return next( Error[404]('version not found: ' + req.params.version) );
});
});
publish(app, auth, storage, config);
app.get('/:package/-/:filename', can('access'), function(req, res, next) {
let stream = storage.get_tarball(req.params.package, req.params.filename);
stream.on('content-length', function(v) {
res.header('Content-Length', v);
});
stream.on('error', function(err) {
return res.report_error(err);
});
res.header('Content-Type', 'application/octet-stream');
stream.pipe(res);
});
// searching packages
app.get('/-/all(\/since)?', function(req, res, next) {
let received_end = false;
let response_finished = false;
let processing_pkgs = 0;
let firstPackage = true;
res.status(200);
/*
* Offical NPM registry (registry.npmjs.org) no longer return whole database,
* They only return packages matched with keyword in `referer: search pkg-name`,
* And NPM client will request server in every search.
*
* The magic number 99999 was sent by NPM registry. Modify it may caused strange
* behaviour in the future.
*
* BTW: NPM will not return result if user-agent does not contain string 'npm',
* See: method 'request' in up-storage.js
*
* If there is no cache in local, NPM will request /-/all, then get response with
* _updated: 99999, 'Date' in response header was Mon, 10 Oct 1983 00:12:48 GMT,
* this will make NPM always query from server
*
* Data structure also different, whel request /-/all, response is an object, but
* when request /-/all/since, response is an array
*/
const respShouldBeArray = req.path.endsWith('/since');
res.set('Date', 'Mon, 10 Oct 1983 00:12:48 GMT');
const check_finish = function() {
if (!received_end) {
return;
}
if (processing_pkgs) {
return;
}
if (response_finished) {
return;
}
response_finished = true;
if (respShouldBeArray) {
res.end(']\n');
} else {
res.end('}\n');
}
};
if (respShouldBeArray) {
res.write('[');
} else {
res.write('{"_updated":' + 99999);
}
let stream = storage.search(req.query.startkey || 0, {req: req});
stream.on('data', function each(pkg) {
processing_pkgs++;
auth.allow_access(pkg.name, req.remote_user, function(err, allowed) {
processing_pkgs--;
if (err) {
if (err.status && String(err.status).match(/^4\d\d$/)) {
// auth plugin returns 4xx user error,
// that's equivalent of !allowed basically
allowed = false;
} else {
stream.abort(err);
}
}
if (allowed) {
if (respShouldBeArray) {
res.write(`${firstPackage ? '' : ','}${JSON.stringify(pkg)}\n`);
if (firstPackage) {
firstPackage = false;
}
} else {
res.write(',\n' + JSON.stringify(pkg.name) + ':' + JSON.stringify(pkg));
}
}
check_finish();
});
});
stream.on('error', function(_err) {
res.socket.destroy();
});
stream.on('end', function() {
received_end = true;
check_finish();
});
});
// placeholder 'cause npm require to be authenticated to publish
// we do not do any real authentication yet
app.post('/_session', Cookies.express(), function(req, res, next) {
res.cookies.set('AuthSession', String(Math.random()), {
// npmjs.org sets 10h expire
expires: new Date(Date.now() + 10*60*60*1000),
});
next({ok: true, name: 'somebody', roles: []});
});
app.get('/-/user/:org_couchdb_user', function(req, res, next) {
res.status(200);
next({
ok: 'you are authenticated as "' + req.remote_user.name + '"',
});
});
app.put('/-/user/:org_couchdb_user/:_rev?/:revision?', function(req, res, next) {
let token = (req.body.name && req.body.password)
? auth.aes_encrypt(req.body.name + ':' + req.body.password).toString('base64')
: undefined;
if (req.remote_user.name != null) {
res.status(201);
return next({
ok: 'you are authenticated as \'' + req.remote_user.name + '\'',
// token: auth.issue_token(req.remote_user),
token: token,
});
} else {
auth.add_user(req.body.name, req.body.password, function(err, user) {
if (err) {
if (err.status >= 400 && err.status < 500) {
// With npm registering is the same as logging in,
// and npm accepts only an 409 error.
// So, changing status code here.
return next( Error[409](err.message) );
}
return next(err);
}
req.remote_user = user;
res.status(201);
return next({
ok: 'user \'' + req.body.name + '\' created',
// token: auth.issue_token(req.remote_user),
token: token,
});
});
}
});
app.delete('/-/user/token/*', function(req, res, next) {
res.status(200);
next({
ok: 'Logged out',
});
});
const tag_package_version = function(req, res, next) {
if (typeof(req.body) !== 'string') {
return next('route');
}
let tags = {};
tags[req.params.tag] = req.body;
storage.merge_tags(req.params.package, tags, function(err) {
if (err) return next(err);
res.status(201);
return next({ok: 'package tagged'});
});
};
// tagging a package
app.put('/:package/:tag',
can('publish'), media('application/json'), tag_package_version);
app.post('/-/package/:package/dist-tags/:tag',
can('publish'), media('application/json'), tag_package_version);
app.put('/-/package/:package/dist-tags/:tag',
can('publish'), media('application/json'), tag_package_version);
app.delete('/-/package/:package/dist-tags/:tag', can('publish'), function(req, res, next) {
let tags = {};
tags[req.params.tag] = null;
storage.merge_tags(req.params.package, tags, function(err) {
if (err) return next(err);
res.status(201);
return next({ok: 'tag removed'});
});
});
app.get('/-/package/:package/dist-tags', can('access'), function(req, res, next) {
storage.get_package(req.params.package, {req: req}, function(err, info) {
if (err) return next(err);
next(info['dist-tags']);
});
});
app.post('/-/package/:package/dist-tags', can('publish'), media('application/json'), expect_json,
function(req, res, next) {
storage.merge_tags(req.params.package, req.body, function(err) {
if (err) return next(err);
res.status(201);
return next({ok: 'tags updated'});
});
});
app.put('/-/package/:package/dist-tags', can('publish'), media('application/json'), expect_json,
function(req, res, next) {
storage.replace_tags(req.params.package, req.body, function(err) {
if (err) return next(err);
res.status(201);
return next({ok: 'tags updated'});
});
});
app.delete('/-/package/:package/dist-tags', can('publish'), media('application/json'),
function(req, res, next) {
storage.replace_tags(req.params.package, {}, function(err) {
if (err) return next(err);
res.status(201);
return next({ok: 'tags removed'});
});
});
// publishing a package
app.put('/:package/:_rev?/:revision?', can('publish'), media('application/json'), expect_json, function(req, res, next) {
let name = req.params.package;
let metadata;
const create_tarball = function(filename, data, cb) {
let stream = storage.add_tarball(name, filename);
stream.on('error', function(err) {
cb(err);
});
stream.on('success', function() {
cb();
});
// this is dumb and memory-consuming, but what choices do we have?
stream.end(new Buffer(data.data, 'base64'));
stream.done();
};
const create_version = function(version, data, cb) {
storage.add_version(name, version, data, null, cb);
};
const add_tags = function(tags, cb) {
storage.merge_tags(name, tags, cb);
};
const after_change = function(err, ok_message) {
// old npm behaviour
if (metadata._attachments == null) {
if (err) return next(err);
res.status(201);
return next({ok: ok_message, success: true});
}
// npm-registry-client 0.3+ embeds tarball into the json upload
// https://github.com/isaacs/npm-registry-client/commit/e9fbeb8b67f249394f735c74ef11fe4720d46ca0
// issue https://github.com/rlidwka/sinopia/issues/31, dealing with it here:
if (typeof(metadata._attachments) !== 'object'
|| Object.keys(metadata._attachments).length !== 1
|| typeof(metadata.versions) !== 'object'
|| Object.keys(metadata.versions).length !== 1) {
// npm is doing something strange again
// if this happens in normal circumstances, report it as a bug
return next( Error[400]('unsupported registry call') );
}
if (err && err.status != 409) {
return next(err);
}
// at this point document is either created or existed before
const t1 = Object.keys(metadata._attachments)[0];
create_tarball(Path.basename(t1), metadata._attachments[t1], function(err) {
if (err) {
return next(err);
}
const t2 = Object.keys(metadata.versions)[0];
metadata.versions[t2].readme = metadata.readme != null ? String(metadata.readme) : '';
create_version(t2, metadata.versions[t2], function(err) {
if (err) {
return next(err);
}
add_tags(metadata['dist-tags'], function(err) {
if (err) {
return next(err);
}
notify(metadata, config);
res.status(201);
return next({ok: ok_message, success: true});
});
});
});
};
if (Object.keys(req.body).length == 1 && Utils.is_object(req.body.users)) {
// 501 status is more meaningful, but npm doesn't show error message for 5xx
return next( Error[404]('npm star|unstar calls are not implemented') );
}
try {
metadata = Utils.validate_metadata(req.body, name);
} catch(err) {
return next( Error[422]('bad incoming package data') );
}
if (req.params._rev) {
storage.change_package(name, metadata, req.params.revision, function(err) {
after_change(err, 'package changed');
});
} else {
storage.addPackage(name, metadata, function(err) {
after_change(err, 'created new package');
});
}
});
// unpublishing an entire package
app.delete('/:package/-rev/*', can('publish'), function(req, res, next) {
storage.remove_package(req.params.package, function(err) {
if (err) {
return next(err);
}
res.status(201);
return next({ok: 'package removed'});
});
});
// removing a tarball
app.delete('/:package/-/:filename/-rev/:revision', can('publish'), function(req, res, next) {
storage.remove_tarball(req.params.package, req.params.filename, req.params.revision, function(err) {
if (err) {
return next(err);
}
res.status(201);
return next({ok: 'tarball removed'});
});
});
// uploading package tarball
app.put('/:package/-/:filename/*', can('publish'), media('application/octet-stream'), function(req, res, next) {
const name = req.params.package;
const stream = storage.add_tarball(name, req.params.filename);
req.pipe(stream);
// checking if end event came before closing
let complete = false;
req.on('end', function() {
complete = true;
stream.done();
});
req.on('close', function() {
if (!complete) {
stream.abort();
}
});
stream.on('error', function(err) {
return res.report_error(err);
});
stream.on('success', function() {
res.status(201);
return next({
ok: 'tarball uploaded successfully',
});
});
});
// adding a version
app.put('/:package/:version/-tag/:tag', can('publish'), media('application/json'), expect_json, function(req, res, next) {
let name = req.params.package;
let version = req.params.version;
let tag = req.params.tag;
storage.add_version(name, version, req.body, tag, function(err) {
if (err) return next(err);
res.status(201);
return next({ok: 'package published'});
});
});
// npm ping
app.get('/-/ping', function(req, res, next) {
next({});
});
ping(app);
return app;
};

View File

@ -0,0 +1,83 @@
'use strict';
const Middleware = require('../../middleware');
const constant = require('../../utils/const');
const media = Middleware.media;
const expect_json = Middleware.expect_json;
module.exports = function(route, auth, storage) {
const can = Middleware.allow(auth);
const tag_package_version = function(req, res, next) {
if (typeof(req.body) !== 'string') {
return next('route');
}
let tags = {};
tags[req.params.tag] = req.body;
storage.merge_tags(req.params.package, tags, function(err) {
if (err) return next(err);
res.status(201);
return next({ok: 'package tagged'});
});
};
// tagging a package
route.put('/:package/:tag',
can('publish'), media(constant.CONTENT_JSON), tag_package_version);
route.post('/-/package/:package/dist-tags/:tag',
can('publish'), media(constant.CONTENT_JSON), tag_package_version);
route.put('/-/package/:package/dist-tags/:tag',
can('publish'), media(constant.CONTENT_JSON), tag_package_version);
route.delete('/-/package/:package/dist-tags/:tag', can('publish'), function(req, res, next) {
let tags = {};
tags[req.params.tag] = null;
storage.merge_tags(req.params.package, tags, function(err) {
if (err) {
return next(err);
}
res.status(201);
return next({
ok: 'tag removed',
});
});
});
route.get('/-/package/:package/dist-tags', can('access'), function(req, res, next) {
storage.get_package(req.params.package, {req: req}, function(err, info) {
if (err) return next(err);
next(info['dist-tags']);
});
});
route.post('/-/package/:package/dist-tags', can('publish'), media(constant.CONTENT_JSON), expect_json,
function(req, res, next) {
storage.merge_tags(req.params.package, req.body, function(err) {
if (err) return next(err);
res.status(201);
return next({ok: 'tags updated'});
});
});
route.put('/-/package/:package/dist-tags', can('publish'), media(constant.CONTENT_JSON), expect_json,
function(req, res, next) {
storage.replace_tags(req.params.package, req.body, function(err) {
if (err) return next(err);
res.status(201);
return next({ok: 'tags updated'});
});
});
route.delete('/-/package/:package/dist-tags', can('publish'), media(constant.CONTENT_JSON),
function(req, res, next) {
storage.replace_tags(req.params.package, {}, function(err) {
if (err) return next(err);
res.status(201);
return next({ok: 'tags removed'});
});
});
};

View File

@ -0,0 +1,54 @@
'use strict';
const _ = require('lodash');
const createError = require('http-errors');
const Middleware = require('../../middleware');
const Utils = require('../../../utils');
module.exports = function(route, auth, storage, config) {
const can = Middleware.allow(auth);
// TODO: anonymous user?
route.get('/:package/:version?', can('access'), function(req, res, next) {
storage.get_package(req.params.package, {req: req}, function(err, info) {
if (err) {
return next(err);
}
info = Utils.filter_tarball_urls(info, req, config);
let version = req.params.version;
if (_.isNil(version)) {
return next(info);
}
let t = Utils.get_version(info, version);
if (_.isNil(t) === false) {
return next(t);
}
if (_.isNil(info['dist-tags']) === false) {
if (_.isNil(info['dist-tags'][version]) === false) {
version = info['dist-tags'][version];
t = Utils.get_version(info, version);
if (_.isNil(t)) {
return next(t);
}
}
}
return next( createError[404]('version not found: ' + req.params.version) );
});
});
route.get('/:package/-/:filename', can('access'), function(req, res) {
const stream = storage.get_tarball(req.params.package, req.params.filename);
stream.on('content-length', function(v) {
res.header('Content-Length', v);
});
stream.on('error', function(err) {
return res.report_error(err);
});
res.header('Content-Type', 'application/octet-stream');
stream.pipe(res);
});
};

View File

@ -0,0 +1,7 @@
'use strict';
module.exports = function(route) {
route.get('/-/ping', function(req, res, next) {
next({});
});
};

View File

@ -0,0 +1,188 @@
'use strict';
const _ = require('lodash');
const Path = require('path');
const createError = require('http-errors');
const Middleware = require('../../middleware');
const Notify = require('../../../notify');
const Utils = require('../../../utils');
const constant = require('../../utils/const');
const media = Middleware.media;
const expect_json = Middleware.expect_json;
const notify = Notify.notify;
module.exports = function(router, auth, storage, config) {
const can = Middleware.allow(auth);
// publishing a package
router.put('/:package/:_rev?/:revision?', can('publish'), media(constant.CONTENT_JSON), expect_json, function(req, res, next) {
const name = req.params.package;
let metadata;
const create_tarball = function(filename, data, cb) {
let stream = storage.add_tarball(name, filename);
stream.on('error', function(err) {
cb(err);
});
stream.on('success', function() {
cb();
});
// this is dumb and memory-consuming, but what choices do we have?
stream.end(new Buffer(data.data, 'base64'));
stream.done();
};
const create_version = function(version, data, cb) {
storage.add_version(name, version, data, null, cb);
};
const add_tags = function(tags, cb) {
storage.merge_tags(name, tags, cb);
};
const after_change = function(err, ok_message) {
// old npm behaviour
if (_.isNil(metadata._attachments)) {
if (err) return next(err);
res.status(201);
return next({
ok: ok_message,
success: true,
});
}
// npm-registry-client 0.3+ embeds tarball into the json upload
// https://github.com/isaacs/npm-registry-client/commit/e9fbeb8b67f249394f735c74ef11fe4720d46ca0
// issue https://github.com/rlidwka/sinopia/issues/31, dealing with it here:
if (typeof(metadata._attachments) !== 'object'
|| Object.keys(metadata._attachments).length !== 1
|| typeof(metadata.versions) !== 'object'
|| Object.keys(metadata.versions).length !== 1) {
// npm is doing something strange again
// if this happens in normal circumstances, report it as a bug
return next( createError[400]('unsupported registry call') );
}
if (err && err.status != 409) {
return next(err);
}
// at this point document is either created or existed before
const t1 = Object.keys(metadata._attachments)[0];
create_tarball(Path.basename(t1), metadata._attachments[t1], function(err) {
if (err) {
return next(err);
}
const t2 = Object.keys(metadata.versions)[0];
metadata.versions[t2].readme = _.isNil(metadata.readme) === false ? String(metadata.readme) : '';
create_version(t2, metadata.versions[t2], function(err) {
if (err) {
return next(err);
}
add_tags(metadata['dist-tags'], function(err) {
if (err) {
return next(err);
}
notify(metadata, config);
res.status(201);
return next({ok: ok_message, success: true});
});
});
});
};
if (Object.keys(req.body).length === 1 && Utils.is_object(req.body.users)) {
// 501 status is more meaningful, but npm doesn't show error message for 5xx
return next( createError[404]('npm star|unstar calls are not implemented') );
}
try {
metadata = Utils.validate_metadata(req.body, name);
} catch(err) {
return next( createError[422]('bad incoming package data') );
}
if (req.params._rev) {
storage.change_package(name, metadata, req.params.revision, function(err) {
after_change(err, 'package changed');
});
} else {
storage.addPackage(name, metadata, function(err) {
after_change(err, 'created new package');
});
}
});
// unpublishing an entire package
router.delete('/:package/-rev/*', can('publish'), function(req, res, next) {
storage.remove_package(req.params.package, function(err) {
if (err) {
return next(err);
}
res.status(201);
return next({ok: 'package removed'});
});
});
// removing a tarball
router.delete('/:package/-/:filename/-rev/:revision', can('publish'), function(req, res, next) {
storage.remove_tarball(req.params.package, req.params.filename, req.params.revision, function(err) {
if (err) {
return next(err);
}
res.status(201);
return next({ok: 'tarball removed'});
});
});
// uploading package tarball
router.put('/:package/-/:filename/*', can('publish'), media('application/octet-stream'), function(req, res, next) {
const name = req.params.package;
const stream = storage.add_tarball(name, req.params.filename);
req.pipe(stream);
// checking if end event came before closing
let complete = false;
req.on('end', function() {
complete = true;
stream.done();
});
req.on('close', function() {
if (!complete) {
stream.abort();
}
});
stream.on('error', function(err) {
return res.report_error(err);
});
stream.on('success', function() {
res.status(201);
return next({
ok: 'tarball uploaded successfully',
});
});
});
// adding a version
router.put('/:package/:version/-tag/:tag', can('publish'), media(constant.CONTENT_JSON), expect_json, function(req, res, next) {
let name = req.params.package;
let version = req.params.version;
let tag = req.params.tag;
storage.add_version(name, version, req.body, tag, function(err) {
if (err) {
return next(err);
}
res.status(201);
return next({
ok: 'package published',
});
});
});
};

View File

@ -0,0 +1,99 @@
'use strict';
module.exports = function(route, auth, storage) {
// searching packages
route.get('/-/all(\/since)?', function(req, res) {
let received_end = false;
let response_finished = false;
let processing_pkgs = 0;
let firstPackage = true;
res.status(200);
/*
* Offical NPM registry (registry.npmjs.org) no longer return whole database,
* They only return packages matched with keyword in `referer: search pkg-name`,
* And NPM client will request server in every search.
*
* The magic number 99999 was sent by NPM registry. Modify it may caused strange
* behaviour in the future.
*
* BTW: NPM will not return result if user-agent does not contain string 'npm',
* See: method 'request' in up-storage.js
*
* If there is no cache in local, NPM will request /-/all, then get response with
* _updated: 99999, 'Date' in response header was Mon, 10 Oct 1983 00:12:48 GMT,
* this will make NPM always query from server
*
* Data structure also different, whel request /-/all, response is an object, but
* when request /-/all/since, response is an array
*/
const respShouldBeArray = req.path.endsWith('/since');
res.set('Date', 'Mon, 10 Oct 1983 00:12:48 GMT');
const check_finish = function() {
if (!received_end) {
return;
}
if (processing_pkgs) {
return;
}
if (response_finished) {
return;
}
response_finished = true;
if (respShouldBeArray) {
res.end(']\n');
} else {
res.end('}\n');
}
};
if (respShouldBeArray) {
res.write('[');
} else {
res.write('{"_updated":' + 99999);
}
let stream = storage.search(req.query.startkey || 0, {req: req});
stream.on('data', function each(pkg) {
processing_pkgs++;
auth.allow_access(pkg.name, req.remote_user, function(err, allowed) {
processing_pkgs--;
if (err) {
if (err.status && String(err.status).match(/^4\d\d$/)) {
// auth plugin returns 4xx user error,
// that's equivalent of !allowed basically
allowed = false;
} else {
stream.abort(err);
}
}
if (allowed) {
if (respShouldBeArray) {
res.write(`${firstPackage ? '' : ','}${JSON.stringify(pkg)}\n`);
if (firstPackage) {
firstPackage = false;
}
} else {
res.write(',\n' + JSON.stringify(pkg.name) + ':' + JSON.stringify(pkg));
}
}
check_finish();
});
});
stream.on('error', function(_err) {
res.socket.destroy();
});
stream.on('end', function() {
received_end = true;
check_finish();
});
});
};

View File

@ -0,0 +1,70 @@
'use strict';
const _ = require('lodash');
const Cookies = require('cookies');
const createError = require('http-errors');
module.exports = function(route, auth) {
route.get('/-/user/:org_couchdb_user', function(req, res, next) {
res.status(200);
next({
ok: 'you are authenticated as "' + req.remote_user.name + '"',
});
});
route.put('/-/user/:org_couchdb_user/:_rev?/:revision?', function(req, res, next) {
let token = (req.body.name && req.body.password)
? auth.aes_encrypt(req.body.name + ':' + req.body.password).toString('base64')
: undefined;
if (_.isNil(req.remote_user.name) === false) {
res.status(201);
return next({
ok: 'you are authenticated as \'' + req.remote_user.name + '\'',
// token: auth.issue_token(req.remote_user),
token: token,
});
} else {
auth.add_user(req.body.name, req.body.password, function(err, user) {
if (err) {
if (err.status >= 400 && err.status < 500) {
// With npm registering is the same as logging in,
// and npm accepts only an 409 error.
// So, changing status code here.
return next( createError[409](err.message) );
}
return next(err);
}
req.remote_user = user;
res.status(201);
return next({
ok: 'user \'' + req.body.name + '\' created',
// token: auth.issue_token(req.remote_user),
token: token,
});
});
}
});
route.delete('/-/user/token/*', function(req, res, next) {
res.status(200);
next({
ok: 'Logged out',
});
});
// placeholder 'cause npm require to be authenticated to publish
// we do not do any real authentication yet
route.post('/_session', Cookies.express(), function(req, res, next) {
res.cookies.set('AuthSession', String(Math.random()), {
// npmjs.org sets 10h expire
expires: new Date(Date.now() + 10 * 60 * 60 * 1000),
});
next({
ok: true,
name: 'somebody',
roles: [],
});
});
};

View File

@ -0,0 +1,15 @@
'use strict';
module.exports = function(route) {
route.get('/whoami', function(req, res, next) {
if (req.headers.referer === 'whoami') {
next({username: req.remote_user.name});
} else {
next('route');
}
});
route.get('/-/whoami', function(req, res, next) {
next({username: req.remote_user.name});
});
};

View File

@ -1,55 +1,57 @@
'use strict';
let async = require('async');
let bodyParser = require('body-parser');
let Cookies = require('cookies');
let express = require('express');
let fs = require('fs');
let Handlebars = require('handlebars');
let renderReadme = require('render-readme');
let Search = require('../search');
let Middleware = require('./middleware');
let Utils = require('../utils');
let match = Middleware.match;
let validate_name = Middleware.validate_name;
let validate_pkg = Middleware.validate_package;
const async = require('async');
const bodyParser = require('body-parser');
const Cookies = require('cookies');
const escape = require('js-string-escape');
const express = require('express');
const fs = require('fs');
const Handlebars = require('handlebars');
const marked = require('marked');
const Search = require('../search');
const Middleware = require('./middleware');
const Utils = require('../utils');
const match = Middleware.match;
const validateName = Middleware.validate_name;
const validatePkg = Middleware.validate_package;
const securityIframe = Middleware.securityIframe;
module.exports = function(config, auth, storage) {
Search.configureStorage(storage);
/* eslint new-cap:off */
let app = express.Router();
let can = Middleware.allow(auth);
const app = express.Router();
/* eslint new-cap:off */
const can = Middleware.allow(auth);
let template;
// validate all of these params as a package name
// this might be too harsh, so ask if it causes trouble
app.param('package', validate_pkg);
app.param('filename', validate_name);
app.param('version', validate_name);
app.param('package', validatePkg);
app.param('filename', validateName);
app.param('version', validateName);
app.param('anything', match(/.*/));
app.use(Cookies.express());
app.use(bodyParser.urlencoded({extended: false}));
app.use(auth.cookie_middleware());
app.use(function(req, res, next) {
// disable loading in frames (clickjacking, etc.)
res.header('X-Frame-Options', 'deny');
next();
});
Search.configureStorage(storage);
app.use(securityIframe);
Handlebars.registerPartial('entry', fs.readFileSync(require.resolve('./ui/entry.hbs'), 'utf8'));
let template;
if (config.web && config.web.template) {
template = Handlebars.compile(fs.readFileSync(config.web.template, 'utf8'));
} else {
template = Handlebars.compile(fs.readFileSync(require.resolve('./ui/index.hbs'), 'utf8'));
}
app.get('/', function(req, res, next) {
let base = Utils.combineBaseUrl(Utils.getWebProtocol(req), req.get('host'), config.url_prefix);
res.setHeader('Content-Type', 'text/html');
storage.get_local(function(err, packages) {
if (err) throw err; // that function shouldn't produce any
if (err) {
throw err;
} // that function shouldn't produce any
async.filterSeries(packages, function(pkg, cb) {
auth.allow_access(pkg.name, req.remote_user, function(err, allowed) {
setImmediate(function() {
@ -69,13 +71,15 @@ module.exports = function(config, auth, storage) {
return 1;
}
});
next(template({
name: config.web && config.web.title ? config.web.title : 'Verdaccio',
tagline: config.web && config.web.tagline ? config.web.tagline : '',
let json = {
packages: packages,
tagline: config.web && config.web.tagline ? config.web.tagline : '',
baseUrl: base,
username: req.remote_user.name,
};
next(template({
name: config.web && config.web.title ? config.web.title : 'Verdaccio',
data: escape(JSON.stringify(json)),
}));
});
});
@ -85,7 +89,9 @@ module.exports = function(config, auth, storage) {
app.get('/-/static/:filename', function(req, res, next) {
let file = __dirname + '/static/' + req.params.filename;
res.sendFile(file, function(err) {
if (!err) return;
if (!err) {
return;
}
if (err.status === 404) {
next();
} else {
@ -101,10 +107,9 @@ module.exports = function(config, auth, storage) {
});
app.post('/-/login', function(req, res, next) {
auth.authenticate(req.body.user, req.body.pass, function(err, user) {
auth.authenticate(req.body.user, req.body.pass, (err, user) => {
if (!err) {
req.remote_user = user;
// res.cookies.set('token', auth.issue_token(req.remote_user))
let str = req.body.user + ':' + req.body.pass;
res.cookies.set('token', auth.aes_encrypt(str).toString('base64'));
@ -127,10 +132,12 @@ module.exports = function(config, auth, storage) {
const packages = [];
const getData = function(i) {
storage.get_package(results[i].ref, function(err, entry) {
storage.get_package(results[i].ref, (err, entry) => {
if (!err && entry) {
auth.allow_access(entry.name, req.remote_user, function(err, allowed) { // TODO: This may cause performance issue?
if (err || !allowed) return;
if (err || !allowed) {
return;
}
packages.push(entry.versions[entry['dist-tags'].latest]);
});
@ -153,10 +160,15 @@ module.exports = function(config, auth, storage) {
app.get('/-/readme(/@:scope?)?/:package/:version?', can('access'), function(req, res, next) {
let packageName = req.params.package;
if (req.params.scope) packageName = '@'+ req.params.scope + '/' + packageName;
if (req.params.scope) {
packageName = `@${req.params.scope}/${packageName}`;
}
storage.get_package(packageName, {req: req}, function(err, info) {
if (err) return next(err);
next( renderReadme(info.readme || 'ERROR: No README data found!') );
if (err) {
return next(err);
}
res.set('Content-Type', 'text/plain');
next( marked(info.readme || 'ERROR: No README data found!') );
});
});
return app;

View File

@ -3,12 +3,14 @@
'use strict';
const crypto = require('crypto');
const Error = require('http-errors');
const _ = require('lodash');
const createError = require('http-errors');
const utils = require('../utils');
const Logger = require('../logger');
module.exports.match = function match(regexp) {
return function(req, res, next, value, name) {
return function(req, res, next, value) {
if (regexp.exec(value)) {
next();
} else {
@ -17,6 +19,12 @@ module.exports.match = function match(regexp) {
};
};
module.exports.securityIframe = function securityIframe(req, res, next) {
// disable loading in frames (clickjacking, etc.)
res.header('X-Frame-Options', 'deny');
next();
};
module.exports.validate_name = function validate_name(req, res, next, value, name) {
if (value.charAt(0) === '-') {
// special case in couchdb usually
@ -24,7 +32,7 @@ module.exports.validate_name = function validate_name(req, res, next, value, nam
} else if (utils.validate_name(value)) {
next();
} else {
next( Error[403]('invalid ' + name) );
next( createError[403]('invalid ' + name) );
}
};
@ -35,24 +43,32 @@ module.exports.validate_package = function validate_package(req, res, next, valu
} else if (utils.validate_package(value)) {
next();
} else {
next( Error[403]('invalid ' + name) );
next( createError[403]('invalid ' + name) );
}
};
module.exports.media = function media(expect) {
return function(req, res, next) {
if (req.headers['content-type'] !== expect) {
next( Error[415]('wrong content-type, expect: ' + expect
+ ', got: '+req.headers['content-type']) );
next( createError[415]('wrong content-type, expect: ' + expect
+ ', got: '+req.headers['content-type']) );
} else {
next();
}
};
};
module.exports.encodeScopePackage = function(req, res, next) {
if (req.url.indexOf('@') !== -1) {
// e.g.: /@org/pkg/1.2.3 -> /@org%2Fpkg/1.2.3, /@org%2Fpkg/1.2.3 -> /@org%2Fpkg/1.2.3
req.url = req.url.replace(/^(\/@[^\/%]+)\/(?!$)/, '$1%2F');
}
next();
};
module.exports.expect_json = function expect_json(req, res, next) {
if (!utils.is_object(req.body)) {
return next( Error[400]('can\'t parse incoming json') );
return next( createError[400]('can\'t parse incoming json') );
}
next();
};
@ -65,7 +81,7 @@ module.exports.anti_loop = function(config) {
for (let i=0; i<arr.length; i++) {
let m = arr[i].match(/\s*(\S+)\s+(\S+)/);
if (m && m[2] === config.server_id) {
return next( Error[508]('loop detected') );
return next( createError[508]('loop detected') );
}
}
}
@ -84,11 +100,12 @@ function md5sum(data) {
return crypto.createHash('md5').update(data).digest('hex');
}
module.exports.allow = function(auth) {
return function(action) {
return function(req, res, next) {
req.pause();
auth['allow_'+action](req.params.package, req.remote_user, function(error, allowed) {
auth['allow_' + action](req.params.package, req.remote_user, function(error, allowed) {
req.resume();
if (error) {
next(error);
@ -97,7 +114,7 @@ module.exports.allow = function(auth) {
} else {
// last plugin (that's our built-in one) returns either
// cb(err) or cb(null, true), so this should never happen
throw Error('bug in the auth plugin system');
throw createError('bug in the auth plugin system');
}
});
};
@ -111,12 +128,12 @@ module.exports.final = function(body, req, res, next) {
}
try {
if (typeof(body) === 'string' || typeof(body) === 'object') {
if (_.isString(body) || _.isObject(body)) {
if (!res.getHeader('Content-type')) {
res.header('Content-type', 'application/json');
}
if (typeof(body) === 'object' && body != null) {
if (typeof(body) === 'object' && _.isNil(body) === false) {
if (typeof(body.error) === 'string') {
res._verdaccio_error = body.error;
}
@ -135,7 +152,9 @@ module.exports.final = function(body, req, res, next) {
// as an error handler, we can't report error properly,
// and should just close socket
if (err.message.match(/set headers after they are sent/)) {
if (res.socket != null) res.socket.destroy();
if (_.isNil(res.socket) === false) {
res.socket.destroy();
}
return;
} else {
throw err;
@ -150,17 +169,26 @@ module.exports.log = function(req, res, next) {
req.log = Logger.logger.child({sub: 'in'});
let _auth = req.headers.authorization;
if (_auth != null) req.headers.authorization = '<Classified>';
if (_.isNil(_auth) === false) {
req.headers.authorization = '<Classified>';
}
let _cookie = req.headers.cookie;
if (_cookie != null) req.headers.cookie = '<Classified>';
if (_.isNil(_cookie) === false) {
req.headers.cookie = '<Classified>';
}
req.url = req.originalUrl;
req.log.info( {req: req, ip: req.ip}
, '@{ip} requested \'@{req.method} @{req.url}\'' );
, '@{ip} requested \'@{req.method} @{req.url}\'' );
req.originalUrl = req.url;
if (_auth != null) req.headers.authorization = _auth;
if (_cookie != null) req.headers.cookie = _cookie;
if (_.isNil(_auth) === false) {
req.headers.authorization = _auth;
}
if (_.isNil(_cookie) === false) {
req.headers.cookie = _cookie;
}
let bytesin = 0;
req.on('data', function(chunk) {
@ -187,7 +215,10 @@ module.exports.log = function(req, res, next) {
req.url = req.originalUrl;
req.log.warn({
request: {method: req.method, url: req.url},
request: {
method: req.method,
url: req.url,
},
level: 35, // http
user: req.remote_user && req.remote_user.name,
remoteIP,
@ -205,9 +236,11 @@ module.exports.log = function(req, res, next) {
log(true);
});
let _end = res.end;
const _end = res.end;
res.end = function(buf) {
if (buf) bytesout += buf.length;
if (buf) {
bytesout += buf.length;
}
_end.apply(res, arguments);
log();
};

5
lib/web/utils/const.js Normal file
View File

@ -0,0 +1,5 @@
'use strict';
const CONTENT_JSON = 'application/json';
module.exports.CONTENT_JSON = CONTENT_JSON;