HTTPS Monitor using Real Browsers + Limited plugin support (#1787)
parent
d99d37898e
commit
e5ca67d062
@ -0,0 +1,24 @@
|
|||||||
|
const childProcess = require("child_process");
|
||||||
|
|
||||||
|
class Git {
|
||||||
|
|
||||||
|
static clone(repoURL, cwd, targetDir = ".") {
|
||||||
|
let result = childProcess.spawnSync("git", [
|
||||||
|
"clone",
|
||||||
|
repoURL,
|
||||||
|
targetDir,
|
||||||
|
], {
|
||||||
|
cwd: cwd,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(result.stderr.toString("utf-8"));
|
||||||
|
} else {
|
||||||
|
return result.stdout.toString("utf-8") + result.stderr.toString("utf-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Git,
|
||||||
|
};
|
@ -0,0 +1,19 @@
|
|||||||
|
class MonitorType {
|
||||||
|
|
||||||
|
name = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Monitor} monitor
|
||||||
|
* @param {Heartbeat} heartbeat
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async check(monitor, heartbeat) {
|
||||||
|
throw new Error("You need to override check()");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
MonitorType,
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
class Plugin {
|
||||||
|
async load() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async unload() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Plugin,
|
||||||
|
};
|
@ -0,0 +1,235 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const { log } = require("../src/util");
|
||||||
|
const path = require("path");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { Git } = require("./git");
|
||||||
|
const childProcess = require("child_process");
|
||||||
|
|
||||||
|
class PluginsManager {
|
||||||
|
|
||||||
|
static disable = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin List
|
||||||
|
* @type {PluginWrapper[]}
|
||||||
|
*/
|
||||||
|
pluginList = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugins Dir
|
||||||
|
*/
|
||||||
|
pluginsDir;
|
||||||
|
|
||||||
|
server;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {UptimeKumaServer} server
|
||||||
|
*/
|
||||||
|
constructor(server) {
|
||||||
|
this.server = server;
|
||||||
|
|
||||||
|
if (!PluginsManager.disable) {
|
||||||
|
this.pluginsDir = "./data/plugins/";
|
||||||
|
|
||||||
|
if (! fs.existsSync(this.pluginsDir)) {
|
||||||
|
fs.mkdirSync(this.pluginsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("plugin", "Scanning plugin directory");
|
||||||
|
let list = fs.readdirSync(this.pluginsDir);
|
||||||
|
|
||||||
|
this.pluginList = [];
|
||||||
|
for (let item of list) {
|
||||||
|
this.loadPlugin(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
log.warn("PLUGIN", "Skip scanning plugin directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install a Plugin
|
||||||
|
*/
|
||||||
|
async loadPlugin(name) {
|
||||||
|
log.info("plugin", "Load " + name);
|
||||||
|
let plugin = new PluginWrapper(this.server, this.pluginsDir + name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await plugin.load();
|
||||||
|
this.pluginList.push(plugin);
|
||||||
|
} catch (e) {
|
||||||
|
log.error("plugin", "Failed to load plugin: " + this.pluginsDir + name);
|
||||||
|
log.error("plugin", "Reason: " + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a Plugin
|
||||||
|
* @param {string} repoURL Git repo url
|
||||||
|
* @param {string} name Directory name, also known as plugin unique name
|
||||||
|
*/
|
||||||
|
downloadPlugin(repoURL, name) {
|
||||||
|
log.info("plugin", "Installing plugin: " + name + " " + repoURL);
|
||||||
|
let result = Git.clone(repoURL, this.pluginsDir, name);
|
||||||
|
log.info("plugin", "Install result: " + result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a plugin
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
async removePlugin(name) {
|
||||||
|
log.info("plugin", "Removing plugin: " + name);
|
||||||
|
for (let plugin of this.pluginList) {
|
||||||
|
if (plugin.info.name === name) {
|
||||||
|
await plugin.unload();
|
||||||
|
|
||||||
|
// Delete the plugin directory
|
||||||
|
fs.rmSync(this.pluginsDir + name, {
|
||||||
|
recursive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pluginList.splice(this.pluginList.indexOf(plugin), 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.warn("plugin", "Plugin not found: " + name);
|
||||||
|
throw new Error("Plugin not found: " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Update a plugin
|
||||||
|
* Only available for plugins which were downloaded from the official list
|
||||||
|
* @param pluginID
|
||||||
|
*/
|
||||||
|
updatePlugin(pluginID) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the plugin list from server + local installed plugin list
|
||||||
|
* Item will be merged if the `name` is the same.
|
||||||
|
* @returns {Promise<[]>}
|
||||||
|
*/
|
||||||
|
async fetchPluginList() {
|
||||||
|
const res = await axios.get("https://uptime.kuma.pet/c/plugins.json");
|
||||||
|
const list = res.data.pluginList;
|
||||||
|
|
||||||
|
for (let plugin of this.pluginList) {
|
||||||
|
let find = false;
|
||||||
|
// Try to merge
|
||||||
|
for (let remotePlugin of list) {
|
||||||
|
if (remotePlugin.name === plugin.info.name) {
|
||||||
|
find = true;
|
||||||
|
remotePlugin.installed = true;
|
||||||
|
remotePlugin.name = plugin.info.name;
|
||||||
|
remotePlugin.fullName = plugin.info.fullName;
|
||||||
|
remotePlugin.description = plugin.info.description;
|
||||||
|
remotePlugin.version = plugin.info.version;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local plugin
|
||||||
|
if (!find) {
|
||||||
|
plugin.info.local = true;
|
||||||
|
list.push(plugin.info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort Installed first, then sort by name
|
||||||
|
return list.sort((a, b) => {
|
||||||
|
if (a.installed === b.installed) {
|
||||||
|
if ( a.fullName < b.fullName ) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if ( a.fullName > b.fullName ) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} else if (a.installed) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PluginWrapper {
|
||||||
|
|
||||||
|
server = undefined;
|
||||||
|
pluginDir = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must be an `new-able` class.
|
||||||
|
* @type {function}
|
||||||
|
*/
|
||||||
|
pluginClass = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {Plugin}
|
||||||
|
*/
|
||||||
|
object = undefined;
|
||||||
|
info = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {UptimeKumaServer} server
|
||||||
|
* @param {string} pluginDir
|
||||||
|
*/
|
||||||
|
constructor(server, pluginDir) {
|
||||||
|
this.server = server;
|
||||||
|
this.pluginDir = pluginDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
let indexFile = this.pluginDir + "/index.js";
|
||||||
|
let packageJSON = this.pluginDir + "/package.json";
|
||||||
|
|
||||||
|
if (fs.existsSync(indexFile)) {
|
||||||
|
// Install dependencies
|
||||||
|
childProcess.execSync("npm install", {
|
||||||
|
cwd: this.pluginDir,
|
||||||
|
env: {
|
||||||
|
PLAYWRIGHT_BROWSERS_PATH: "../../browsers", // Special handling for read-browser-monitor
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pluginClass = require(path.join(process.cwd(), indexFile));
|
||||||
|
|
||||||
|
let pluginClassType = typeof this.pluginClass;
|
||||||
|
|
||||||
|
if (pluginClassType === "function") {
|
||||||
|
this.object = new this.pluginClass(this.server);
|
||||||
|
await this.object.load();
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid plugin, it does not export a class");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(packageJSON)) {
|
||||||
|
this.info = require(path.join(process.cwd(), packageJSON));
|
||||||
|
} else {
|
||||||
|
this.info.fullName = this.pluginDir;
|
||||||
|
this.info.name = "[unknown]";
|
||||||
|
this.info.version = "[unknown-version]";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.info.installed = true;
|
||||||
|
log.info("plugin", `${this.info.fullName} v${this.info.version} loaded`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unload() {
|
||||||
|
await this.object.unload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
PluginsManager,
|
||||||
|
PluginWrapper
|
||||||
|
};
|
@ -0,0 +1,65 @@
|
|||||||
|
const { checkLogin } = require("../util-server");
|
||||||
|
const { PluginManager } = require("../plugins-manager");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handlers for plugins
|
||||||
|
* @param {Socket} socket Socket.io instance
|
||||||
|
* @param {UptimeKumaServer} server
|
||||||
|
*/
|
||||||
|
module.exports.pluginsHandler = (socket, server) => {
|
||||||
|
|
||||||
|
const pluginManager = server.getPluginManager();
|
||||||
|
|
||||||
|
// Get Plugin List
|
||||||
|
socket.on("getPluginList", async (callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
if (PluginManager.disable) {
|
||||||
|
throw new Error("Plugin Disabled: In order to enable plugin feature, you need to use the default data directory: ./data/");
|
||||||
|
}
|
||||||
|
|
||||||
|
let pluginList = await pluginManager.fetchPluginList();
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
pluginList,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("installPlugin", async (repoURL, name, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
pluginManager.downloadPlugin(repoURL, name);
|
||||||
|
await pluginManager.loadPlugin(name);
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("uninstallPlugin", async (name, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
await pluginManager.removePlugin(name);
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="! (!plugin.installed && plugin.local)" class="plugin-item pt-4 pb-2">
|
||||||
|
<div class="info">
|
||||||
|
<h5>{{ plugin.fullName }}</h5>
|
||||||
|
<p class="description">
|
||||||
|
{{ plugin.description }}
|
||||||
|
</p>
|
||||||
|
<span class="version">{{ $t("Version") }}: {{ plugin.version }} <a v-if="plugin.repo" :href="plugin.repo" target="_blank">Repo</a></span>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<button v-if="status === 'installing'" class="btn btn-primary" disabled>{{ $t("installing") }}</button>
|
||||||
|
<button v-else-if="status === 'uninstalling'" class="btn btn-danger" disabled>{{ $t("uninstalling") }}</button>
|
||||||
|
<button v-else-if="plugin.installed || status === 'installed'" class="btn btn-danger" @click="deleteConfirm">{{ $t("uninstall") }}</button>
|
||||||
|
<button v-else class="btn btn-primary" @click="install">{{ $t("install") }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="uninstall">
|
||||||
|
{{ $t("confirmUninstallPlugin") }}
|
||||||
|
</Confirm>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Confirm from "./Confirm.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Confirm,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
plugin: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Show confirmation for deleting a tag
|
||||||
|
*/
|
||||||
|
deleteConfirm() {
|
||||||
|
this.$refs.confirmDelete.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
install() {
|
||||||
|
this.status = "installing";
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("installPlugin", this.plugin.repo, this.plugin.name, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.status = "";
|
||||||
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
|
this.plugin.installed = true;
|
||||||
|
} else {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
uninstall() {
|
||||||
|
this.status = "uninstalling";
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("uninstallPlugin", this.plugin.name, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.status = "";
|
||||||
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
|
this.plugin.installed = false;
|
||||||
|
} else {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.plugin-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.info {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="mt-3">{{ remotePluginListMsg }}</div>
|
||||||
|
<PluginItem v-for="plugin in remotePluginList" :key="plugin.id" :plugin="plugin" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import PluginItem from "../PluginItem.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
PluginItem
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
remotePluginList: [],
|
||||||
|
remotePluginListMsg: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
pluginList() {
|
||||||
|
return this.$parent.$parent.$parent.pluginList;
|
||||||
|
},
|
||||||
|
settings() {
|
||||||
|
return this.$parent.$parent.$parent.settings;
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
return this.$parent.$parent.$parent.saveSettings;
|
||||||
|
},
|
||||||
|
settingsLoaded() {
|
||||||
|
return this.$parent.$parent.$parent.settingsLoaded;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.loadList();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
loadList() {
|
||||||
|
this.remotePluginListMsg = this.$t("Loading") + "...";
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("getPluginList", (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.remotePluginList = res.pluginList;
|
||||||
|
this.remotePluginListMsg = "";
|
||||||
|
} else {
|
||||||
|
this.remotePluginListMsg = this.$t("loadingError") + " " + res.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
Loading…
Reference in new issue