Skip to content

Commit

Permalink
Merge branch 'v5' into v5-expand-timer-api
Browse files Browse the repository at this point in the history
  • Loading branch information
zunderscore committed Feb 9, 2024
2 parents f2048be + e587704 commit e3a6569
Show file tree
Hide file tree
Showing 19 changed files with 828 additions and 646 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"electron-window-state": "^4.1.1",
"eventsource": "^1.0.7",
"express": "^4.17.1",
"expressionish": "github:SReject/expressionish#87101258cc46c1cee83a0e7d4c3ca58689ccba27",
"expressionish": "github:SReject/expressionish#6b8893e182dcf94a9704687196e95c5175e6ff1e",
"extra-life-ts": "^0.4.0",
"fflate": "^0.8.1",
"form-data": "^4.0.0",
Expand Down
2 changes: 2 additions & 0 deletions src/backend/auth/firebot-device-auth-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class FirebotDeviceAuthProvider {
});

this.streamerProvider.onRefresh((userId, token) => this.onRefresh("streamer", userId, token));
this.streamerProvider.onRefreshFailure(() => accountAccess.setAccountTokenIssue("streamer"));
} else {
this.streamerProvider = null;
}
Expand All @@ -82,6 +83,7 @@ class FirebotDeviceAuthProvider {
});

this.botProvider.onRefresh((userId, token) => this.onRefresh("bot", userId, token));
this.botProvider.onRefreshFailure(() => accountAccess.setAccountTokenIssue("bot"));
} else {
this.botProvider = null;
}
Expand Down
13 changes: 12 additions & 1 deletion src/backend/common/account-access.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,16 +215,27 @@ frontendCommunicator.on("getAccounts", () => {
return cache;
});

