Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: remote browser support #3904

Merged
merged 27 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dc72c86
[empty commit] pull request for remote browser support
adamhancock Oct 16, 2023
d2da454
Remote browser: Added UI screens and DB tables.
adamhancock Oct 17, 2023
77ed507
Merge branch 'master' into remotebrowsers
adamhancock Oct 17, 2023
05bf328
Remote browser working
adamhancock Oct 17, 2023
c6aed63
Merge branch 'master' into remotebrowsers
adamhancock Oct 17, 2023
c9fa14a
Fixing tests
adamhancock Oct 18, 2023
a9206b2
Fix tests
adamhancock Oct 18, 2023
f7e8f48
Fix tests
adamhancock Oct 18, 2023
dfec5ad
Merge branch 'master' into remotebrowsers
adamhancock Oct 18, 2023
d737d86
fix tests
adamhancock Oct 18, 2023
4105f9f
Test browser
adamhancock Oct 18, 2023
fff2e59
Merge branch 'master' into remotebrowsers
adamhancock Oct 19, 2023
54586e3
revert init_db.js
adamhancock Oct 19, 2023
5881f45
Merge branch 'master' into remotebrowsers
adamhancock Oct 19, 2023
f93a781
Changed drop down to ActionSelect
adamhancock Oct 21, 2023
58233fc
Merge branch 'master' into remotebrowsers
adamhancock Oct 21, 2023
463c2ba
Fix translations
adamhancock Oct 23, 2023
50c5b08
added remote browsers toggle
adamhancock Oct 23, 2023
4613c12
revert changes package-lock
adamhancock Oct 23, 2023
cabf809
Fix bad english
adamhancock Oct 23, 2023
a08b9bc
Set default remote browser
adamhancock Oct 24, 2023
74ce3fb
Merge branch 'master' into remotebrowsers
adamhancock Nov 2, 2023
0599823
Merge branch 'master' into remotebrowsers
adamhancock Nov 23, 2023
f206498
Remote browsers Requested changes
adamhancock Nov 30, 2023
a1c4e1e
Merge branch 'master' into remotebrowsers
adamhancock Nov 30, 2023
eab43eb
fixed description.
adamhancock Nov 30, 2023
af257bb
Merge branch 'master' into remotebrowsers
adamhancock Nov 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -153,6 +153,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