949 lines
36 KiB
JavaScript
949 lines
36 KiB
JavaScript
/**
|
|
* @fileOverview Node.js Bridge Server Application for Bridge Pay
|
|
* @preserve Copyright 2017 Comcarde Ltd.
|
|
* @author Keith Symington
|
|
* @see #bridge_server-core
|
|
*/
|
|
/* eslint-disable no-process-env, no-process-exit, no-console, import/max-dependencies */
|
|
|
|
/**
|
|
* Requirements.
|
|
*/
|
|
const path = require('path');
|
|
const exitCodes = require('./exitcodes.js');
|
|
const logUtils = require('./utils/logging');
|
|
|
|
const logger = logUtils(__filename, 'bridge:server');
|
|
|
|
/**
|
|
* Environment defines. Use to change endpoints automatically in other sections of the code.
|
|
* This is global
|
|
*/
|
|
global.DEPLOYMENT_ENVS = ['AWS', 'Bluemix', 'Azure', 'Flexiion', 'Local'];
|
|
global.CURRENT_DEPLOYMENT_ENV = 'Azure'; // Sets the default environment.
|
|
|
|
/**
|
|
* Parse the command line using minimist to find command line parameters.
|
|
* Valid command line parameters are:
|
|
* <dl>
|
|
* <dt>--path <pathPrefix></dt><dd>Prefix for the javascript modules path. Defaults to /node_server/ComServe/</dd>
|
|
* <dt>--config <configFile></dt><dd>Path to the config file. Defaults to <pathPrefix>/ComServe/config.js</dd>
|
|
* <dt>--env <deploymentEnv></dt><dd>Deployment environment switch to change end points. Defaults to 'Azure'</dd>
|
|
* </dl>
|
|
* @type {object}
|
|
*/
|
|
const opts = {
|
|
string: ['path', 'config', 'env'],
|
|
alias: {
|
|
path: ['p'],
|
|
env: ['e']
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Use the opts above to parse the command line, and store the parameters in argv
|
|
* @type object
|
|
*/
|
|
const argv = require('minimist')(process.argv.slice(2), opts);
|
|
|
|
/**
|
|
* If the env property is present, change the environment to the requested one. This is taken from the command line.
|
|
* Most of the environmental differences are endpoints.
|
|
*/
|
|
if (argv.hasOwnProperty('env')) {
|
|
if (global.DEPLOYMENT_ENVS.indexOf(argv.env) < 0) {
|
|
console.log('\nBad deployment environment. Options are: ' + JSON.stringify(global.DEPLOYMENT_ENVS));
|
|
process.exit(exitCodes.EXIT_CODE_NO_ENVIRONMENT);
|
|
} else {
|
|
global.CURRENT_DEPLOYMENT_ENV = argv.env;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change default paths based on environment.
|
|
* Note that adding the command line switch overrides everything else.
|
|
*/
|
|
if (argv.hasOwnProperty('path')) {
|
|
global.rootPath = argv.path;
|
|
} else {
|
|
switch (global.CURRENT_DEPLOYMENT_ENV) {
|
|
case 'Azure':
|
|
global.rootPath = '/home/comcardeadmin/node_server/';
|
|
break;
|
|
case 'Bluemix':
|
|
global.rootPath = '/node_server/';
|
|
break;
|
|
case 'Flexiion':
|
|
global.rootPath = '/home/flexops/node_server/';
|
|
break;
|
|
default:
|
|
global.rootPath = path.join(__dirname, '/'); // Expected to end with a '/'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store the command line parameters. This will also normalise the path to the OS.
|
|
*/
|
|
global.pathPrefix = global.rootPath + 'ComServe/';
|
|
if (argv.hasOwnProperty('config')) {
|
|
global.configFile = argv.config;
|
|
} else {
|
|
global.configFile = global.rootPath + 'ComServe/config.js';
|
|
}
|
|
global.rootPath = path.normalize(global.rootPath);
|
|
global.pathPrefix = path.normalize(global.pathPrefix);
|
|
global.configFile = path.normalize(global.configFile);
|
|
|
|
/**
|
|
* Log what startup params we are using.
|
|
*/
|
|
console.log('\nLoading Bridge Node Server config files...');
|
|
console.log('Source Path Prefix:', global.pathPrefix);
|
|
console.log('ConfigFile:', global.configFile);
|
|
|
|
/**
|
|
* Load the basic files. Always config first.
|
|
*/
|
|
let config;
|
|
let utils;
|
|
try {
|
|
// eslint-disable-next-line global-require
|
|
config = require(global.configFile);
|
|
// eslint-disable-next-line global-require
|
|
utils = require(global.pathPrefix + 'utils.js');
|
|
} catch (error) {
|
|
console.log('Unable to load configuration files: ' + error);
|
|
process.exit(exitCodes.EXIT_CODE_CONFIG_FILE_ERROR);
|
|
}
|
|
|
|
/**
|
|
* Print server information.
|
|
*/
|
|
console.log(utils.CarriageReturn + 'COMCARDE BRIDGE NODE SERVER (' + config.CCServerName + ', VIP ' + config.CCServerIP + ')');
|
|
console.log(global.CURRENT_DEPLOYMENT_ENV + ' Deployment (' + config.CCServerGroup + ', UUID ' + config.CCUUID + ')');
|
|
console.log('Config: https://' + config.CCWebsiteAddress + ', ' + config.CCServerReleaseType + ' V' + config.CCServerVersion +
|
|
' (Node: ' + config.ServerCommit + ', Portal: ' + config.PortalCommit + ').');
|
|
|
|
/**
|
|
* Load the rest of the include files.
|
|
*/
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const express = require('express');
|
|
const helmet = require('helmet');
|
|
const async = require('async');
|
|
const url = require('url');
|
|
const querystring = require('querystring');
|
|
|
|
const mainDB = require(global.pathPrefix + 'mainDB.js');
|
|
const log = require(global.pathPrefix + 'log.js');
|
|
const sms = require(global.pathPrefix + 'sms.js');
|
|
const hJSON = require(global.pathPrefix + 'hJSON.js');
|
|
const credorax = require(global.pathPrefix + 'credorax.js');
|
|
const worldpay = require(global.pathPrefix + 'worldpay.js');
|
|
const rateLimit = require(global.pathPrefix + 'rate_limit.js');
|
|
const migrations = require(global.pathPrefix + 'migrations.js');
|
|
|
|
/**
|
|
* Load default images.
|
|
*/
|
|
try {
|
|
let inputImage;
|
|
inputImage = fs.readFileSync(global.rootPath + 'WebApp/defaultSelfie.png');
|
|
config.defaultSelfieData = Buffer.from(inputImage).toString('base64');
|
|
inputImage = fs.readFileSync(global.rootPath + 'WebApp/defaultCompanyLogo0.png');
|
|
config.defaultCompanyLogo0Data = Buffer.from(inputImage).toString('base64');
|
|
console.log('Default image data loaded for \'defaultSelfie\' and \'CompanyLogo0\'');
|
|
} catch (error) {
|
|
console.log('Unable to load default images: ' + error);
|
|
process.exit(exitCodes.EXIT_CODE_NO_DEFAULT_IMAGES);
|
|
}
|
|
|
|
/**
|
|
* Note whether verbose mode is on or off.
|
|
*/
|
|
if (log.verbose) {
|
|
console.log('Verbose mode is on. Events, warnings and errors will be written to stdout.' + utils.CarriageReturn);
|
|
} else {
|
|
console.log('Verbose mode is off. Only errors will be written to stdout.' + utils.CarriageReturn);
|
|
}
|
|
|
|
/**
|
|
* System state defines and web server configuration. Since offload to load balancer, the code only uses HTTP.
|
|
* The SSL certificate has been offloaded to the balancer for performance reasons.
|
|
*/
|
|
const startupServices = {
|
|
httpOnline: 1
|
|
};
|
|
|
|
/**
|
|
* Load and pre-compile the templates
|
|
*/
|
|
const templates = require(global.pathPrefix + '../utils/templates.js');
|
|
templates.initTemplates();
|
|
|
|
/**
|
|
* Web server defines.
|
|
*/
|
|
const verboseWebServer = 1; // Additional web server logging for debug.
|
|
const serverHTTPport = config.serverHttpPort; // Will be redirected to HTTPS.
|
|
|
|
const rootServerDirectory = global.rootPath + 'WebApp';
|
|
const rootPortalDirectory = global.rootPath + 'portal/';
|
|
let filesServed = 0;
|
|
const longTickTime = 15 * 12; // Change this value for the long tick return. Multiply by 5 seconds for real time.
|
|
let longTick = longTickTime;
|
|
|
|
/**
|
|
* Define the mongodb configuration parameters.
|
|
*/
|
|
let cert;
|
|
let key;
|
|
let mongoConnectOptions = {};
|
|
if (config.mongoUseSSL) {
|
|
const certPath = path.join(
|
|
__dirname,
|
|
config.mongoCACertBase64
|
|
);
|
|
cert = fs.readFileSync(certPath);
|
|
key = fs.readFileSync(certPath);
|
|
mongoConnectOptions = {
|
|
ssl: true,
|
|
sslKey: key,
|
|
sslCert: cert
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Connect to the mongodb server and open the collections.
|
|
* Function takes no parameters.
|
|
*/
|
|
function startupDatabase() {
|
|
// eslint-disable-next-line no-negated-condition
|
|
if (!mainDB.dbOnline) {
|
|
try {
|
|
/**
|
|
* Attempt to open the database connection.
|
|
*/
|
|
mainDB.MClient.connect(
|
|
mainDB.dbAddress,
|
|
mongoConnectOptions,
|
|
(err, db) => {
|
|
if (err) {
|
|
if (!utils.systemState.dbWaiting) {
|
|
log.system(
|
|
'CRITICAL',
|
|
('Could not connect to primary database. ' + JSON.stringify(err) + ' Retrying every 5 seconds...'),
|
|
'node_server.startupDatabase',
|
|
'',
|
|
'System',
|
|
'127.0.0.1');
|
|
utils.systemState.dbWaiting = 1;
|
|
}
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Connect to collections.
|
|
*/
|
|
mainDB.mdb = db;
|
|
if (mainDB.mdb) {
|
|
mainDB.collectionAccount = mainDB.mdb.collection(mainDB.dbAccount);
|
|
mainDB.collectionAccountArchive = mainDB.mdb.collection(mainDB.dbAccountArchive);
|
|
mainDB.collectionPaymentInstrument = mainDB.mdb.collection(mainDB.dbPaymentInstrument);
|
|
mainDB.collectionPaymentInstrumentArchive = mainDB.mdb.collection(mainDB.dbPaymentInstrumentArchive);
|
|
mainDB.collectionAddresses = mainDB.mdb.collection(mainDB.dbAddresses);
|
|
mainDB.collectionAddressArchive = mainDB.mdb.collection(mainDB.dbAddressArchive);
|
|
mainDB.collectionBridgeLogin = mainDB.mdb.collection(mainDB.dbBridgeLogin);
|
|
mainDB.collectionClient = mainDB.mdb.collection(mainDB.dbClient);
|
|
mainDB.collectionClientArchive = mainDB.mdb.collection(mainDB.dbClientArchive);
|
|
mainDB.collectionDevice = mainDB.mdb.collection(mainDB.dbDevice);
|
|
mainDB.collectionDeviceArchive = mainDB.mdb.collection(mainDB.dbDeviceArchive);
|
|
mainDB.collectionImages = mainDB.mdb.collection(mainDB.dbImages);
|
|
mainDB.collectionItems = mainDB.mdb.collection(mainDB.dbItems);
|
|
mainDB.collectionMessages = mainDB.mdb.collection(mainDB.dbMessages);
|
|
mainDB.collectionMessagesArchive = mainDB.mdb.collection(mainDB.dbMessagesArchive);
|
|
mainDB.collectionPayCode = mainDB.mdb.collection(mainDB.dbPayCode);
|
|
mainDB.collectionSystemLog = mainDB.mdb.collection(mainDB.dbLog);
|
|
mainDB.collectionTransaction = mainDB.mdb.collection(mainDB.dbTransaction);
|
|
mainDB.collectionTransactionArchive = mainDB.mdb.collection(mainDB.dbTransactionArchive);
|
|
mainDB.collectionTransactionHistory = mainDB.mdb.collection(mainDB.dbTransactionHistory);
|
|
mainDB.collectionTwoFARequests = mainDB.mdb.collection(mainDB.dbTwoFARequests);
|
|
mainDB.collectionActivityLog = mainDB.mdb.collection(mainDB.dbActivityLog);
|
|
|
|
/**
|
|
* Test the database connection. Set up a test log entry.
|
|
*/
|
|
const logData = {};
|
|
logData.DateTime = new Date();
|
|
logData.ServerID = config.CCServerName + ' (VIP ' + config.CCServerIP + ')';
|
|
logData.Class = 'STARTUP';
|
|
logData.Function = 'node_server.startupDatabase';
|
|
logData.Code = '';
|
|
logData.Info = 'SERVER ONLINE: ' + config.CCServerName + ', Config: https://' + config.CCWebsiteAddress + ', ' +
|
|
config.CCServerReleaseType + ' V' + config.CCServerVersion + ' (Node: ' + config.ServerCommit + ', Portal: ' +
|
|
config.PortalCommit + ').';
|
|
logData.User = 'System';
|
|
logData.Source = '127.0.0.1';
|
|
|
|
/**
|
|
* Write the new log entry.
|
|
*/
|
|
mainDB.dbOnline = 1;
|
|
utils.systemState.dbWaiting = 0;
|
|
// eslint-disable-next-line no-shadow
|
|
mainDB.addObject(mainDB.collectionSystemLog, logData, undefined, false, (err) => {
|
|
if ((err) || (mainDB.dbOnline === 0)) { // Unable to store info.
|
|
log.system(
|
|
'CRITICAL',
|
|
'Database connection test failed. Will retry in 5 seconds.',
|
|
'node_server.startupDatabase',
|
|
'',
|
|
'System',
|
|
'127.0.0.1');
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Success. Database online.
|
|
*/
|
|
log.system(
|
|
'STARTUP',
|
|
('Connected to requested primary database ' + config.mongoDBAddress),
|
|
'node_server.startupDatabase',
|
|
'',
|
|
'System',
|
|
'127.0.0.1');
|
|
|
|
/**
|
|
* Update the logger utils to connect to this db instance
|
|
*/
|
|
logUtils.init.initMongoTransport(db);
|
|
logger.info(
|
|
{}, // No request
|
|
'Connected to requested primary database',
|
|
{
|
|
dbAddress: config.mongoDBAddress
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Scan the database if required - this runs in the background using a cursor.
|
|
* It is suggested that this is run for deployment of new versions then subsequently disabled.
|
|
*/
|
|
if (config.databaseUpdate) {
|
|
mainDB.updateDatabase();
|
|
}
|
|
if (config.migrateEmailToID) {
|
|
migrations.migrateClientNameToID();
|
|
}
|
|
|
|
/**
|
|
* Get the current number of text messages that are left.
|
|
*/
|
|
const tempSMSTestMode = sms.smsTestMode;
|
|
sms.smsTestMode = true;
|
|
sms.sendSMS(null, sms.adminMobile, (config.CCServerName + ' startup complete.'),
|
|
// eslint-disable-next-line no-shadow
|
|
(err, smsBalance) => {
|
|
if (err) {
|
|
sms.smsTestMode = tempSMSTestMode;
|
|
log.system(
|
|
'CRITICAL',
|
|
('Cannot send SMS or connect to SMS server. ' + err),
|
|
'node_server.startupDatabase',
|
|
'',
|
|
'System',
|
|
'127.0.0.1');
|
|
return;
|
|
}
|
|
sms.smsTestMode = tempSMSTestMode;
|
|
if (50 < smsBalance) {
|
|
log.system(
|
|
'STARTUP',
|
|
('Successfully connected to TextLocal (SMS balance is ' +
|
|
smsBalance + ').'),
|
|
'node_server.startupDatabase',
|
|
'',
|
|
'System',
|
|
'127.0.0.1');
|
|
} else {
|
|
log.system(
|
|
'WARNING',
|
|
('Successfully connected to TextLocal but balance is low (' +
|
|
smsBalance + ' remaining).'),
|
|
'node_server.startupDatabase',
|
|
'',
|
|
'System',
|
|
'127.0.0.1');
|
|
}
|
|
});
|
|
});
|
|
} else { // Error connecting to collections. Force shutdown.
|
|
log.system(
|
|
'CRITICAL',
|
|
'Could not open collections. Please contact the administrator to ensure they are set up.',
|
|
'node_server.startupDatabase',
|
|
'',
|
|
'System',
|
|
'127.0.0.1');
|
|
utils.systemState.shutdownTick = 2;
|
|
}
|
|
});
|
|
} catch (error) {
|
|
log.system(
|
|
'WARNING',
|
|
('Database still attempting to connect. ' + error),
|
|
'node_server.startupDatabase',
|
|
'',
|
|
'System',
|
|
'127.0.0.1');
|
|
}
|
|
} else {
|
|
log.system(
|
|
'ERROR',
|
|
'Erroneous call to startupDatabase() ignored as database is already online.',
|
|
'node_server.startupDatabase',
|
|
'',
|
|
'System',
|
|
'127.0.0.1');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* First fast timeout to start up the system.
|
|
*/
|
|
setTimeout(systemCheck, 100);
|
|
|
|
/**
|
|
* System check watchdog. Runs every 5 seconds and is used to manage shutdown.
|
|
* Can also be used for general housekeeping.
|
|
*
|
|
* @type {function} systemCheck
|
|
*
|
|
* Need to ignore eslint complexity warnings here:
|
|
*/
|
|
// eslint-disable-next-line complexity
|
|
function systemCheck() {
|
|
/**
|
|
* The shutdown tick allows housekeeping before shutdown.
|
|
*/
|
|
if (utils.systemState.shutdownTick === -1) {
|
|
// Check the database state.
|
|
if (!mainDB.dbOnline) {
|
|
/**
|
|
* Database is not online. Figure out why.
|
|
*/
|
|
if (utils.systemState.firstTime) {
|
|
/**
|
|
* OK, just starting up for the first time. Connect to database.
|
|
*/
|
|
utils.systemState.firstTime = 0;
|
|
log.system(
|
|
'STARTUP',
|
|
'Initialising database and web servers...',
|
|
'node_server.systemCheck',
|
|
'',
|
|
'System',
|
|
'127.0.0.1');
|
|
startupDatabase();
|
|
} else {
|
|
/**
|
|
* Looks like we lost connection. Try to start up again.
|
|
*/
|
|
startupDatabase();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes every 15 minutes.
|
|
*/
|
|
if (longTick === 0) {
|
|
let status = '';
|
|
|
|
/**
|
|
* HTTP services:
|
|
*/
|
|
if (startupServices.httpOnline) {
|
|
status += 'HTTP:80 Up, ';
|
|
} else {
|
|
status += 'HTTP:80 Down, ';
|
|
}
|
|
if (mainDB.dbOnline) {
|
|
status += 'MDB Up, ';
|
|
} else {
|
|
status += 'MDB Down, ';
|
|
}
|
|
status += 'WWW ' + filesServed + ', ';
|
|
status += 'JSON ' + hJSON.JSONServed + ', ';
|
|
status += 'SMS ' + sms.smsCredits + ', ';
|
|
status += 'CRX ' + credorax.primaryFailedComms + ', ';
|
|
status += 'WP ' + worldpay.primaryFailedComms + '.';
|
|
|
|
/**
|
|
* Output the status information.
|
|
*/
|
|
log.system(
|
|
'SERVER',
|
|
status,
|
|
'node_server.systemCheck',
|
|
'',
|
|
'System',
|
|
'127.0.0.1');
|
|
|
|
/**
|
|
* Reduce the number of failed comms for active acquirers.
|
|
* Credorax
|
|
*/
|
|
if (config.credoraxCurrentGateway === config.credoraxPrimaryGateway) {
|
|
if (credorax.primaryFailedComms > 0) {
|
|
credorax.primaryFailedComms -= config.credoraxChangeRate;
|
|
}
|
|
} else {
|
|
log.system(
|
|
'WARNING',
|
|
'System using secondary Credorax server.',
|
|
'node_server.systemCheck',
|
|
'',
|
|
'System',
|
|
'127.0.0.1');
|
|
}
|
|
|
|
/**
|
|
* Worldpay
|
|
*/
|
|
if (worldpay.primaryFailedComms > config.worldpayNotificationThreshold) {
|
|
log.system(
|
|
'WARNING',
|
|
'Unexpected number of communciations failures with Worldpay primary gateway.',
|
|
'node_server.systemCheck',
|
|
'',
|
|
'System',
|
|
'127.0.0.1');
|
|
}
|
|
if (worldpay.primaryFailedComms > 0) {
|
|
worldpay.primaryFailedComms -= config.worldpayChangeRate;
|
|
}
|
|
|
|
/**
|
|
* Reset the tick.
|
|
*/
|
|
longTick = longTickTime;
|
|
} else {
|
|
/**
|
|
* Do nothing other than decrement.
|
|
*/
|
|
longTick--;
|
|
}
|
|
} else if (utils.systemState.shutdownTick > 1) {
|
|
/**
|
|
* Tick set to higher than 1 - shutdown requested.
|
|
*/
|
|
utils.systemState.shutdownTick -= 1;
|
|
|
|
/**
|
|
* Close off the servers.
|
|
*/
|
|
startupServices.httpOnline = 0;
|
|
|
|
/**
|
|
* Close off the database.
|
|
*/
|
|
if (mainDB.dbOnline === 1) {
|
|
/**
|
|
* Database is online. Log shutdown info.
|
|
*/
|
|
const logData = {};
|
|
logData.DateTime = new Date();
|
|
logData.ServerID = config.CCServerName + ' (VIP ' + config.CCServerIP + ')';
|
|
logData.Class = 'SHUTDOWN';
|
|
logData.Function = 'node_server.systemCheck';
|
|
logData.Code = '';
|
|
logData.Info = 'Servers offline. Database shutdown in progress...';
|
|
logData.User = 'System';
|
|
logData.Source = '127.0.0.1';
|
|
console.log('[' + logData.DateTime.toISOString() + '] ' + logData.Class + ': ' + logData.Info);
|
|
|
|
/**
|
|
* Add the info to the log.
|
|
*/
|
|
mainDB.collectionSystemLog.insert(logData, (err) => {
|
|
if (err) {
|
|
console.log('[' + String(new Date().toISOString()) +
|
|
'] ERROR: Database write error during shutdown. Shutdown complete.' + utils.CarriageReturn);
|
|
process.exit(exitCodes.EXIT_CODE_DATABASE_WRITE_ERROR);
|
|
} else {
|
|
/**
|
|
* Close off database.
|
|
*/
|
|
// eslint-disable-next-line no-shadow
|
|
mainDB.mdb.close((err) => {
|
|
if (err) {
|
|
console.log('[' + String(new Date().toISOString()) +
|
|
'] ERROR: Could not correctly close database. Shutdown complete.' +
|
|
utils.CarriageReturn);
|
|
process.exit(exitCodes.EXIT_CODE_DATABASE_NOT_CLOSED);
|
|
} else {
|
|
console.log('[' + String(new Date().toISOString()) +
|
|
'] SHUTDOWN: Cleanup complete. Exiting process.' + utils.CarriageReturn);
|
|
process.exit(exitCodes.EXIT_CODE_SUCCESS);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
console.log('[' + String(new Date().toISOString()) +
|
|
'] ERROR: Database unexpectedly offline. Shutdown complete.' + utils.CarriageReturn);
|
|
process.exit(exitCodes.EXIT_CODE_DATABASE_OFFLINE);
|
|
}
|
|
} else if (utils.systemState.shutdownTick === 1) {
|
|
/**
|
|
* Give the database time to shut down.
|
|
*/
|
|
console.log('[' + String(new Date().toISOString()) +
|
|
'] SHUTDOWN: Waiting for database shutdown. 5 seconds until forced termination...');
|
|
utils.systemState.shutdownTick -= 1;
|
|
} else if (utils.systemState.shutdownTick === 0) {
|
|
/**
|
|
* Shut down anyway.
|
|
*/
|
|
console.log('[' + String(new Date().toISOString()) +
|
|
'] WARNING: Node server shutdown forced. Database may not have been properly closed.' + utils.CarriageReturn);
|
|
process.exit(exitCodes.EXIT_CODE_FORCED_SHUTDOWN);
|
|
}
|
|
|
|
/**
|
|
* Set five second tick.
|
|
*/
|
|
setTimeout(systemCheck, 5000);
|
|
}
|
|
|
|
/**
|
|
* Simple web server functionality.
|
|
* For reasons to do with the way Express leaves ports open, a custom web server is used in this instance.
|
|
*
|
|
* @param {!object} req - The Mongo collection in which the object exists.
|
|
* @param {!object} req.connection - Detail about the connection.
|
|
* @param {!object} res - The search parameters for the object(s) to delete in JSON format.
|
|
* @param {!function} res.writeHead - Write the response header.
|
|
* @param {!function} res.end - Return the header.
|
|
* @param {!string} remoteAddress - the remote address the request is made from
|
|
* @param {!string} protocolPort - Protocol followed by incoming port e.g. 'HTTP:80'.
|
|
* @param {!string} location - Optional callback for async operation.
|
|
*/
|
|
function serveFile(req, res, remoteAddress, protocolPort, location) {
|
|
/**
|
|
* Default behaviour is to look for a file to send.
|
|
*/
|
|
const filename = location;
|
|
filesServed++;
|
|
|
|
/**
|
|
* Check for a null.
|
|
*/
|
|
// eslint-disable-next-line no-negated-condition
|
|
if (filename.indexOf('\0') !== -1) {
|
|
log.system(
|
|
'ATTACK',
|
|
'Null byte in path rejected.',
|
|
'node_server.serveFile',
|
|
'',
|
|
'UU',
|
|
(remoteAddress + ' (' + protocolPort + ')'));
|
|
res.sendStatus(400);
|
|
} else {
|
|
/**
|
|
* Check for someone trying to escape the root directory.
|
|
*/
|
|
const normalizedFile = path.normalize((rootServerDirectory + filename));
|
|
const normalizedRootDir = path.normalize(rootServerDirectory);
|
|
// eslint-disable-next-line no-negated-condition
|
|
if (normalizedFile.indexOf(normalizedRootDir) !== 0) {
|
|
log.system(
|
|
'ATTACK',
|
|
'Directory traversal rejected.',
|
|
'node_server.serveFile',
|
|
'',
|
|
'UU',
|
|
(remoteAddress + ' (' + protocolPort + ')'));
|
|
res.sendStatus(403);
|
|
} else {
|
|
/**
|
|
* All good. Serve the file.
|
|
*/
|
|
async.series([
|
|
function(callback) {
|
|
fs.readFile(normalizedFile, (err, data) => {
|
|
if (err) {
|
|
/**
|
|
* Error reading file. Pass error forward.
|
|
*/
|
|
log.system(
|
|
'WARNING',
|
|
('404 File not found. [' + normalizedFile + ']'),
|
|
'node_server.serveFile',
|
|
'',
|
|
'UU',
|
|
(remoteAddress + ' (' + protocolPort + ')'));
|
|
return callback(err);
|
|
} else {
|
|
/**
|
|
* Read successfully.
|
|
*/
|
|
if (verboseWebServer) {
|
|
log.system(
|
|
'FILE',
|
|
'File returned [' + normalizedFile + ']',
|
|
'node_server.serveFile',
|
|
'',
|
|
'UU',
|
|
(remoteAddress + ' (' + protocolPort + ')'));
|
|
}
|
|
|
|
/**
|
|
* Deal with extensions.
|
|
*/
|
|
switch (path.extname(filename)) {
|
|
case '.png':
|
|
res.writeHead(200, {'Content-Type': 'image/png'});
|
|
break;
|
|
default:
|
|
res.writeHead(200);
|
|
}
|
|
|
|
/**
|
|
* Fill with the rest of the data. Watch for zero length files.
|
|
*/
|
|
if (data) {
|
|
res.end(data);
|
|
} else {
|
|
res.end();
|
|
}
|
|
return callback();
|
|
}
|
|
});
|
|
}],
|
|
|
|
/**
|
|
* Final clause which is executed after everything else or when an error is detected.
|
|
*/
|
|
(err) => {
|
|
if (err) {
|
|
res.sendStatus(404);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Web Server elements. Processes an HTTP request, be it JSON or HTTP.
|
|
*
|
|
* @param {!object} req - The Mongo collection in which the object exists.
|
|
* @param {!object} req.connection - Detail about the connection.
|
|
* @param {!object} req.url - Detail about the requested url.
|
|
* @param {!string[]} req.headers - The headers in the request packet.
|
|
* @param {!object} res - The search parameters for the object(s) to delete in JSON format.
|
|
* @param {!function} res.writeHead - Write the response header.
|
|
* @param {!function} res.end - Return the header.
|
|
* @param {!function} res.setHeader - Sets the response header.
|
|
* @param {!string} remoteAddress - the remote address the request is made from
|
|
* @param {!string} protocolPort - Protocol followed by incoming port e.g. 'HTTP:80'
|
|
*/
|
|
function processRequest(req, res, remoteAddress, protocolPort) {
|
|
try {
|
|
/**
|
|
* Parse the URL in case anything needs to be removed.
|
|
*/
|
|
const currentUrl = url.parse(req.url);
|
|
|
|
/**
|
|
* Switch on path name.
|
|
*/
|
|
switch (currentUrl.pathname.toUpperCase()) {
|
|
case '/SERVER_POST': // Use this for JSON requests. All requests should use one of the two.
|
|
hJSON.handleJSONRequest(req, res, remoteAddress, protocolPort, querystring.parse(currentUrl.query), hJSON.REST);
|
|
break;
|
|
default:
|
|
/*
|
|
* Default action is to consider this a file request.
|
|
*/
|
|
serveFile(req, res, remoteAddress, protocolPort, currentUrl.pathname);
|
|
}
|
|
} catch (error) {
|
|
/**
|
|
* Unhandled exception.
|
|
*/
|
|
log.system(
|
|
'CRITICAL',
|
|
('Unhandled error condition. ' + error.name + ' (' + error.message + ')'),
|
|
'node_server.processRequest',
|
|
'',
|
|
'UU',
|
|
(remoteAddress + ' (' + protocolPort + ')'));
|
|
res.status(500).json({
|
|
code: -1,
|
|
info: 'Unexpected server error'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* HTTP server (80).
|
|
*/
|
|
const appHttp = express();
|
|
const serverHTTP = http.createServer(appHttp);
|
|
|
|
/**
|
|
* Set up an error handler
|
|
*/
|
|
serverHTTP.on('error', (err) => {
|
|
log.system(
|
|
'CRITICAL',
|
|
String(err),
|
|
'node_server.serverHTTP.on',
|
|
'',
|
|
'UU',
|
|
'127.0.0.1');
|
|
});
|
|
|
|
/**
|
|
* Next start up the listener.
|
|
*/
|
|
serverHTTP.listen(serverHTTPport);
|
|
serverHTTP.timeout = utils.webTimeout;
|
|
|
|
/*
|
|
* Security related settings
|
|
* See https://www.npmjs.com/package/helmet for more on why we need these
|
|
*/
|
|
const ninetyDaysInS = 90 * 24 * 60 * 60;
|
|
appHttp.use(helmet.frameguard({action: 'deny'})); // Protect against click-jacking
|
|
appHttp.use(helmet.xssFilter()); // Browser internal xss protection
|
|
if (config.useHTTPS) {
|
|
appHttp.use(helmet.hsts({ // Request *subsequent* browser visits use https
|
|
maxAge: ninetyDaysInS // for the next 90 days (not enforceable).
|
|
}));
|
|
}
|
|
appHttp.use(helmet.hidePoweredBy()); // Hide the "x-powered-by: Express" header
|
|
appHttp.use(helmet.ieNoOpen()); // IE specific issue
|
|
appHttp.use(helmet.noSniff()); // Prevent dynamic mime type "sniffing"
|
|
appHttp.set('trust proxy', config.CCServerIP); // Sets the proxy up correctly which is required for containers.
|
|
|
|
/**
|
|
* Load the swagger API router to handle `/api/*` routes
|
|
*/
|
|
const initConsoleApi = require('./swagger_api/api_server.js');
|
|
|
|
/**
|
|
* Load the integration API router to handle `/int/*` routes
|
|
*/
|
|
const initIntegrationApi = require('./integration_api/int_api_server.js');
|
|
|
|
const integrationApiRouter = initIntegrationApi.init();
|
|
appHttp.use('/int', integrationApiRouter);
|
|
|
|
/**
|
|
* Load the dev API router to handle `/dev/*` routes
|
|
*/
|
|
const initDevApi = require('./dev_api/dev_server.js');
|
|
|
|
const devApiRouter = initDevApi.init();
|
|
appHttp.use('/dev', devApiRouter);
|
|
|
|
/*
|
|
* Load the router to serve the web console from /portal/
|
|
*/
|
|
const portalRouterFactory = require('./portal-router.js');
|
|
|
|
const portalRouter = portalRouterFactory(rootPortalDirectory);
|
|
appHttp.use('/portal', portalRouter);
|
|
|
|
/*
|
|
* Redirect any calls to '/' to the portal.
|
|
*/
|
|
appHttp.get('/', (req, res) => {
|
|
res.redirect('/portal/login');
|
|
});
|
|
|
|
/*
|
|
* Load the router to serve the metrics from /metrics/
|
|
*/
|
|
const promRouterFactory = require('./prometheus-router.js');
|
|
|
|
const promRouter = promRouterFactory();
|
|
appHttp.use('/metrics', promRouter);
|
|
|
|
/**
|
|
* Enable rate limits for the other paths
|
|
*/
|
|
rateLimit.enableLimits(appHttp);
|
|
|
|
/*
|
|
* Load the swagger definitions of the API. This is asynchronous, but must be
|
|
* loaded before setting up the processRequest handler.
|
|
*/
|
|
(async () => {
|
|
const consoleApiRouter = await initConsoleApi(mainDB.dbAddress,
|
|
mongoConnectOptions,
|
|
'WebConsoleSessions');
|
|
appHttp.use('/api', consoleApiRouter);
|
|
|
|
/**
|
|
* Route everything else to the processRequest handlers.
|
|
*/
|
|
appHttp.all('*', (req, res) => {
|
|
if (startupServices.httpOnline && utils.isLBHTTPS(req, res)) {
|
|
/**
|
|
* Different firewall headers depending on the source of the data.
|
|
* To get in to this code the services have been called from a trusted proxy.
|
|
* Technically the protocolPort should always be 'HTTPS:443' if the code has
|
|
* reached here, but it is taken from the headers if available for verification.
|
|
*/
|
|
let remoteAddress;
|
|
let protocolPort;
|
|
switch (global.CURRENT_DEPLOYMENT_ENV) {
|
|
case 'Azure':
|
|
remoteAddress = req.ip.split(':')[0];
|
|
protocolPort = req.protocol + ':' + req.headers['x-forwarded-port'];
|
|
break;
|
|
case 'Bluemix':
|
|
remoteAddress = req.headers.$wsra;
|
|
protocolPort = req.headers.$wssc + ':' + req.headers.$wssp;
|
|
break;
|
|
case 'Flexiion':
|
|
default:
|
|
remoteAddress = req.ip;
|
|
protocolPort = req.protocol + ':443';
|
|
}
|
|
|
|
/**
|
|
* Process the request.
|
|
*/
|
|
processRequest(req, res, remoteAddress, protocolPort);
|
|
}
|
|
});
|
|
})();
|
|
|
|
/**
|
|
* Indicate startup is complete.
|
|
*/
|
|
if (startupServices.httpOnline) {
|
|
log.system(
|
|
'STARTUP',
|
|
('HTTP server listening on port ' + serverHTTPport + '.'),
|
|
'node_server.serverHTTP',
|
|
'',
|
|
'System',
|
|
'127.0.0.1');
|
|
} else {
|
|
log.system(
|
|
'WARNING',
|
|
('HTTP server attached to port ' + serverHTTPport + ' but service is disabled.'),
|
|
'node_server.serverHTTP',
|
|
'',
|
|
'System',
|
|
'127.0.0.1');
|
|
}
|