/* globals Meteor Restivus logger processWebhookMessage*/ // TODO: remove globals import { HTTP } from 'meteor/http'; import { Random } from 'meteor/random'; import Fiber from 'fibers'; import Future from 'fibers/future'; import _ from 'underscore'; import s from 'underscore.string'; import vm from 'vm'; import moment from 'moment'; const Api = new Restivus({ enableCors: true, apiPath: 'hooks/', auth: { user() { const payloadKeys = Object.keys(this.bodyParams); const payloadIsWrapped = (this.bodyParams && this.bodyParams.payload) && payloadKeys.length === 1; if (payloadIsWrapped && this.request.headers['content-type'] === 'application/x-www-form-urlencoded') { try { this.bodyParams = JSON.parse(this.bodyParams.payload); } catch ({ message }) { return { error: { statusCode: 400, body: { success: false, error: message, }, }, }; } } this.integration = RocketChat.models.Integrations.findOne({ _id: this.request.params.integrationId, token: decodeURIComponent(this.request.params.token), }); if (!this.integration) { logger.incoming.info('Invalid integration id', this.request.params.integrationId, 'or token', this.request.params.token); return { error: { statusCode: 404, body: { success: false, error: 'Invalid integration id or token provided.', }, }, }; } const user = RocketChat.models.Users.findOne({ _id: this.integration.userId, }); return { user }; }, }, }); const compiledScripts = {}; function buildSandbox(store = {}) { const sandbox = { scriptTimeout(reject) { return setTimeout(() => reject('timed out'), 3000); }, _, s, console, moment, Fiber, Promise, Livechat: RocketChat.Livechat, Store: { set(key, val) { return store[key] = val; }, get(key) { return store[key]; }, }, HTTP(method, url, options) { try { return { result: HTTP.call(method, url, options), }; } catch (error) { return { error, }; } }, }; Object.keys(RocketChat.models).filter((k) => !k.startsWith('_')).forEach((k) => sandbox[k] = RocketChat.models[k]); return { store, sandbox }; } function getIntegrationScript(integration) { const compiledScript = compiledScripts[integration._id]; if (compiledScript && +compiledScript._updatedAt === +integration._updatedAt) { return compiledScript.script; } const script = integration.scriptCompiled; const { sandbox, store } = buildSandbox(); try { logger.incoming.info('Will evaluate script of Trigger', integration.name); logger.incoming.debug(script); const vmScript = vm.createScript(script, 'script.js'); vmScript.runInNewContext(sandbox); if (sandbox.Script) { compiledScripts[integration._id] = { script: new sandbox.Script(), store, _updatedAt: integration._updatedAt, }; return compiledScripts[integration._id].script; } } catch ({ stack }) { logger.incoming.error('[Error evaluating Script in Trigger', integration.name, ':]'); logger.incoming.error(script.replace(/^/gm, ' ')); logger.incoming.error('[Stack:]'); logger.incoming.error(stack.replace(/^/gm, ' ')); throw RocketChat.API.v1.failure('error-evaluating-script'); } if (!sandbox.Script) { logger.incoming.error('[Class "Script" not in Trigger', integration.name, ']'); throw RocketChat.API.v1.failure('class-script-not-found'); } } function createIntegration(options, user) { logger.incoming.info('Add integration', options.name); logger.incoming.debug(options); Meteor.runAsUser(user._id, function() { switch (options.event) { case 'newMessageOnChannel': if (options.data == null) { options.data = {}; } if ((options.data.channel_name != null) && options.data.channel_name.indexOf('#') === -1) { options.data.channel_name = `#${ options.data.channel_name }`; } return Meteor.call('addOutgoingIntegration', { username: 'rocket.cat', urls: [options.target_url], name: options.name, channel: options.data.channel_name, triggerWords: options.data.trigger_words, }); case 'newMessageToUser': if (options.data.username.indexOf('@') === -1) { options.data.username = `@${ options.data.username }`; } return Meteor.call('addOutgoingIntegration', { username: 'rocket.cat', urls: [options.target_url], name: options.name, channel: options.data.username, triggerWords: options.data.trigger_words, }); } }); return RocketChat.API.v1.success(); } function removeIntegration(options, user) { logger.incoming.info('Remove integration'); logger.incoming.debug(options); const integrationToRemove = RocketChat.models.Integrations.findOne({ urls: options.target_url, }); Meteor.runAsUser(user._id, () => Meteor.call('deleteOutgoingIntegration', integrationToRemove._id)); return RocketChat.API.v1.success(); } function executeIntegrationRest() { logger.incoming.info('Post integration:', this.integration.name); logger.incoming.debug('@urlParams:', this.urlParams); logger.incoming.debug('@bodyParams:', this.bodyParams); if (this.integration.enabled !== true) { return { statusCode: 503, body: 'Service Unavailable', }; } const defaultValues = { channel: this.integration.channel, alias: this.integration.alias, avatar: this.integration.avatar, emoji: this.integration.emoji, }; if (this.integration.scriptEnabled && this.integration.scriptCompiled && this.integration.scriptCompiled.trim() !== '') { let script; try { script = getIntegrationScript(this.integration); } catch (e) { logger.incoming.warn(e); return RocketChat.API.v1.failure(e.message); } this.request.setEncoding('utf8'); const content_raw = this.request.read(); const request = { url: { hash: this.request._parsedUrl.hash, search: this.request._parsedUrl.search, query: this.queryParams, pathname: this.request._parsedUrl.pathname, path: this.request._parsedUrl.path, }, url_raw: this.request.url, url_params: this.urlParams, content: this.bodyParams, content_raw, headers: this.request.headers, body: this.request.body, user: { _id: this.user._id, name: this.user.name, username: this.user.username, }, }; try { const { sandbox } = buildSandbox(compiledScripts[this.integration._id].store); sandbox.script = script; sandbox.request = request; const result = Future.fromPromise(vm.runInNewContext(` new Promise((resolve, reject) => { Fiber(() => { scriptTimeout(reject); try { resolve(script.process_incoming_request({ request: request })); } catch(e) { reject(e); } }).run(); }).catch((error) => { throw new Error(error); }); `, sandbox, { timeout: 3000, })).wait(); if (!result) { logger.incoming.debug('[Process Incoming Request result of Trigger', this.integration.name, ':] No data'); return RocketChat.API.v1.success(); } else if (result && result.error) { return RocketChat.API.v1.failure(result.error); } this.bodyParams = result && result.content; this.scriptResponse = result.response; if (result.user) { this.user = result.user; } logger.incoming.debug('[Process Incoming Request result of Trigger', this.integration.name, ':]'); logger.incoming.debug('result', this.bodyParams); } catch ({ stack }) { logger.incoming.error('[Error running Script in Trigger', this.integration.name, ':]'); logger.incoming.error(this.integration.scriptCompiled.replace(/^/gm, ' ')); logger.incoming.error('[Stack:]'); logger.incoming.error(stack.replace(/^/gm, ' ')); return RocketChat.API.v1.failure('error-running-script'); } } // TODO: Turn this into an option on the integrations - no body means a success // TODO: Temporary fix for https://github.com/RocketChat/Rocket.Chat/issues/7770 until the above is implemented if (!this.bodyParams || (_.isEmpty(this.bodyParams) && !this.integration.scriptEnabled)) { // return RocketChat.API.v1.failure('body-empty'); return RocketChat.API.v1.success(); } this.bodyParams.bot = { i: this.integration._id }; try { const message = processWebhookMessage(this.bodyParams, this.user, defaultValues); if (_.isEmpty(message)) { return RocketChat.API.v1.failure('unknown-error'); } if (this.scriptResponse) { logger.incoming.debug('response', this.scriptResponse); } return RocketChat.API.v1.success(this.scriptResponse); } catch ({ error, message }) { return RocketChat.API.v1.failure(error || message); } } function addIntegrationRest() { return createIntegration(this.bodyParams, this.user); } function removeIntegrationRest() { return removeIntegration(this.bodyParams, this.user); } function integrationSampleRest() { logger.incoming.info('Sample Integration'); return { statusCode: 200, body: [ { token: Random.id(24), channel_id: Random.id(), channel_name: 'general', timestamp: new Date, user_id: Random.id(), user_name: 'rocket.cat', text: 'Sample text 1', trigger_word: 'Sample', }, { token: Random.id(24), channel_id: Random.id(), channel_name: 'general', timestamp: new Date, user_id: Random.id(), user_name: 'rocket.cat', text: 'Sample text 2', trigger_word: 'Sample', }, { token: Random.id(24), channel_id: Random.id(), channel_name: 'general', timestamp: new Date, user_id: Random.id(), user_name: 'rocket.cat', text: 'Sample text 3', trigger_word: 'Sample', }, ], }; } function integrationInfoRest() { logger.incoming.info('Info integration'); return { statusCode: 200, body: { success: true, }, }; } Api.addRoute(':integrationId/:userId/:token', { authRequired: true }, { post: executeIntegrationRest, get: executeIntegrationRest, }); Api.addRoute(':integrationId/:token', { authRequired: true }, { post: executeIntegrationRest, get: executeIntegrationRest, }); Api.addRoute('sample/:integrationId/:userId/:token', { authRequired: true }, { get: integrationSampleRest, }); Api.addRoute('sample/:integrationId/:token', { authRequired: true }, { get: integrationSampleRest, }); Api.addRoute('info/:integrationId/:userId/:token', { authRequired: true }, { get: integrationInfoRest, }); Api.addRoute('info/:integrationId/:token', { authRequired: true }, { get: integrationInfoRest, }); Api.addRoute('add/:integrationId/:userId/:token', { authRequired: true }, { post: addIntegrationRest, }); Api.addRoute('add/:integrationId/:token', { authRequired: true }, { post: addIntegrationRest, }); Api.addRoute('remove/:integrationId/:userId/:token', { authRequired: true }, { post: removeIntegrationRest, }); Api.addRoute('remove/:integrationId/:token', { authRequired: true }, { post: removeIntegrationRest, });