openhim-core
Version:
The OpenHIM core application that provides logging and routing of http requests
466 lines (401 loc) • 14.9 kB
JavaScript
import logger from 'winston'
import request from 'request'
import * as Channels from '../model/channels'
import { TransactionModelAPI } from '../model/transactions'
import * as authorisation from './authorisation'
import * as tcpAdapter from '../tcpAdapter'
import * as server from '../server'
import * as polling from '../polling'
import * as routerMiddleware from '../middleware/router'
import * as utils from '../utils'
import { config } from '../config'
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.polling = 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
*/
export 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 logger.error(err) } })
} else if (channel.type === 'polling') {
return polling.registerPollingChannel(channel, (err) => { if (err) { return logger.error(err) } })
}
}
}
export 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(', ')}`
}
}
export function isTimeoutValid (channel) {
if (channel.timeout == null) {
return true
}
return typeof channel.timeout === 'number' && channel.timeout > 0 && channel.timeout <= 3600000
}
export 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
*/
export 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
logger.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
*/
export 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')
}
}
export 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 logger.error(err) } })
} else {
return tcpAdapter.notifyMasterToStopTCPServer(channel._id, (err) => { if (err) { return logger.error(err) } })
}
} else if (channel.type === 'polling') {
if (Channels.isChannelEnabled(channel)) {
return polling.registerPollingChannel(channel, (err) => { if (err) { return logger.error(err) } })
} else {
return polling.removePollingChannel(channel, (err) => { if (err) { return logger.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
*/
export 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'
logger.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 logger.error(err) } })
} else if (channel.type === 'polling') {
return polling.removePollingChannel(channel, (err) => { if (err) { return logger.error(err) } })
}
}
}
/*
* Deletes a specific channels details
*/
export 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 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'
logger.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
*/
export 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 {
logger.info(`Manually Polling channel ${channel._id}`)
const options = {
url: `http://${config.polling.host}:${config.polling.pollingPort}/trigger`,
headers: {
'channel-id': channel._id,
'X-OpenHIM-LastRunAt': new Date()
}
}
await new Promise((resolve) => {
request(options, function (err) {
if (err) {
logger.error(err)
ctx.status = 500
resolve()
return
}
logger.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')
}
}