UNPKG

openhim-core

Version:

The OpenHIM core application that provides logging and routing of http requests

577 lines (467 loc) 17.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getChannels = getChannels; exports.validateMethod = validateMethod; exports.isTimeoutValid = isTimeoutValid; exports.isMaxBodyDaysValid = isMaxBodyDaysValid; exports.addChannel = addChannel; exports.getChannel = getChannel; exports.getChannelAudits = getChannelAudits; exports.updateChannel = updateChannel; exports.removeChannel = removeChannel; exports.triggerChannel = triggerChannel; var _winston = _interopRequireDefault(require("winston")); var _request = _interopRequireDefault(require("request")); var Channels = _interopRequireWildcard(require("../model/channels")); var _transactions = require("../model/transactions"); var authorisation = _interopRequireWildcard(require("./authorisation")); var tcpAdapter = _interopRequireWildcard(require("../tcpAdapter")); var server = _interopRequireWildcard(require("../server")); var polling = _interopRequireWildcard(require("../polling")); var routerMiddleware = _interopRequireWildcard(require("../middleware/router")); var utils = _interopRequireWildcard(require("../utils")); var _config = require("../config"); function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const { ChannelModel } = Channels; const MAX_BODY_AGE_MESSAGE = `Channel property maxBodyAgeDays has to be a number that's valid and requestBody or responseBody must be true.`; const TIMEOUT_SECONDS_MESSAGE = `Channel property timeoutSeconds has to be a number greater than 1 and less than an 3600`; _config.config.polling = _config.config.get('polling'); function isPathValid(channel) { if (channel.routes != null) { for (const route of Array.from(channel.routes)) { // There cannot be both path and pathTransform. pathTransform must be valid if (route.path && route.pathTransform || route.pathTransform && !/s\/.*\/.*/.test(route.pathTransform)) { return false; } } } return true; } /* * Retrieves the list of active channels */ async function getChannels(ctx) { try { ctx.body = await authorisation.getUserViewableChannels(ctx.authenticated); } catch (err) { utils.logAndSetResponse(ctx, 500, `Could not fetch all channels via the API: ${err}`, 'error'); } } function processPostAddTriggers(channel) { if (channel.type && Channels.isChannelEnabled(channel)) { if ((channel.type === 'tcp' || channel.type === 'tls') && server.isTcpHttpReceiverRunning()) { return tcpAdapter.notifyMasterToStartTCPServer(channel._id, err => { if (err) { return _winston.default.error(err); } }); } else if (channel.type === 'polling') { return polling.registerPollingChannel(channel, err => { if (err) { return _winston.default.error(err); } }); } } } function validateMethod(channel) { const { methods = [] } = channel || {}; if (methods.length === 0) { return; } if (!/http/i.test(channel.type || 'http')) { return `Channel method can't be defined if channel type is not http`; } const mapCount = methods.reduce((dictionary, method) => { if (dictionary[method] == null) { dictionary[method] = 0; } dictionary[method] += 1; return dictionary; }, {}); const repeats = Object.keys(mapCount).filter(k => mapCount[k] > 1).sort(); if (repeats.length > 0) { return `Channel methods can't be repeated. Repeated methods are ${repeats.join(', ')}`; } } function isTimeoutValid(channel) { if (channel.timeout == null) { return true; } return typeof channel.timeout === 'number' && channel.timeout > 0 && channel.timeout <= 3600000; } function isMaxBodyDaysValid(channel) { if (channel.maxBodyAgeDays == null) { return true; } if (!channel.requestBody && !channel.responseBody) { return false; } return typeof channel.maxBodyAgeDays === 'number' && channel.maxBodyAgeDays > 0 && channel.maxBodyAgeDays < 36500; } /* * Creates a new channel */ async function addChannel(ctx) { // Test if the user is authorised if (authorisation.inGroup('admin', ctx.authenticated) === false) { utils.logAndSetResponse(ctx, 403, `User ${ctx.authenticated.email} is not an admin, API access to addChannel denied.`, 'info'); return; } // Get the values to use const channelData = ctx.request.body; // Set the user creating the channel for auditing purposes channelData.updatedBy = utils.selectAuditFields(ctx.authenticated); try { const channel = new ChannelModel(channelData); if (!isPathValid(channel)) { ctx.body = 'Channel cannot have both path and pathTransform. pathTransform must be of the form s/from/to[/g]'; ctx.status = 400; return; } if (channel.priority != null && channel.priority < 1) { ctx.body = 'Channel priority cannot be below 1 (= Highest priority)'; ctx.status = 400; return; } let methodValidation = validateMethod(channel); if (methodValidation != null) { ctx.body = methodValidation; ctx.status = 400; return; } if (!isTimeoutValid(channel)) { ctx.body = TIMEOUT_SECONDS_MESSAGE; ctx.status = 400; return; } const numPrimaries = routerMiddleware.numberOfPrimaryRoutes(channel.routes); if (numPrimaries === 0) { ctx.body = 'Channel must have a primary route'; ctx.status = 400; return; } if (numPrimaries > 1) { ctx.body = 'Channel cannot have a multiple primary routes'; ctx.status = 400; return; } if (!isMaxBodyDaysValid(channelData)) { ctx.body = MAX_BODY_AGE_MESSAGE; ctx.status = 400; return; } await channel.save(); // All ok! So set the result ctx.body = 'Channel successfully created'; ctx.status = 201; _winston.default.info('User %s created channel with id %s', ctx.authenticated.email, channel.id); channelData._id = channel._id; processPostAddTriggers(channelData); } catch (err) { // Error! So inform the user utils.logAndSetResponse(ctx, 400, `Could not add channel via the API: ${err}`, 'error'); } } /* * Retrieves the details for a specific channel */ async function getChannel(ctx, channelId) { // Get the values to use const id = unescape(channelId); try { // Try to get the channel let result = null; let accessDenied = false; // if admin allow acces to all channels otherwise restrict result set if (authorisation.inGroup('admin', ctx.authenticated) === false) { result = await ChannelModel.findOne({ _id: id, txViewAcl: { $in: ctx.authenticated.groups } }).exec(); const adminResult = await ChannelModel.findById(id).exec(); if (adminResult != null) { accessDenied = true; } } else { result = await ChannelModel.findById(id).exec(); } // Test if the result if valid if (result === null) { if (accessDenied) { // Channel exists but this user doesn't have access ctx.body = `Access denied to channel with Id: '${id}'.`; ctx.status = 403; } else { // Channel not found! So inform the user ctx.body = `We could not find a channel with Id:'${id}'.`; ctx.status = 404; } } else { // All ok! So set the result ctx.body = result; } } catch (err) { // Error! So inform the user utils.logAndSetResponse(ctx, 500, `Could not fetch channel by Id '${id}' via the API: ${err}`, 'error'); } } async function getChannelAudits(ctx, channelId) { if (!authorisation.inGroup('admin', ctx.authenticated)) { utils.logAndSetResponse(ctx, 403, `User ${ctx.authenticated.email} is not an admin, API access to addChannel denied.`, 'info'); return; } try { const channel = await ChannelModel.findById(channelId).exec(); if (channel) { ctx.body = await channel.patches.find({ $and: [{ ref: channel.id }, { ops: { $elemMatch: { path: { $ne: '/lastBodyCleared' } } } }] }).sort({ _id: -1 }).exec(); } else { ctx.body = []; } } catch (err) { utils.logAndSetResponse(ctx, 500, `Could not fetch all channels via the API: ${err}`, 'error'); } } function processPostUpdateTriggers(channel) { if (channel.type) { if ((channel.type === 'tcp' || channel.type === 'tls') && server.isTcpHttpReceiverRunning()) { if (Channels.isChannelEnabled(channel)) { return tcpAdapter.notifyMasterToStartTCPServer(channel._id, err => { if (err) { return _winston.default.error(err); } }); } else { return tcpAdapter.notifyMasterToStopTCPServer(channel._id, err => { if (err) { return _winston.default.error(err); } }); } } else if (channel.type === 'polling') { if (Channels.isChannelEnabled(channel)) { return polling.registerPollingChannel(channel, err => { if (err) { return _winston.default.error(err); } }); } else { return polling.removePollingChannel(channel, err => { if (err) { return _winston.default.error(err); } }); } } } } async function findChannelByIdAndUpdate(id, channelData) { const channel = await ChannelModel.findById(id); if (channelData.maxBodyAgeDays != null && channel.maxBodyAgeDays != null && channelData.maxBodyAgeDays !== channel.maxBodyAgeDays) { channelData.lastBodyCleared = undefined; } channel.set(channelData); return channel.save(); } /* * Updates the details for a specific channel */ async function updateChannel(ctx, channelId) { // Test if the user is authorised if (authorisation.inGroup('admin', ctx.authenticated) === false) { utils.logAndSetResponse(ctx, 403, `User ${ctx.authenticated.email} is not an admin, API access to updateChannel denied.`, 'info'); return; } // Get the values to use const id = unescape(channelId); const channelData = ctx.request.body; // Set the user updating the channel for auditing purposes channelData.updatedBy = utils.selectAuditFields(ctx.authenticated); const updatedChannel = await ChannelModel.findById(id); // This is so you can see how the channel will look as a whole before saving updatedChannel.set(channelData); if (!utils.isNullOrWhitespace(channelData.type) && utils.isNullOrEmpty(channelData.methods)) { // Empty the methods if the type has changed from http if (channelData.type !== 'http') { channelData.methods = []; } } else { const { type } = updatedChannel; let { methods } = updatedChannel; let methodValidation = validateMethod({ type, methods }); if (methodValidation != null) { ctx.body = methodValidation; ctx.status = 400; return; } } if (!isTimeoutValid(channelData)) { ctx.body = TIMEOUT_SECONDS_MESSAGE; ctx.status = 400; return; } // Ignore _id if it exists, user cannot change the internal id if (typeof channelData._id !== 'undefined') { delete channelData._id; } if (!isPathValid(channelData)) { utils.logAndSetResponse(ctx, 400, 'Channel cannot have both path and pathTransform. pathTransform must be of the form s/from/to[/g]', 'info'); return; } if (channelData.priority != null && channelData.priority < 1) { ctx.body = 'Channel priority cannot be below 1 (= Highest priority)'; ctx.status = 400; return; } if (channelData.routes != null) { const numPrimaries = routerMiddleware.numberOfPrimaryRoutes(channelData.routes); if (numPrimaries === 0) { ctx.body = 'Channel must have a primary route'; ctx.status = 400; return; } if (numPrimaries > 1) { ctx.body = 'Channel cannot have a multiple primary routes'; ctx.status = 400; return; } } if (!isTimeoutValid(updatedChannel)) { ctx.body = TIMEOUT_SECONDS_MESSAGE; ctx.status = 400; return; } if (!isMaxBodyDaysValid(updatedChannel)) { ctx.body = MAX_BODY_AGE_MESSAGE; ctx.status = 400; return; } try { const channel = await findChannelByIdAndUpdate(id, channelData); // All ok! So set the result ctx.body = 'The channel was successfully updated'; _winston.default.info('User %s updated channel with id %s', ctx.authenticated.email, id); return processPostUpdateTriggers(channel); } catch (err) { // Error! So inform the user utils.logAndSetResponse(ctx, 500, `Could not update channel by id: ${id} via the API: ${err}`, 'error'); } } function processPostDeleteTriggers(channel) { if (channel.type) { if ((channel.type === 'tcp' || channel.type === 'tls') && server.isTcpHttpReceiverRunning()) { return tcpAdapter.notifyMasterToStopTCPServer(channel._id, err => { if (err) { return _winston.default.error(err); } }); } else if (channel.type === 'polling') { return polling.removePollingChannel(channel, err => { if (err) { return _winston.default.error(err); } }); } } } /* * Deletes a specific channels details */ async function removeChannel(ctx, channelId) { // Test if the user is authorised if (authorisation.inGroup('admin', ctx.authenticated) === false) { utils.logAndSetResponse(ctx, 403, `User ${ctx.authenticated.email} is not an admin, API access to removeChannel denied.`, 'info'); return; } // Get the values to use const id = unescape(channelId); try { let channel; const numExistingTransactions = await _transactions.TransactionModelAPI.countDocuments({ channelID: id }).exec(); // Try to get the channel (Call the function that emits a promise and Koa will wait for the function to complete) if (numExistingTransactions === 0) { // safe to remove channel = await ChannelModel.findByIdAndRemove(id).exec(); } else { // not safe to remove. just flag as deleted channel = await findChannelByIdAndUpdate(id, { status: 'deleted', updatedBy: utils.selectAuditFields(ctx.authenticated) }); } // All ok! So set the result ctx.body = 'The channel was successfully deleted'; _winston.default.info(`User ${ctx.authenticated.email} removed channel with id ${id}`); return processPostDeleteTriggers(channel); } catch (err) { // Error! So inform the user utils.logAndSetResponse(ctx, 500, `Could not remove channel by id: ${id} via the API: ${err}`, 'error'); } } /* * Manually Triggers Polling Channel */ async function triggerChannel(ctx, channelId) { // Test if the user is authorised if (authorisation.inGroup('admin', ctx.authenticated) === false) { utils.logAndSetResponse(ctx, 403, `User ${ctx.authenticated.email} is not an admin, API access to removeChannel denied.`, 'info'); return; } // Get the values to use const id = unescape(channelId); // need to initialize return status otherwise will always return 404 ctx.status = 200; try { const channel = await ChannelModel.findById(id).exec(); // Test if the result if valid if (channel === null) { // Channel not found! So inform the user ctx.body = `We could not find a channel with Id:'${id}'.`; ctx.status = 404; return; } else { _winston.default.info(`Manually Polling channel ${channel._id}`); const options = { url: `http://${_config.config.polling.host}:${_config.config.polling.pollingPort}/trigger`, headers: { 'channel-id': channel._id, 'X-OpenHIM-LastRunAt': new Date() } }; await new Promise(resolve => { (0, _request.default)(options, function (err) { if (err) { _winston.default.error(err); ctx.status = 500; resolve(); return; } _winston.default.info(`Channel Successfully polled ${channel._id}`); // Return success status ctx.status = 200; resolve(); }); }); } } catch (err) { // Error! So inform the user utils.logAndSetResponse(ctx, 500, `Could not fetch channel by Id '${id}' via the API: ${err}`, 'error'); } } //# sourceMappingURL=channels.js.map