1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-11-13 03:35:52 +01:00
verdaccio/lib/index-api.js

480 lines
15 KiB
JavaScript
Raw Normal View History

'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;
2015-04-08 22:54:59 +02:00
module.exports = function(config, auth, storage) {
let app = express.Router();
let can = Middleware.allow(auth);
let notify = Notify.notify;
// 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('tag', validate_name);
app.param('version', validate_name);
app.param('revision', validate_name);
app.param('token', validate_name);
// these can't be safely put into express url for some reason
app.param('_rev', match(/^-rev$/));
app.param('org_couchdb_user', match(/^org\.couchdb\.user:/));
app.param('anything', match(/.*/));
app.use(auth.basic_middleware());
// app.use(auth.bearer_middleware())
app.use(bodyParser.json({strict: false, limit: config.max_body_size || '10mb'}));
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();
});
2014-11-16 13:37:50 +01:00
// for "npm whoami"
app.get('/whoami', function(req, res, next) {
if (req.headers.referer === 'whoami') {
next({username: req.remote_user.name});
2014-11-16 13:37:50 +01:00
} else {
next('route');
2014-11-16 13:37:50 +01:00
}
});
app.get('/-/whoami', function(req, res, next) {
next({username: req.remote_user.name});
});
2014-11-16 13:37:50 +01:00
// TODO: anonymous user?
app.get('/:package/:version?', can('access'), function(req, res, next) {
storage.get_package(req.params.package, {req: req}, function(err, info) {
if (err) return next(err);
info = Utils.filter_tarball_urls(info, req, config);
let version = req.params.version;
if (!version) return next(info);
let t = Utils.get_version(info, version);
if (t != null) return next(t);
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);
}
}
return next( Error[404]('version not found: ' + req.params.version) );
});
});
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
2017-04-21 20:33:55 +02:00
app.get('/-/all(\/since)?', function(req, res, next) {
2017-04-23 22:28:35 +02:00
let received_end = false;
let response_finished = false;
let processing_pkgs = 0;
let firstPackage = true;
2017-04-23 22:28:35 +02:00
res.status(200);
2017-04-21 20:33:55 +02:00
/*
* 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
2017-04-21 20:33:55 +02:00
*
* Data structure also different, whel request /-/all, response is an object, but
* when request /-/all/since, response is an array
*/
2017-04-23 22:28:35 +02:00
const respShouldBeArray = req.path.endsWith('/since');
res.set('Date', 'Mon, 10 Oct 1983 00:12:48 GMT');
2017-04-21 20:33:55 +02:00
if (respShouldBeArray) {
2017-04-23 22:28:35 +02:00
res.write('[');
2017-04-21 20:33:55 +02:00
} 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);
2015-04-08 22:54:59 +02:00
}
}
if (allowed) {
2017-04-21 20:33:55 +02:00
if (respShouldBeArray) {
2017-04-23 22:28:35 +02:00
res.write(`${firstPackage ? '' : ','}${JSON.stringify(pkg)}\n`);
2017-04-21 20:33:55 +02:00
if (firstPackage) {
2017-04-23 22:28:35 +02:00
firstPackage = false;
2017-04-21 20:33:55 +02:00
}
} else {
2017-04-23 22:28:35 +02:00
res.write(',\n' + JSON.stringify(pkg.name) + ':' + JSON.stringify(pkg));
2017-04-21 20:33:55 +02:00
}
}
2015-04-08 22:54:59 +02:00
check_finish();
});
});
stream.on('error', function(_err) {
res.socket.destroy();
});
stream.on('end', function() {
received_end = true;
check_finish();
});
function check_finish() {
if (!received_end) return;
if (processing_pkgs) return;
if (response_finished) return;
2017-04-23 22:28:35 +02:00
response_finished = true;
2017-04-21 20:33:55 +02:00
if (respShouldBeArray) {
2017-04-23 22:28:35 +02:00
res.end(']\n');
2017-04-21 20:33:55 +02:00
} else {
2017-04-23 22:28:35 +02:00
res.end('}\n');
2017-04-21 20:33:55 +02:00
}
}
});
// 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);
2014-11-13 19:32:31 +01:00
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);
2014-11-13 19:32:31 +01:00
return next({
ok: 'you are authenticated as \'' + req.remote_user.name + '\'',
// token: auth.issue_token(req.remote_user),
token: token,
});
} else {
if (typeof(req.body.name) !== 'string' || typeof(req.body.password) !== 'string') {
if (typeof(req.body.password_sha)) {
return next( Error[422]('your npm version is outdated\nPlease update to npm@1.4.5 or greater.\nSee https://github.com/rlidwka/sinopia/issues/93 for details.') );
} else {
return next( Error[422]('user/password is not found in request (npm issue?)') );
}
}
2014-11-16 13:37:50 +01:00
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);
2014-11-16 13:37:50 +01:00
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);
2016-08-10 12:14:08 +02:00
next({
ok: 'Logged out',
});
});
2016-08-10 12:14:08 +02:00
2015-05-10 17:23:31 +02:00
function tag_package_version(req, res, next) {
if (typeof(req.body) !== 'string') return next('route');
let tags = {};
tags[req.params.tag] = req.body;
2015-05-10 17:23:31 +02:00
storage.merge_tags(req.params.package, tags, function(err) {
if (err) return next(err);
res.status(201);
return next({ok: 'package tagged'});
});
2015-05-10 17:23:31 +02:00
}
// tagging a package
app.put('/:package/:tag',
can('publish'), media('application/json'), tag_package_version);
2015-05-10 17:23:31 +02:00
app.post('/-/package/:package/dist-tags/:tag',
can('publish'), media('application/json'), tag_package_version);
2015-05-10 17:23:31 +02:00
app.put('/-/package/:package/dist-tags/:tag',
can('publish'), media('application/json'), tag_package_version);
2015-05-10 17:23:31 +02:00
app.delete('/-/package/:package/dist-tags/:tag', can('publish'), function(req, res, next) {
let tags = {};
tags[req.params.tag] = null;
2015-05-10 17:23:31 +02:00
storage.merge_tags(req.params.package, tags, function(err) {
if (err) return next(err);
res.status(201);
return next({ok: 'tag removed'});
});
});
2015-05-10 17:23:31 +02:00
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);
2015-05-10 17:23:31 +02:00
next(info['dist-tags']);
});
});
2015-05-10 17:23:31 +02:00
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'});
});
});
2015-05-10 17:23:31 +02:00
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'});
});
});
2015-05-10 17:23:31 +02:00
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;
if (Object.keys(req.body).length == 1 && Utils.is_object(req.body.users)) {
// 501 status is more meaningful, but npm doesn't show error message for 5xx
return next( Error[404]('npm star|unstar calls are not implemented') );
}
try {
var metadata = Utils.validate_metadata(req.body, name);
} catch(err) {
return next( Error[422]('bad incoming package data') );
}
if (req.params._rev) {
storage.change_package(name, metadata, req.params.revision, function(err) {
after_change(err, 'package changed');
});
} else {
storage.add_package(name, metadata, function(err) {
after_change(err, 'created new package');
});
}
function after_change(err, ok_message) {
// old npm behaviour
if (metadata._attachments == null) {
if (err) return next(err);
res.status(201);
return next({ok: ok_message});
}
// npm-registry-client 0.3+ embeds tarball into the json upload
// https://github.com/isaacs/npm-registry-client/commit/e9fbeb8b67f249394f735c74ef11fe4720d46ca0
// issue #31, dealing with it here:
if (typeof(metadata._attachments) !== 'object'
|| Object.keys(metadata._attachments).length !== 1
|| typeof(metadata.versions) !== 'object'
|| Object.keys(metadata.versions).length !== 1) {
// npm is doing something strange again
// if this happens in normal circumstances, report it as a bug
return next( Error[400]('unsupported registry call') );
}
if (err && err.status != 409) return next(err);
// at this point document is either created or existed before
let t1 = Object.keys(metadata._attachments)[0];
2014-11-16 18:44:46 +01:00
create_tarball(Path.basename(t1), metadata._attachments[t1], function(err) {
if (err) return next(err);
let 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});
});
});
});
}
function create_tarball(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();
}
function create_version(version, data, cb) {
storage.add_version(name, version, data, null, cb);
}
function add_tags(tags, cb) {
storage.merge_tags(name, tags, cb);
}
});
// unpublishing an entire package
app.delete('/:package/-rev/*', can('publish'), function(req, res, next) {
storage.remove_package(req.params.package, function(err) {
if (err) return next(err);
res.status(201);
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) {
let name = req.params.package;
let 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);
2014-11-13 19:32:31 +01:00
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'});
});
});
return app;
};