UNPKG

twilio

Version:
323 lines (311 loc) 12.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getExpectedTwilioSignature = getExpectedTwilioSignature; exports.getExpectedBodyHash = getExpectedBodyHash; exports.validateRequest = validateRequest; exports.validateBody = validateBody; exports.validateRequestWithBody = validateRequestWithBody; exports.validateIncomingRequest = validateIncomingRequest; exports.validateExpressRequest = validateExpressRequest; exports.webhook = webhook; const scmp = require("scmp"); const crypto_1 = __importDefault(require("crypto")); const url_1 = __importDefault(require("url")); const querystring_1 = require("querystring"); /** * Utility function to construct the URL string, since Node.js url library won't include standard port numbers * * @param parsedUrl - The parsed url object that Twilio requested on your server * @returns URL with standard port number included */ function buildUrlWithStandardPort(parsedUrl) { let url = ""; const port = parsedUrl.protocol === "https:" ? ":443" : ":80"; url += parsedUrl.protocol ? parsedUrl.protocol + "//" : ""; url += parsedUrl.username; url += parsedUrl.password ? ":" + parsedUrl.password : ""; url += parsedUrl.username || parsedUrl.password ? "@" : ""; url += parsedUrl.host ? parsedUrl.host + port : ""; url += parsedUrl.pathname + parsedUrl.search + parsedUrl.hash; return url; } /** Utility function to add a port number to a URL @param parsedUrl - The parsed url object that Twilio requested on your server @returns URL with port */ function addPort(parsedUrl) { if (!parsedUrl.port) { return buildUrlWithStandardPort(parsedUrl); } return parsedUrl.toString(); } /** Utility function to remove a port number from a URL @param parsedUrl - The parsed url object that Twilio requested on your server @returns URL without port */ function removePort(parsedUrl) { parsedUrl = new URL(parsedUrl); // prevent mutation of original URL object parsedUrl.port = ""; return parsedUrl.toString(); } function withLegacyQuerystring(url) { const parsedUrl = new URL(url); if (parsedUrl.search) { const qs = (0, querystring_1.parse)(parsedUrl.search.slice(1)); parsedUrl.search = ""; return parsedUrl.toString() + "?" + (0, querystring_1.stringify)(qs); } return url; } /** Utility function to convert request parameter to a string format @param paramName - The request parameter name @param paramValue - The request parameter value @returns Formatted parameter string */ function toFormUrlEncodedParam(paramName, paramValue) { if (paramValue instanceof Array) { return Array.from(new Set(paramValue)) .sort() .map((val) => toFormUrlEncodedParam(paramName, val)) .reduce((acc, val) => acc + val, ""); } return paramName + paramValue; } /** Utility function to get the expected signature for a given request @param authToken - The auth token, as seen in the Twilio portal @param url - The full URL (with query string) you configured to handle this request @param params - the parameters sent with this request @returns signature */ function getExpectedTwilioSignature(authToken, url, params) { if (url.indexOf("bodySHA256") !== -1 && params === null) { params = {}; } var data = Object.keys(params) .sort() .reduce((acc, key) => acc + toFormUrlEncodedParam(key, params[key]), url); return crypto_1.default .createHmac("sha1", authToken) .update(Buffer.from(data, "utf-8")) .digest("base64"); } /** Utility function to get the expected body hash for a given request's body @param body - The plain-text body of the request */ function getExpectedBodyHash(body) { return crypto_1.default .createHash("sha256") .update(Buffer.from(body, "utf-8")) .digest("hex"); } /** Utility function to validate an incoming request is indeed from Twilio @param authToken - The auth token, as seen in the Twilio portal @param twilioHeader - The value of the X-Twilio-Signature header from the request @param url - The full URL (with query string) you configured to handle this request @param params - the parameters sent with this request @returns valid */ function validateRequest(authToken, twilioHeader, url, params) { twilioHeader = twilioHeader || ""; const urlObject = new URL(url); /* * Check signature of the url with and without the port number * and with and without the legacy querystring (special chars are encoded when using `new URL()`) * since signature generation on the back end is inconsistent */ const isValidSignatureWithoutPort = validateSignatureWithUrl(authToken, twilioHeader, removePort(urlObject), params); if (isValidSignatureWithoutPort) { return true; } const isValidSignatureWithPort = validateSignatureWithUrl(authToken, twilioHeader, addPort(urlObject), params); if (isValidSignatureWithPort) { return true; } const isValidSignatureWithLegacyQuerystringWithoutPort = validateSignatureWithUrl(authToken, twilioHeader, withLegacyQuerystring(removePort(urlObject)), params); if (isValidSignatureWithLegacyQuerystringWithoutPort) { return true; } const isValidSignatureWithLegacyQuerystringWithPort = validateSignatureWithUrl(authToken, twilioHeader, withLegacyQuerystring(addPort(urlObject)), params); return isValidSignatureWithLegacyQuerystringWithPort; } function validateSignatureWithUrl(authToken, twilioHeader, url, params) { const signatureWithoutPort = getExpectedTwilioSignature(authToken, url, params); return scmp(Buffer.from(twilioHeader), Buffer.from(signatureWithoutPort)); } function validateBody(body, bodyHash) { var expectedHash = getExpectedBodyHash(body); return scmp(Buffer.from(bodyHash), Buffer.from(expectedHash)); } /** Utility function to validate an incoming request is indeed from Twilio. This also validates the request body against the bodySHA256 post parameter. @param authToken - The auth token, as seen in the Twilio portal @param twilioHeader - The value of the X-Twilio-Signature header from the request @param url - The full URL (with query string) you configured to handle this request @param body - The body of the request @returns valid */ function validateRequestWithBody(authToken, twilioHeader, url, body) { const urlObject = new URL(url); return (validateRequest(authToken, twilioHeader, url, {}) && validateBody(body, urlObject.searchParams.get("bodySHA256") || "")); } /** Utility function to validate an incoming request is indeed from Twilio. adapted from https://github.com/crabasa/twiliosig @param request - A request object (based on Express implementation http://expressjs.com/api.html#req.params) @param authToken - The auth token, as seen in the Twilio portal @param opts - options for request validation: -> url: The full URL (with query string) you used to configure the webhook with Twilio - overrides host/protocol options -> host: manually specify the host name used by Twilio in a number's webhook config -> protocol: manually specify the protocol used by Twilio in a number's webhook config */ function validateIncomingRequest(request, authToken, opts) { var options = opts || {}; var webhookUrl; if (options.url) { // Let the user specify the full URL webhookUrl = options.url; } else { // Use configured host/protocol, or infer based on request var protocol = options.protocol || request.protocol; var host = options.host || request.headers.host; webhookUrl = url_1.default.format({ protocol: protocol, host: host, pathname: request.originalUrl, }); if (request.originalUrl.search(/\?/) >= 0) { webhookUrl = webhookUrl.replace(/%3F/g, "?"); } } if (webhookUrl.indexOf("bodySHA256") > 0) { return validateRequestWithBody(authToken, request.header("X-Twilio-Signature") || "", webhookUrl, request.rawBody || "{}"); } else { return validateRequest(authToken, request.header("X-Twilio-Signature") || "", webhookUrl, request.body || {}); } } function validateExpressRequest(request, authToken, opts) { return validateIncomingRequest(request, authToken, opts); } /** Express middleware to accompany a Twilio webhook. Provides Twilio request validation, and makes the response a little more friendly for our TwiML generator. Request validation requires the express.urlencoded middleware to have been applied (e.g. app.use(express.urlencoded()); in your app config). Options: - validate: {Boolean} whether or not the middleware should validate the request came from Twilio. Default true. If the request does not originate from Twilio, we will return a text body and a 403. If there is no configured auth token and validate=true, this is an error condition, so we will return a 500. - host: manually specify the host name used by Twilio in a number's webhook config - protocol: manually specify the protocol used by Twilio in a number's webhook config - url: The full URL (with query string) you used to configure the webhook with Twilio - overrides host/protocol options Returns a middleware function. Examples: var webhookMiddleware = twilio.webhook(); var webhookMiddleware = twilio.webhook('asdha9dhjasd'); //init with auth token var webhookMiddleware = twilio.webhook({ validate:false // don't attempt request validation }); var webhookMiddleware = twilio.webhook({ host: 'hook.twilio.com', protocol: 'https' }); */ function webhook(opts, authToken) { let token; let options = undefined; // Narrowing the args if (opts) { if (typeof opts === "string") { token = opts; } if (typeof opts === "object") { options = opts; } } if (authToken) { if (typeof authToken === "string") { token = authToken; } if (typeof authToken === "object") { options = authToken; } } if (!options) options = {}; if (options.validate == undefined) options.validate = true; // Process arguments var tokenString; for (var i = 0, l = arguments.length; i < l; i++) { var arg = arguments[i]; if (typeof arg === "string") { tokenString = arg; } else { options = Object.assign(options || {}, arg); } } // set auth token from input or environment variable if (options) { options.authToken = tokenString ? tokenString : process.env.TWILIO_AUTH_TOKEN; } // Create middleware function return function hook(request, response, next) { // Do validation if requested if (options?.validate) { // Check if the 'X-Twilio-Signature' header exists or not if (!request.header("X-Twilio-Signature")) { return response .type("text/plain") .status(400) .send("No signature header error - X-Twilio-Signature header does not exist, maybe this request is not coming from Twilio."); } // Check for a valid auth token if (!options?.authToken) { console.error("[Twilio]: Error - Twilio auth token is required for webhook request validation."); response .type("text/plain") .status(500) .send("Webhook Error - we attempted to validate this request without first configuring our auth token."); } else { // Check that the request originated from Twilio var valid = validateExpressRequest(request, options?.authToken, { url: options?.url, host: options?.host, protocol: options?.protocol, }); if (valid) { next(); } else { return response .type("text/plain") .status(403) .send("Twilio Request Validation Failed."); } } } else { next(); } }; }