Skip to content

Commit

Permalink
Feature: remote browser support (#3904)
Browse files Browse the repository at this point in the history
* [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.
  • Loading branch information
adamhancock committed Dec 1, 2023
1 parent 0294118 commit 6278000
Show file tree
Hide file tree
Showing 15 changed files with 579 additions and 7 deletions.
21 changes: 21 additions & 0 deletions db/knex_migrations/2023-10-16-0000-create-remote-browsers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
exports.up = function (knex) {
return knex.schema
.createTable("remote_browser", function (table) {
table.increments("id");
table.string("name", 255).notNullable();
table.string("url", 255).notNullable();
table.integer("user_id").unsigned();
}).alterTable("monitor", function (table) {
// Add new column monitor.remote_browser
table.integer("remote_browser").nullable().defaultTo(null).unsigned()
.index()
.references("id")
.inTable("remote_browser");
});
};

exports.down = function (knex) {
return knex.schema.dropTable("remote_browser").alterTable("monitor", function (table) {
table.dropColumn("remote_browser");
});
};
27 changes: 26 additions & 1 deletion server/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,37 @@ 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,
sendHeartbeatList,
sendProxyList,
sendAPIKeyList,
sendInfo,
sendDockerHostList
sendDockerHostList,
sendRemoteBrowserList,
};
1 change: 1 addition & 0 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ class Monitor extends BeanModel {
kafkaProducerAllowAutoTopicCreation: this.getKafkaProducerAllowAutoTopicCreation(),
kafkaProducerMessage: this.kafkaProducerMessage,
screenshot,
remote_browser: this.remote_browser,
};

if (includeSensitiveData) {
Expand Down
17 changes: 17 additions & 0 deletions server/model/remote_browser.js
Original file line number Diff line number Diff line change
@@ -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;
33 changes: 29 additions & 4 deletions server/monitor-types/real-browser-monitor-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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";
Expand All @@ -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();

Expand Down Expand Up @@ -237,4 +261,5 @@ module.exports = {
RealBrowserMonitorType,
testChrome,
resetChrome,
testRemoteBrowser,
};
84 changes: 84 additions & 0 deletions server/remote-browser.js
Original file line number Diff line number Diff line change
@@ -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,
};
6 changes: 5 additions & 1 deletion server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -827,6 +828,7 @@ let needSetup = false;
bean.kafkaProducerAllowAutoTopicCreation =
monitor.kafkaProducerAllowAutoTopicCreation;
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
bean.remote_browser = monitor.remote_browser;

bean.validate();

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -1616,6 +1619,7 @@ async function afterLogin(socket, user) {
sendProxyList(socket);
sendDockerHostList(socket);
sendAPIKeyList(socket);
sendRemoteBrowserList(socket);

await sleep(500);

Expand Down
82 changes: 82 additions & 0 deletions server/socket-handlers/remote-browser-socket-handler.js
Original file line number Diff line number Diff line change
@@ -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,
});
}
});
};
Loading

0 comments on commit 6278000

Please sign in to comment.