verdaccio/lib/index-api.js

493 lines
15 KiB
JavaScript

'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;
module.exports = function(config, auth, storage) {
/* eslint new-cap:off */
const app = express.Router();
const can = Middleware.allow(auth);
const 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();
});
// 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});
});
// 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
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({});
});
return app;
};