Skip to content

Commit

Permalink
feat: use isolated-vm to run integration scripts (#30229)
Browse files Browse the repository at this point in the history
Co-authored-by: Marcos Spessatto Defendi <[email protected]>
Co-authored-by: Tasso Evangelista <[email protected]>
  • Loading branch information
3 people authored Sep 26, 2023
1 parent 8a02759 commit 9261368
Show file tree
Hide file tree
Showing 32 changed files with 1,306 additions and 550 deletions.
8 changes: 8 additions & 0 deletions .changeset/thirty-pumpkins-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@rocket.chat/core-typings': minor
'@rocket.chat/rest-typings': minor
'@rocket.chat/tools': minor
'@rocket.chat/meteor': minor
---

Added option to select between two script engine options for the integrations
5 changes: 5 additions & 0 deletions apps/meteor/.docker/Dockerfile.alpine
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ RUN set -x \
&& npm install [email protected] \
&& mv node_modules/sharp npm/node_modules/sharp \
# End hack for sharp
# Start hack for isolated-vm...
&& rm -rf npm/node_modules/isolated-vm \
&& npm install [email protected] \
&& mv node_modules/isolated-vm npm/node_modules/isolated-vm \
# End hack for isolated-vm
&& cd npm \
&& npm rebuild bcrypt --build-from-source \
&& npm cache clear --force \
Expand Down
155 changes: 13 additions & 142 deletions apps/meteor/app/integrations/server/api/api.js
Original file line number Diff line number Diff line change
@@ -1,114 +1,21 @@
import { Integrations, Users } from '@rocket.chat/models';
import * as Models from '@rocket.chat/models';
import { Random } from '@rocket.chat/random';
import { Livechat } from 'meteor/rocketchat:livechat';
import moment from 'moment';
import _ from 'underscore';
import { VM, VMScript } from 'vm2';

import * as s from '../../../../lib/utils/stringUtils';
import { deasyncPromise } from '../../../../server/deasync/deasync';
import { httpCall } from '../../../../server/lib/http/call';
import { API, APIClass, defaultRateLimiterOptions } from '../../../api/server';
import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage';
import { settings } from '../../../settings/server';
import { IsolatedVMScriptEngine } from '../lib/isolated-vm/isolated-vm';
import { VM2ScriptEngine } from '../lib/vm2/vm2';
import { incomingLogger } from '../logger';
import { addOutgoingIntegration } from '../methods/outgoing/addOutgoingIntegration';
import { deleteOutgoingIntegration } from '../methods/outgoing/deleteOutgoingIntegration';

const DISABLE_INTEGRATION_SCRIPTS = ['yes', 'true'].includes(String(process.env.DISABLE_INTEGRATION_SCRIPTS).toLowerCase());
const vm2Engine = new VM2ScriptEngine(true);
const ivmEngine = new IsolatedVMScriptEngine(true);

export const forbiddenModelMethods = ['registerModel', 'getCollectionName'];

const compiledScripts = {};
function buildSandbox(store = {}) {
const httpAsync = async (method, url, options) => {
try {
return {
result: await httpCall(method, url, options),
};
} catch (error) {
return { error };
}
};

const sandbox = {
scriptTimeout(reject) {
return setTimeout(() => reject('timed out'), 3000);
},
_,
s,
console,
moment,
Promise,
Livechat,
Store: {
set(key, val) {
store[key] = val;
return val;
},
get(key) {
return store[key];
},
},
HTTP: (method, url, options) => {
// TODO: deprecate, track and alert
return deasyncPromise(httpAsync(method, url, options));
},
// TODO: Export fetch as the non deprecated method
};
Object.keys(Models)
.filter((k) => !forbiddenModelMethods.includes(k))
.forEach((k) => {
sandbox[k] = Models[k];
});
return { store, sandbox };
}

function getIntegrationScript(integration) {
if (DISABLE_INTEGRATION_SCRIPTS) {
throw API.v1.failure('integration-scripts-disabled');
}

const compiledScript = compiledScripts[integration._id];
if (compiledScript && +compiledScript._updatedAt === +integration._updatedAt) {
return compiledScript.script;
}

const script = integration.scriptCompiled;
const { sandbox, store } = buildSandbox();
try {
incomingLogger.info({ msg: 'Will evaluate script of Trigger', integration: integration.name });
incomingLogger.debug(script);

const vmScript = new VMScript(`${script}; Script;`, 'script.js');
const vm = new VM({
sandbox,
});

const ScriptClass = vm.run(vmScript);

if (ScriptClass) {
compiledScripts[integration._id] = {
script: new ScriptClass(),
store,
_updatedAt: integration._updatedAt,
};

return compiledScripts[integration._id].script;
}
} catch (err) {
incomingLogger.error({
msg: 'Error evaluating Script in Trigger',
integration: integration.name,
script,
err,
});
throw API.v1.failure('error-evaluating-script');
}

incomingLogger.error({ msg: 'Class "Script" not in Trigger', integration: integration.name });
throw API.v1.failure('class-script-not-found');
function getEngine(integration) {
return integration.scriptEngine === 'isolated-vm' ? ivmEngine : vm2Engine;
}

async function createIntegration(options, user) {
Expand Down Expand Up @@ -178,20 +85,9 @@ async function executeIntegrationRest() {
emoji: this.integration.emoji,
};

if (
!DISABLE_INTEGRATION_SCRIPTS &&
this.integration.scriptEnabled &&
this.integration.scriptCompiled &&
this.integration.scriptCompiled.trim() !== ''
) {
let script;
try {
script = getIntegrationScript(this.integration);
} catch (e) {
incomingLogger.error(e);
return API.v1.failure(e.message);
}
const scriptEngine = getEngine(this.integration);

if (scriptEngine.integrationHasValidScript(this.integration)) {
this.request.setEncoding('utf8');
const content_raw = this.request.read();

Expand All @@ -216,37 +112,12 @@ async function executeIntegrationRest() {
},
};

try {
const { sandbox } = buildSandbox(compiledScripts[this.integration._id].store);
sandbox.script = script;
sandbox.request = request;

const vm = new VM({
timeout: 3000,
sandbox,
});

const result = await new Promise((resolve, reject) => {
process.nextTick(async () => {
try {
const scriptResult = await vm.run(`
new Promise((resolve, reject) => {
scriptTimeout(reject);
try {
resolve(script.process_incoming_request({ request: request }));
} catch(e) {
reject(e);
}
}).catch((error) => { throw new Error(error); });
`);

resolve(scriptResult);
} catch (e) {
reject(e);
}
});
});
const result = await scriptEngine.processIncomingRequest({
integration: this.integration,
request,
});

try {
if (!result) {
incomingLogger.debug({
msg: 'Process Incoming Request result of Trigger has no data',
Expand Down
Loading

0 comments on commit 9261368

Please sign in to comment.