The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Rocket.Chat/packages/rocketchat-integrations/server/api/api.js

405 lines
11 KiB

/* 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,
});