UNPKG

openhim-core

Version:

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

938 lines (788 loc) 29.4 kB
import './winston-transport-workaround' import 'babel-polyfill' import cluster from 'cluster' import nconf from 'nconf' import atna from 'atna-audit' import os from 'os' import chokidar from 'chokidar' import fs from 'fs' import http from 'http' import https from 'https' import tls from 'tls' import net from 'net' import dgram from 'dgram' import uuidv4 from 'uuid/v4' import pem from 'pem' import logger from 'winston' import 'winston-mongodb' import Agenda from 'agenda' import mongoose from 'mongoose' import * as koaMiddleware from './koaMiddleware' import * as koaApi from './koaApi' import * as tlsAuthentication from './middleware/tlsAuthentication' import { UserModel } from './model/users' import { KeystoreModel } from './model/keystore' import * as alerts from './alerts' import * as reports from './reports' import * as polling from './polling' import * as tcpAdapter from './tcpAdapter' import * as auditing from './auditing' import * as tasks from './tasks' import * as upgradeDB from './upgradeDB' import * as autoRetry from './autoRetry' import * as bodyCull from './bodyCull' import { config, appRoot, connectionAgenda } from './config' mongoose.Promise = Promise config.mongo = config.get('mongo') config.authentication = config.get('authentication') config.router = config.get('router') config.api = config.get('api') config.rerun = config.get('rerun') config.tcpAdapter = config.get('tcpAdapter') config.logger = config.get('logger') config.mongoLogger = config.get('mongoLogger') config.alerts = config.get('alerts') config.polling = config.get('polling') config.reports = config.get('reports') config.auditing = config.get('auditing') config.agenda = config.get('agenda') config.certificateManagement = config.get('certificateManagement') config.bodyCull = config.get('bodyCull') const himSourceID = config.get('auditing').auditEvents.auditSourceID const currentVersion = require('../package.json').version const numCPUs = require('os').cpus().length let ensureKeystore logger.remove(logger.transports.Console) const winstonLogFormat = logger.format.printf(info => { return `${info.timestamp} [${info.label}] ${info.level}: ${info.message}`; }) let clusterArg = nconf.get('cluster') function defer () { const deferred = { promise: null, resolve: null, reject: null } deferred.promise = new Promise((resolve, reject) => { deferred.resolve = resolve deferred.reject = reject }) return deferred } export function setupCertificateWatcher () { const certFile = config.certificateManagement.certPath const keyFile = config.certificateManagement.keyPath const watcher = chokidar.watch([certFile, keyFile], { usePolling: true }).on('ready', () => { logger.info('Certificate/Key watch paths:', watcher.getWatched()) return watcher.on('change', (path, stats) => { for (const id in cluster.workers) { const worker = cluster.workers[id] logger.debug(`Restarting worker ${worker.id}...`) worker.send({ type: 'restart' }) } }) }) return watcher } /* eslint no-inner-declarations: 0 */ // Configure clustering if relevent if (cluster.isMaster && !module.parent) { process.title = 'Core' // configure master logger let clusterSize logger.add(new logger.transports.Console({ format: logger.format.combine( logger.format.label({label: 'master'}), logger.format.timestamp(), logger.format.colorize(), winstonLogFormat ), level: config.logger.level })) if (config.logger.logToDB === true) { logger.add(new logger.transports.MongoDB({ db: config.mongo.url, label: 'master', options: config.mongoLogger.options, level: 'debug', capped: config.logger.capDBLogs, cappedSize: config.logger.capSize })) } if ((clusterArg == null)) { clusterArg = 1 } if (clusterArg === 'auto') { clusterSize = numCPUs } else { clusterSize = clusterArg } if ((typeof clusterSize !== 'number') || ((clusterSize % 1) !== 0) || (clusterSize < 1)) { throw new Error(`invalid --cluster argument entered: ${clusterArg}. Please enter a positive number or 'auto'.`) } logger.info(`Running OpenHIM Core JS version ${currentVersion}`) logger.info(`Clustering the OpenHIM with ${clusterSize} workers...`) function addWorker () { let worker = cluster.fork() return worker.on('message', (msg) => { let id logger.debug(`Message received from worker ${worker.id}`, msg) if (msg.type === 'restart-all') { // restart all workers logger.debug('Restarting all workers...') return (() => { const result = [] for (id in cluster.workers) { worker = cluster.workers[id] logger.debug(`Restarting worker ${worker.id}...`) result.push(worker.send({ type: 'restart' })) } return result })() } else if (msg.type === 'start-tcp-channel') { // start tcp channel on all workers logger.info(`Starting TCP channel for channel: ${msg.channelID}`) return (() => { const result1 = [] for (id in cluster.workers) { worker = cluster.workers[id] logger.debug(`Starting TCP channel on worker ${worker.id}...`) result1.push(worker.send(msg)) } return result1 })() } else if (msg.type === 'stop-tcp-channel') { // stop tcp channel on all workers logger.info(`Stopping TCP channel for channel: ${msg.channelID}`) return (() => { const result2 = [] for (id in cluster.workers) { worker = cluster.workers[id] logger.debug(`Stopping TCP channel on worker ${worker.id}...`) result2.push(worker.send(msg)) } return result2 })() } else if (msg.type === 'get-uptime') { // send response back to worker requesting uptime return worker.send({ type: 'get-uptime', masterUptime: process.uptime() }) } return [] }) } // upgrade the database if needed upgradeDB.upgradeDb(() => { // start all workers for (let i = 1, end = clusterSize, asc = end >= 1; asc ? i <= end : i >= end; asc ? i++ : i--) { addWorker() } cluster.on('exit', (worker, code, signal) => { logger.warn(`worker ${worker.process.pid} died`) if (!worker.suicide) { // respawn addWorker() } }) cluster.on('online', worker => logger.info(`worker with pid ${worker.process.pid} is online`)) return cluster.on('listening', (worker, address) => logger.debug(`worker ${worker.id} is now connected to ${address.address}:${address.port}`)) }) // setup watcher if watchFSForCert is enabled if (config.certificateManagement.watchFSForCert) { exports.setupCertificateWatcher() } } else { /* Setup Worker */ // configure worker logger let stop logger.add(new logger.transports.Console({ format: logger.format.combine( logger.format.label({label: ((cluster.worker != null ? cluster.worker.id : undefined) != null) ? `worker${cluster.worker.id}` : undefined}), logger.format.timestamp(), logger.format.colorize(), winstonLogFormat ), level: config.logger.level })) if (config.logger.logToDB === true && logger.default.transports.mongodb == null) { logger.add(new logger.transports.MongoDB({ db: config.mongo.url, options: config.mongoLogger.options, label: ((cluster.worker != null ? cluster.worker.id : undefined) != null) ? `worker${cluster.worker.id}` : undefined, level: 'debug', capped: config.logger.capDBLogs, cappedSize: config.logger.capSize })) } let httpServer = null let httpsServer = null let apiHttpsServer = null let rerunServer = null let tcpHttpReceiver = null let pollingServer = null let auditUDPServer = null let auditTlsServer = null let auditTcpServer = null const activeHttpConnections = {} const activeHttpsConnections = {} const activeApiConnections = {} const activeRerunConnections = {} const activeTcpConnections = {} const activePollingConnections = {} function trackConnection (map, socket) { // save active socket const id = uuidv4() map[id] = socket // remove socket once it closes socket.on('close', () => { map[id] = null return delete map[id] }) // log any socket errors return socket.on('error', err => logger.error(err)) } exports.isTcpHttpReceiverRunning = () => tcpHttpReceiver != null const rootUser = { firstname: 'Super', surname: 'User', email: 'root@openhim.org', passwordAlgorithm: 'sha512', passwordHash: '943a856bba65aad6c639d5c8d4a11fc8bb7fe9de62ae307aec8cf6ae6c1faab722127964c71db4bdd2ea2cdf60c6e4094dcad54d4522ab2839b65ae98100d0fb', passwordSalt: 'd9bcb40e-ae65-478f-962e-5e5e5e7d0a01', groups: ['admin'] } // password = 'openhim-password' // Job scheduler let agenda = null function startAgenda () { const deferred = defer() agenda = new Agenda({ mongo: connectionAgenda }) agenda.on('start', job => logger.info(`starting job: ${job.attrs.name}, Last Ran at: ${job.attrs.lastRunAt}`)) agenda.on('fail', (err, job) => logger.error(`Job ${job.attrs.name} failed with ${err.message}`)) agenda.on('complete', job => logger.info(`Job ${job.attrs.name} has completed`)) agenda.on('ready', () => { if (config.alerts.enableAlerts) { alerts.setupAgenda(agenda) } if (config.reports.enableReports) { reports.setupAgenda(agenda) } if (config.bodyCull.enabled) { bodyCull.setupAgenda(agenda) } autoRetry.setupAgenda(agenda) if (config.polling.enabled) { return polling.setupAgenda(agenda, () => // give workers a change to setup agenda tasks setTimeout(() => { agenda.start() deferred.resolve() return logger.info('Started agenda job scheduler') } , config.agenda.startupDelay) ) } // Start agenda anyway for the other servers agenda.start() return deferred.resolve() }) return deferred.promise } function stopAgenda () { agenda.stop().then(() => { logger.info('Stopped agenda job scheduler') }) } function startHttpServer (httpPort, bindAddress, app) { const deferred = defer() httpServer = http.createServer(app.callback()) // set the socket timeout httpServer.setTimeout(+config.router.timeout, () => logger.info('HTTP socket timeout reached')) httpServer.listen(httpPort, bindAddress, () => { logger.info(`HTTP listening on port ${httpPort}`) return deferred.resolve() }) // listen for server error httpServer.on('error', err => logger.error(`An httpServer error occured: ${err}`)) // listen for client error httpServer.on('clientError', err => logger.error(`An httpServer clientError occured: ${err}`)) httpServer.on('connection', socket => trackConnection(activeHttpConnections, socket)) return deferred.promise } function startHttpsServer (httpsPort, bindAddress, app) { const deferred = defer() const mutualTLS = config.authentication.enableMutualTLSAuthentication tlsAuthentication.getServerOptions(mutualTLS, (err, options) => { if (err) { return deferred.reject(err) } httpsServer = https.createServer(options, app.callback()) // set the socket timeout httpsServer.setTimeout(+config.router.timeout, () => logger.info('HTTPS socket timeout reached')) httpsServer.listen(httpsPort, bindAddress, () => { logger.info(`HTTPS listening on port ${httpsPort}`) return deferred.resolve() }) // listen for server error httpsServer.on('error', err => logger.error(`An httpsServer error occured: ${err}`)) // listen for client error httpsServer.on('clientError', err => logger.error(`An httpsServer clientError occured: ${err}`)) return httpsServer.on('secureConnection', socket => trackConnection(activeHttpsConnections, socket)) }) return deferred.promise } // Ensure that a root user always exists const ensureRootUser = callback => UserModel.findOne({ email: 'root@openhim.org' }, (err, user) => { if (err) { return callback(err) } if (!user) { user = new UserModel(rootUser) return user.save((err) => { if (err) { logger.error(`Could not save root user: ${err}`) return callback(err) } logger.info('Root user created.') return callback() }) } return callback() }) // Ensure that a default keystore always exists and is up to date ensureKeystore = function (callback) { const getServerCertDetails = (cert, callback) => pem.readCertificateInfo(cert, (err, certInfo) => { if (err) { logger.error(err.stack) return callback(err) } return pem.getFingerprint(cert, (err, fingerprint) => { if (err) { logger.error(err.stack) return callback(err) } certInfo.data = cert certInfo.fingerprint = fingerprint.fingerprint return callback(certInfo) }) }) return KeystoreModel.findOne({}, (err, keystore) => { let cert let certPath let keyPath if (err) { logger.error(err.stack) return callback(err) } if ((keystore == null)) { // set default keystore if (config.certificateManagement.watchFSForCert) { // use cert from filesystem ({ certPath } = config.certificateManagement); ({ keyPath } = config.certificateManagement) } else { // use default self-signed certs certPath = `${appRoot}/resources/certs/default/cert.pem` keyPath = `${appRoot}/resources/certs/default/key.pem` } cert = fs.readFileSync(certPath) return getServerCertDetails(cert, (certInfo) => { keystore = new KeystoreModel({ cert: certInfo, key: fs.readFileSync(keyPath), ca: [] }) return keystore.save((err, keystore) => { if (err) { logger.error(`Could not save keystore: ${err.stack}`) return callback(err) } logger.info('Default keystore created.') return callback() }) }) } else if (config.certificateManagement.watchFSForCert) { // update cert to latest cert = fs.readFileSync(config.certificateManagement.certPath) return getServerCertDetails(cert, (certInfo) => { keystore.cert = certInfo keystore.key = fs.readFileSync(config.certificateManagement.keyPath) return keystore.save((err, keystore) => { if (err) { logger.error(`Could not save keystore: ${err.stack}`) return callback(err) } logger.info('Updated keystore with cert and key from filesystem.') return callback() }) }) } return callback() }) } function startApiServer (apiPort, bindAddress, app) { const deferred = defer() // mutualTLS not applicable for the API - set false const mutualTLS = false tlsAuthentication.getServerOptions(mutualTLS, (err, options) => { if (err) { logger.error(`Could not fetch https server options: ${err}`) } apiHttpsServer = https.createServer(options, app.callback()) apiHttpsServer.listen(apiPort, bindAddress, () => { logger.info(`API HTTPS listening on port ${apiPort}`) return ensureRootUser(() => deferred.resolve()) }) return apiHttpsServer.on('secureConnection', socket => trackConnection(activeApiConnections, socket)) }) return deferred.promise } function startTCPServersAndHttpReceiver (tcpHttpReceiverPort, app) { const deferred = defer() tcpHttpReceiver = http.createServer(app.callback()) tcpHttpReceiver.listen(tcpHttpReceiverPort, config.tcpAdapter.httpReceiver.host, () => { logger.info(`HTTP receiver for Socket adapter listening on port ${tcpHttpReceiverPort}`) return tcpAdapter.startupServers((err) => { if (err) { logger.error(err) } return deferred.resolve() }) }) tcpHttpReceiver.on('connection', socket => trackConnection(activeTcpConnections, socket)) return deferred.promise } function startRerunServer (httpPort, app) { const deferredHttp = defer() rerunServer = http.createServer(app.callback()) rerunServer.listen(httpPort, config.rerun.host, () => { logger.info(`Transaction Rerun HTTP listening on port ${httpPort}`) return deferredHttp.resolve() }) rerunServer.on('connection', socket => trackConnection(activeRerunConnections, socket)) return deferredHttp.promise } function startPollingServer (pollingPort, app) { const deferred = defer() pollingServer = http.createServer(app.callback()) pollingServer.listen(pollingPort, config.polling.host, (err) => { if (err) { logger.error(err) } logger.info(`Polling port listening on port ${pollingPort}`) return deferred.resolve() }) pollingServer.on('connection', socket => trackConnection(activePollingConnections, socket)) return deferred.promise } function startAuditUDPServer (auditUDPPort, bindAddress) { const deferred = defer() auditUDPServer = dgram.createSocket('udp4') auditUDPServer.on('listening', () => { logger.info(`Auditing UDP server listening on port ${auditUDPPort}`) return deferred.resolve() }) auditUDPServer.on('message', (msg, rinfo) => { logger.info(`[Auditing UDP] Received message from ${rinfo.address}:${rinfo.port}`) return auditing.processAudit(msg, () => logger.info('[Auditing UDP] Processed audit')) }) auditUDPServer.on('error', (err) => { if (err.code === 'EADDRINUSE') { // ignore to allow only 1 worker to bind (workaround for: https://github.com/joyent/node/issues/9261) return deferred.resolve() } logger.error(`UDP Audit server error: ${err}`, err) return deferred.reject(err) }) auditUDPServer.bind({ port: auditUDPPort, address: bindAddress, exclusive: true }) // workaround for: https://github.com/joyent/node/issues/9261 return deferred.promise } // function to start the TCP/TLS Audit server function startAuditTcpTlsServer (type, auditPort, bindAddress) { const deferred = defer() // data handler function handler (sock) { let message = '' let length = 0 sock.on('data', (data) => { // convert to string and concatenate message += data.toString() // check if length is is still zero and first occurannce of space if ((length === 0) && (message.indexOf(' ') !== -1)) { // get index of end of message length const lengthIndex = message.indexOf(' ') // source message length const lengthValue = message.substr(0, lengthIndex) // remove white spaces length = parseInt(lengthValue.trim(), 10) // update message to remove length - add one extra character to remove the space message = message.substr(lengthIndex + 1) } if (isNaN(length)) { logger.info(`[Auditing ${type}] No length supplied`) sock.destroy() } logger.debug(`Length prefix is: ${length} and message length so far is ${Buffer.byteLength(message)}`) // if sourced length equals message length then full message received if (length === Buffer.byteLength(message)) { logger.info(`[Auditing ${type}] Received message from ${sock.remoteAddress}`) auditing.processAudit(message, () => logger.info(`[Auditing ${type}] Processed audit`)) // reset message and length variables message = '' length = 0 } }) return sock.on('error', err => logger.error(err)) } if (type === 'TLS') { tlsAuthentication.getServerOptions(true, (err, options) => { if (err) { return deferred.reject(err) } auditTlsServer = tls.createServer(options, handler) return auditTlsServer.listen(auditPort, bindAddress, () => { logger.info(`Auditing TLS server listening on port ${auditPort}`) return deferred.resolve() }) }) } else if (type === 'TCP') { auditTcpServer = net.createServer(handler) auditTcpServer.listen(auditPort, bindAddress, () => { logger.info(`Auditing TCP server listening on port ${auditPort}`) return deferred.resolve() }) } return deferred.promise } exports.start = function (ports, done) { const bindAddress = config.get('bindAddress') logger.info(`Starting OpenHIM server on ${bindAddress}...`) const promises = [] return ensureKeystore(() => { if (ports.httpPort || ports.httpsPort) { koaMiddleware.setupApp((app) => { if (ports.httpPort) { promises.push(startHttpServer(ports.httpPort, bindAddress, app)) } if (ports.httpsPort) { promises.push(startHttpsServer(ports.httpsPort, bindAddress, app)) } return promises }) } if (ports.apiPort && config.api.enabled) { koaApi.setupApp(app => promises.push(startApiServer(ports.apiPort, bindAddress, app))) } if (ports.rerunHttpPort) { koaMiddleware.rerunApp(app => promises.push(startRerunServer(ports.rerunHttpPort, app))) if (config.rerun.processor.enabled) { const deferred = defer() tasks.start(() => deferred.resolve()) promises.push(deferred.promise) } } if (ports.tcpHttpReceiverPort) { koaMiddleware.tcpApp(app => promises.push(startTCPServersAndHttpReceiver(ports.tcpHttpReceiverPort, app))) } if (ports.pollingPort) { koaMiddleware.pollingApp(app => promises.push(startPollingServer(ports.pollingPort, app))) } if (ports.auditUDPPort) { promises.push(startAuditUDPServer(ports.auditUDPPort, bindAddress)) } if (ports.auditTlsPort) { promises.push(startAuditTcpTlsServer('TLS', ports.auditTlsPort, bindAddress)) } if (ports.auditTcpPort) { promises.push(startAuditTcpTlsServer('TCP', ports.auditTcpPort, bindAddress)) } promises.push(startAgenda()) return Promise.all(promises).then(() => { let audit = atna.construct.appActivityAudit(true, himSourceID, os.hostname(), 'system') audit = atna.construct.wrapInSyslog(audit) return auditing.sendAuditEvent(audit, (err) => { if (err) return done(err) logger.info('Processed start audit event') logger.info(`OpenHIM server started: ${new Date()}`) return done() }) }).catch(done) }) } // wait for any running tasks before trying to stop anything function stopTasksProcessor (callback) { if (tasks.isRunning()) { return tasks.stop(callback) } return callback() } exports.stop = (stop = done => stopTasksProcessor(() => { if (typeof done !== 'function') { done = () => { } } let socket const promises = [] function stopServer (server, serverType) { const deferred = defer() server.close(() => { logger.info(`Stopped ${serverType} server`) return deferred.resolve() }) return deferred.promise } if (httpServer) { promises.push(stopServer(httpServer, 'HTTP')) } if (httpsServer) { promises.push(stopServer(httpsServer, 'HTTPS')) } if (apiHttpsServer) { promises.push(stopServer(apiHttpsServer, 'API HTTP')) } if (rerunServer) { promises.push(stopServer(rerunServer, 'Rerun HTTP')) } if (pollingServer) { promises.push(stopServer(pollingServer, 'Polling HTTP')) } if (agenda) { stopAgenda() } if (auditTlsServer) { promises.push(stopServer(auditTlsServer, 'Audit TLS').promise) } if (auditTcpServer) { promises.push(stopServer(auditTcpServer, 'Audit TCP').promise) } if (auditUDPServer) { try { auditUDPServer.close() logger.info('Stopped Audit UDP server') } catch (err) { logger.error('Failed to stop auditUDServer with err:', err) } } // ignore errors when shutting down the server, sometimes its already stopped if (tcpHttpReceiver) { promises.push(stopServer(tcpHttpReceiver, 'TCP HTTP Receiver')) const deferred = defer() tcpAdapter.stopServers((err) => { if (err) { return deferred.reject(err) } deferred.resolve() }) promises.push(deferred.promise) } // close active connection so that servers can stop for (const key in activeHttpConnections) { socket = activeHttpConnections[key] socket.destroy() } for (const key in activeHttpsConnections) { socket = activeHttpsConnections[key] socket.destroy() } for (const key in activeApiConnections) { socket = activeApiConnections[key] socket.destroy() } for (const key in activeRerunConnections) { socket = activeRerunConnections[key] socket.destroy() } for (const key in activeTcpConnections) { socket = activeTcpConnections[key] socket.destroy() } for (const key in activePollingConnections) { socket = activePollingConnections[key] socket.destroy() } return Promise.all(promises).then(() => { httpServer = null httpsServer = null apiHttpsServer = null rerunServer = null tcpHttpReceiver = null pollingServer = null auditUDPServer = null auditTlsServer = null auditTcpServer = null agenda = null let audit = atna.construct.appActivityAudit(false, himSourceID, os.hostname(), 'system') audit = atna.construct.wrapInSyslog(audit) return auditing.sendAuditEvent(audit, () => { logger.info('Processed stop audit event') logger.info('Server shutdown complete.') return done() }) }) })) const lookupServerPorts = () => ({ httpPort: config.router.httpPort, httpsPort: config.router.httpsPort, apiPort: config.api.httpsPort, rerunHttpPort: config.rerun.httpPort, tcpHttpReceiverPort: config.tcpAdapter.httpReceiver.httpPort, pollingPort: config.polling.pollingPort, auditUDPPort: config.auditing.servers.udp.enabled ? config.auditing.servers.udp.port : undefined, auditTlsPort: config.auditing.servers.tls.enabled ? config.auditing.servers.tls.port : undefined, auditTcpPort: config.auditing.servers.tcp.enabled ? config.auditing.servers.tcp.port : undefined }) if (!module.parent) { // start the server const ports = lookupServerPorts() exports.start(ports, () => { // setup shutdown listeners process.on('exit', stop) // interrupt signal, e.g. ctrl-c process.on('SIGINT', () => stop(process.exit)) // terminate signal process.on('SIGTERM', () => stop(process.exit)) // restart on message return process.on('message', (msg) => { if (msg.type === 'restart') { exports.restartServer() } }) }) } exports.restartServer = function (ports, done) { if (typeof ports === 'function') { done = ports ports = null } if ((typeof port === 'undefined' || ports === null)) { ports = lookupServerPorts() } return exports.stop(() => exports.start(ports, () => { if (done) { done() } })) } exports.startRestartServerTimeout = function (done) { if (cluster.isMaster) { // restart myself in 2s setTimeout(() => { logger.debug('Master restarting itself...') return exports.restartServer() } , 2000) } else { // notify master to restart all workers in 2s setTimeout(() => { logger.debug('Sending restart cluster message...') return process.send({ type: 'restart-all' }) } , 2000) } return done() } // function to return process uptimes exports.getUptime = function (callback) { if (cluster.isMaster) { // send reponse back to API request const uptime = { master: process.uptime() } return callback(null, uptime) } // send request to master process.send({ type: 'get-uptime' }) const processEvent = function (uptime) { if (uptime.type === 'get-uptime') { uptime = { master: uptime.masterUptime } // remove eventListner process.removeListener('message', processEvent) // send reponse back to API request callback(null, uptime) } } // listen for response from master return process.on('message', processEvent) } } if (process.env.NODE_ENV === 'test') { exports.ensureKeystore = ensureKeystore }