UNPKG

sipgateio

Version:

The official Node.js library for sipgate.io

281 lines 13.1 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebhookResponse = exports.serverAddressesMatch = exports.createWebhookModule = void 0; const webhook_types_1 = require("./webhook.types"); const http_1 = require("http"); const __1 = require(".."); const webhook_errors_1 = require("./webhook.errors"); const signatureVerifier_1 = require("./signatureVerifier"); const xml_js_1 = require("xml-js"); const qs_1 = require("qs"); const audioUtils_1 = require("./audioUtils"); const createWebhookModule = () => ({ createServer: createWebhookServer, }); exports.createWebhookModule = createWebhookModule; const SIPGATE_WEBHOOK_IP_ADDRESSES = [ '217.116.118.254', '212.9.46.32', ]; const createWebhookServer = (serverOptions) => __awaiter(void 0, void 0, void 0, function* () { const handlers = { [webhook_types_1.EventType.NEW_CALL]: () => { return; }, }; return new Promise((resolve, reject) => { const requestHandler = (req, res) => __awaiter(void 0, void 0, void 0, function* () { const requestBody = yield collectRequestData(req); if (!(0, exports.serverAddressesMatch)(req, serverOptions)) { console.error(webhook_errors_1.WebhookErrorMessage.SERVERADDRESS_DOES_NOT_MATCH); } if (!serverOptions.skipSignatureVerification) { if (!isSipgateOrigin(req, SIPGATE_WEBHOOK_IP_ADDRESSES)) { console.error(webhook_errors_1.WebhookErrorMessage.INVALID_ORIGIN); res.end(`<?xml version="1.0" encoding="UTF-8"?><Error message="${webhook_errors_1.WebhookErrorMessage.INVALID_ORIGIN}" />`); return; } if (!(0, signatureVerifier_1.isSipgateSignature)(req.headers['x-sipgate-signature'], requestBody)) { console.error(webhook_errors_1.WebhookErrorMessage.SIPGATE_SIGNATURE_VERIFICATION_FAILED); res.end(`<?xml version="1.0" encoding="UTF-8"?><Error message="${webhook_errors_1.WebhookErrorMessage.SIPGATE_SIGNATURE_VERIFICATION_FAILED}" />`); return; } } res.setHeader('Content-Type', 'application/xml'); const requestBodyJSON = parseRequestBodyJSON(requestBody); const requestCallback = handlers[requestBodyJSON.event]; if (requestCallback === undefined) { res.end(`<?xml version="1.0" encoding="UTF-8"?><Error message="No handler for ${requestBodyJSON.event} event" />`); return; } const callbackResult = requestCallback(requestBodyJSON) || undefined; const responseObject = createResponseObject(callbackResult instanceof Promise ? yield callbackResult : callbackResult, serverOptions.serverAddress); if (handlers[webhook_types_1.EventType.ANSWER]) { responseObject.Response['_attributes'].onAnswer = serverOptions.serverAddress; } if (handlers[webhook_types_1.EventType.HANGUP]) { responseObject.Response['_attributes'].onHangup = serverOptions.serverAddress; } const xmlResponse = createXmlResponse(responseObject); res.end(xmlResponse); }); const server = (0, http_1.createServer)(requestHandler).on('error', reject); server.listen({ port: serverOptions.port, hostname: serverOptions.hostname || 'localhost', }, () => { resolve({ onNewCall: (handler) => { handlers[webhook_types_1.EventType.NEW_CALL] = handler; }, onAnswer: (handler) => { if (!serverOptions.serverAddress) throw new Error(webhook_errors_1.WebhookErrorMessage.SERVERADDRESS_MISSING_FOR_FOLLOWUPS); handlers[webhook_types_1.EventType.ANSWER] = handler; }, onHangUp: (handler) => { if (!serverOptions.serverAddress) throw new Error(webhook_errors_1.WebhookErrorMessage.SERVERADDRESS_MISSING_FOR_FOLLOWUPS); handlers[webhook_types_1.EventType.HANGUP] = handler; }, onData: (handler) => { if (!serverOptions.serverAddress) throw new Error(webhook_errors_1.WebhookErrorMessage.SERVERADDRESS_MISSING_FOR_FOLLOWUPS); handlers[webhook_types_1.EventType.DATA] = handler; }, stop: () => { if (server) { server.close(); } }, getHttpServer: () => server, }); }); }); }); const parseRequestBodyJSON = (body) => { body = body .replace(/user%5B%5D/g, 'users%5B%5D') .replace(/userId%5B%5D/g, 'userIds%5B%5D') .replace(/fullUserId%5B%5D/g, 'fullUserIds%5B%5D') .replace(/origCallId/g, 'originalCallId'); const parsedBody = (0, qs_1.parse)(body); if ('from' in parsedBody && parsedBody.from !== 'anonymous') { parsedBody.from = `+${parsedBody.from}`; } if ('to' in parsedBody && parsedBody.to !== 'anonymous') { parsedBody.to = `+${parsedBody.to}`; } if ('diversion' in parsedBody && parsedBody.diversion !== 'anonymous') { parsedBody.diversion = `+${parsedBody.diversion}`; } if ('answeringNumber' in parsedBody && parsedBody.answeringNumber !== 'anonymous') { parsedBody.answeringNumber = `+${parsedBody.answeringNumber}`; } return parsedBody; }; const collectRequestData = (request) => { return new Promise((resolve, reject) => { if (request.headers['content-type'] && !request.headers['content-type'].includes('application/x-www-form-urlencoded')) { reject(); } let body = ''; request.on('data', (chunk) => { body += chunk.toString(); }); request.on('end', () => { resolve(body); }); }); }; const createResponseObject = (responseObject, serverAddress) => { if (responseObject && isGatherObject(responseObject)) { responseObject.Gather._attributes['onData'] = serverAddress; } return { _declaration: { _attributes: { version: '1.0', encoding: 'utf-8' } }, Response: Object.assign({ _attributes: {} }, responseObject), }; }; const createXmlResponse = (responseObject) => { const options = { compact: true, ignoreComment: true, spaces: 4, }; return (0, xml_js_1.js2xml)(responseObject, options); }; const isGatherObject = (gatherCandidate) => { return (gatherCandidate === null || gatherCandidate === void 0 ? void 0 : gatherCandidate.Gather) !== undefined; }; const isSipgateOrigin = (req, validOrigins) => { const requestHeaders = req.headers['x-forwarded-for']; if (requestHeaders === undefined) { return false; } if (typeof requestHeaders === 'string') { return validOrigins.includes(requestHeaders); } return requestHeaders.some((requestHeader) => validOrigins.includes(requestHeader)); }; const serverAddressesMatch = ({ headers: { host }, url }, { serverAddress }) => { const actual = new URL(`http://${host}${url}`); const expected = new URL(serverAddress); function paramsToObject(entries) { const result = {}; for (const [key, value] of entries) { result[key] = value; } return result; } return [ actual.hostname === expected.hostname, actual.pathname === expected.pathname, JSON.stringify(paramsToObject(actual.searchParams.entries())) === JSON.stringify(paramsToObject(expected.searchParams.entries())), ].every((filter) => filter === true); }; exports.serverAddressesMatch = serverAddressesMatch; exports.WebhookResponse = { gatherDTMF: (gatherOptions) => __awaiter(void 0, void 0, void 0, function* () { if (gatherOptions.maxDigits < 1) { throw new Error(`\n\n${webhook_errors_1.WebhookErrorMessage.INVALID_DTMF_MAX_DIGITS}\nYour maxDigits was: ${gatherOptions.maxDigits}\n`); } if (gatherOptions.timeout < 0) { throw new Error(`\n\n${webhook_errors_1.WebhookErrorMessage.INVALID_DTMF_TIMEOUT}\nYour timeout was: ${gatherOptions.timeout}\n`); } const gatherObject = { Gather: { _attributes: { maxDigits: String(gatherOptions.maxDigits), timeout: String(gatherOptions.timeout), }, }, }; if (gatherOptions.announcement) { const validationResult = yield (0, audioUtils_1.validateAnnouncementAudio)(gatherOptions.announcement); if (!validationResult.isValid) { throw new Error(`\n\n${webhook_errors_1.WebhookErrorMessage.AUDIO_FORMAT_ERROR}\nYour format was: ${JSON.stringify(validationResult.metadata)}\n`); } gatherObject.Gather['Play'] = { Url: gatherOptions.announcement, }; } return gatherObject; }), hangUpCall: () => { return { Hangup: {} }; }, playAudio: (playOptions) => __awaiter(void 0, void 0, void 0, function* () { const validationResult = yield (0, audioUtils_1.validateAnnouncementAudio)(playOptions.announcement); if (!validationResult.isValid) { throw new Error(`\n\n${webhook_errors_1.WebhookErrorMessage.AUDIO_FORMAT_ERROR}\nYour format was: ${JSON.stringify(validationResult.metadata)}\n`); } return { Play: { Url: playOptions.announcement } }; }), playAudioAndHangUp: (playOptions, client, callId, timeout) => __awaiter(void 0, void 0, void 0, function* () { const validationResult = yield (0, audioUtils_1.validateAnnouncementAudio)(playOptions.announcement); if (!validationResult.isValid) { throw new Error(`\n\n${webhook_errors_1.WebhookErrorMessage.AUDIO_FORMAT_ERROR}\nYour format was: ${JSON.stringify(validationResult.metadata)}\n`); } let duration = validationResult.metadata.duration ? validationResult.metadata.duration * 1000 : 0; duration += timeout ? timeout : 0; setTimeout(() => { const rtcm = (0, __1.createRTCMModule)(client); // ignore errors, which were happening when the callee already hung up the phone before the announcement had ended rtcm.hangUp({ callId }).catch(() => { }); }, duration); return { Play: { Url: playOptions.announcement } }; }), playAudioAndTransfer: (playOptions, transferOptions, client, callId, timeout) => __awaiter(void 0, void 0, void 0, function* () { const validationResult = yield (0, audioUtils_1.validateAnnouncementAudio)(playOptions.announcement); if (!validationResult.isValid) { throw new Error(`\n\n${webhook_errors_1.WebhookErrorMessage.AUDIO_FORMAT_ERROR}\nYour format was: ${JSON.stringify(validationResult.metadata)}\n`); } let duration = validationResult.metadata.duration ? validationResult.metadata.duration * 1000 : 0; duration += timeout ? timeout : 0; setTimeout(() => { const rtcm = (0, __1.createRTCMModule)(client); // ignore errors, which were happening when the callee already hung up the phone before the announcement had ended rtcm.transfer({ callId }, transferOptions).catch(() => { }); }, duration); return { Play: { Url: playOptions.announcement } }; }), redirectCall: (redirectOptions) => { return { Dial: { _attributes: { callerId: redirectOptions.callerId, anonymous: String(redirectOptions.anonymous), }, Number: redirectOptions.numbers, }, }; }, rejectCall: (rejectOptions) => { return { Reject: { _attributes: { reason: rejectOptions.reason } } }; }, sendToVoicemail: () => { return { Dial: { Voicemail: {} } }; }, }; //# sourceMappingURL=webhook.js.map