Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@seratch
Last active September 18, 2020 06:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seratch/98f4ab384b187ece90086197e24dfbd2 to your computer and use it in GitHub Desktop.
Save seratch/98f4ab384b187ece90086197e24dfbd2 to your computer and use it in GitHub Desktop.
Passing request headers to Bolt JS
const { LogLevel } = require("@slack/logger");
const { App } = require("@slack/bolt");
const { MyExpressReceiver } = require("./receiver")
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
logLevel: process.env.SLACK_LOG_LEVEL || LogLevel.DEBUG,
receiver: new MyExpressReceiver({
signingSecret: process.env.SLACK_SIGNING_SECRET
}),
});
async function moveAdditionalPropsToContext(args) {
// additionalProperties is a workaround as of @slack/bolt 2.3.0
Object.assign(args.context, args.body.additionalProperties);
delete args.body.additionalProperties;
return await args.next();
}
async function printMiddlewareArgs(args) {
const copiedArgs = JSON.parse(JSON.stringify(args));
copiedArgs.context.botToken = 'xoxb-***';
if (copiedArgs.context.userToken) {
copiedArgs.context.userToken = 'xoxp-***';
}
copiedArgs.client = {};
copiedArgs.logger = {};
args.logger.debug(
"Dumping request data for debugging...\n\n" +
JSON.stringify(copiedArgs, null, 2) +
"\n"
);
const result = await args.next();
args.logger.debug("next() call completed");
return result;
}
app.use(moveAdditionalPropsToContext);
app.use(printMiddlewareArgs);
app.event("app_mention", async ({ logger, event, say }) => {
await say({ text: `:wave: <@${event.user}> Hi there!` });
});
(async () => {
await app.start(process.env.PORT || 3000);
console.log("⚡️ Bolt app is running!");
})();
// [DEBUG] bolt-app Dumping request data for debugging...
// {
// "body": {
// "token": "verification-token",
// "team_id": "T111",
// "enterprise_id": "E013Y3SHLAY",
// "api_app_id": "A0160BH7X62",
// "event": {
// "client_msg_id": "11ea3aad-1d8e-4b7e-a7d4-b71dcc6370a5",
// "type": "app_mention",
// "text": "<@W111>",
// "user": "W111",
// "ts": "1600410693.000300",
// "team": "T111",
// "blocks": [
// {
// "type": "rich_text",
// "block_id": "CF6y",
// "elements": [
// {
// "type": "rich_text_section",
// "elements": [
// {
// "type": "user",
// "user_id": "W111"
// }
// ]
// }
// ]
// }
// ],
// "channel": "C111",
// "event_ts": "1600410693.000300"
// },
// "type": "event_callback",
// "event_id": "Ev111",
// "event_time": 1600410693,
// "authed_users": [
// "W111"
// ]
// },
// "context": {
// "botToken": "xoxb-***",
// "botUserId": "W111",
// "botId": "B111",
// "http": {
// "headers": {
// "host": "www.example.com",
// "user-agent": "Slackbot 1.0 (+https://api.slack.com/robots)",
// "accept": "*/*",
// "accept-encoding": "gzip,deflate",
// "content-type": "application/json",
// "x-slack-signature": "v0=xsdfdsfdsfdsfdsfdsfds",
// "x-slack-request-timestamp": "1600410694",
// "content-length": "599",
// "x-forwarded-proto": "https",
// "x-forwarded-for": "54.146.233.202"
// }
// }
// },
// "client": {},
// "logger": {}
// }
{
"name": "bolt-starter",
"version": "0.0.1",
"description": "Bolt app starter",
"main": "index.js",
"scripts": {
"local": "node_modules/.bin/nodemon index.js",
"start": "node index.js"
},
"repository": {
"type": "git",
"url": "git@github.com:seratch/bolt-starter.git"
},
"keywords": [
"Slack",
"Bolt"
],
"author": "@seratch",
"license": "MIT",
"bugs": {
"url": "https://github.com/seratch/bolt-starter/issues"
},
"homepage": "https://github.com/seratch/bolt-starter#readme",
"dependencies": {
"@slack/bolt": "^2.3.0",
"dotenv": "^8.2.0"
},
"devDependencies": {
"nodemon": "^2.0.4"
}
}
const { createServer } = require('http');
const express = require('express');
const rawBody = require('raw-body');
const querystring = require('querystring');
const crypto = require('crypto');
const tsscmp = require('tsscmp');
const { ConsoleLogger } = require('@slack/logger');
const { ReceiverAuthenticityError, ReceiverMultipleAckError } = require('@slack/bolt/dist/errors');
class MyExpressReceiver {
constructor({
signingSecret = '',
logger = new ConsoleLogger(),
endpoints = { events: '/slack/events' },
}) {
this.app = express();
this.server = createServer(this.app);
const expressMiddleware = [
verifySignatureAndParseRawBody(logger, signingSecret),
respondToSslCheck,
respondToUrlVerification,
this.requestHandler.bind(this),
];
this.logger = logger;
const endpointList = typeof endpoints === 'string' ? [endpoints] : Object.values(endpoints);
this.router = express.Router();
endpointList.forEach((endpoint) => {
this.router.post(endpoint, ...expressMiddleware);
});
this.app.use(this.router);
}
async requestHandler(req, res) {
var _a;
let isAcknowledged = false;
setTimeout(() => {
if (!isAcknowledged) {
this.logger.error('An incoming event was not acknowledged within 3 seconds. ' +
'Ensure that the ack() argument is called in a listener.');
}
}, 3001);
let storedResponse;
// As of @slack/bolt v2.3.0, adding a temporary field to body is the only way to tell additional data to App
req.body["additionalProperties"] = {
http: {
headers: req.headers
}
}
const event = {
body: req.body,
// Ideally, we would like to go with this way, but it doesn't work as of v2.3.0
// context: {
// http: {
// headers: req.headers
// },
// },
ack: async (response) => {
if (isAcknowledged) {
throw new ReceiverMultipleAckError();
}
isAcknowledged = true;
if (!response) {
res.send('');
}
else if (typeof response === 'string') {
res.send(response);
}
else {
res.json(response);
}
},
};
try {
await ((_a = this.bolt) === null || _a === void 0 ? void 0 : _a.processEvent(event));
if (storedResponse !== undefined) {
if (typeof storedResponse === 'string') {
res.send(storedResponse);
}
else {
res.json(storedResponse);
}
}
}
catch (err) {
res.status(500).send();
throw err;
}
}
init(bolt) {
this.bolt = bolt;
}
start(port) {
return new Promise((resolve, reject) => {
try {
this.server.listen(port, () => {
resolve(this.server);
});
}
catch (error) {
reject(error);
}
});
}
stop() {
return new Promise((resolve, reject) => {
this.server.close((error) => {
if (error !== undefined) {
reject(error);
return;
}
resolve();
});
});
}
}
const respondToSslCheck = (req, res, next) => {
if (req.body && req.body.ssl_check) {
res.send();
return;
}
next();
};
const respondToUrlVerification = (req, res, next) => {
if (req.body && req.body.type && req.body.type === 'url_verification') {
res.json({ challenge: req.body.challenge });
return;
}
next();
};
function verifySignatureAndParseRawBody(logger, signingSecret) {
return async (req, res, next) => {
let stringBody;
const preparsedRawBody = req.rawBody;
if (preparsedRawBody !== undefined) {
stringBody = preparsedRawBody.toString();
}
else {
stringBody = (await rawBody(req)).toString();
}
try {
req.body = verifySignatureAndParseBody(signingSecret, stringBody, req.headers);
}
catch (error) {
if (error) {
if (error instanceof ReceiverAuthenticityError) {
logError(logger, 'Request verification failed', error);
return res.status(401).send();
}
logError(logger, 'Parsing request body failed', error);
return res.status(400).send();
}
}
return next();
};
}
function logError(logger, message, error) {
const logMessage = 'code' in error
? `${message} (code: ${error.code}, message: ${error.message})`
: `${message} (error: ${error})`;
logger.warn(logMessage);
}
function verifyRequestSignature(signingSecret, body, signature, requestTimestamp) {
if (signature === undefined || requestTimestamp === undefined) {
throw new ReceiverAuthenticityError('Slack request signing verification failed. Some headers are missing.');
}
const ts = Number(requestTimestamp);
if (isNaN(ts)) {
throw new ReceiverAuthenticityError('Slack request signing verification failed. Timestamp is invalid.');
}
const fiveMinutesAgo = Math.floor(Date.now() / 1000) - 60 * 5;
if (ts < fiveMinutesAgo) {
throw new ReceiverAuthenticityError('Slack request signing verification failed. Timestamp is too old.');
}
const hmac = crypto.createHmac('sha256', signingSecret);
const [version, hash] = signature.split('=');
hmac.update(`${version}:${ts}:${body}`);
if (!tsscmp(hash, hmac.digest('hex'))) {
throw new ReceiverAuthenticityError('Slack request signing verification failed. Signature mismatch.');
}
}
function verifySignatureAndParseBody(signingSecret, body, headers) {
const {
'x-slack-signature': signature,
'x-slack-request-timestamp': requestTimestamp,
'content-type': contentType,
} = headers;
verifyRequestSignature(signingSecret, body, signature, requestTimestamp);
return parseRequestBody(body, contentType);
}
function parseRequestBody(stringBody, contentType) {
if (contentType === 'application/x-www-form-urlencoded') {
const parsedBody = querystring.parse(stringBody);
if (typeof parsedBody.payload === 'string') {
return JSON.parse(parsedBody.payload);
}
return parsedBody;
}
return JSON.parse(stringBody);
}
exports.MyExpressReceiver = MyExpressReceiver;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment