summaryrefslogtreecommitdiffstats
path: root/server/socket-handlers
diff options
context:
space:
mode:
Diffstat (limited to 'server/socket-handlers')
-rw-r--r--server/socket-handlers/api-key-socket-handler.js155
-rw-r--r--server/socket-handlers/chart-socket-handler.js38
-rw-r--r--server/socket-handlers/cloudflared-socket-handler.js122
-rw-r--r--server/socket-handlers/database-socket-handler.js42
-rw-r--r--server/socket-handlers/docker-socket-handler.js82
-rw-r--r--server/socket-handlers/general-socket-handler.js127
-rw-r--r--server/socket-handlers/maintenance-socket-handler.js337
-rw-r--r--server/socket-handlers/proxy-socket-handler.js61
-rw-r--r--server/socket-handlers/remote-browser-socket-handler.js82
-rw-r--r--server/socket-handlers/status-page-socket-handler.js374
10 files changed, 1420 insertions, 0 deletions
diff --git a/server/socket-handlers/api-key-socket-handler.js b/server/socket-handlers/api-key-socket-handler.js
new file mode 100644
index 0000000..f76b909
--- /dev/null
+++ b/server/socket-handlers/api-key-socket-handler.js
@@ -0,0 +1,155 @@
+const { checkLogin } = require("../util-server");
+const { log } = require("../../src/util");
+const { R } = require("redbean-node");
+const { nanoid } = require("nanoid");
+const passwordHash = require("../password-hash");
+const apicache = require("../modules/apicache");
+const APIKey = require("../model/api_key");
+const { Settings } = require("../settings");
+const { sendAPIKeyList } = require("../client");
+
+/**
+ * Handlers for API keys
+ * @param {Socket} socket Socket.io instance
+ * @returns {void}
+ */
+module.exports.apiKeySocketHandler = (socket) => {
+ // Add a new api key
+ socket.on("addAPIKey", async (key, callback) => {
+ try {
+ checkLogin(socket);
+
+ let clearKey = nanoid(40);
+ let hashedKey = passwordHash.generate(clearKey);
+ key["key"] = hashedKey;
+ let bean = await APIKey.save(key, socket.userID);
+
+ log.debug("apikeys", "Added API Key");
+ log.debug("apikeys", key);
+
+ // Append key ID and prefix to start of key seperated by _, used to get
+ // correct hash when validating key.
+ let formattedKey = "uk" + bean.id + "_" + clearKey;
+ await sendAPIKeyList(socket);
+
+ // Enable API auth if the user creates a key, otherwise only basic
+ // auth will be used for API.
+ await Settings.set("apiKeysEnabled", true);
+
+ callback({
+ ok: true,
+ msg: "successAdded",
+ msgi18n: true,
+ key: formattedKey,
+ keyID: bean.id,
+ });
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("getAPIKeyList", async (callback) => {
+ try {
+ checkLogin(socket);
+ await sendAPIKeyList(socket);
+ callback({
+ ok: true,
+ });
+ } catch (e) {
+ console.error(e);
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("deleteAPIKey", async (keyID, callback) => {
+ try {
+ checkLogin(socket);
+
+ log.debug("apikeys", `Deleted API Key: ${keyID} User ID: ${socket.userID}`);
+
+ await R.exec("DELETE FROM api_key WHERE id = ? AND user_id = ? ", [
+ keyID,
+ socket.userID,
+ ]);
+
+ apicache.clear();
+
+ callback({
+ ok: true,
+ msg: "successDeleted",
+ msgi18n: true,
+ });
+
+ await sendAPIKeyList(socket);
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("disableAPIKey", async (keyID, callback) => {
+ try {
+ checkLogin(socket);
+
+ log.debug("apikeys", `Disabled Key: ${keyID} User ID: ${socket.userID}`);
+
+ await R.exec("UPDATE api_key SET active = 0 WHERE id = ? ", [
+ keyID,
+ ]);
+
+ apicache.clear();
+
+ callback({
+ ok: true,
+ msg: "successDisabled",
+ msgi18n: true,
+ });
+
+ await sendAPIKeyList(socket);
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("enableAPIKey", async (keyID, callback) => {
+ try {
+ checkLogin(socket);
+
+ log.debug("apikeys", `Enabled Key: ${keyID} User ID: ${socket.userID}`);
+
+ await R.exec("UPDATE api_key SET active = 1 WHERE id = ? ", [
+ keyID,
+ ]);
+
+ apicache.clear();
+
+ callback({
+ ok: true,
+ msg: "successEnabled",
+ msgi18n: true,
+ });
+
+ await sendAPIKeyList(socket);
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+};
diff --git a/server/socket-handlers/chart-socket-handler.js b/server/socket-handlers/chart-socket-handler.js
new file mode 100644
index 0000000..654db0e
--- /dev/null
+++ b/server/socket-handlers/chart-socket-handler.js
@@ -0,0 +1,38 @@
+const { checkLogin } = require("../util-server");
+const { UptimeCalculator } = require("../uptime-calculator");
+const { log } = require("../../src/util");
+
+module.exports.chartSocketHandler = (socket) => {
+ socket.on("getMonitorChartData", async (monitorID, period, callback) => {
+ try {
+ checkLogin(socket);
+
+ log.debug("monitor", `Get Monitor Chart Data: ${monitorID} User ID: ${socket.userID}`);
+
+ if (period == null) {
+ throw new Error("Invalid period.");
+ }
+
+ let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID);
+
+ let data;
+ if (period <= 24) {
+ data = uptimeCalculator.getDataArray(period * 60, "minute");
+ } else if (period <= 720) {
+ data = uptimeCalculator.getDataArray(period, "hour");
+ } else {
+ data = uptimeCalculator.getDataArray(period / 24, "day");
+ }
+
+ callback({
+ ok: true,
+ data,
+ });
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+};
diff --git a/server/socket-handlers/cloudflared-socket-handler.js b/server/socket-handlers/cloudflared-socket-handler.js
new file mode 100644
index 0000000..809191f
--- /dev/null
+++ b/server/socket-handlers/cloudflared-socket-handler.js
@@ -0,0 +1,122 @@
+const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
+const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
+const { UptimeKumaServer } = require("../uptime-kuma-server");
+const { log } = require("../../src/util");
+const io = UptimeKumaServer.getInstance().io;
+
+const prefix = "cloudflared_";
+const cloudflared = new CloudflaredTunnel();
+
+/**
+ * Change running state
+ * @param {string} running Is it running?
+ * @param {string} message Message to pass
+ * @returns {void}
+ */
+cloudflared.change = (running, message) => {
+ io.to("cloudflared").emit(prefix + "running", running);
+ io.to("cloudflared").emit(prefix + "message", message);
+};
+
+/**
+ * Emit an error message
+ * @param {string} errorMessage Error message to send
+ * @returns {void}
+ */
+cloudflared.error = (errorMessage) => {
+ io.to("cloudflared").emit(prefix + "errorMessage", errorMessage);
+};
+
+/**
+ * Handler for cloudflared
+ * @param {Socket} socket Socket.io instance
+ * @returns {void}
+ */
+module.exports.cloudflaredSocketHandler = (socket) => {
+
+ socket.on(prefix + "join", async () => {
+ try {
+ checkLogin(socket);
+ socket.join("cloudflared");
+ io.to(socket.userID).emit(prefix + "installed", cloudflared.checkInstalled());
+ io.to(socket.userID).emit(prefix + "running", cloudflared.running);
+ io.to(socket.userID).emit(prefix + "token", await setting("cloudflaredTunnelToken"));
+ } catch (error) { }
+ });
+
+ socket.on(prefix + "leave", async () => {
+ try {
+ checkLogin(socket);
+ socket.leave("cloudflared");
+ } catch (error) { }
+ });
+
+ socket.on(prefix + "start", async (token) => {
+ try {
+ checkLogin(socket);
+ if (token && typeof token === "string") {
+ await setSetting("cloudflaredTunnelToken", token);
+ cloudflared.token = token;
+ } else {
+ cloudflared.token = null;
+ }
+ cloudflared.start();
+ } catch (error) { }
+ });
+
+ socket.on(prefix + "stop", async (currentPassword, callback) => {
+ try {
+ checkLogin(socket);
+ const disabledAuth = await setting("disableAuth");
+ if (!disabledAuth) {
+ await doubleCheckPassword(socket, currentPassword);
+ }
+ cloudflared.stop();
+ } catch (error) {
+ callback({
+ ok: false,
+ msg: error.message,
+ });
+ }
+ });
+
+ socket.on(prefix + "removeToken", async () => {
+ try {
+ checkLogin(socket);
+ await setSetting("cloudflaredTunnelToken", "");
+ } catch (error) { }
+ });
+
+};
+
+/**
+ * Automatically start cloudflared
+ * @param {string} token Cloudflared tunnel token
+ * @returns {Promise<void>}
+ */
+module.exports.autoStart = async (token) => {
+ if (!token) {
+ token = await setting("cloudflaredTunnelToken");
+ } else {
+ // Override the current token via args or env var
+ await setSetting("cloudflaredTunnelToken", token);
+ console.log("Use cloudflared token from args or env var");
+ }
+
+ if (token) {
+ console.log("Start cloudflared");
+ cloudflared.token = token;
+ cloudflared.start();
+ }
+};
+
+/**
+ * Stop cloudflared
+ * @returns {Promise<void>}
+ */
+module.exports.stop = async () => {
+ log.info("cloudflared", "Stop cloudflared");
+ if (cloudflared) {
+ cloudflared.stop();
+ }
+};
diff --git a/server/socket-handlers/database-socket-handler.js b/server/socket-handlers/database-socket-handler.js
new file mode 100644
index 0000000..ee2394b
--- /dev/null
+++ b/server/socket-handlers/database-socket-handler.js
@@ -0,0 +1,42 @@
+const { checkLogin } = require("../util-server");
+const Database = require("../database");
+
+/**
+ * Handlers for database
+ * @param {Socket} socket Socket.io instance
+ * @returns {void}
+ */
+module.exports.databaseSocketHandler = (socket) => {
+
+ // Post or edit incident
+ socket.on("getDatabaseSize", async (callback) => {
+ try {
+ checkLogin(socket);
+ callback({
+ ok: true,
+ size: Database.getSize(),
+ });
+ } catch (error) {
+ callback({
+ ok: false,
+ msg: error.message,
+ });
+ }
+ });
+
+ socket.on("shrinkDatabase", async (callback) => {
+ try {
+ checkLogin(socket);
+ await Database.shrink();
+ callback({
+ ok: true,
+ });
+ } catch (error) {
+ callback({
+ ok: false,
+ msg: error.message,
+ });
+ }
+ });
+
+};
diff --git a/server/socket-handlers/docker-socket-handler.js b/server/socket-handlers/docker-socket-handler.js
new file mode 100644
index 0000000..95a60bc
--- /dev/null
+++ b/server/socket-handlers/docker-socket-handler.js
@@ -0,0 +1,82 @@
+const { sendDockerHostList } = require("../client");
+const { checkLogin } = require("../util-server");
+const { DockerHost } = require("../docker");
+const { log } = require("../../src/util");
+
+/**
+ * Handlers for docker hosts
+ * @param {Socket} socket Socket.io instance
+ * @returns {void}
+ */
+module.exports.dockerSocketHandler = (socket) => {
+ socket.on("addDockerHost", async (dockerHost, dockerHostID, callback) => {
+ try {
+ checkLogin(socket);
+
+ let dockerHostBean = await DockerHost.save(dockerHost, dockerHostID, socket.userID);
+ await sendDockerHostList(socket);
+
+ callback({
+ ok: true,
+ msg: "Saved.",
+ msgi18n: true,
+ id: dockerHostBean.id,
+ });
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("deleteDockerHost", async (dockerHostID, callback) => {
+ try {
+ checkLogin(socket);
+
+ await DockerHost.delete(dockerHostID, socket.userID);
+ await sendDockerHostList(socket);
+
+ callback({
+ ok: true,
+ msg: "successDeleted",
+ msgi18n: true,
+ });
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("testDockerHost", async (dockerHost, callback) => {
+ try {
+ checkLogin(socket);
+
+ let amount = await DockerHost.testDockerHost(dockerHost);
+ let msg;
+
+ if (amount >= 1) {
+ msg = "Connected Successfully. Amount of containers: " + amount;
+ } else {
+ msg = "Connected Successfully, but there are no containers?";
+ }
+
+ callback({
+ ok: true,
+ msg,
+ });
+
+ } catch (e) {
+ log.error("docker", e);
+
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+};
diff --git a/server/socket-handlers/general-socket-handler.js b/server/socket-handlers/general-socket-handler.js
new file mode 100644
index 0000000..50dcd94
--- /dev/null
+++ b/server/socket-handlers/general-socket-handler.js
@@ -0,0 +1,127 @@
+const { log } = require("../../src/util");
+const { Settings } = require("../settings");
+const { sendInfo } = require("../client");
+const { checkLogin } = require("../util-server");
+const GameResolver = require("gamedig/lib/GameResolver");
+const { testChrome } = require("../monitor-types/real-browser-monitor-type");
+const fs = require("fs");
+const path = require("path");
+
+let gameResolver = new GameResolver();
+let gameList = null;
+
+/**
+ * Get a game list via GameDig
+ * @returns {object[]} list of games supported by GameDig
+ */
+function getGameList() {
+ if (gameList == null) {
+ gameList = gameResolver._readGames().games.sort((a, b) => {
+ if ( a.pretty < b.pretty ) {
+ return -1;
+ }
+ if ( a.pretty > b.pretty ) {
+ return 1;
+ }
+ return 0;
+ });
+ }
+ return gameList;
+}
+
+/**
+ * Handler for general events
+ * @param {Socket} socket Socket.io instance
+ * @param {UptimeKumaServer} server Uptime Kuma server
+ * @returns {void}
+ */
+module.exports.generalSocketHandler = (socket, server) => {
+ socket.on("initServerTimezone", async (timezone) => {
+ try {
+ checkLogin(socket);
+ log.debug("generalSocketHandler", "Timezone: " + timezone);
+ await Settings.set("initServerTimezone", true);
+ await server.setTimezone(timezone);
+ await sendInfo(socket);
+ } catch (e) {
+ log.warn("initServerTimezone", e.message);
+ }
+ });
+
+ socket.on("getGameList", async (callback) => {
+ try {
+ checkLogin(socket);
+ callback({
+ ok: true,
+ gameList: getGameList(),
+ });
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("testChrome", (executable, callback) => {
+ try {
+ checkLogin(socket);
+ // Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
+ testChrome(executable).then((version) => {
+ callback({
+ ok: true,
+ msg: {
+ key: "foundChromiumVersion",
+ values: [ version ],
+ },
+ msgi18n: true,
+ });
+ }).catch((e) => {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ });
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("getPushExample", (language, callback) => {
+
+ try {
+ let dir = path.join("./extra/push-examples", language);
+ let files = fs.readdirSync(dir);
+
+ for (let file of files) {
+ if (file.startsWith("index.")) {
+ callback({
+ ok: true,
+ code: fs.readFileSync(path.join(dir, file), "utf8"),
+ });
+ return;
+ }
+ }
+ } catch (e) {
+
+ }
+
+ callback({
+ ok: false,
+ msg: "Not found",
+ });
+ });
+
+ // Disconnect all other socket clients of the user
+ socket.on("disconnectOtherSocketClients", async () => {
+ try {
+ checkLogin(socket);
+ server.disconnectAllSocketClients(socket.userID, socket.id);
+ } catch (e) {
+ log.warn("disconnectAllSocketClients", e.message);
+ }
+ });
+};
diff --git a/server/socket-handlers/maintenance-socket-handler.js b/server/socket-handlers/maintenance-socket-handler.js
new file mode 100644
index 0000000..7de13fe
--- /dev/null
+++ b/server/socket-handlers/maintenance-socket-handler.js
@@ -0,0 +1,337 @@
+const { checkLogin } = require("../util-server");
+const { log } = require("../../src/util");
+const { R } = require("redbean-node");
+const apicache = require("../modules/apicache");
+const { UptimeKumaServer } = require("../uptime-kuma-server");
+const Maintenance = require("../model/maintenance");
+const server = UptimeKumaServer.getInstance();
+
+/**
+ * Handlers for Maintenance
+ * @param {Socket} socket Socket.io instance
+ * @returns {void}
+ */
+module.exports.maintenanceSocketHandler = (socket) => {
+ // Add a new maintenance
+ socket.on("addMaintenance", async (maintenance, callback) => {
+ try {
+ checkLogin(socket);
+
+ log.debug("maintenance", maintenance);
+
+ let bean = await Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
+ bean.user_id = socket.userID;
+ let maintenanceID = await R.store(bean);
+
+ server.maintenanceList[maintenanceID] = bean;
+ await bean.run(true);
+
+ await server.sendMaintenanceList(socket);
+
+ callback({
+ ok: true,
+ msg: "successAdded",
+ msgi18n: true,
+ maintenanceID,
+ });
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ // Edit a maintenance
+ socket.on("editMaintenance", async (maintenance, callback) => {
+ try {
+ checkLogin(socket);
+
+ let bean = server.getMaintenance(maintenance.id);
+
+ if (bean.user_id !== socket.userID) {
+ throw new Error("Permission denied.");
+ }
+
+ await Maintenance.jsonToBean(bean, maintenance);
+ await R.store(bean);
+ await bean.run(true);
+ await server.sendMaintenanceList(socket);
+
+ callback({
+ ok: true,
+ msg: "Saved.",
+ msgi18n: true,
+ maintenanceID: bean.id,
+ });
+
+ } catch (e) {
+ console.error(e);
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ // Add a new monitor_maintenance
+ socket.on("addMonitorMaintenance", async (maintenanceID, monitors, callback) => {
+ try {
+ checkLogin(socket);
+
+ await R.exec("DELETE FROM monitor_maintenance WHERE maintenance_id = ?", [
+ maintenanceID
+ ]);
+
+ for await (const monitor of monitors) {
+ let bean = R.dispense("monitor_maintenance");
+
+ bean.import({
+ monitor_id: monitor.id,
+ maintenance_id: maintenanceID
+ });
+ await R.store(bean);
+ }
+
+ apicache.clear();
+
+ callback({
+ ok: true,
+ msg: "successAdded",
+ msgi18n: true,
+ });
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ // Add a new monitor_maintenance
+ socket.on("addMaintenanceStatusPage", async (maintenanceID, statusPages, callback) => {
+ try {
+ checkLogin(socket);
+
+ await R.exec("DELETE FROM maintenance_status_page WHERE maintenance_id = ?", [
+ maintenanceID
+ ]);
+
+ for await (const statusPage of statusPages) {
+ let bean = R.dispense("maintenance_status_page");
+
+ bean.import({
+ status_page_id: statusPage.id,
+ maintenance_id: maintenanceID
+ });
+ await R.store(bean);
+ }
+
+ apicache.clear();
+
+ callback({
+ ok: true,
+ msg: "successAdded",
+ msgi18n: true,
+ });
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("getMaintenance", async (maintenanceID, callback) => {
+ try {
+ checkLogin(socket);
+
+ log.debug("maintenance", `Get Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
+
+ let bean = await R.findOne("maintenance", " id = ? AND user_id = ? ", [
+ maintenanceID,
+ socket.userID,
+ ]);
+
+ callback({
+ ok: true,
+ maintenance: await bean.toJSON(),
+ });
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("getMaintenanceList", async (callback) => {
+ try {
+ checkLogin(socket);
+ await server.sendMaintenanceList(socket);
+ callback({
+ ok: true,
+ });
+ } catch (e) {
+ console.error(e);
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("getMonitorMaintenance", async (maintenanceID, callback) => {
+ try {
+ checkLogin(socket);
+
+ log.debug("maintenance", `Get Monitors for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
+
+ let monitors = await R.getAll("SELECT monitor.id FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [
+ maintenanceID,
+ ]);
+
+ callback({
+ ok: true,
+ monitors,
+ });
+
+ } catch (e) {
+ console.error(e);
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("getMaintenanceStatusPage", async (maintenanceID, callback) => {
+ try {
+ checkLogin(socket);
+
+ log.debug("maintenance", `Get Status Pages for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
+
+ let statusPages = await R.getAll("SELECT status_page.id, status_page.title FROM maintenance_status_page msp JOIN status_page ON msp.status_page_id = status_page.id WHERE msp.maintenance_id = ? ", [
+ maintenanceID,
+ ]);
+
+ callback({
+ ok: true,
+ statusPages,
+ });
+
+ } catch (e) {
+ console.error(e);
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("deleteMaintenance", async (maintenanceID, callback) => {
+ try {
+ checkLogin(socket);
+
+ log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
+
+ if (maintenanceID in server.maintenanceList) {
+ server.maintenanceList[maintenanceID].stop();
+ delete server.maintenanceList[maintenanceID];
+ }
+
+ await R.exec("DELETE FROM maintenance WHERE id = ? AND user_id = ? ", [
+ maintenanceID,
+ socket.userID,
+ ]);
+
+ apicache.clear();
+
+ callback({
+ ok: true,
+ msg: "successDeleted",
+ msgi18n: true,
+ });
+
+ await server.sendMaintenanceList(socket);
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("pauseMaintenance", async (maintenanceID, callback) => {
+ try {
+ checkLogin(socket);
+
+ log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
+
+ let maintenance = server.getMaintenance(maintenanceID);
+
+ if (!maintenance) {
+ throw new Error("Maintenance not found");
+ }
+
+ maintenance.active = false;
+ await R.store(maintenance);
+ maintenance.stop();
+
+ apicache.clear();
+
+ callback({
+ ok: true,
+ msg: "successPaused",
+ msgi18n: true,
+ });
+
+ await server.sendMaintenanceList(socket);
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("resumeMaintenance", async (maintenanceID, callback) => {
+ try {
+ checkLogin(socket);
+
+ log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
+
+ let maintenance = server.getMaintenance(maintenanceID);
+
+ if (!maintenance) {
+ throw new Error("Maintenance not found");
+ }
+
+ maintenance.active = true;
+ await R.store(maintenance);
+ await maintenance.run();
+
+ apicache.clear();
+
+ callback({
+ ok: true,
+ msg: "successResumed",
+ msgi18n: true,
+ });
+
+ await server.sendMaintenanceList(socket);
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+};
diff --git a/server/socket-handlers/proxy-socket-handler.js b/server/socket-handlers/proxy-socket-handler.js
new file mode 100644
index 0000000..9e80371
--- /dev/null
+++ b/server/socket-handlers/proxy-socket-handler.js
@@ -0,0 +1,61 @@
+const { checkLogin } = require("../util-server");
+const { Proxy } = require("../proxy");
+const { sendProxyList } = require("../client");
+const { UptimeKumaServer } = require("../uptime-kuma-server");
+const server = UptimeKumaServer.getInstance();
+
+/**
+ * Handlers for proxy
+ * @param {Socket} socket Socket.io instance
+ * @returns {void}
+ */
+module.exports.proxySocketHandler = (socket) => {
+ socket.on("addProxy", async (proxy, proxyID, callback) => {
+ try {
+ checkLogin(socket);
+
+ const proxyBean = await Proxy.save(proxy, proxyID, socket.userID);
+ await sendProxyList(socket);
+
+ if (proxy.applyExisting) {
+ await Proxy.reloadProxy();
+ await server.sendMonitorList(socket);
+ }
+
+ callback({
+ ok: true,
+ msg: "Saved.",
+ msgi18n: true,
+ id: proxyBean.id,
+ });
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+
+ socket.on("deleteProxy", async (proxyID, callback) => {
+ try {
+ checkLogin(socket);
+
+ await Proxy.delete(proxyID, socket.userID);
+ await sendProxyList(socket);
+ await Proxy.reloadProxy();
+
+ callback({
+ ok: true,
+ msg: "successDeleted",
+ msgi18n: true,
+ });
+
+ } catch (e) {
+ callback({
+ ok: false,
+ msg: e.message,
+ });
+ }
+ });
+};
diff --git a/server/socket-handlers/remote-browser-socket-handler.js b/server/socket-handlers/remote-browser-socket-handler.js
new file mode 100644
index 0000000..ae53030
--- /dev/null
+++ b/server/socket-handlers/remote-browser-socket-handler.js
@@ -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,
+ });
+ }
+ });
+};
diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js
new file mode 100644
index 0000000..0804da1
--- /dev/null
+++ b/server/socket-handlers/status-page-socket-handler.js
@@ -0,0 +1,374 @@
+const { R } = require("redbean-node");
+const { checkLogin, setSetting } = require("../util-server");
+const dayjs = require("dayjs");
+const { log } = require("../../src/util");
+const ImageDataURI = require("../image-data-uri");
+const Database = require("../database");
+const apicache = require("../modules/apicache");
+const StatusPage = require("../model/status_page");
+const { UptimeKumaServer } = require("../uptime-kuma-server");
+
+/**
+ * Socket handlers for status page
+ * @param {Socket} socket Socket.io instance to add listeners on
+ * @returns {void}
+ */
+module.exports.statusPageSocketHandler = (socket) => {
+
+ // Post or edit incident
+ socket.on("postIncident", async (slug, incident, callback) => {
+ try {
+ checkLogin(socket);
+
+ let statusPageID = await StatusPage.slugToID(slug);
+
+ if (!statusPageID) {
+ throw new Error("slug is not found");
+ }
+
+ await R.exec("UPDATE incident SET pin = 0 WHERE status_page_id = ? ", [
+ statusPageID
+ ]);
+
+ let incidentBean;
+
+ if (incident.id) {
+ incidentBean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [
+ incident.id,
+ statusPageID
+ ]);
+ }
+
+ if (incidentBean == null) {
+ incidentBean = R.dispense("incident");
+ }
+
+ incidentBean.title = incident.title;
+ incidentBean.content = incident.content;
+ incidentBean.style = incident.style;
+ incidentBean.pin = true;
+ incidentBean.status_page_id = statusPageID;
+
+ if (incident.id) {
+ incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
+ } else {
+ incidentBean.createdDate = R.isoDateTime(dayjs.utc());
+ }
+
+ await R.store(incidentBean);
+
+ callback({
+ ok: true,
+ incident: incidentBean.toPublicJSON(),
+ });
+ } catch (error) {
+ callback({
+ ok: false,
+ msg: error.message,
+ });
+ }
+ });
+
+ socket.on("unpinIncident", async (slug, callback) => {
+ try {
+ checkLogin(socket);
+
+ let statusPageID = await StatusPage.slugToID(slug);
+
+ await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1 AND status_page_id = ? ", [
+ statusPageID
+ ]);
+
+ callback({
+ ok: true,
+ });
+ } catch (error) {
+ callback({
+ ok: false,
+ msg: error.message,
+ });
+ }
+ });
+
+ socket.on("getStatusPage", async (slug, callback) => {
+ try {
+ checkLogin(socket);
+
+ let statusPage = await R.findOne("status_page", " slug = ? ", [
+ slug
+ ]);
+
+ if (!statusPage) {
+ throw new Error("No slug?");
+ }
+
+ callback({
+ ok: true,
+ config: await statusPage.toJSON(),
+ });
+ } catch (error) {
+ callback({
+ ok: false,
+ msg: error.message,
+ });
+ }
+ });
+
+ // Save Status Page
+ // imgDataUrl Only Accept PNG!
+ socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
+ try {
+ checkLogin(socket);
+
+ // Save Config
+ let statusPage = await R.findOne("status_page", " slug = ? ", [
+ slug
+ ]);
+
+ if (!statusPage) {
+ throw new Error("No slug?");
+ }
+
+ checkSlug(config.slug);
+
+ const header = "data:image/png;base64,";
+
+ // Check logo format
+ // If is image data url, convert to png file
+ // Else assume it is a url, nothing to do
+ if (imgDataUrl.startsWith("data:")) {
+ if (! imgDataUrl.startsWith(header)) {
+ throw new Error("Only allowed PNG logo.");
+ }
+
+ const filename = `logo${statusPage.id}.png`;
+
+ // Convert to file
+ await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + filename);
+ config.logo = `/upload/${filename}?t=` + Date.now();
+
+ } else {
+ config.logo = imgDataUrl;
+ }
+
+ statusPage.slug = config.slug;
+ statusPage.title = config.title;
+ statusPage.description = config.description;
+ statusPage.icon = config.logo;
+ statusPage.autoRefreshInterval = config.autoRefreshInterval,
+ statusPage.theme = config.theme;
+ //statusPage.published = ;
+ //statusPage.search_engine_index = ;
+ statusPage.show_tags = config.showTags;
+ //statusPage.password = null;
+ statusPage.footer_text = config.footerText;
+ statusPage.custom_css = config.customCSS;
+ statusPage.show_powered_by = config.showPoweredBy;
+ statusPage.show_certificate_expiry = config.showCertificateExpiry;
+ statusPage.modified_date = R.isoDateTime();
+ statusPage.google_analytics_tag_id = config.googleAnalyticsId;
+
+ await R.store(statusPage);
+
+ await statusPage.updateDomainNameList(config.domainNameList);
+ await StatusPage.loadDomainMappingList();
+
+ // Save Public Group List
+ const groupIDList = [];
+ let groupOrder = 1;
+
+ for (let group of publicGroupList) {
+ let groupBean;
+ if (group.id) {
+ groupBean = await R.findOne("group", " id = ? AND public = 1 AND status_page_id = ? ", [
+ group.id,
+ statusPage.id
+ ]);
+ } else {
+ groupBean = R.dispense("group");
+ }
+
+ groupBean.status_page_id = statusPage.id;
+ groupBean.name = group.name;
+ groupBean.public = true;
+ groupBean.weight = groupOrder++;
+
+ await R.store(groupBean);
+
+ await R.exec("DELETE FROM monitor_group WHERE group_id = ? ", [
+ groupBean.id
+ ]);
+
+ let monitorOrder = 1;
+
+ for (let monitor of group.monitorList) {
+ let relationBean = R.dispense("monitor_group");
+ relationBean.weight = monitorOrder++;
+ relationBean.group_id = groupBean.id;
+ relationBean.monitor_id = monitor.id;
+
+ if (monitor.sendUrl !== undefined) {
+ relationBean.send_url = monitor.sendUrl;
+ }
+
+ await R.store(relationBean);
+ }
+
+ groupIDList.push(groupBean.id);
+ group.id = groupBean.id;
+ }
+
+ // Delete groups that are not in the list
+ log.debug("socket", "Delete groups that are not in the list");
+ const slots = groupIDList.map(() => "?").join(",");
+
+ const data = [
+ ...groupIDList,
+ statusPage.id
+ ];
+ await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots}) AND status_page_id = ?`, data);
+
+ const server = UptimeKumaServer.getInstance();
+
+ // Also change entry page to new slug if it is the default one, and slug is changed.
+ if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
+ server.entryPage = "statusPage-" + statusPage.slug;
+ await setSetting("entryPage", server.entryPage, "general");
+ }
+
+ apicache.clear();
+
+ callback({
+ ok: true,
+ publicGroupList,
+ });
+
+ } catch (error) {
+ log.error("socket", error);
+
+ callback({
+ ok: false,
+ msg: error.message,
+ });
+ }
+ });
+
+ // Add a new status page
+ socket.on("addStatusPage", async (title, slug, callback) => {
+ try {
+ checkLogin(socket);
+
+ title = title?.trim();
+ slug = slug?.trim();
+
+ // Check empty
+ if (!title || !slug) {
+ throw new Error("Please input all fields");
+ }
+
+ // Make sure slug is string
+ if (typeof slug !== "string") {
+ throw new Error("Slug -Accept string only");
+ }
+
+ // lower case only
+ slug = slug.toLowerCase();
+
+ checkSlug(slug);
+
+ let statusPage = R.dispense("status_page");
+ statusPage.slug = slug;
+ statusPage.title = title;
+ statusPage.theme = "auto";
+ statusPage.icon = "";
+ statusPage.autoRefreshInterval = 300;
+ await R.store(statusPage);
+
+ callback({
+ ok: true,
+ msg: "successAdded",
+ msgi18n: true,
+ });
+
+ } catch (error) {
+ console.error(error);
+ callback({
+ ok: false,
+ msg: error.message,
+ });
+ }
+ });
+
+ // Delete a status page
+ socket.on("deleteStatusPage", async (slug, callback) => {
+ const server = UptimeKumaServer.getInstance();
+
+ try {
+ checkLogin(socket);
+
+ let statusPageID = await StatusPage.slugToID(slug);
+
+ if (statusPageID) {
+
+ // Reset entry page if it is the default one.
+ if (server.entryPage === "statusPage-" + slug) {
+ server.entryPage = "dashboard";
+ await setSetting("entryPage", server.entryPage, "general");
+ }
+
+ // No need to delete records from `status_page_cname`, because it has cascade foreign key.
+ // But for incident & group, it is hard to add cascade foreign key during migration, so they have to be deleted manually.
+
+ // Delete incident
+ await R.exec("DELETE FROM incident WHERE status_page_id = ? ", [
+ statusPageID
+ ]);
+
+ // Delete group
+ await R.exec("DELETE FROM `group` WHERE status_page_id = ? ", [
+ statusPageID
+ ]);
+
+ // Delete status_page
+ await R.exec("DELETE FROM status_page WHERE id = ? ", [
+ statusPageID
+ ]);
+
+ } else {
+ throw new Error("Status Page is not found");
+ }
+
+ callback({
+ ok: true,
+ });
+ } catch (error) {
+ callback({
+ ok: false,
+ msg: error.message,
+ });
+ }
+ });
+};
+
+/**
+ * Check slug a-z, 0-9, - only
+ * Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
+ * @param {string} slug Slug to test
+ * @returns {void}
+ * @throws Slug is not valid
+ */
+function checkSlug(slug) {
+ if (typeof slug !== "string") {
+ throw new Error("Slug must be string");
+ }
+
+ slug = slug.trim();
+
+ if (!slug) {
+ throw new Error("Slug cannot be empty");
+ }
+
+ if (!slug.match(/^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/)) {
+ throw new Error("Invalid Slug");
+ }
+}