internals.Any.prototype.options = function (options) { var obj = this.clone(); obj._settings = Hoek.applyToDefaults(obj._settings || {}, options); return obj; };
}); }, /** * Subscribe to a fanout exchange. Automatic reconnection to a new channel on connection error/lost. * * @param {object} params Function params * @param {string} params.exchange Exchange name * @param {object} [params.options] Exchange/queue settings (same as amqp) * @param {string} [params.queue] Queue to send in if no routing key is specified (default to queue '') * @param {function} [params.waitingFunc] Function to call on connection to the channel * @param {function} params.receiveFunc Function to call on message consumption (take message object in parameter) * @returns {*} */ subscribe(params) { const settings = hoek.applyToDefaults(defaultRabbit, params); const subFunc = channel => ( channel.assertExchange(settings.exchange, 'fanout', _.pick(settings.options, exchangeOpt)) .then(() => channel.assertQueue(settings.queue, _.pick(settings.options, queueOpt))) .then(queueOk => internals._bind(channel, queueOk.queue, settings)) .then(queue => ( internals._consume({ channel, queue, options : settings.options, receiveFunc : settings.receiveFunc, }) )) .then(settings.waitingFunc) );
exports.register = function (server, options, next) { internals = Hoek.applyToDefaults(internals, options); server.dependency(['bedwetter', 'dogwater'], internals.after); next(); };
exports.register = function (server, options, next) { options = Hoek.applyToDefaults({ basePath: '' }, options); server.route({ method: 'GET', path: options.basePath + '/sessions', config: { auth: { strategy: 'simple', scope: 'admin' }, validate: { query: { fields: Joi.string(), sort: Joi.string(), limit: Joi.number().default(20), page: Joi.number().default(1) } }, pre: [ AuthPlugin.preware.ensureAdminGroup('root') ] }, handler: function (request, reply) { var Session = request.server.plugins['hapi-mongo-models'].Session; var query = {}; var fields = request.query.fields; var sort = request.query.sort; var limit = request.query.limit; var page = request.query.page; Session.pagedFind(query, fields, sort, limit, page, function (err, results) { if (err) { return reply(err); } reply(results); }); } }); server.route({ method: 'GET', path: options.basePath + '/sessions/{id}', config: { auth: { strategy: 'simple', scope: 'admin' }, pre: [ AuthPlugin.preware.ensureAdminGroup('root') ] }, handler: function (request, reply) { var Session = request.server.plugins['hapi-mongo-models'].Session; Session.findById(request.params.id, function (err, session) { if (err) { return reply(err); } if (!session) { return reply({ message: 'Document not found.' }).code(404); } reply(session); }); } }); server.route({ method: 'DELETE', path: options.basePath + '/sessions/{id}', config: { auth: { strategy: 'simple', scope: 'admin' }, pre: [ AuthPlugin.preware.ensureAdminGroup('root') ] }, handler: function (request, reply) { var Session = request.server.plugins['hapi-mongo-models'].Session; Session.findByIdAndRemove(request.params.id, function (err, count) { if (err) { return reply(err); } if (count === 0) { return reply({ message: 'Document not found.' }).code(404); } reply({ message: 'Success.' }); }); } }); next(); };
var before = lab.before; var after = lab.after; var expect = Code.expect; var StorageMongo = require('../lib/storage-mongo'); var StorageFS = require('../lib/storage-fs'); var mongodb = require('mongodb'); var _ = require('lodash'); var utils = require('../lib/utils'); var Hoek = require('hoek'); var rimraf = require('rimraf'); var configMongo = require('./config.json'); var configFS = Hoek.applyToDefaults(configMongo, { "storage": { "type": "fs", "path": ".scoped-registry-tmp" } }); var fixPublish = require('./fixtures/publish_joi.json'); var fixPublish2 = require('./fixtures/publish_dummy.json'); var fixPublish3 = require('./fixtures/publish_joi_update.json'); describe('storage', function() { _.each([{ store: StorageFS, name: 'Filesystem', config: configFS }, { store: StorageMongo, name: 'MongoDB',
exports.register = function (server, options, next) { options = Hoek.applyToDefaults({ basePath: '' }, options); server.route({ method: 'GET', path: options.basePath + '/admin-groups', config: { auth: { strategy: 'simple', scope: 'admin' }, validate: { query: { fields: Joi.string(), sort: Joi.string().default('_id'), limit: Joi.number().default(20), page: Joi.number().default(1) } }, pre: [ AuthPlugin.preware.ensureAdminGroup('root') ] }, handler: function (request, reply) { var AdminGroup = request.server.plugins['hapi-mongo-models'].AdminGroup; var query = {}; var fields = request.query.fields; var sort = request.query.sort; var limit = request.query.limit; var page = request.query.page; AdminGroup.pagedFind(query, fields, sort, limit, page, function (err, results) { if (err) { return reply(err); } reply(results); }); } }); server.route({ method: 'GET', path: options.basePath + '/admin-groups/{id}', config: { auth: { strategy: 'simple', scope: 'admin' }, pre: [ AuthPlugin.preware.ensureAdminGroup('root') ] }, handler: function (request, reply) { var AdminGroup = request.server.plugins['hapi-mongo-models'].AdminGroup; AdminGroup.findById(request.params.id, function (err, adminGroup) { if (err) { return reply(err); } if (!adminGroup) { return reply({ message: 'Document not found.' }).code(404); } reply(adminGroup); }); } }); server.route({ method: 'POST', path: options.basePath + '/admin-groups', config: { auth: { strategy: 'simple', scope: 'admin' }, validate: { payload: { name: Joi.string().required() } }, pre: [ AuthPlugin.preware.ensureAdminGroup('root') ] }, handler: function (request, reply) { var AdminGroup = request.server.plugins['hapi-mongo-models'].AdminGroup; var name = request.payload.name; AdminGroup.create(name, function (err, adminGroup) { if (err) { return reply(err); } reply(adminGroup); }); } }); server.route({ method: 'PUT', path: options.basePath + '/admin-groups/{id}', config: { auth: { strategy: 'simple', scope: 'admin' }, validate: { payload: { name: Joi.string().required() } }, pre: [ AuthPlugin.preware.ensureAdminGroup('root') ] }, handler: function (request, reply) { var AdminGroup = request.server.plugins['hapi-mongo-models'].AdminGroup; var id = request.params.id; var update = { $set: { name: request.payload.name } }; AdminGroup.findByIdAndUpdate(id, update, function (err, adminGroup) { if (err) { return reply(err); } if (!adminGroup) { return reply({ message: 'Document not found.' }).code(404); } reply(adminGroup); }); } }); server.route({ method: 'PUT', path: options.basePath + '/admin-groups/{id}/permissions', config: { auth: { strategy: 'simple', scope: 'admin' }, validate: { payload: { permissions: Joi.object().required() } }, pre: [ AuthPlugin.preware.ensureAdminGroup('root') ] }, handler: function (request, reply) { var AdminGroup = request.server.plugins['hapi-mongo-models'].AdminGroup; var id = request.params.id; var update = { $set: { permissions: request.payload.permissions } }; AdminGroup.findByIdAndUpdate(id, update, function (err, adminGroup) { if (err) { return reply(err); } reply(adminGroup); }); } }); server.route({ method: 'DELETE', path: options.basePath + '/admin-groups/{id}', config: { auth: { strategy: 'simple', scope: 'admin' }, pre: [ AuthPlugin.preware.ensureAdminGroup('root') ] }, handler: function (request, reply) { var AdminGroup = request.server.plugins['hapi-mongo-models'].AdminGroup; AdminGroup.findByIdAndDelete(request.params.id, function (err, adminGroup) { if (err) { return reply(err); } if (!adminGroup) { return reply({ message: 'Document not found.' }).code(404); } reply({ message: 'Success.' }); }); } }); next(); };
internals.Object.prototype._base = function (value, state, options) { let target = value; const errors = []; const finish = () => { return { value: target, errors: errors.length ? errors : null }; }; if (typeof value === 'string' && options.convert) { try { value = JSON.parse(value); } catch (parseErr) { } } const type = this._flags.func ? 'function' : 'object'; if (!value || typeof value !== type || Array.isArray(value)) { errors.push(this.createError(type + '.base', null, state, options)); return finish(); } // Skip if there are no other rules to test if (!this._inner.renames.length && !this._inner.dependencies.length && !this._inner.children && // null allows any keys !this._inner.patterns.length) { target = value; return finish(); } // Ensure target is a local copy (parsed) or shallow copy if (target === value) { if (type === 'object') { target = Object.create(Object.getPrototypeOf(value)); } else { target = function () { return value.apply(this, arguments); }; target.prototype = Hoek.clone(value.prototype); } const valueKeys = Object.keys(value); for (let i = 0; i < valueKeys.length; ++i) { target[valueKeys[i]] = value[valueKeys[i]]; } } else { target = value; } // Rename keys const renamed = {}; for (let i = 0; i < this._inner.renames.length; ++i) { const item = this._inner.renames[i]; if (item.options.ignoreUndefined && target[item.from] === undefined) { continue; } if (!item.options.multiple && renamed[item.to]) { errors.push(this.createError('object.rename.multiple', { from: item.from, to: item.to }, state, options)); if (options.abortEarly) { return finish(); } } if (Object.prototype.hasOwnProperty.call(target, item.to) && !item.options.override && !renamed[item.to]) { errors.push(this.createError('object.rename.override', { from: item.from, to: item.to }, state, options)); if (options.abortEarly) { return finish(); } } if (target[item.from] === undefined) { delete target[item.to]; } else { target[item.to] = target[item.from]; } renamed[item.to] = true; if (!item.options.alias) { delete target[item.from]; } } // Validate schema if (!this._inner.children && // null allows any keys !this._inner.patterns.length && !this._inner.dependencies.length) { return finish(); } const unprocessed = Hoek.mapToObject(Object.keys(target)); // Children mustn't inherit the current label if it exists const childOptions = options.language && options.language.label ? Hoek.applyToDefaults(options, { language: { label: null } }, true) : options; if (this._inner.children) { for (let i = 0; i < this._inner.children.length; ++i) { const child = this._inner.children[i]; const key = child.key; const item = target[key]; delete unprocessed[key]; const localState = { key, path: (state.path || '') + (state.path && key ? '.' : '') + key, parent: target, reference: state.reference }; const result = child.schema._validate(item, localState, childOptions); if (result.errors) { errors.push(this.createError('object.child', { key, reason: result.errors }, localState, childOptions)); if (options.abortEarly) { return finish(); } } if (child.schema._flags.strip || (result.value === undefined && result.value !== item)) { delete target[key]; } else if (result.value !== undefined) { target[key] = result.value; } } } // Unknown keys let unprocessedKeys = Object.keys(unprocessed); if (unprocessedKeys.length && this._inner.patterns.length) { for (let i = 0; i < unprocessedKeys.length; ++i) { const key = unprocessedKeys[i]; for (let j = 0; j < this._inner.patterns.length; ++j) { const pattern = this._inner.patterns[j]; if (pattern.regex.test(key)) { delete unprocessed[key]; const item = target[key]; const localState = { key, path: (state.path ? state.path + '.' : '') + key, parent: target, reference: state.reference }; const result = pattern.rule._validate(item, localState, options); if (result.errors) { errors.push(this.createError('object.child', { key, reason: result.errors }, localState, options)); if (options.abortEarly) { return finish(); } } if (result.value !== undefined) { target[key] = result.value; } } } } unprocessedKeys = Object.keys(unprocessed); } if ((this._inner.children || this._inner.patterns.length) && unprocessedKeys.length) { if (options.stripUnknown || options.skipFunctions) { for (let i = 0; i < unprocessedKeys.length; ++i) { const key = unprocessedKeys[i]; if (options.stripUnknown) { delete target[key]; delete unprocessed[key]; } else if (typeof target[key] === 'function') { delete unprocessed[key]; } } unprocessedKeys = Object.keys(unprocessed); } if (unprocessedKeys.length && (this._flags.allowUnknown !== undefined ? !this._flags.allowUnknown : !options.allowUnknown)) { for (let i = 0; i < unprocessedKeys.length; ++i) { errors.push(this.createError('object.allowUnknown', null, { key: unprocessedKeys[i], path: state.path + (state.path ? '.' : '') + unprocessedKeys[i] }, childOptions)); } } } // Validate dependencies for (let i = 0; i < this._inner.dependencies.length; ++i) { const dep = this._inner.dependencies[i]; const err = internals[dep.type].call(this, dep.key !== null && value[dep.key], dep.peers, target, { key: dep.key, path: (state.path || '') + (dep.key ? '.' + dep.key : '') }, options); if (err) { errors.push(err); if (options.abortEarly) { return finish(); } } } return finish(); };
.then(sealed => super.create(hoek.applyToDefaults(config, { value: sealed })));
internals.Manager.prototype.render = function (filename, context, options, callback) { var self = this; context = context || {}; options = options || {}; var engine = null; var fileExtension = Path.extname(filename).slice(1); var extension = fileExtension || self._defaultExtension; if (!extension) { return callback(Boom.badImplementation('Unknown extension and no defaultExtension configured for view template: ' + filename)); } engine = self._engines[extension]; if (!engine) { return callback(Boom.badImplementation('No view engine found for file: ' + filename)); } var settings = Hoek.applyToDefaults(engine.config, options); var templatePath = this._path(filename + (fileExtension ? '' : engine.suffix), settings); if (templatePath.isBoom) { return callback(templatePath); } this._compile(templatePath, engine, settings, function (err, compiled) { if (err) { return callback(err); } // No layout if (!settings.layout) { compiled(context, settings.runtimeOptions, function (err, rendered) { if (err) { return callback(Boom.badImplementation(err.message, err)); } return callback(null, rendered, settings); }); return; } // With layout if (context.hasOwnProperty(settings.layoutKeyword)) { return callback(Boom.badImplementation('settings.layoutKeyword conflict', { context: context, keyword: settings.layoutKeyword })); } var layoutPath = self._path((settings.layout === true ? 'layout' : settings.layout) + engine.suffix, settings, true); if (layoutPath.isBoom) { return callback(layoutPath); } self._compile(layoutPath, engine, settings, function (err, layout) { if (err) { return callback(err); } compiled(context, settings.runtimeOptions, function (err, rendered) { if (err) { return callback(Boom.badImplementation(err.message, err)); } context[settings.layoutKeyword] = rendered; layout(context, settings.runtimeOptions, function (err, rendered) { delete context[settings.layoutKeyword]; if (err) { return callback(Boom.badImplementation(err.message, err)); } return callback(null, rendered, settings); }); }); }); }); };
exports.register = function (plugin, options, next) { let settings = Hoek.applyToDefaults(defaults, options); const publicDirPath = __dirname + Path.sep + '..' + Path.sep + 'public'; const swaggerDirPath = publicDirPath + Path.sep + 'swaggerui'; // add routing swagger json plugin.route([{ method: 'GET', path: settings.jsonPath, config: { auth: settings.auth, handler: (request, reply) => { Joi.assert(settings, schema); Builder.getSwaggerJSON(settings, request, function (err, json) { reply(json).type('application/json'); }); }, plugins: { 'hapi-swagger': false } } }]); // only add 'inert' and 'vision' based routes if needed if (settings.enableDocumentation === true) { // make sure we have other plug-in dependencies plugin.dependency(['inert', 'vision'], (pluginWithDependencies, nextWithDependencies) => { // add routing for swaggerui static assets /swaggerui/ pluginWithDependencies.views({ engines: { html: { module: require('handlebars') } }, path: swaggerDirPath }); pluginWithDependencies.route([{ method: 'GET', path: settings.documentationPath, config: { auth: settings.auth }, handler: (request, reply) => { reply.view('index.html', {}); } },{ method: 'GET', path: settings.swaggerUIPath + '{path*}', config: { auth: settings.auth }, handler: { directory: { path: swaggerDirPath + Path.sep, listing: true, index: false } } },{ method: 'GET', path: settings.swaggerUIPath + 'extend.js', config: { auth: settings.auth }, handler: { file: publicDirPath + Path.sep + 'extend.js' } }]); appendDataContext(pluginWithDependencies, settings); nextWithDependencies(); }); } // TODO: need to work how to test this as it need a request object // Undocument API interface, it may change /* $lab:coverage:off$ */ plugin.expose('getJSON', function (exposeOptions, request, callback) { // use either options passed to function or plug-in scope options let exposeSettings = {}; if (exposeOptions && Utilities.hasProperties(exposeOptions)) { exposeSettings = Hoek.applyToDefaults(defaults, exposeOptions); Joi.assert(exposeSettings, schema); } else { exposeSettings = Hoek.clone(settings); } Builder.getSwaggerJSON(exposeSettings, request, callback); }); /* $lab:coverage:on$ */ next(); };
/** * Instantiate a Secret class * @method createClass * @param config * @return {Secret} */ createClass(config) { const c = hoek.applyToDefaults(config, { password: this[password] }); return new Secret(c); }
exports.register = function (server, options, next) { options = Hoek.applyToDefaults({ basePath: '' }, options); server.route({ method: 'GET', path: options.basePath + '/accounts', config: { auth: { strategy: 'session', scope: 'admin' }, validate: { query: { username: Joi.string().allow(''), fields: Joi.string(), sort: Joi.string().default('_id'), limit: Joi.number().default(20), page: Joi.number().default(1) } } }, handler: function (request, reply) { var Account = request.server.plugins['hapi-mongo-models'].Account; var query = {}; if (request.query.username) { query['user.name'] = new RegExp('^.*?' + request.query.username + '.*$', 'i'); } var fields = request.query.fields; var sort = request.query.sort; var limit = request.query.limit; var page = request.query.page; Account.pagedFind(query, fields, sort, limit, page, function (err, results) { if (err) { return reply(err); } reply(results); }); } }); server.route({ method: 'GET', path: options.basePath + '/accounts/{id}', config: { auth: { strategy: 'session', scope: 'admin' } }, handler: function (request, reply) { var Account = request.server.plugins['hapi-mongo-models'].Account; Account.findById(request.params.id, function (err, account) { if (err) { return reply(err); } if (!account) { return reply({ message: 'Document not found.' }).code(404); } reply(account); }); } }); server.route({ method: 'GET', path: options.basePath + '/accounts/my', config: { auth: { strategy: 'session', scope: 'account' } }, handler: function (request, reply) { var Account = request.server.plugins['hapi-mongo-models'].Account; var id = request.auth.credentials.roles.account._id.toString(); var fields = Account.fieldsAdapter('user name timeCreated'); Account.findById(id, fields, function (err, account) { if (err) { return reply(err); } if (!account) { return reply({ message: 'Document not found. That is strange.' }).code(404); } reply(account); }); } }); server.route({ method: 'POST', path: options.basePath + '/accounts', config: { auth: { strategy: 'session', scope: 'admin' }, validate: { payload: { name: Joi.string().required() } } }, handler: function (request, reply) { var Account = request.server.plugins['hapi-mongo-models'].Account; var name = request.payload.name; Account.create(name, function (err, account) { if (err) { return reply(err); } reply(account); }); } }); server.route({ method: 'PUT', path: options.basePath + '/accounts/{id}', config: { auth: { strategy: 'session', scope: 'admin' }, validate: { payload: { nameFirst: Joi.string().required(), nameMiddle: Joi.string().allow('', null), nameLast: Joi.string().required() } } }, handler: function (request, reply) { var Account = request.server.plugins['hapi-mongo-models'].Account; var id = request.params.id; var update = { $set: { name: { first: request.payload.nameFirst, middle: request.payload.nameMiddle, last: request.payload.nameLast } } }; Account.findByIdAndUpdate(id, update, function (err, account) { if (err) { return reply(err); } if (!account) { return reply({ message: 'Document not found.' }).code(404); } reply(account); }); } }); server.route({ method: 'PUT', path: options.basePath + '/accounts/my', config: { auth: { strategy: 'session', scope: 'account' }, validate: { payload: { nameFirst: Joi.string().required(), nameMiddle: Joi.string().allow(''), nameLast: Joi.string().required() } } }, handler: function (request, reply) { var Account = request.server.plugins['hapi-mongo-models'].Account; var id = request.auth.credentials.roles.account._id.toString(); var update = { $set: { name: { first: request.payload.nameFirst, middle: request.payload.nameMiddle, last: request.payload.nameLast } } }; var findOptions = { fields: Account.fieldsAdapter('user name timeCreated') }; Account.findByIdAndUpdate(id, update, findOptions, function (err, account) { if (err) { return reply(err); } reply(account); }); } }); server.route({ method: 'PUT', path: options.basePath + '/accounts/{id}/user', config: { auth: { strategy: 'session', scope: 'admin' }, validate: { payload: { username: Joi.string().lowercase().required() } }, pre: [{ assign: 'account', method: function (request, reply) { var Account = request.server.plugins['hapi-mongo-models'].Account; Account.findById(request.params.id, function (err, account) { if (err) { return reply(err); } if (!account) { return reply({ message: 'Document not found.' }).takeover().code(404); } reply(account); }); } }, { assign: 'user', method: function (request, reply) { var User = request.server.plugins['hapi-mongo-models'].User; User.findByUsername(request.payload.username, function (err, user) { if (err) { return reply(err); } if (!user) { return reply({ message: 'User document not found.' }).takeover().code(404); } if (user.roles && user.roles.account && user.roles.account.id !== request.params.id) { var response = { message: 'User is already linked to another account. Unlink first.' }; return reply(response).takeover().code(409); } reply(user); }); } }, { assign: 'userCheck', method: function (request, reply) { if (request.pre.account.user && request.pre.account.user.id !== request.pre.user._id.toString()) { var response = { message: 'Account is already linked to another user. Unlink first.' }; return reply(response).takeover().code(409); } reply(true); } }] }, handler: function (request, reply) { Async.auto({ account: function (done) { var Account = request.server.plugins['hapi-mongo-models'].Account; var id = request.params.id; var update = { $set: { user: { id: request.pre.user._id.toString(), name: request.pre.user.username } } }; Account.findByIdAndUpdate(id, update, done); }, user: function (done) { var User = request.server.plugins['hapi-mongo-models'].User; var id = request.pre.user._id; var update = { $set: { 'roles.account': { id: request.pre.account._id.toString(), name: request.pre.account.name.first + ' ' + request.pre.account.name.last } } }; User.findByIdAndUpdate(id, update, done); } }, function (err, results) { if (err) { return reply(err); } reply(results.account); }); } }); server.route({ method: 'DELETE', path: options.basePath + '/accounts/{id}/user', config: { auth: { strategy: 'session', scope: 'admin' }, pre: [{ assign: 'account', method: function (request, reply) { var Account = request.server.plugins['hapi-mongo-models'].Account; Account.findById(request.params.id, function (err, account) { if (err) { return reply(err); } if (!account) { return reply({ message: 'Document not found.' }).takeover().code(404); } if (!account.user || !account.user.id) { return reply(account).takeover(); } reply(account); }); } }, { assign: 'user', method: function (request, reply) { var User = request.server.plugins['hapi-mongo-models'].User; User.findById(request.pre.account.user.id, function (err, user) { if (err) { return reply(err); } if (!user) { return reply({ message: 'User document not found.' }).takeover().code(404); } reply(user); }); } }] }, handler: function (request, reply) { Async.auto({ account: function (done) { var Account = request.server.plugins['hapi-mongo-models'].Account; var id = request.params.id; var update = { $unset: { user: undefined } }; Account.findByIdAndUpdate(id, update, done); }, user: function (done) { var User = request.server.plugins['hapi-mongo-models'].User; var id = request.pre.user._id.toString(); var update = { $unset: { 'roles.account': undefined } }; User.findByIdAndUpdate(id, update, done); } }, function (err, results) { if (err) { return reply(err); } reply(results.account); }); } }); server.route({ method: 'POST', path: options.basePath + '/accounts/{id}/notes', config: { auth: { strategy: 'session', scope: 'admin' }, validate: { payload: { data: Joi.string().required() } } }, handler: function (request, reply) { var Account = request.server.plugins['hapi-mongo-models'].Account; var id = request.params.id; var update = { $push: { notes: { data: request.payload.data, timeCreated: new Date(), userCreated: { id: request.auth.credentials.user._id.toString(), name: request.auth.credentials.user.username } } } }; Account.findByIdAndUpdate(id, update, function (err, account) { if (err) { return reply(err); } reply(account); }); } }); server.route({ method: 'POST', path: options.basePath + '/accounts/{id}/status', config: { auth: { strategy: 'session', scope: 'admin' }, validate: { payload: { status: Joi.string().required() } }, pre: [{ assign: 'status', method: function (request, reply) { var Status = request.server.plugins['hapi-mongo-models'].Status; Status.findById(request.payload.status, function (err, status) { if (err) { return reply(err); } reply(status); }); } }] }, handler: function (request, reply) { var Account = request.server.plugins['hapi-mongo-models'].Account; var id = request.params.id; var newStatus = { id: request.pre.status._id.toString(), name: request.pre.status.name, timeCreated: new Date(), userCreated: { id: request.auth.credentials.user._id.toString(), name: request.auth.credentials.user.username } }; var update = { $set: { 'status.current': newStatus }, $push: { 'status.log': newStatus } }; Account.findByIdAndUpdate(id, update, function (err, account) { if (err) { return reply(err); } reply(account); }); } }); server.route({ method: 'DELETE', path: options.basePath + '/accounts/{id}', config: { auth: { strategy: 'session', scope: 'admin' }, pre: [ AuthPlugin.preware.ensureAdminGroup('root') ] }, handler: function (request, reply) { var Account = request.server.plugins['hapi-mongo-models'].Account; Account.findByIdAndDelete(request.params.id, function (err, account) { if (err) { return reply(err); } if (!account) { return reply({ message: 'Document not found.' }).code(404); } reply({ message: 'Success.' }); }); } }); next(); };
var base = { path: '/limited', method: 'get', handler: function(request, reply) { reply(); } }; exports.offByDefault = base; exports.defaults = Hoek.applyToDefaults(base, { config: { plugins: { 'hapi-limiter': { enable: true } } } }); exports.overrides = Hoek.applyToDefaults(base, { config: { plugins: { 'hapi-limiter': { enable: true, limit: 5, ttl: 8000, generateKeyFunc: function(request) { return 'customkey'; }
.then((channel) => { const settings = hoek.applyToDefaults(defaultRabbit, params); const message = hoek.applyToDefaults(defaultMessage, params.message); let rpcPromise = new Promise((resolve) => { const correlationId = uuid.v1(); const replyFunc = (msg) => { if (msg.properties.correlationId === correlationId) { resolve(msg); } }; // declare anonyme queue for RPC answer channel.assertQueue('', { exclusive : true, autoDelete : true, }) .then(queueOk => queueOk.queue) .then((answerQueue) => { internals._consume({ channel, queue : answerQueue, options : { exclusive : true, }, receiveFunc : replyFunc, }); return answerQueue; }) .then((queue) => { // sending the message with replyTo set with the anonymous queue const msgOpt = _.extend({}, message.options, { correlationId, replyTo : queue, }); return channel.assertQueue(settings.queue, _.pick(settings.options, queueOpt)) .then(queueOk => channel.publish('', queueOk.queue, new Buffer(message.content), _.pick(msgOpt, messageOpt))) .then(() => queue); }); }); if (!_.isUndefined(settings.RPCTimeout)) { rpcPromise = rpcPromise.timeout(settings.RPCTimeout) .catch((err) => { channel.close(); return Promise.reject(err); }); } return rpcPromise .then((msg) => { channel.close(); return msg; }) .then((msg) => { settings.receiveFunc(msg); return msg; }); })
internals.Any.prototype._validate = function (value, state, options, reference) { var self = this; // Setup state and settings state = state || { key: '', path: '', parent: null, reference: reference }; if (this._settings) { options = Hoek.applyToDefaults(options, this._settings); } var errors = []; var finish = function () { return { value: (value !== undefined) ? value : (Ref.isRef(self._flags.default) ? self._flags.default(state.parent) : self._flags.default), errors: errors.length ? errors : null }; }; // Check allowed and denied values using the original value if (this._valids.has(value, state, this._flags.insensitive)) { return finish(); } if (this._invalids.has(value, state, this._flags.insensitive)) { errors.push(Errors.create(value === '' ? 'any.empty' : (value === undefined ? 'any.required' : 'any.invalid'), null, state, options)); if (options.abortEarly || value === undefined) { // No reason to keep validating missing value return finish(); } } // Convert value and validate type if (this._base) { var base = this._base.call(this, value, state, options); if (base.errors) { value = base.value; errors = errors.concat(base.errors); return finish(); // Base error always aborts early } if (base.value !== value) { value = base.value; // Check allowed and denied values using the converted value if (this._valids.has(value, state, this._flags.insensitive)) { return finish(); } if (this._invalids.has(value, state, this._flags.insensitive)) { errors.push(Errors.create('any.invalid', null, state, options)); if (options.abortEarly) { return finish(); } } } } // Required values did not match if (this._flags.allowOnly) { errors.push(Errors.create('any.allowOnly', { valids: this._valids.toString(false) }, state, options)); if (options.abortEarly) { return finish(); } } // Validate tests for (var i = 0, il = this._tests.length; i < il; ++i) { var test = this._tests[i]; var err = test.func.call(this, value, state, options); if (err) { errors.push(err); if (options.abortEarly) { return finish(); } } } return finish(); };
Object.keys(jobs).forEach((jobName) => { jobs[jobName].cache = Hoek.applyToDefaults(cache, { job: Hoek.reach(cacheConfig, `job.${jobName}`, { default: [] }) }); });
exports.register = function (server, options, next) { const settings = Hoek.applyToDefaults(internals.defaults, options); server.register({ register: require('hapi-auth-cookie') }, (err) => { Hoek.assert(!err, err); const authCookieOptions = { password: settings.session.cookie.password, cookie: settings.session.cookie.name, redirectTo: settings.viewPath + '/login', isSecure: false }; server.auth.strategy('session', 'cookie', authCookieOptions); }); server.register({ register: require('bell') }, (err) => { Hoek.assert(!err, err); const bellAuthOptions = { provider: 'github', password: settings.session.bell.github.password, scope: ['user', 'repo'], clientId: settings.session.bell.github.clientId, clientSecret: settings.session.bell.github.clientSecret, isSecure: false }; server.auth.strategy('github', 'bell', bellAuthOptions); }); settings.info = server.info; const utils = new Utils(settings); settings.Utils = utils; const ui = new Ui(settings); server.register([Inert, Vision], Hoek.ignore); server.views({ path: __dirname + '/../views', partialsPath: __dirname + '/../views/partials', helpersPath: __dirname + '/../views/helpers', engines: { html: require('handlebars') } }); server.route([ { method: 'GET', path: '/', config: { //auth: 'session', handler: ui.redirectHome, description: 'redirect / to /gills/jobs' } }, { method: 'GET', path: settings.viewPath + '/jobs', config: { //auth: 'session', handler: ui.Job.getJobsView, description: 'get jobs' } }, { method: 'GET', path: settings.viewPath + '/job', config: { //auth: 'session', handler: ui.Job.getJobCreateView, description: 'get job create view' } }, { method: 'POST', path: settings.viewPath + '/job', config: { //auth: 'session', handler: ui.Job.createJob, description: 'create job', validate: { payload: Schema.createJobSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}', config: { //auth: 'session', handler: ui.Job.getJobView, description: 'get job view', validate: { params: Schema.requiredJobSchema } } }, { method: 'GET', path: settings.viewPath + '/queue/{jobId}/add', config: { //auth: 'session', handler: ui.Queue.addJob, description: 'add job to queue', validate: { params: Schema.requiredJobSchema } } }, { method: 'GET', path: settings.viewPath + '/queue/{jobId}/remove', config: { //auth: 'session', handler: ui.Queue.removeJob, description: 'remove job from queue', validate: { params: Schema.requiredJobSchema } } }, { method: 'GET', path: settings.viewPath + '/queue/clear', config: { //auth: 'session', handler: ui.Queue.clearQueue, description: 'clear jobs from queue' } }, { method: 'GET', path: settings.viewPath + '/queue/{jobId}/pr/{pr}/remove', config: { //auth: 'session', handler: ui.Queue.removePullRequest, description: 'remove pr from queue', validate: { params: Schema.prJobSchema } } }, { method: 'POST', path: settings.viewPath + '/job/{jobId}', config: { //auth: 'session', handler: ui.Job.updateJob, description: 'update job', validate: { params: Schema.requiredJobSchema, payload: Schema.updateJobSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}/delete', config: { //auth: 'session', handler: ui.Job.deleteJob, description: 'delete job', validate: { params: Schema.requiredJobSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}/workspace/delete', config: { //auth: 'session', handler: ui.Job.deleteWorkspace, description: 'delete workspace', validate: { params: Schema.requiredJobSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}/commits', config: { //auth: 'session', handler: ui.Job.getCommitsView, description: 'get commits', validate: { params: Schema.requiredJobSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}/run/{runId}', config: { //auth: 'session', handler: ui.Run.getRunView, description: 'get run view', validate: { params: Schema.requiredRunSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}/run/{runId}/archive/{file}', config: { //auth: 'session', handler: ui.Run.getFileView, description: 'get file view', validate: { params: Schema.getFileSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}/run/{runId}/test', config: { //auth: 'session', handler: ui.Run.getTestView, description: 'get test view', validate: { params: Schema.requiredRunSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}/run/{runId}/coverage', config: { //auth: 'session', handler: ui.Run.getCoverageView, description: 'get coverage view', validate: { params: Schema.requiredRunSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}/run/{runId}/delete', config: { //auth: 'session', handler: ui.Run.deleteRun, description: 'delete run', validate: { params: Schema.requiredRunSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}/runs/delete', config: { //auth: 'session', handler: ui.Run.deleteRuns, description: 'delete runs', validate: { params: Schema.requiredJobSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}/run/{runId}/cancel', config: { //auth: 'session', handler: ui.Run.cancelRun, description: 'cancel run', validate: { params: Schema.requiredRunSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}/pr/{pr}/merge', config: { //auth: 'session', handler: ui.Job.mergePullRequest, description: 'merge pull request', validate: { params: Schema.prJobSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}/pr/{pr}/retry', config: { //auth: 'session', handler: ui.Job.retryPullRequest, description: 'retry pull request', validate: { params: Schema.prJobSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}/pr/{pr}/start', config: { //auth: 'session', handler: ui.Job.startPullRequest, description: 'start pull request', validate: { params: Schema.prJobSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}/pr/{pr}/run/{runId}', config: { //auth: 'session', handler: ui.Run.getPullRequestRunView, description: 'get pull request run view', validate: { params: Schema.prRunSchema } } }, { method: 'GET', path: settings.viewPath + '/job/{jobId}/pr/{pr}/run/{runId}/cancel', config: { //auth: 'session', handler: ui.Run.cancelPullRequest, description: 'cancel pull request', validate: { params: Schema.prRunSchema } } }, { method: 'GET', path: settings.viewPath + '/reel', config: { //auth: 'session', handler: ui.Reel.getReelCreateView, description: 'create reel view' } }, { method: 'POST', path: settings.viewPath + '/reel', config: { //auth: 'session', handler: ui.Reel.createReel, description: 'create reel', validate: { payload: Schema.createReelSchema } } }, { method: 'GET', path: settings.viewPath + '/reel/{reelId}', config: { //auth: 'session', handler: ui.Reel.getReelView, description: 'get reel view', validate: { params: Schema.requiredReelSchema } } }, { method: 'POST', path: settings.viewPath + '/reel/{reelId}', config: { //auth: 'session', handler: ui.Reel.updateReel, description: 'update reel', validate: { params: Schema.requiredReelSchema, payload: Schema.updateReelSchema } } }, { method: 'GET', path: settings.viewPath + '/reel/{reelId}/delete', config: { //auth: 'session', handler: ui.Reel.deleteReel, description: 'delete reel', validate: { params: Schema.requiredReelSchema } } }, { method: 'POST', path: settings.viewPath + '/user', config: { //auth: 'session', handler: ui.User.createUser, description: 'create user', validate: { payload: Schema.createUserSchema } } }, { method: 'GET', path: settings.viewPath + '/user', config: { //auth: 'session', handler: ui.User.getUserCreateView, description: 'create user view' } }, { method: 'GET', path: settings.viewPath + '/user/{userId}', config: { //auth: 'session', handler: ui.User.getUserView, description: 'get user view', validate: { params: Schema.requiredUserSchema } } }, { method: 'POST', path: settings.viewPath + '/user/{userId}', config: { //auth: 'session', handler: ui.User.updateUser, description: 'update user', validate: { params: Schema.requiredUserSchema, payload: Schema.updateUserSchema } } }, { method: 'GET', path: settings.viewPath + '/users', config: { //auth: 'session', handler: ui.User.getUsersView, description: 'get users' } }, { method: 'GET', path: settings.viewPath + '/user/{userId}/delete', config: { //auth: 'session', handler: ui.User.deleteUser, description: 'delete user', validate: { params: Schema.requiredUserSchema } } }, { method: 'POST', path: settings.viewPath + '/login', config: { auth: false, handler: ui.User.loginUser, description: 'login user', validate: { payload: Schema.loginUserSchema } } }, { method: 'GET', path: settings.viewPath + '/logout', config: { //auth: 'session', handler: ui.User.logoutUser, description: 'logout user' } }, { method: 'GET', path: settings.viewPath + '/login', config: { auth: false, handler: ui.User.getLoginView, description: 'login view' } }, { method: 'GET', path: settings.viewPath + '/login/github', config: { auth: 'github', handler: ui.User.loginGithub, description: 'github login' } }, { method: 'GET', path: settings.viewPath + '/css/{path*}', config: css }, { method: 'GET', path: settings.viewPath + '/js/{path*}', config: js }, { method: 'GET', path: settings.viewPath + '/fonts/{path*}', config: fonts } ]); next(); };
var Downloads = function (override) { this.options = Hoek.applyToDefaults(internals._defaults, options || {}); }
exports.start = function (argv) { process.once('SIGUSR2', function () { process.exit(0); }); var start = 0; var config = internals.getConfig(argv); config = Hoek.applyToDefaults(internals.defaults, config); internals.requestOptions = Url.parse(config.url); internals.requestOptions.method = 'POST'; internals.requestOptions.headers = { 'content-type': 'application/json' }; internals.requestOptions.agent = false; var determineStart = function (next) { if (config.useLastIndex) { internals.lastIndexPath += ('_' + Path.basename(config.path)); } if (config.useLastIndex && Fs.existsSync(internals.lastIndexPath)) { var lastContents = Fs.readFileSync(internals.lastIndexPath).toString().split('\n'); start = parseInt(lastContents[lastContents.length - 1]); start = isNaN(start) ? 0 : start; Fs.truncateSync(internals.lastIndexPath); return next(); } if (!config.onlySendNew) { return next(); } Fs.exists(config.path, function (exists) { if (!exists) { return next(); } Fs.stat(config.path, function (err, stat) { if (!err) { start = stat.size ? stat.size - 1 : 0; } next(); }); }); }; var processLog = function () { Fs.exists(config.path, function (exists) { if (!exists) { return; } Fs.stat(config.path, function (err, stat) { if (err) { console.error(err); return; } if (stat.size < start) { start = 0; } internals.getLog(config.path, start, function (bytesRead, log) { start += bytesRead; internals.broadcast(log); if (config.useLastIndex) { internals.logLastIndex(start); } }); }); }); }; determineStart(function () { setInterval(processLog, config.interval); }); };
config.sites.forEach((site, index) => { config.sites[index].wait = site.wait || config.wait config.sites[index] = Hoek.applyToDefaults(internals.siteDefaults, site); config.sites[index].keywords = config.sites[index].keywords.concat(config.keywords) })
internals.fluffy = function (server, options) { Hoek.assert(options, 'Invalid options'); Hoek.assert(options.fluffy, 'Missing required Fluffy configuration'); Hoek.assert(options.fluffy.oz, 'Missing required Oz configuration'); const settings = Hoek.applyToDefaults(internals.defaults, options); // Add protocol endpoints const endpoint = (e, o) => { const route = { auth: false, // Override any defaults handler: function (request, reply) { e(request.raw.req, request.payload, o, reply); } }; return route; }; // Oz routes server.route([ { method: 'POST', path: settings.urls.app, config: endpoint(Oz.endpoints.app, options.fluffy.oz) }, { method: 'POST', path: settings.urls.reissue, config: endpoint(Oz.endpoints.reissue, options.fluffy.oz) }, { method: 'POST', path: settings.urls.rsvp, config: endpoint(Oz.endpoints.rsvp, options.fluffy.oz) }, { method: 'POST', path: settings.urls.delegate, config: endpoint(Oz.endpoints.delegate, options.fluffy.oz) } ]); // Fluffy routes if (options.fluffy.authenticationServer) { server.route([ { method: 'POST', path: settings.urls.fluffyFedereate, config: endpoint(Fluffy.endpoints.authenticationServer.federate, settings.fluffy) }, { method: 'POST', path: settings.urls.fluffyFederateRsvp, config: endpoint(Fluffy.endpoints.authenticationServer.federateRsvp, settings.fluffy) } ]); } if (options.fluffy.service) { server.route([ { method: 'POST', path: settings.urls.fluffyApp, config: endpoint(Fluffy.endpoints.service.app, settings.fluffy) }, { method: 'POST', path: settings.urls.fluffyRsvp, config: endpoint(Fluffy.endpoints.service.rsvp, settings.fluffy) } ]); } const scheme = { api: { settings }, authenticate: function (request, reply) { Oz.server.authenticate(request.raw.req, settings.fluffy.oz.encryptionPassword, { hawk: settings.fluffy.oz.hawk }, (err, credentials, artifacts) => { const result = { credentials: credentials, artifacts: artifacts }; if (err) { return reply(err, null, result); } return reply.continue(result); }); } }; return scheme; };
const register = (server, options) => { Joi.assert(options, internals.schema); const settings = Hoek.applyToDefaults(internals.defaults, options); const routeDefaults = { key: settings.key, restful: settings.restful, source: 'payload' }; server.state(settings.key, settings.cookieOptions); server.ext('onPostAuth', (request, h) => { // If skip function enabled. Call it and if returns true, do not attempt to do anything with crumb. if (settings.skip && settings.skip(request, h)) { return h.continue; } // Validate incoming crumb if (typeof request.route.settings.plugins._crumb === 'undefined') { if (request.route.settings.plugins.crumb || !request.route.settings.plugins.hasOwnProperty('crumb') && settings.autoGenerate) { request.route.settings.plugins._crumb = Hoek.applyToDefaults(routeDefaults, request.route.settings.plugins.crumb || {}); } else { request.route.settings.plugins._crumb = false; } } // Set crumb cookie and calculate crumb if ((settings.autoGenerate || request.route.settings.plugins._crumb) && (request.route.settings.cors ? checkCORS(request) : true)) { generate(request, h); } // Validate crumb let routeIsRestful; if (request.route.settings.plugins._crumb && request.route.settings.plugins._crumb.restful !== undefined) { routeIsRestful = request.route.settings.plugins._crumb.restful; } if (routeIsRestful === false || !routeIsRestful && settings.restful === false) { if (request.method !== 'post' || !request.route.settings.plugins._crumb) { return h.continue; } const content = request[request.route.settings.plugins._crumb.source]; if (!content || content instanceof Stream) { throw Boom.forbidden(); } if (content[request.route.settings.plugins._crumb.key] !== request.plugins.crumb) { throw Boom.forbidden(); } // Remove crumb delete request[request.route.settings.plugins._crumb.source][request.route.settings.plugins._crumb.key]; } else { if (request.method !== 'post' && request.method !== 'put' && request.method !== 'patch' && request.method !== 'delete' || !request.route.settings.plugins._crumb) { return h.continue; } const header = request.headers['x-csrf-token']; if (!header) { throw Boom.forbidden(); } if (header !== request.plugins.crumb) { throw Boom.forbidden(); } } return h.continue; }); server.ext('onPreResponse', (request, h) => { // Add to view context const response = request.response; if (settings.addToViewContext && request.plugins.crumb && request.route.settings.plugins._crumb && !response.isBoom && response.variety === 'view') { response.source.context = response.source.context || {}; response.source.context[request.route.settings.plugins._crumb.key] = request.plugins.crumb; } return h.continue; }); const checkCORS = function (request) { if (request.headers.origin) { return request.info.cors.isOriginMatch; } return true; }; const generate = function (request, h) { let crumb = request.state[settings.key]; if (!crumb) { crumb = Cryptiles.randomString(settings.size); h.state(settings.key, crumb, settings.cookieOptions); } request.plugins.crumb = crumb; return request.plugins.crumb; }; server.expose({ generate }); };
exports = module.exports = internals.Route = function (options, connection, plugin) { // Apply plugin environment (before schema validation) var realm = plugin.realm; if (realm.modifiers.route.vhost || realm.modifiers.route.prefix) { options = Hoek.cloneWithShallow(options, ['config']); // config is left unchanged options.path = (realm.modifiers.route.prefix ? realm.modifiers.route.prefix + (options.path !== '/' ? options.path : '') : options.path); options.vhost = realm.modifiers.route.vhost || options.vhost; } // Setup and validate route configuration Hoek.assert(options.path, 'Route missing path'); Hoek.assert(options.handler || (options.config && options.config.handler), 'Missing or undefined handler:', options.method, options.path); Hoek.assert(!!options.handler ^ !!(options.config && options.config.handler), 'Handler must only appear once:', options.method, options.path); // XOR Hoek.assert(options.path === '/' || options.path[options.path.length - 1] !== '/' || !connection.settings.router.stripTrailingSlash, 'Path cannot end with a trailing slash when connection configured to strip:', options.method, options.path); Hoek.assert(/^[a-zA-Z0-9!#\$%&'\*\+\-\.^_`\|~]+$/.test(options.method), 'Invalid method name:', options.method, options.path); options = Schema.apply('route', options, options.path); var handler = options.handler || options.config.handler; var method = options.method.toLowerCase(); Hoek.assert(method !== 'head', 'Method name not allowed:', options.method, options.path); // Apply settings in order: {connection} <- {handler} <- {realm} <- {route} var handlerDefaults = Handler.defaults(method, handler, connection.server); var base = Hoek.applyToDefaultsWithShallow(connection.settings.routes, handlerDefaults, ['bind']); base = Hoek.applyToDefaultsWithShallow(base, realm.settings, ['bind']); this.settings = Hoek.applyToDefaultsWithShallow(base, options.config || {}, ['bind']); this.settings.handler = handler; this.settings = Schema.apply('routeConfig', this.settings, options.path); var socketTimeout = (this.settings.timeout.socket === undefined ? 2 * 60 * 1000 : this.settings.timeout.socket); Hoek.assert(!this.settings.timeout.server || !socketTimeout || this.settings.timeout.server < socketTimeout, 'Server timeout must be shorter than socket timeout:', options.path); Hoek.assert(!this.settings.payload.timeout || !socketTimeout || this.settings.payload.timeout < socketTimeout, 'Payload timeout must be shorter than socket timeout:', options.path); this.connection = connection; this.server = connection.server; this.path = options.path; this.method = method; this.plugin = plugin; this.public = { method: this.method, path: this.path, vhost: this.vhost, realm: this.plugin.realm, settings: this.settings }; this.settings.vhost = options.vhost; this.settings.plugins = this.settings.plugins || {}; // Route-specific plugins settings, namespaced using plugin name this.settings.app = this.settings.app || {}; // Route-specific application settings // Path parsing this._analysis = this.connection._router.analyze(this.path); this.params = this._analysis.params; this.fingerprint = this._analysis.fingerprint; // Validation var validation = this.settings.validate; if (this.method === 'get') { // Assert on config, not on merged settings Hoek.assert(!options.config || !options.config.payload, 'Cannot set payload settings on HEAD or GET request:', options.path); Hoek.assert(!options.config || !options.config.validate || !options.config.validate.payload, 'Cannot validate HEAD or GET requests:', options.path); validation.payload = null; } ['headers', 'params', 'query', 'payload'].forEach(function (type) { validation[type] = internals.compileRule(validation[type]); }); if (this.settings.response.schema !== undefined || this.settings.response.status) { var rule = this.settings.response.schema; this.settings.response.status = this.settings.response.status || {}; var statuses = Object.keys(this.settings.response.status); if (rule === true && !statuses.length) { this.settings.response = null; } else { this.settings.response.schema = internals.compileRule(rule); for (var i = 0, il = statuses.length; i < il; ++i) { var code = statuses[i]; this.settings.response.status[code] = internals.compileRule(this.settings.response.status[code]); } } } else { this.settings.response = null; } // Payload parsing if (this.method === 'get') { this.settings.payload = null; } else { if (this.settings.payload.allow) { this.settings.payload.allow = [].concat(this.settings.payload.allow); } } Hoek.assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled:', options.method, options.path); Hoek.assert(!this.settings.jsonp || typeof this.settings.jsonp === 'string', 'Bad route JSONP parameter name:', options.path); // Authentication configuration this.settings.auth = this.connection.auth._setupRoute(this.settings.auth, options.path); // Cache if (this.method === 'get' && (this.settings.cache.expiresIn || this.settings.cache.expiresAt)) { this.settings.cache._statuses = Hoek.mapToObject(this.settings.cache.statuses); this._cache = new Catbox.Policy({ expiresIn: this.settings.cache.expiresIn, expiresAt: this.settings.cache.expiresAt }); } // CORS if (this.settings.cors) { this.settings.cors = Hoek.applyToDefaults(Defaults.cors, this.settings.cors); var cors = this.settings.cors; cors._headers = cors.headers.concat(cors.additionalHeaders).join(','); cors._methods = cors.methods.concat(cors.additionalMethods).join(','); cors._exposedHeaders = cors.exposedHeaders.concat(cors.additionalExposedHeaders).join(','); if (cors.origin.length) { cors._origin = { any: false, qualified: [], qualifiedString: '', wildcards: [] }; if (cors.origin.indexOf('*') !== -1) { Hoek.assert(cors.origin.length === 1, 'Cannot specify cors.origin * together with other values'); cors._origin.any = true; } else { for (var c = 0, cl = cors.origin.length; c < cl; ++c) { var origin = cors.origin[c]; if (origin.indexOf('*') !== -1) { cors._origin.wildcards.push(new RegExp('^' + Hoek.escapeRegex(origin).replace(/\\\*/g, '.*').replace(/\\\?/g, '.') + '$')); } else { cors._origin.qualified.push(origin); } } Hoek.assert(cors.matchOrigin || !cors._origin.wildcards.length, 'Cannot include wildcard origin values with matchOrigin disabled'); cors._origin.qualifiedString = cors._origin.qualified.join(' '); } } } // Security if (this.settings.security) { this.settings.security = Hoek.applyToDefaults(Defaults.security, this.settings.security); var security = this.settings.security; if (security.hsts) { if (security.hsts === true) { security._hsts = 'max-age=15768000'; } else if (typeof security.hsts === 'number') { security._hsts = 'max-age=' + security.hsts; } else { security._hsts = 'max-age=' + (security.hsts.maxAge || 15768000); if (security.hsts.includeSubdomains || security.hsts.includeSubDomains) { security._hsts += '; includeSubDomains'; } if (security.hsts.preload) { security._hsts += '; preload'; } } } if (security.xframe) { if (security.xframe === true) { security._xframe = 'DENY'; } else if (typeof security.xframe === 'string') { security._xframe = security.xframe.toUpperCase(); } else if (security.xframe.rule === 'allow-from') { if (!security.xframe.source) { security._xframe = 'SAMEORIGIN'; } else { security._xframe = 'ALLOW-FROM ' + security.xframe.source; } } else { security._xframe = security.xframe.rule.toUpperCase(); } } } // Handler this.settings.handler = Handler.configure(this.settings.handler, this); this._prerequisites = Handler.prerequisites(this.settings.pre, this.server); // Route lifecycle this._extensions = { onPreAuth: this._combineExtensions('onPreAuth'), onPostAuth: this._combineExtensions('onPostAuth'), onPreHandler: this._combineExtensions('onPreHandler'), onPostHandler: this._combineExtensions('onPostHandler'), onPreResponse: this._combineExtensions('onPreResponse') }; this._cycle = null; this.rebuild(); };
server.ext('onPostAuth', (request, h) => { // If skip function enabled. Call it and if returns true, do not attempt to do anything with crumb. if (settings.skip && settings.skip(request, h)) { return h.continue; } // Validate incoming crumb if (typeof request.route.settings.plugins._crumb === 'undefined') { if (request.route.settings.plugins.crumb || !request.route.settings.plugins.hasOwnProperty('crumb') && settings.autoGenerate) { request.route.settings.plugins._crumb = Hoek.applyToDefaults(routeDefaults, request.route.settings.plugins.crumb || {}); } else { request.route.settings.plugins._crumb = false; } } // Set crumb cookie and calculate crumb if ((settings.autoGenerate || request.route.settings.plugins._crumb) && (request.route.settings.cors ? checkCORS(request) : true)) { generate(request, h); } // Validate crumb let routeIsRestful; if (request.route.settings.plugins._crumb && request.route.settings.plugins._crumb.restful !== undefined) { routeIsRestful = request.route.settings.plugins._crumb.restful; } if (routeIsRestful === false || !routeIsRestful && settings.restful === false) { if (request.method !== 'post' || !request.route.settings.plugins._crumb) { return h.continue; } const content = request[request.route.settings.plugins._crumb.source]; if (!content || content instanceof Stream) { throw Boom.forbidden(); } if (content[request.route.settings.plugins._crumb.key] !== request.plugins.crumb) { throw Boom.forbidden(); } // Remove crumb delete request[request.route.settings.plugins._crumb.source][request.route.settings.plugins._crumb.key]; } else { if (request.method !== 'post' && request.method !== 'put' && request.method !== 'patch' && request.method !== 'delete' || !request.route.settings.plugins._crumb) { return h.continue; } const header = request.headers['x-csrf-token']; if (!header) { throw Boom.forbidden(); } if (header !== request.plugins.crumb) { throw Boom.forbidden(); } } return h.continue; });
exports = module.exports = internals.Server = function (/* host, port, options */) { // all optional Hoek.assert(this.constructor === internals.Server, 'Server must be instantiated using new'); var Pack = require('./pack'); // Delayed required to avoid circular dependencies // Register as event emitter Events.EventEmitter.call(this); // Validate arguments Hoek.assert(arguments.length <= 4, 'Too many arguments'); // 4th is for internal Pack usage var argMap = { string: 'host', number: 'port', object: 'options' }; var args = {}; for (var a = 0, al = arguments.length; a < al; ++a) { var argVal = arguments[a]; if (argVal === undefined) { continue; } if (argVal instanceof Pack) { args.pack = arguments[a]; continue; } var type = typeof argVal; if (type === 'string' && isFinite(+argVal)) { type = 'number'; argVal = +argVal; } var key = argMap[type]; Hoek.assert(key, 'Bad server constructor arguments: no match for arg type:', type); Hoek.assert(!args[key], 'Bad server constructor arguments: duplicated arg type:', type, '(values: `' + args[key] + '`, `' + argVal + '`)'); args[key] = argVal; } this.settings = Hoek.applyToDefaultsWithShallow(Defaults.server, args.options || {}, ['app', 'plugins', 'views']); Schema.assert('server', this.settings); this.settings.labels = Hoek.unique([].concat(this.settings.labels)); // Convert string to array and removes duplicates // Set basic configuration this._unixDomainSocket = (args.host && args.host.indexOf('/') !== -1); this._windowsNamedPipe = (args.host && args.host.indexOf('\\\\.\\pipe\\') === 0); Hoek.assert(!this._unixDomainSocket || args.port === undefined, 'Cannot specify port with a UNIX domain socket'); Hoek.assert(!this._windowsNamedPipe || args.port === undefined, 'Cannot specify port with a Windows named pipe'); this._host = (args.host ? (this._unixDomainSocket ? Path.resolve(args.host) : (this._windowsNamedPipe ? args.host : args.host.toLowerCase())) : ''); this._port = (args.port !== undefined ? args.port : (this.settings.tls ? 443 : 80)); this._onConnection = null; // Used to remove event listener on stop Hoek.assert(!this.settings.location || this.settings.location.charAt(this.settings.location.length - 1) !== '/', 'Location setting must not contain a trailing \'/\''); var socketTimeout = (this.settings.timeout.socket === undefined ? 2 * 60 * 1000 : this.settings.timeout.socket); Hoek.assert(!this.settings.timeout.server || !socketTimeout || this.settings.timeout.server < socketTimeout, 'Server timeout must be shorter than socket timeout'); Hoek.assert(!this.settings.timeout.client || !socketTimeout || this.settings.timeout.client < socketTimeout, 'Client timeout must be shorter than socket timeout'); // Server facilities this._started = false; this.auth = new Auth(this); // Required before _router this._router = new Router(this); this._etags = (this.settings.files.etagsCacheMaxSize ? LruCache({ max: this.settings.files.etagsCacheMaxSize }) : null); // Server load Hoek.assert(this.settings.load.sampleInterval || (!this.settings.load.maxEventLoopDelay && !this.settings.load.maxHeapUsedBytes && !this.settings.load.maxRssBytes), 'Load sample interval must be set in enable load limits'); this._eventLoopTimer = null; this._loadBench = new Hoek.Bench(); this.load = { eventLoopDelay: 0, heapUsed: 0, rss: 0 }; /* onRequest: New request, before handing over to the router (allows changes to the request method, url, etc.) onPreAuth: After cookie parse and before authentication (skipped if state error) onPostAuth: After authentication (and payload processing) and before validation (skipped if auth or payload error) onPreHandler: After validation and body parsing, before route handler (skipped if auth or validation error) onPostHandler: After route handler returns, before sending response (skipped if onPreHandler not called) onPreResponse: Before response is sent (always called) */ this._ext = new Ext(['onRequest', 'onPreAuth', 'onPostAuth', 'onPreHandler', 'onPostHandler', 'onPreResponse']); this._stateDefinitions = {}; this._registrations = {}; if (args.pack) { this.pack = args.pack; } else { this.pack = new Pack({ cache: this.settings.cache, debug: this.settings.debug }); this.pack._server(this); } this.plugins = {}; // Registered plugin APIs by plugin name this.app = {}; // Place for application-specific state without conflicts with hapi, should not be used by plugins this.methods = this.pack._methods.methods; // Method functions // Generate CORS headers this.settings.cors = Hoek.applyToDefaults(Defaults.cors, this.settings.cors); if (this.settings.cors) { this.settings.cors._headers = this.settings.cors.headers.concat(this.settings.cors.additionalHeaders).join(', '); this.settings.cors._methods = this.settings.cors.methods.concat(this.settings.cors.additionalMethods).join(', '); this.settings.cors._exposedHeaders = this.settings.cors.exposedHeaders.concat(this.settings.cors.additionalExposedHeaders).join(', '); if (this.settings.cors.origin.length) { this.settings.cors._origin = { any: false, qualified: [], qualifiedString: '', wildcards: [] }; if (this.settings.cors.origin.indexOf('*') !== -1) { Hoek.assert(this.settings.cors.origin.length === 1, 'Cannot specify cors.origin * together with other values'); this.settings.cors._origin.any = true; } else { for (var c = 0, cl = this.settings.cors.origin.length; c < cl; ++c) { var origin = this.settings.cors.origin[c]; if (origin.indexOf('*') !== -1) { this.settings.cors._origin.wildcards.push(new RegExp('^' + Hoek.escapeRegex(origin).replace(/\\\*/g, '.*').replace(/\\\?/g, '.') + '$')); } else { this.settings.cors._origin.qualified.push(origin); } } Hoek.assert(this.settings.cors.matchOrigin || !this.settings.cors._origin.wildcards.length, 'Cannot include wildcard origin values with matchOrigin disabled'); this.settings.cors._origin.qualifiedString = this.settings.cors._origin.qualified.join(' '); } } } // Generate security headers this.settings.security = Hoek.applyToDefaults(Defaults.security, this.settings.security); if (this.settings.security) { if (this.settings.security.hsts) { if (this.settings.security.hsts === true) { this.settings.security._hsts = 'max-age=15768000'; } else if (typeof this.settings.security.hsts === 'number') { this.settings.security._hsts = 'max-age=' + this.settings.security.hsts; } else { this.settings.security._hsts = 'max-age=' + (this.settings.security.hsts.maxAge || 15768000); if (this.settings.security.hsts.includeSubdomains) { this.settings.security._hsts += '; includeSubdomains'; } } } if (this.settings.security.xframe) { if (this.settings.security.xframe === true) { this.settings.security._xframe = 'DENY'; } else if (typeof this.settings.security.xframe === 'string') { this.settings.security._xframe = this.settings.security.xframe.toUpperCase(); } else { if (this.settings.security.xframe.rule === 'allow-from') { if (!this.settings.security.xframe.source) { this.settings.security._xframe = 'SAMEORIGIN'; } else { this.settings.security._xframe = 'ALLOW-FROM ' + this.settings.security.xframe.source; } } else { this.settings.security._xframe = this.settings.security.xframe.rule.toUpperCase(); } } } } // Initialize Views this._views = (this.settings.views ? new Views.Manager(this.settings.views) : null); // Create server if (this.settings.tls) { this.listener = Https.createServer(this.settings.tls, this._dispatch()); } else { this.listener = Http.createServer(this._dispatch()); } this._agents = {}; if (this.settings.maxSockets !== false) { this._agents.https = new Https.Agent(); this._agents.https.maxSockets = this.settings.maxSockets; this._agents.insecureAgent = new Https.Agent({ rejectUnauthorized: false }); this._agents.insecureAgent.maxSockets = this.settings.maxSockets; this._agents.http = new Http.Agent(); this._agents.http.maxSockets = this.settings.maxSockets; } // Server information this.info = { host: this._host || '0.0.0.0' }; if (this._unixDomainSocket || this._windowsNamedPipe) { this.info.port = 0; this.info.protocol = (this._unixDomainSocket ? 'unix' : 'windows'); this.info.uri = this.info.protocol + ':' + this._host; } else { this.info.port = this._port || 0; this.info.protocol = (this.settings.tls ? 'https' : 'http'); if (this.info.port) { this.info.uri = this.info.protocol + '://' + (this._host || Os.hostname() || 'localhost') + ':' + this.info.port; } } };
exports.register = function (server, options, next) { options = Hoek.applyToDefaults({ basePath: '' }, options); server.route({ method: 'POST', path: options.basePath + '/login', config: { validate: { payload: { username: Joi.string().required(), password: Joi.string().required() } }, pre: [{ assign: 'abuseDetected', method: function (request, reply) { var AuthAttempt = request.server.plugins['hapi-mongo-models'].AuthAttempt; var ip = request.info.remoteAddress; var username = request.payload.username; AuthAttempt.abuseDetected(ip, username, function (err, detected) { if (err) { return reply(err); } if (detected) { return reply({ message: 'Maximum number of auth attempts reached. Please try again later.' }).takeover().code(400); } reply(); }); } },{ assign: 'user', method: function (request, reply) { var User = request.server.plugins['hapi-mongo-models'].User; var username = request.payload.username; var password = request.payload.password; User.findByCredentials(username, password, function (err, user) { if (err) { return reply(err); } reply(user); }); } },{ assign: 'logAttempt', method: function (request, reply) { if (request.pre.user) { return reply(); } var AuthAttempt = request.server.plugins['hapi-mongo-models'].AuthAttempt; var ip = request.info.remoteAddress; var username = request.payload.username; AuthAttempt.create(ip, username, function (err, authAttempt) { if (err) { return reply(err); } return reply({ message: 'Username and password combination not found or account is inactive.' }).takeover().code(400); }); } },{ assign: 'session', method: function (request, reply) { var Session = request.server.plugins['hapi-mongo-models'].Session; Session.create(request.pre.user.username, function (err, session) { if (err) { return reply(err); } return reply(session); }); } }] }, handler: function (request, reply) { var credentials = request.pre.user.username + ':' + request.pre.session.key; var authHeader = 'Basic ' + new Buffer(credentials).toString('base64'); reply({ user: { _id: request.pre.user._id, username: request.pre.user.username, email: request.pre.user.email, roles: request.pre.user.roles }, session: request.pre.session, authHeader: authHeader }); } }); server.route({ method: 'POST', path: options.basePath + '/login/forgot', config: { validate: { payload: { email: Joi.string().email().required() } }, pre: [{ assign: 'user', method: function (request, reply) { var User = request.server.plugins['hapi-mongo-models'].User; var conditions = { email: request.payload.email.toLowerCase() }; User.findOne(conditions, function (err, user) { if (err) { return reply(err); } if (!user) { return reply({ message: 'Success.' }).takeover(); } reply(user); }); } }] }, handler: function (request, reply) { var Session = request.server.plugins['hapi-mongo-models'].Session; var User = request.server.plugins['hapi-mongo-models'].User; var mailer = request.server.plugins.mailer; Async.auto({ keyHash: function (done) { Session.generateKeyHash(done); }, user: ['keyHash', function (done, results) { var id = request.pre.user._id.toString(); var update = { $set: { resetPassword: { token: results.keyHash.hash, expires: Date.now() + 10000000 } } }; User.findByIdAndUpdate(id, update, done); }], email: ['user', function (done, results) { var options = { subject: 'Reset your ' + Config.get('/projectName') + ' password', to: request.payload.email }; var template = 'forgot-password'; var context = { key: results.keyHash.key }; mailer.sendEmail(options, template, context, done); }] }, function (err, results) { if (err) { return reply(err); } reply({ message: 'Success.' }); }); } }); server.route({ method: 'POST', path: options.basePath + '/login/reset', config: { validate: { payload: { key: Joi.string().required(), email: Joi.string().email().required(), password: Joi.string().required() } }, pre: [{ assign: 'user', method: function (request, reply) { var User = request.server.plugins['hapi-mongo-models'].User; var conditions = { email: request.payload.email.toLowerCase(), 'resetPassword.expires': { $gt: Date.now() } }; User.findOne(conditions, function (err, user) { if (err) { return reply(err); } if (!user) { return reply({ message: 'Invalid email or key.' }).takeover().code(400); } reply(user); }); } }] }, handler: function (request, reply) { var User = request.server.plugins['hapi-mongo-models'].User; Async.auto({ keyMatch: function (done) { var key = request.payload.key; var token = request.pre.user.resetPassword.token; Bcrypt.compare(key, token, done); }, passwordHash: ['keyMatch', function (done, results) { if (!results.keyMatch) { return reply({ message: 'Invalid email or key.' }).takeover().code(400); } User.generatePasswordHash(request.payload.password, done); }], user: ['passwordHash', function (done, results) { var id = request.pre.user._id.toString(); var update = { $set: { password: results.passwordHash.hash }, $unset: { resetPassword: undefined } }; User.findByIdAndUpdate(id, update, done); }] }, function (err, results) { if (err) { return reply(err); } reply({ message: 'Success.' }); }); } }); next(); };