openhim-core
Version:
The OpenHIM core application that provides logging and routing of http requests
577 lines (467 loc) • 17.5 kB
JavaScript
;
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