519 lines
20 KiB
JavaScript
519 lines
20 KiB
JavaScript
/* 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'
|
|
);
|
|
|
|
});
|
|
}
|