Feature: remote browser support (#3904)

* [empty commit] pull request for remote browser support

* Remote browser: Added UI screens and DB tables.

* Remote browser working

* Fixing tests

* Fix tests

* Fix tests

* fix tests

* Test browser

* revert init_db.js

* Changed drop down to ActionSelect

* Fix translations

* added remote browsers toggle

* revert changes package-lock

* Fix bad english

* Set default remote browser

* Remote browsers Requested changes

* fixed description.
This commit is contained in:
Adam Hancock
2023-12-01 07:29:10 +00:00
committed by GitHub
parent 0294118cbf
commit 62780001f7
15 changed files with 579 additions and 7 deletions

View File

@@ -185,6 +185,30 @@ async function sendDockerHostList(socket) {
return list;
}
/**
* Send list of docker hosts to client
* @param {Socket} socket Socket.io socket instance
* @returns {Promise<Bean[]>} List of docker hosts
*/
async function sendRemoteBrowserList(socket) {
const timeLogger = new TimeLogger();
let result = [];
let list = await R.find("remote_browser", " user_id = ? ", [
socket.userID,
]);
for (let bean of list) {
result.push(bean.toJSON());
}
io.to(socket.userID).emit("remoteBrowserList", result);
timeLogger.print("Send Remote Browser List");
return list;
}
module.exports = {
sendNotificationList,
sendImportantHeartbeatList,
@@ -192,5 +216,6 @@ module.exports = {
sendProxyList,
sendAPIKeyList,
sendInfo,
sendDockerHostList
sendDockerHostList,
sendRemoteBrowserList,
};

View File

@@ -154,6 +154,7 @@ class Monitor extends BeanModel {
kafkaProducerAllowAutoTopicCreation: this.getKafkaProducerAllowAutoTopicCreation(),
kafkaProducerMessage: this.kafkaProducerMessage,
screenshot,
remote_browser: this.remote_browser,
};
if (includeSensitiveData) {

View File

@@ -0,0 +1,17 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
class RemoteBrowser extends BeanModel {
/**
* Returns an object that ready to parse to JSON
* @returns {object} Object ready to parse
*/
toJSON() {
return {
id: this.id,
url: this.url,
name: this.name,
};
}
}
module.exports = RemoteBrowser;

View File

@@ -8,6 +8,7 @@ const path = require("path");
const Database = require("../database");
const jwt = require("jsonwebtoken");
const config = require("../config");
const { RemoteBrowser } = require("../remote-browser");
let browser = null;
@@ -85,6 +86,19 @@ async function getBrowser() {
return browser;
}
/**
* Get the current instance of the browser. If there isn't one, create it
* @param {integer} remoteBrowserID Path to executable
* @param {integer} userId User ID
* @returns {Promise<Browser>} The browser
*/
async function getRemoteBrowser(remoteBrowserID, userId) {
let remoteBrowser = await RemoteBrowser.get(remoteBrowserID, userId);
log.debug("MONITOR", `Using remote browser: ${remoteBrowser.name} (${remoteBrowser.id})`);
browser = chromium.connect(remoteBrowser.url);
return browser;
}
/**
* Prepare the chrome executable path
* @param {string} executablePath Path to chrome executable
@@ -191,11 +205,21 @@ async function testChrome(executablePath) {
throw new Error(e.message);
}
}
// test remote browser
/**
* TODO: connect remote browser? https://playwright.dev/docs/api/class-browsertype#browser-type-connect
*
* @param {string} remoteBrowserURL Remote Browser URL
* @returns {Promise<boolean>} Returns if connection worked
*/
async function testRemoteBrowser(remoteBrowserURL) {
try {
const browser = await chromium.connect(remoteBrowserURL);
browser.version();
await browser.close();
return true;
} catch (e) {
throw new Error(e.message);
}
}
class RealBrowserMonitorType extends MonitorType {
name = "real-browser";
@@ -204,7 +228,7 @@ class RealBrowserMonitorType extends MonitorType {
* @inheritdoc
*/
async check(monitor, heartbeat, server) {
const browser = await getBrowser();
const browser = monitor.remote_browser ? await getRemoteBrowser(monitor.remote_browser, monitor.user_id) : await getBrowser();
const context = await browser.newContext();
const page = await context.newPage();
@@ -237,4 +261,5 @@ module.exports = {
RealBrowserMonitorType,
testChrome,
resetChrome,
testRemoteBrowser,
};

84
server/remote-browser.js Normal file
View File

@@ -0,0 +1,84 @@
const { R } = require("redbean-node");
const { testRemoteBrowser } = require("./monitor-types/real-browser-monitor-type.js");
class RemoteBrowser {
/**
* Gets remote browser from ID
* @param {number} remoteBrowserID ID of the remote browser
* @param {number} userID ID of the user who created the remote browser
* @returns {Promise<Bean>} Remote Browser
*/
static async get(remoteBrowserID, userID) {
let bean = await R.findOne("remote_browser", " id = ? AND user_id = ? ", [ remoteBrowserID, userID ]);
if (!bean) {
throw new Error("Remote browser not found");
}
return bean;
}
/**
* Save a Remote Browser
* @param {object} remoteBrowser Remote Browser to save
* @param {?number} remoteBrowserID ID of the Remote Browser to update
* @param {number} userID ID of the user who adds the Remote Browser
* @returns {Promise<Bean>} Updated Remote Browser
*/
static async save(remoteBrowser, remoteBrowserID, userID) {
let bean;
if (remoteBrowserID) {
bean = await R.findOne("remote_browser", " id = ? AND user_id = ? ", [ remoteBrowserID, userID ]);
if (!bean) {
throw new Error("Remote browser not found");
}
} else {
bean = R.dispense("remote_browser");
}
bean.user_id = userID;
bean.name = remoteBrowser.name;
bean.url = remoteBrowser.url;
await R.store(bean);
return bean;
}
/**
* Delete a Remote Browser
* @param {number} remoteBrowserID ID of the Remote Browser to delete
* @param {number} userID ID of the user who created the Remote Browser
* @returns {Promise<void>}
*/
static async delete(remoteBrowserID, userID) {
let bean = await R.findOne("remote_browser", " id = ? AND user_id = ? ", [ remoteBrowserID, userID ]);
if (!bean) {
throw new Error("Remote Browser not found");
}
// Delete removed remote browser from monitors if exists
await R.exec("UPDATE monitor SET remote_browser = null WHERE remote_browser = ?", [ remoteBrowserID ]);
await R.trash(bean);
}
/**
* Tests the connection to Remote Browser
* @param {object} remoteBrowser Docker host to check for
* @returns {boolean} Returns if connection worked
*/
static async test(remoteBrowser) {
const testResult = await testRemoteBrowser(remoteBrowser.id, remoteBrowser.user_id);
return testResult;
}
}
module.exports = {
RemoteBrowser,
};

View File

@@ -131,9 +131,10 @@ const testMode = !!args["test"] || false;
const e2eTestMode = !!args["e2e"] || false;
// Must be after io instantiation
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList } = require("./client");
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList, sendRemoteBrowserList } = require("./client");
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
const { remoteBrowserSocketHandler } = require("./socket-handlers/remote-browser-socket-handler");
const TwoFA = require("./2fa");
const StatusPage = require("./model/status_page");
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
@@ -827,6 +828,7 @@ let needSetup = false;
bean.kafkaProducerAllowAutoTopicCreation =
monitor.kafkaProducerAllowAutoTopicCreation;
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
bean.remote_browser = monitor.remote_browser;
bean.validate();
@@ -1508,6 +1510,7 @@ let needSetup = false;
dockerSocketHandler(socket);
maintenanceSocketHandler(socket);
apiKeySocketHandler(socket);
remoteBrowserSocketHandler(socket);
generalSocketHandler(socket, server);
log.debug("server", "added all socket handlers");
@@ -1616,6 +1619,7 @@ async function afterLogin(socket, user) {
sendProxyList(socket);
sendDockerHostList(socket);
sendAPIKeyList(socket);
sendRemoteBrowserList(socket);
await sleep(500);

View File

@@ -0,0 +1,82 @@
const { sendRemoteBrowserList } = require("../client");
const { checkLogin } = require("../util-server");
const { RemoteBrowser } = require("../remote-browser");
const { log } = require("../../src/util");
const { testRemoteBrowser } = require("../monitor-types/real-browser-monitor-type");
/**
* Handlers for docker hosts
* @param {Socket} socket Socket.io instance
* @returns {void}
*/
module.exports.remoteBrowserSocketHandler = (socket) => {
socket.on("addRemoteBrowser", async (remoteBrowser, remoteBrowserID, callback) => {
try {
checkLogin(socket);
let remoteBrowserBean = await RemoteBrowser.save(remoteBrowser, remoteBrowserID, socket.userID);
await sendRemoteBrowserList(socket);
callback({
ok: true,
msg: "Saved.",
msgi18n: true,
id: remoteBrowserBean.id,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("deleteRemoteBrowser", async (dockerHostID, callback) => {
try {
checkLogin(socket);
await RemoteBrowser.delete(dockerHostID, socket.userID);
await sendRemoteBrowserList(socket);
callback({
ok: true,
msg: "successDeleted",
msgi18n: true,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
socket.on("testRemoteBrowser", async (remoteBrowser, callback) => {
try {
checkLogin(socket);
let check = await testRemoteBrowser(remoteBrowser.url);
log.info("remoteBrowser", "Tested remote browser: " + check);
let msg;
if (check) {
msg = "Connected Successfully.";
}
callback({
ok: true,
msg,
});
} catch (e) {
log.error("remoteBrowser", e);
callback({
ok: false,
msg: e.message,
});
}
});
};