frontendCommunicator.on("logoutAccount", accountType => {
frontendCommunicator.on("logoutAccount", (accountType) => {
logger.debug("got logout request for", accountType);
removeAccount(accountType);
});

function setAccountTokenIssue(accountType) {
if (accountType === "streamer") {
streamerTokenIssue = true;
} else if (accountType === "bot") {
botTokenIssue = true;
} else {
throw new Error("invalid account type");
}
}

exports.events = accountEvents;
exports.updateAccountCache = loadAccountData;
exports.updateAccount = updateAccount;
exports.updateStreamerAccountSettings = updateStreamerAccountSettings;
exports.getAccounts = () => cache;
exports.setAccountTokenIssue = setAccountTokenIssue;
exports.streamerTokenIssue = () => streamerTokenIssue;
exports.botTokenIssue = () => botTokenIssue;
exports.refreshTwitchData = refreshTwitchData;
4 changes: 2 additions & 2 deletions src/backend/common/custom-variable-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,12 @@ exports.getCustomVariable = (name, propertyPath, defaultData = null) => {
return defaultData;
}

if (propertyPath == null || propertyPath === "null") {
if (propertyPath == null || propertyPath === "null" || propertyPath === '') {
return data;
}

try {
const pathNodes = propertyPath.split(".");
const pathNodes = `${propertyPath}`.split(".");
for (let i = 0; i < pathNodes.length; i++) {
if (data == null) {
break;
Expand Down
230 changes: 230 additions & 0 deletions src/backend/common/handlers/js-sandbox/sandbox-eval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { join } from 'node:path';
import { BrowserWindow, MessageChannelMain, session } from 'electron';

import type { Trigger } from '../../../../types/triggers';

const logger = require('../../../logwrapper');
const preloadPath = join(__dirname, 'sandbox-preload.js');
const htmlPath = join(__dirname, './sandbox.html');

const charList = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const handlers = new Map<string, (...args: unknown[]) => unknown>();

interface Sandbox {
finished: boolean,
tunnel: Electron.MessagePortMain,
timeout?: ReturnType<typeof setTimeout>,
window?: Electron.BrowserWindow,

resolve?: (...args: unknown[]) => void,
reject?: (...args: unknown[]) => void
}

export const evalSandboxedJs = async (code: string, args: unknown[], metadata: Trigger["metadata"]) => {
if (<unknown>code instanceof String) {
code = `${code}`;
}
if (typeof code !== 'string' || code === '') {
return;
}

return new Promise((resolve, reject) => {
let { port1: portToBackend, port2: portToSandbox } = new MessageChannelMain();

const sandbox : Sandbox = {
finished: false,
tunnel: portToSandbox
};

// Frees all resources related to the sandbox
const cleanup = () => {
sandbox.finished = true;

if (sandbox.timeout) {
clearTimeout(sandbox.timeout);
sandbox.timeout = null;
}

try {
sandbox.window.webContents.removeAllListeners();
} catch (err) {}
try {
sandbox.window.removeAllListeners();
sandbox.window.destroy();
} catch (err) {}
sandbox.window = null;

try {
sandbox.tunnel.close();
sandbox.tunnel.removeAllListeners();
} catch (err) {}
sandbox.tunnel = null;
portToSandbox = null;

try {
portToBackend.close();
portToBackend.removeAllListeners();
} catch (err) {}
portToBackend = null;
};

// Called when the sandbox successfully returns a result
sandbox.resolve = (result: unknown) => {
if (!sandbox.finished) {
cleanup();
resolve(result);
}
};

// Called when the sandbox ends with an error
sandbox.reject = (reason?: string | Error) => {
if (!sandbox.finished) {
cleanup();
reason = typeof reason === 'string' ? new Error(reason) : reason == null ? new Error('unknown error') : reason;
reject(reason);
}
};

// Listen for messages from sandbox
portToSandbox.on('message', async (event) => {
if (sandbox.finished) {
cleanup();
return;
}
const { id, action, method, parameters, status, result } = event.data;

// Sandbox returned a result for the evaluation
if ((id === 0 || id === '0') && action === 'result') {
if (status === 'ok') {
sandbox.resolve(result);
} else if (status === 'error') {
sandbox.reject(result);
}

// Sandbox is leveraging Firebot.* apis
} else if (action === 'method') {

const base = { id, action: "result" };
if (method === 'metadata') {
sandbox.tunnel.postMessage({ ...base, status: "ok", result: metadata || {} });

} else if (handlers.has(method)) {
try {
const result = await handlers.get(method)(...parameters);
if (sandbox.finished) {
return;
}
sandbox.tunnel.postMessage({ ...base, status: "ok", result });
} catch (err) {
sandbox.tunnel.postMessage({ ...base, status: "error", result: err.message });
}
} else {
sandbox.tunnel.postMessage({ ...base, status: "error", result: "unknown method"});
}
}
});

// Start listening for messages from sandbox
portToSandbox.start();

// Generate a unique session id for the sandbox; this equates to each sandbox getting its own session data(LocalStorage, cache, etc)
let sandboxSessionId = '', index = 10;
while (index) {
sandboxSessionId += charList[Math.floor(62 * Math.random())];
index -= 1;
}
sandboxSessionId = `firebot-sandbox-${Date.now()}-${sandboxSessionId}`;

// Create a new, hidden, browser window
sandbox.window = new BrowserWindow({
show: false,
title: 'Firebot - $JS Eval Sandbox',
webPreferences: {
preload: preloadPath,

// Sandbox the context
sandbox: true,
nodeIntegration: false,
contextIsolation: true,

// Use a unique session for each sandbox.
// Creates a unique local/SessionStorage instance, cache, etc for the sandbox
session: session.fromPartition(sandboxSessionId, { cache: false }),

// Loosen web-request restrictions
webSecurity: false,

// Tighten restrictions
// No autoplay without user interaction and since the window is
// never shown to the user there will never be a user gesture thus
// no playing of audio or video.
autoplayPolicy: 'user-gesture-required',

// Disable abusable and/or irrelevent features
disableDialogs: true,
webgl: false,
images: false,
enableWebSQL: false
}
});

// Prevent sandbox js from opening windows
sandbox.window.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));

// Prevent sandbox js from navigating away from sandbox page
sandbox.window.webContents.on('will-navigate', event => event.preventDefault());

// Prevent sandbox from altering page title
sandbox.window.on('page-title-updated', event => event.preventDefault());

// Cleanup the sandbox if it becomes unresponsive
sandbox.window.on('unresponsive', () => sandbox.reject('sandbox unresponsive'));

// Cleanup the sandbox if the window closes
sandbox.window.on('closed', () => sandbox.reject('sandbox closed'));

// Reroute console.* from the sandbox to the logger
sandbox.window.webContents.on('console-message', (event, level, message) => {
if (level === 2 && /^%cElectron /i.test(message)) {
return;
}
switch (level) {
case 1:
logger.info(`($evalJS Sandbox) ${message}`);
break;

case 2:
logger.warn(`($evalJS Sandbox) ${message}`);
break;

case 3:
logger.error(`($evalJS Sandbox) ${message}`);
break;

default:
logger.verbose(`($evalJS Sandbox) ${message}`);
}
});

// Wait for the contents of the sandbox window to be ready
sandbox.window.on('ready-to-show', () => {

// Give evaluation 15s to resolve
sandbox.timeout = setTimeout(() => sandbox.reject('eval timed out'), 15000);

// send the message port the sandbox should use to the preload script
sandbox.window.webContents.postMessage('firebot-port', null, [portToBackend]);

// tell sandbox the code to evaluate
sandbox.tunnel.postMessage({
id: 0,
action: 'method',
method: 'evaluate',
parameters: [code, ...args]
});
});

// load the sandbox html
sandbox.window.loadFile(htmlPath);
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
if ((typeof id !== 'string' && typeof id !== 'number') || typeof action !== 'string') {
return;
}
console.log(`Message from Firebot: ${JSON.stringify(event.data)}`);

// Result of calling a Firebot.* api method
if (
Expand Down Expand Up @@ -114,7 +113,8 @@
const metadata = await Firebot.metadata();

// Wrap the code to evaluate
const evaluate = new Function('Firebot', 'metadata', 'parameters', `return (async ()=>{"use strict";${parameters[0]}})()`);
const AsyncFunction = (async function () {}).constructor;
const evaluate = new AsyncFunction('Firebot', 'metadata', 'parameters', parameters[0]);

// Attempt to call the evaluator function
const result = await evaluate(Firebot, metadata, parameters.slice(1));
Expand Down
1 change: 1 addition & 0 deletions src/backend/effects/builtin-effect-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ exports.loadEffects = () => {
'delete-chat-message',
'dice',
'effect-group',
'eval-js',
'file-writer',
'html',
'http-request',
Expand Down
Loading

0 comments on commit e3a6569

Please sign in to comment.