bridge-node-server/node_server/ComServe/hJSON.js

519 lines
20 KiB
JavaScript
Raw Permalink Normal View History

2018-06-24 20:15:03 +00:00
/* eslint-disable no-var, no-unused-vars, vars-on-top */
/* eslint-disable spaced-comment, global-require, lines-around-comment, comma-spacing */
/* eslint-disable no-use-before-define, no-useless-escape, brace-style, padded-blocks */
/* eslint-disable prefer-arrow-callback, promise/always-return, unicorn/catch-error-name */
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Comcarde Node.js JSON Handler
// Provides -Bridge- pay functionality.
// Copyright 2014-2015 Comcarde
// Written by Keith Symington
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Includes
var moment = require('moment');
var querystring = require('querystring');
var crypto = require('crypto');
var mongodb = require('mongodb');
var path = require('path');
var mainDB = require(global.pathPrefix + 'mainDB.js');
var utils = require(global.pathPrefix + 'utils.js');
var log = require(global.pathPrefix + 'log.js');
var sms = require(global.pathPrefix + 'sms.js');
var mailer = require(global.pathPrefix + 'mailer.js');
var auth = require(global.pathPrefix + 'auth.js');
var credorax = require(global.pathPrefix + 'credorax.js');
var config = require(global.configFile);
var SendReport = require(global.pathPrefix + 'hJSON/SendReport');
var validator = require(global.pathPrefix + '../schemas/validator');
// Local variables.
exports.JSONServed = 0;
exports.showPackets = true; // Use to show the incoming packet detail. This is a debug mode.
exports.REST = 1;
exports.form = 2;
/**
* List of commands that are supported by this function. Commands added to
* this list MUST:
* 1. Have a matching handler in 'ComServe/hJSON/<name>.js''
* 2. Have a matching schema for the body in 'schemas/<name>.json'
* 3. Have the approprite errorcode added to the list
*
* WARNING: due to the change in direction, ALL commands are now unsupported!
* They have been moved to `UNSUPPORTED_COMMANDS` for easy reverting if needed.
*/
const SUPPORTED_COMMANDS = {};
const UNSUPPORTED_COMMANDS = {
/**
* Login and Authorisation Commands
*/
AcceptEULA: {validationErrorCode: 292},
Authorise2FARequest: {validationErrorCode: 455},
Get2FARequest: {validationErrorCode: 453},
KeepAlive: {validationErrorCode: 123},
LogOut1: {validationErrorCode: 123},
Login1: {validationErrorCode: 149},
PINReset: {validationErrorCode: 128},
RotateHMAC: {validationErrorCode: 444},
ElevateSession: {validationErrorCode: 558},
/**
* Account Commands
*/
AddAddress: {validationErrorCode: 378},
AddCard: {validationErrorCode: 122},
ChangePIN: {validationErrorCode: 415},
ChangePassword: {validationErrorCode: 418},
DeleteAccount: {validationErrorCode: 151},
DeleteAddress: {validationErrorCode: 384},
GetTransactionDetail: {validationErrorCode: 190},
GetTransactionHistory: {validationErrorCode: 186},
ListAccounts: {validationErrorCode: 519},
ListDeletedAccounts: {
validationErrorCode: 519,
commandHandler: 'ListAccounts'
},
ListAddresses: {validationErrorCode: 376},
SetAccountAddress: {validationErrorCode: 391},
SetDefaultAccount: {validationErrorCode: 300},
/**
* Image Commands
*/
AddImage: {validationErrorCode: 217},
GetImage: {validationErrorCode: 220},
IconCache: {validationErrorCode: -1}, // Has no body, so can't fail validation
ImageCache: {validationErrorCode: 520},
ReportImage: {validationErrorCode: 223},
/**
* Invoice Commands
*/
ConfirmInvoice: {validationErrorCode: 493},
GetInvoice: {validationErrorCode: 513},
ListInvoices: {validationErrorCode: 511},
RejectInvoice: {validationErrorCode: 516},
/**
* Merchant Commands
*/
ListItems: {validationErrorCode: 471},
/**
* Message Commands
*/
DeleteMessage: {validationErrorCode: 485},
GetMessage: {validationErrorCode: 479},
ListMessages: {validationErrorCode: 477},
MarkMessage: {validationErrorCode: 482},
/**
* Payment Commands
*/
CancelPaymentRequest: {validationErrorCode: 161},
ConfirmTransaction: {validationErrorCode: 181},
GetTransactionUpdate: {validationErrorCode: 170},
PayCodeRequest: {validationErrorCode: 150},
RedeemPayCode: {validationErrorCode: 174},
RefundTransaction: {validationErrorCode: 227},
/**
* Registration Commands
*/
AddDevice: {validationErrorCode: 330},
DeleteDevice: {validationErrorCode: 363},
GetClientDetails: {validationErrorCode: 424},
ListDevices: {validationErrorCode: 361},
Register1: {validationErrorCode: 2},
Register2: {validationErrorCode: 124},
Register3: {validationErrorCode: 125},
Register4: {validationErrorCode: 126},
Register6: {validationErrorCode: 140},
Register7: {
validationErrorCode: 141,
paramsValidator: 'Register7.params'
},
Register8: {validationErrorCode: 142},
ResumeDevice: {validationErrorCode: 432},
SetClientDetails: {validationErrorCode: 422},
SetDeviceName: {validationErrorCode: 438},
SuspendDevice: {validationErrorCode: 427},
/**
* Utils functions
*/
PostCodeLookup: {validationErrorCode: 530}
};
/**
* Define where the schemas should be loaded from
*/
const SCHEMA_DIR = path.join(global.pathPrefix, '..', 'schemas');
/**
* Define where the command handlers should be loaded from
*/
const COMMAND_HANDLER_DIR = path.join(global.pathPrefix, 'hJSON');
/**
* Stores the command handlers that are loaded based on the SUPPORTED_COMMANDS
*/
var commandHandlers = {};
/**
* Loads the command handlers base on the list of supported commands
*/
function loadCommandHandlers() {
/**
* Pull in all the command handlers
*/
var commands = Object.keys(SUPPORTED_COMMANDS);
var paramsSchemas = ['defaultCommandOnly.params']; // Default unless otherwise specified
for (var i = 0; i < commands.length; ++i) {
/**
* Default command name is the same as the key
*/
var command = commands[i];
var commandHandler = command;
/**
* Some commands need a custom command handler name.
* e.g. `ListDeletedAccounts` uses `ListAccounts`
*/
var commandInfo = SUPPORTED_COMMANDS[command];
if (commandInfo && commandInfo.commandHandler) {
commandHandler = commandInfo.commandHandler;
}
/**
* `require` the command handler
*/
commandHandlers[command] = require(
path.join(COMMAND_HANDLER_DIR, commandHandler)
);
/**
* Some commands have custom parameters validators. If so, add them
* to the additional schemas list.
*/
if (commandInfo && commandInfo.paramsValidator) {
paramsSchemas.push(commandInfo.paramsValidator);
}
}
/**
* Initialise the validator with the same list of supported commands
*/
var schemas = commands.concat(paramsSchemas);
validator.initialise(schemas, config.isDevEnv, SCHEMA_DIR);
}
/**
* Call loadCommandHandlers immediately to register all the handlers we have
*/
loadCommandHandlers();
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Reads the JSON data out of the packet.
// req and res are the request and response packets.
// remoteAddress: is the source address of the incoming link.
// protocolPort is 'HTTPS:443' for example. This is a text string only that is put to the logs.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
exports.handleJSONRequest = function(req, res, remoteAddress, protocolPort, parameters, type) {
// Local objects.
var serverData = '';
var receivedObject = {};
// Reset number of JSON requests served.
exports.JSONServed++;
////////////////////////////////////////////////////////////////////////////////////////////////////
// Receipt of 'data' function.
////////////////////////////////////////////////////////////////////////////////////////////////////
var dataReceived = function(chunk) {
// Check that the maximum packet size is not above the current limit.
if (serverData.length > utils.maxPacketSize) {
// Too much data. Shut down receive methods.
req.removeListener('data', dataReceived);
req.removeListener('end', requestEnd);
// Return an error code.
res.writeHead(413, {'Content-Type': 'application/json'});
res.end(
'{\"code\":\"280\",' +
'\"info\":\"Packet too large.\"}');
log.system(
'ATTACK',
'Packet too large.',
'hJSON.handleJSONRequest',
'280',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
} else { // Overflow limit not reached. Add the data.
serverData += chunk;
}
};
////////////////////////////////////////////////////////////////////////////////////////////////////
// 'End' of data stream.
////////////////////////////////////////////////////////////////////////////////////////////////////
//jshint -W074
var requestEnd = function() {
// Protect against unhandled exceptions.
try {
// Try to parse the data to see if it is JSON.
try {
// Parse the data received. Some commands are pure rest so skip null data.
if (serverData !== '') {
if (type === exports.REST) {
receivedObject = JSON.parse(serverData);
} else if (type === exports.form) {
receivedObject = querystring.parse(serverData);
}
}
}
catch (err) {
// Unable to process querystring or body JSON.
res.writeHead(200, {'Content-Type': 'application/json'});
if (type === exports.REST) {
res.end(
'{\"code\":\"324\",' +
'\"info\":\"Invalid JSON in packet.\"}');
log.system(
'WARNING',
('Invalid JSON in packet. ' + err.name + ' (' + err.message + ') Message not logged for security reasons.'),
'hJSON.handleJSONRequest',
'324',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
} else if (type === exports.form) {
res.end(
'{\"code\":\"325\",' +
'\"info\":\"Invalid querystring.\"}');
log.system(
'WARNING',
('Invalid querystring. ' + err.name + ' (' + err.message + ') Message not logged for security reasons.'),
'hJSON.handleJSONRequest',
'325',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
}
// Return after error.
return;
}
// Detailed logging of input/output for debug purposes. Never show in the live environment.
if (exports.showPackets && config.isDevEnv) {
if (type === exports.REST) {
log.system(
'INFO',
('[IN] parameters: ' + JSON.stringify(parameters) + ' [REST IN] parsed data: ' +
JSON.stringify(receivedObject)),
'hJSON.showPackets',
'',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
} else if (type === exports.form) {
log.system(
'INFO',
('[IN] parameters: ' + JSON.stringify(parameters) + ' [FORM IN] parsed data: ' +
JSON.stringify(receivedObject)),
'hJSON.showPackets',
'',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
}
}
/**
* Create hmac object.
*/
var hmacData = {};
hmacData.address = 'https://' + req.headers.host + req.url;
hmacData.method = req.method;
hmacData.body = serverData;
if ('bridge-timestamp' in req.headers) {
hmacData.timestamp = req.headers['bridge-timestamp'];
}
if ('bridge-hmac' in req.headers) {
hmacData.hmac = req.headers['bridge-hmac'];
}
/**
* Check for unknown packets. Note there are a couple of exceptions where commands
* can be generated by non-apps.
*/
if (!(('user-agent' in req.headers) && ((req.headers['user-agent'].substr(0,6)) === 'Bridge'))) {
if ((parameters.Command !== 'Register7') &&
(parameters.Command !== 'SendReport')) {
res.status(403).json({
code: '464',
info: 'Forbidden.'
});
log.system(
'WARNING',
'Request forbidden: user-agent is not Bridge.',
'hJSON.handleJSONRequest',
'464',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
return;
}
}
/**
* Build function info.
*/
var functionInfo = {};
functionInfo.name = parameters.Command + '.process';
functionInfo.remote = remoteAddress;
functionInfo.port = protocolPort;
/**
* Check if this is a command that is handled from the dynamically
* loaded and validated commands.
*/
if (commandHandlers.hasOwnProperty(parameters.Command)) {
/**
* It is a dynamically created function so handle it here
*/
doCommand(res, functionInfo, parameters, receivedObject, hmacData);
} else {
/**
* All of the hard-coded JSON commands are examined here. The most common should be put at the top.
* Once all commands have been converted over to using JSON Schema validation we can
* remove this case.
*/
switch (parameters.Command) {
case 'SendReport':
SendReport.process(res, functionInfo, parameters);
break;
////////////////////////////////////////////////////////////////////////////////////////////////////
// Error condition - unknown commands.
////////////////////////////////////////////////////////////////////////////////////////////////////
default:
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(
'{\"code\":\"0\",' +
'\"info\":\"Unknown Command.\"}');
log.system(
'WARNING',
('Unknown \"Command:\" in url (' + parameters.Command + ')'),
'hJSON.handleJSONRequest',
'0',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
}
}
}
catch (err) {
// Processing error. Now actual error returned in message
var responseObj = {
code: '4',
info: 'Unhandled Exception - ' + err.message
};
res.status(200).json(responseObj);
log.system(
'CRITICAL',
('Unhandled Exception - ' + err.name + ' (' + err.message + ')'),
'hJSON.handleJSONRequest',
'4',
'UU',
(remoteAddress + ' (' + protocolPort + ')'));
}
};
////////////////////////////////////////////////////////////////////////////////////////////////////
// Bind events to functions.
////////////////////////////////////////////////////////////////////////////////////////////////////
req.on('data', dataReceived); // Data indicates that information is still arriving.
req.on('end', requestEnd); // End indicates that everything has been read.
};
/**
* Runs the command specified by `parameters.Command`.
* It first validates the body using JSON Schema, then calls the command handler
*
* @param {!Object} res - Response object for returning information.
* @param {!Object} functionInfo - detail on the calling function {!name, !remote, !port}
* @param {?Object} parameters - Input parameters posted on link.
* @param {?Object} receivedObject - Input parameters in message body.
* @param {!Object} hmacData - hmac information {!address, !method, !body, ?timestamp, ?hmac}
*/
function doCommand(res, functionInfo, parameters, receivedObject, hmacData) {
var commandName = parameters.Command;
var commandInfo = SUPPORTED_COMMANDS[commandName];
var paramsSchema = commandInfo.paramsValidator || 'defaultCommandOnly.params';
var bodyValidatorP = validator.validate(commandName, receivedObject);
var paramsValidatorP = validator.validate(paramsSchema, parameters);
Promise.all([bodyValidatorP, paramsValidatorP])
.then(function onValidationSucceeded() {
/**
* Validated, so run the command
*/
try {
commandHandlers[commandName].process(
res,
functionInfo,
parameters,
receivedObject,
hmacData
);
} catch (err) {
/* Processing error. Return it to the caller */
var responseObj = {
code: '4',
info: 'Unhandled exception - ' + err.message
};
auth.respond(
res,
200,
null, // Don't know what device was used
null, // Don't pass in HMAC data as we don't have a device.
functionInfo,
responseObj,
'CRITICAL'
);
}
})
.catch(function onValidationFailed(err) {
var hasErrorDetail = Array.isArray(err.errors) && err.errors.length > 0;
/**
* Failed validation, so return an error
*/
var responseObj = {
code: commandInfo.validationErrorCode ?
commandInfo.validationErrorCode.toString() :
'-1',
info: 'Invalid body in request'
};
/* Add a little more detail if we have it */
if (hasErrorDetail) {
responseObj.info =
'Invalid body' +
err.errors[0].dataPath;
}
/* If we are in dev, also return the detailed error info from the validator */
if (config.isDevEnv && hasErrorDetail) {
responseObj.info += ': ' + err.errors[0].message;
responseObj.devOnlyErrorDetail = err;
}
auth.respond(
res,
200,
null, // Don't know what device was used
null, // Don't pass in HMAC data as we don't have a device.
functionInfo,
responseObj,
'WARNING'
);
});
}