'use strict';
const crypto = require("crypto");
const { CONSTANTS } = require("../common/constants");
const { httpClient } = require("../common/http");
const logger = console;
/**
* utility function to perform webhook signature verification
* @function module:Util/Webhook.verifyMessageFormat
* @return {boolean} true if the webhook message received from Bots is verified successfully.
* @param {string} signature - signature included in the bot message, to be compared to calculated signature.
* @param {Buffer} msgBody - raw message body of the bot message.
* @param {string} encoding - encoding of the raw message body.
* @param {string} secretKey - secretKey used to calculate message signature
* @example
* if (webhookUtil.verifyMessageFromBot(req.get('X-Hub-Signature'), req.rawBody, req.encoding, channelSecretKey)) {
* res.sendStatus(200);
* } else {
* res.sendStatus(403);
* }
*/
function verifyMessageFromBot(signature, msgBody, encoding, secretKey) {
if (!signature || !msgBody || !secretKey) {
return false;
}
const calculatedSig = buildSignatureHeader(msgBody, secretKey, encoding);
if (signature !== calculatedSig) {
return false;
}
return true;
}
/**
* utility function for use with expressjs route in handling the raw message body of the webhook message received from bot.
* Instead of just letting bodyParser.json to parse the raw message to JSON, the rawMessage and its encoding is saved as properties
* 'rawBody' and 'encoding' for use in signature verification in method verifyMessageFormat.
* @function module:Util/Webhook.bodyParserRawMessageVerify
* @param {object} req - expressjs req for the POST route.
* @param {object} res - expressjs res for the POST route.
* @param {Buffer} buf - the raw message body.
* @param {string} encoding - encoding of the raw message body.
* @example
* app.post('/webhook/messages',
* bodyParser.json({
* verify: webhookUtil.bodyParserRawMessageVerify
* }),
* function (req, res) {
* // request body is now available in req.rawBody, req.encoding is also set
* }
* );
*/
function bodyParserRawMessageVerify(req, res, buf, encoding) {
if (req) {
req[CONSTANTS.PARSER_RAW_BODY] = buf;
req[CONSTANTS.PARSER_RAW_ENCODING] = encoding;
}
}
/**
* create the payload signature header.
* @function module:Util/Webhook.buildSignatureHeader
* @param {Buffer} buf - Raw payload as a Buffer, such as `Buffer.from(JSON.stringify(payload), 'utf8')`
* @param {string} secret - secret key of the channel for computing signature
* @param {string} [encoding] - secret key of the channel for computing signature
*/
function buildSignatureHeader(buf, secret, encoding) {
return 'sha256=' + buildSignature(buf, secret, encoding);
}
function buildSignature(buf, secret, encoding) {
const hmac = crypto.createHmac('sha256', Buffer.from(secret || '', encoding || 'utf8'));
if (buf) {
hmac.update(buf);
}
return hmac.digest('hex');
}
/**
* utility function to send message to bot webhook channel, generating the right message with signature
* @function module:Util/Webhook.messageToBot
* @param {string} channelUrl - send the message to this channel url
* @param {string} channelSecretKey - secret key of the channel for computing message signature.
* @param {string} userId - userId is the sender of the message.
* @param {object|string} inMsg - message to be sent to bot
* @param {function} callback - callback function to be invoked after message is sent
* @deprecated use {@link module:Util/Webhook.messageToBotWithProperties} instead
*/
function messageToBot(channelUrl, channelSecretKey, userId, inMsg, callback) {
logger.warn("messageToBot() is deprecated in favor of messageToBotWithProperties()");
messageToBotWithProperties(channelUrl, channelSecretKey, userId, inMsg, null, callback);
}
/**
* utility function to send message to bot webhook channel, generating the right message with signature. This function also allows additional
* properties to be sent along to the bot. A common use case is to add a profile property.
* @function module:Util/Webhook.messageToBotWithProperties
* @param {string} channelUrl - send the message to this channel url
* @param {string} channelSecretKey - secret key of the channel for computing message signature.
* @param {string} userId - userId is the sender of the message.
* @param {object|string} inMsg - message to be sent to bot
* @param {object} [additionalProperties] - additional properties like profile can be added
* @param {function} callback - callback function to be invoked after message is sent
* @example
* webhookUtil.messageToBotWithProperties(
* channelUrl,
* channelSecretKey,
* userId,
* messagePayload,
* {
* "profile": {
* "firstName": 'John',
* "lastName": 'Smith'
* "age": 22,
* "clientType": 'Alexa'
* }
* },
* function (err) {
* if (err) {
* logger.warn("Failed sending message to Bot");
* }
* }
* );
*/
function messageToBotWithProperties(channelUrl, channelSecretKey, userId, inMsg, additionalProperties, callback) {
if (!channelUrl) {
callback(new Error('Channel URL is required'));
return;
}
if (!channelSecretKey) {
callback(new Error('Channel Secret Key is required'));
return;
}
if (!userId) {
callback(new Error('userId is required'));
return;
}
var outMsg = {
userId: userId,
};
outMsg.messagePayload = inMsg;
if (additionalProperties) {
outMsg = Object.assign(outMsg, additionalProperties);
}
const body = Buffer.from(JSON.stringify(outMsg), 'utf8');
const headers = {};
headers['Content-Type'] = 'application/json; charset=utf-8';
headers[CONSTANTS.WEBHOOK_HEADER] = buildSignatureHeader(body, channelSecretKey);
// use http client to post webhook message
const request = httpClient();
request(channelUrl, {
method: 'POST',
body,
headers,
timeout: 60000,
redirect: 'follow',
}).then(() => callback())
.catch(err => callback(new Error(err.message)));
}
/**
* The webhookUtil is a set of utility functions for bot integration via webhook channel.
* While most use cases are accommodated through the {@link module.Middleware.WebhookClient|WebhookClient}
* instance methods and options, direct use of these methods is also possible.
* @module Util/Webhook
*/
module.exports = {
messageToBot,
messageToBotWithProperties,
verifyMessageFromBot,
bodyParserRawMessageVerify,
buildSignatureHeader,
};