bridge-node-server/node_server/swagger_api/api_utils.js
Martin Donnelly 57bd6c8e6a init
2018-06-24 21:15:03 +01:00

264 lines
8.3 KiB
JavaScript

//
// This file contains utilities related to the web-focused API
//
'use strict';
const Q = require('q');
const _ = require('lodash');
const crypto = require('crypto');
const config = require(global.configFile);
const debug = require('debug')('webconsole-api:api_utils');
const apiSecurity = require('./api_security.js');
const utils = require(global.pathPrefix + 'utils.js');
const hashUtil = require(global.pathPrefix + '../utils/hashing.js');
const ERRORS = {
SESSION_REGEN_FAILED: 'Session Regen: Failed',
SESSION_DESTROY_FAILED: 'Session Destroy: Failed'
};
module.exports = {
initSession,
initRecoverySession,
encodePassword,
addPortAndAddress,
ERRORS
};
/**
* Initialises the session for the specified client
*
* @param {Object} req - the express request object (for session and swagger)
* @param {string} email - the email for the client
* @param {Object} data - the data to store in the session
*
* @returns {Promise} - a promise for the completion of this function
*/
function initSessionBase(req, email, data) {
debug(' - initialising session for:', email);
const defer = Q.defer();
if (data.isDeviceSession) {
//
// Device sessions are simpler, and initialised every command. Therefore
// we don't want the session middleware to be saving them to persist them.
// Therefore we prevent saving by clearing req.sessionID
//
req.sessionID = null;
//
// Still store the session data in the standard place if we were persisting
// sessions.
//
req.session.data = data;
//
// And resolve the promise
//
defer.resolve({});
} else {
//
// Webconsole sessions are full sessions
// Reset the session id (for security)
//
const dRegen = Q.defer();
req.session.regenerate((err) => {
if (err) {
debug('- failed to regenerate session: ', err);
dRegen.reject(ERRORS.SESSION_REGEN_FAILED);
} else {
//
// Save the client info into the new session for future use
//
req.session.data = data;
dRegen.resolve(req.session);
}
});
//
// Set the XSRF token as a hash of the session token, salted with
// the users email address, then send it back in the body of the
// response (as JS can't read a token from a cross site request).
//
dRegen.promise
.then((session) => {
const xsrfTokenGen = apiSecurity.generateXsrfToken(session.id, email);
return xsrfTokenGen.on('readable', () => {
const response = {};
const securityDef = req.swagger.swaggerObject.securityDefinitions;
const tokenName = securityDef.bridge_session.name;
response[tokenName] = xsrfTokenGen.read();
defer.resolve(response);
});
})
.catch((error) => {
defer.reject(error);
});
}
return defer.promise;
}
/**
* Initialises the standard session for the specified client
*
* @param {Object} req - the express request object (for session and swagger)
* @param {Object} client - the client object to build the session for
* @param {Object?} device - the device object (for device logins only)
*
* @returns {Promise} - a promise for the completion of this function
*/
function initSession(req, client, device) {
const email = client.ClientName;
//
// Client is a merchant if any of the items in the Merchant array
// have MerchantStatus === 1
// Client is VAT registered if they are both a merchant and have
// a VAT number specified.
//
const isMerchant = _.some(client.Merchant, {
MerchantStatus: 1
});
const isVATRegistered = _.some(client.Merchant, (item) => {
return item.MerchantStatus && item.VATNo;
});
const data = {
client: client._id,
clientID: client.ClientID,
email,
displayName: client.DisplayName,
isMerchant,
isVATRegistered,
FeatureFlags: client.FeatureFlags, // Case needs to match that expected by flags utils
clientObj: client,
deviceObj: device,
isDeviceSession: !_.isUndefined(device)
};
return initSessionBase(req, email, data)
.then((response) => {
//
// Update the response with the other values
//
response.emailConfirmNeeded = !utils.bitsAllSet(
client.ClientStatus,
utils.ClientEmailVerifiedMask
);
response.isMerchant = data.isMerchant;
response.isVATRegistered = data.isVATRegistered;
response.displayName = data.displayName;
if (config.EULAVersion !== client.EULAVersionAccepted) {
response.newEULA = config.EULAVersion;
}
response.featureFlags = client.FeatureFlags;
return Q.resolve(response);
});
}
/**
* Initialises a session for the recovery process.
*
* @param {Object} req - the express request object (for session and swagger)
* @param {Object} client - the client object to build the session for
*
* @returns {Promise} - a promise for the completion of this function
*/
function initRecoverySession(req, client) {
debug('Initialising recoverySession');
const email = client.ClientName;
const data = {
clientID: client.ClientID,
email,
level: apiSecurity.SESSION_TYPES.RECOVERY
};
return initSessionBase(req, email, data);
}
/**
* Encodes the password to the hash that is stored in the database. This is
* a 2 step process for the web-api:
* 1: run a sha-256 hash on the password (to match what the apps do internally
* 2: use the hashUtils to generate the full hash of this sha-256 hash
*
* @param {string} password - the password to hash
*
* @returns {promise} - a promise that resolves the hashed value and salt
*/
function encodePassword(password) {
//
// Hash the password, to match what the mobile clients do internally.
// Note, the hashing is async, so we use a promises.
//
const deferred = Q.defer();
const promise = deferred.promise;
const hasher = crypto.createHash('sha256');
hasher.setEncoding('hex');
hasher.end(password, 'utf8');
hasher.on('readable', () => {
const passwordHash = hasher.read();
deferred.resolve(passwordHash);
});
//
// Next we need to pass the password to the hashing code to get it in the
// latest format
//
return promise.then((hashedPassword) => {
return hashUtil.generateHash(Number(config.passwordCryptoVersion), hashedPassword);
});
}
//
// Dynamically adds the remoteAddress and protocolPort to the req
//
// @param {Object} req - request object, with lots of important information
// @param {Object} def - definition that we are currently being called for
//
// @param {Object} connectionData - connection data that needs to be logged
//
function addPortAndAddress(req) {
/**
* 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';
}
const connectionData = {};
connectionData.remoteAddress = remoteAddress;
connectionData.protocolPort = protocolPort;
return connectionData;
}