diff --git a/db/patch-add-monitor-checks-table.sql b/db/patch-add-monitor-checks-table.sql new file mode 100644 index 0000000..e032ee7 --- /dev/null +++ b/db/patch-add-monitor-checks-table.sql @@ -0,0 +1,42 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +BEGIN TRANSACTION; + +-- Create new monitor_checks table +create table monitor_checks +( + id INTEGER + constraint monitor_checks_pk + primary key autoincrement, + type VARCHAR(50) not null, + value TEXT, + monitor_id INTEGER NOT NULL, + CONSTRAINT "monitor_checks_monitor_id_fk" + FOREIGN KEY ("monitor_id") + REFERENCES "monitor" ("id") + ON DELETE CASCADE ON UPDATE CASCADE +); + +create unique index monitor_checks_id_uindex + on monitor_checks (id); + + +-- Copy over the http status to the new monitor_checks table as a separate check +insert into monitor_checks(monitor_id, type, value) +select id, 'HTTP_STATUS_CODE_SHOULD_EQUAL', accepted_statuscodes_json +from monitor; + +-- Copy over the keyword column from the monitor table to the new monitor_checks table as a separate check +insert into monitor_checks(monitor_id, type, value) +select id, 'RESPONSE_SHOULD_CONTAIN_TEXT', keyword +from monitor +WHERE monitor.type = 'keyword'; + +-- Delete the http status and keyword columns from the monitor table +ALTER TABLE monitor DROP COLUMN accepted_statuscodes_json; +ALTER TABLE monitor DROP COLUMN keyword; + +UPDATE monitor SET type = 'http' WHERE type = 'keyword'; + +COMMIT; + + diff --git a/server/database.js b/server/database.js index 41d91e8..d7e86f7 100644 --- a/server/database.js +++ b/server/database.js @@ -4,6 +4,7 @@ const { setSetting, setting } = require("./util-server"); const { debug, sleep } = require("../src/util"); const dayjs = require("dayjs"); const knex = require("knex"); +const MonitorCheck = require("./model/monitor-check"); /** * Database & App Data Folder @@ -52,6 +53,7 @@ class Database { "patch-http-monitor-method-body-and-headers.sql": true, "patch-2fa-invalidate-used-token.sql": true, "patch-notification_sent_history.sql": true, + "patch-add-monitor-checks-table.sql": true, } /** @@ -110,6 +112,7 @@ class Database { // Auto map the model to a bean object R.freeze(true); await R.autoloadModels("./server/model"); + R.modelList["monitor_checks"] = MonitorCheck; await R.exec("PRAGMA foreign_keys = ON"); // Change to WAL diff --git a/server/model/monitor-check.js b/server/model/monitor-check.js new file mode 100644 index 0000000..0bcf887 --- /dev/null +++ b/server/model/monitor-check.js @@ -0,0 +1,16 @@ +const { BeanModel } = require("redbean-node/dist/bean-model"); + +class MonitorCheck extends BeanModel { + /** + * Return a object that ready to parse to JSON + */ + async toJSON() { + return { + id: this.id, + type: this.type, + value: this.value, + }; + } +} + +module.exports = MonitorCheck; diff --git a/server/model/monitor.js b/server/model/monitor.js index 980e0ac..82d020c 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -6,12 +6,33 @@ dayjs.extend(utc); dayjs.extend(timezone); const axios = require("axios"); const { Prometheus } = require("../prometheus"); -const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util"); -const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, errorLog } = require("../util-server"); +const { + debug, + UP, + DOWN, + PENDING, + flipStatus, + TimeLogger, + MONITOR_CHECK_HTTP_CODE_TYPES, + MONITOR_CHECK_SELECTOR_TYPES, + HTTP_STATUS_CODE_SHOULD_EQUAL, + RESPONSE_SHOULD_CONTAIN_TEXT, +} = require("../../src/util"); +const { + tcping, + ping, + dnsResolve, + checkCertificate, + checkStatusCode, + getTotalClientInRoom, + setting, + errorLog, +} = require("../util-server"); const { R } = require("redbean-node"); const { BeanModel } = require("redbean-node/dist/bean-model"); const { Notification } = require("../notification"); const { demoMode } = require("../config"); +const validateMonitorChecks = require("../validate-monitor-checks"); const version = require("../../package.json").version; const apicache = require("../modules/apicache"); @@ -50,6 +71,7 @@ class Monitor extends BeanModel { } const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]); + const checks = await this.getMonitorChecks(); return { id: this.id, @@ -66,17 +88,20 @@ class Monitor extends BeanModel { type: this.type, interval: this.interval, retryInterval: this.retryInterval, - keyword: this.keyword, ignoreTls: this.getIgnoreTls(), upsideDown: this.isUpsideDown(), maxredirects: this.maxredirects, - accepted_statuscodes: this.getAcceptedStatuscodes(), dns_resolve_type: this.dns_resolve_type, dns_resolve_server: this.dns_resolve_server, dns_last_result: this.dns_last_result, pushToken: this.pushToken, notificationIDList, + checks: checks, tags: tags, + + // Deprecated: Use the values in the checks list instead + accepted_statuscodes: ((this.checks || []).find(check => check.type === HTTP_STATUS_CODE_SHOULD_EQUAL) || {}).value, + keyword: ((this.checks || []).find(check => check.type === RESPONSE_SHOULD_CONTAIN_TEXT) || {}).value, }; } @@ -96,10 +121,6 @@ class Monitor extends BeanModel { return Boolean(this.upsideDown); } - getAcceptedStatuscodes() { - return JSON.parse(this.accepted_statuscodes_json); - } - start(io) { let previousBeat = null; let retries = 0; @@ -112,7 +133,7 @@ class Monitor extends BeanModel { // undefined if not https let tlsInfo = undefined; - if (! previousBeat) { + if (!previousBeat) { previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [ this.id, ]); @@ -130,14 +151,14 @@ class Monitor extends BeanModel { } // Duration - if (! isFirstBeat) { + if (!isFirstBeat) { bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), "second"); } else { bean.duration = 0; } try { - if (this.type === "http" || this.type === "keyword") { + if (this.type === "http") { // Do not do any queries/high loading things before the "bean.ping" let startTime = dayjs().valueOf(); @@ -154,12 +175,10 @@ class Monitor extends BeanModel { }, httpsAgent: new https.Agent({ maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) - rejectUnauthorized: ! this.getIgnoreTls(), + rejectUnauthorized: !this.getIgnoreTls(), }), maxRedirects: this.maxredirects, - validateStatus: (status) => { - return checkStatusCode(status, this.getAcceptedStatuscodes()); - }, + validateStatus: undefined, }; debug(`[${this.name}] Axios Request`); @@ -195,26 +214,11 @@ class Monitor extends BeanModel { console.log(res.data); } - if (this.type === "http") { - bean.status = UP; - } else { - - let data = res.data; - - // Convert to string for object/array - if (typeof data !== "string") { - data = JSON.stringify(data); - } - - if (data.includes(this.keyword)) { - bean.msg += ", keyword is found"; - bean.status = UP; - } else { - throw new Error(bean.msg + ", but keyword is not found"); - } + bean.status = UP; - } + validateMonitorChecks(res, await this.getMonitorChecks(), bean); + bean.ping = dayjs().valueOf() - startTime; } else if (this.type === "port") { bean.ping = await tcping(this.hostname, this.port); bean.msg = ""; @@ -258,7 +262,7 @@ class Monitor extends BeanModel { if (this.dnsLastResult !== dnsMessage) { R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [ dnsMessage, - this.id + this.id, ]); } @@ -269,7 +273,7 @@ class Monitor extends BeanModel { let heartbeatCount = await R.count("heartbeat", " monitor_id = ? AND time > ? ", [ this.id, - time + time, ]); debug("heartbeatCount" + heartbeatCount + " " + time); @@ -339,7 +343,7 @@ class Monitor extends BeanModel { retries = 0; } catch (error) { - + bean.status = DOWN; bean.msg = error.message; // If UP come in here, it must be upside down mode @@ -397,7 +401,7 @@ class Monitor extends BeanModel { previousBeat = bean; - if (! this.isStop) { + if (!this.isStop) { if (demoMode) { if (beatInterval < 20) { @@ -526,9 +530,11 @@ class Monitor extends BeanModel { let avgPing = parseInt(await R.getCell(` SELECT AVG(ping) FROM heartbeat - WHERE time > DATETIME('now', ? || ' hours') - AND ping IS NOT NULL - AND monitor_id = ? `, [ + WHERE time + > DATETIME('now' + , ? || ' hours') + AND ping IS NOT NULL + AND monitor_id = ? `, [ -duration, monitorID, ])); @@ -562,30 +568,31 @@ class Monitor extends BeanModel { // e.g. If the last beat's duration is bigger that the 24hrs window, it will use the duration between the (beat time - window margin) (THEN case in SQL) let result = await R.getRow(` SELECT - -- SUM all duration, also trim off the beat out of time window + -- SUM all duration, also trim off the beat out of time window SUM( CASE WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration - THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 + THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 ELSE duration - END - ) AS total_duration, + END + ) AS total_duration, - -- SUM all uptime duration, also trim off the beat out of time window + -- SUM all uptime duration, also trim off the beat out of time window SUM( CASE WHEN (status = 1) - THEN + THEN CASE WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 ELSE duration - END + END END - ) AS uptime_duration + ) AS uptime_duration FROM heartbeat - WHERE time > ? - AND monitor_id = ? + WHERE time + > ? + AND monitor_id = ? `, [ startTime, startTime, startTime, startTime, startTime, monitorID, @@ -605,7 +612,7 @@ class Monitor extends BeanModel { } else { // Handle new monitor with only one beat, because the beat's duration = 0 - let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ])); + let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [monitorID])); if (status === UP) { uptime = 1; @@ -618,6 +625,9 @@ class Monitor extends BeanModel { /** * Send Uptime * @param duration : int Hours + * @param io + * @param monitorID + * @param userID */ static async sendUptime(duration, io, monitorID, userID) { const uptime = await this.calcUptime(duration, monitorID); @@ -730,6 +740,21 @@ class Monitor extends BeanModel { debug("No notification, no need to send cert notification"); } } + + async getMonitorChecks() { + const checks = await R.getAll("SELECT mc.type, mc.value FROM monitor_checks mc WHERE mc.monitor_id = ?", [this.id]); + + checks.forEach(check => { + if (MONITOR_CHECK_HTTP_CODE_TYPES.includes(check.type) && typeof check.value === "string" && check.value.startsWith("[")) { + check.value = JSON.parse(check.value); + } + if (MONITOR_CHECK_SELECTOR_TYPES.includes(check.type) && typeof check.value === "string" && check.value.startsWith("{")) { + check.value = JSON.parse(check.value); + } + }); + + return checks; + } } module.exports = Monitor; diff --git a/server/server.js b/server/server.js index d1fd7ff..d0e87ad 100644 --- a/server/server.js +++ b/server/server.js @@ -5,7 +5,7 @@ const config = require("./config"); debug(args); -if (! process.env.NODE_ENV) { +if (!process.env.NODE_ENV) { process.env.NODE_ENV = "production"; } @@ -41,7 +41,17 @@ console.log("Importing this project modules"); debug("Importing Monitor"); const Monitor = require("./model/monitor"); debug("Importing Settings"); -const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, errorLog } = require("./util-server"); +const { + getSettings, + setSettings, + setting, + initJWTSecret, + checkLogin, + startUnitTest, + updateMonitorChecks, + FBSD, + errorLog +} = require("./util-server"); debug("Importing Notification"); const { Notification } = require("./notification"); @@ -106,7 +116,7 @@ if (sslKey && sslCert) { console.log("Server Type: HTTPS"); server = https.createServer({ key: fs.readFileSync(sslKey), - cert: fs.readFileSync(sslCert) + cert: fs.readFileSync(sslCert), }, app); } else { console.log("Server Type: HTTP"); @@ -117,7 +127,12 @@ const io = new Server(server); module.exports.io = io; // Must be after io instantiation -const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo } = require("./client"); +const { + sendNotificationList, + sendHeartbeatList, + sendImportantHeartbeatList, + sendInfo, +} = require("./client"); const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler"); const databaseSocketHandler = require("./socket-handlers/database-socket-handler"); @@ -198,7 +213,7 @@ exports.entryPage = "dashboard"; // Robots.txt app.get("/robots.txt", async (_request, response) => { let txt = "User-agent: *\nDisallow:"; - if (! await setting("searchEngineIndex")) { + if (!await setting("searchEngineIndex")) { txt += " /"; } response.setHeader("Content-Type", "text/plain"); @@ -532,13 +547,16 @@ exports.entryPage = "dashboard"; let notificationIDList = monitor.notificationIDList; delete monitor.notificationIDList; - monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); - delete monitor.accepted_statuscodes; + const checks = monitor.checks; + delete monitor.checks; + // Store monitor info bean.import(monitor); bean.user_id = socket.userID; await R.store(bean); + await updateMonitorChecks(bean.id, checks); + await updateMonitorNotification(bean.id, notificationIDList); await startMonitor(socket.userID, bean.id); @@ -563,7 +581,7 @@ exports.entryPage = "dashboard"; try { checkLogin(socket); - let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ]); + let bean = await R.findOne("monitor", " id = ? ", [monitor.id]); if (bean.user_id !== socket.userID) { throw new Error("Permission denied."); @@ -580,17 +598,19 @@ exports.entryPage = "dashboard"; bean.hostname = monitor.hostname; bean.maxretries = monitor.maxretries; bean.port = monitor.port; - bean.keyword = monitor.keyword; bean.ignoreTls = monitor.ignoreTls; bean.upsideDown = monitor.upsideDown; bean.maxredirects = monitor.maxredirects; - bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); bean.dns_resolve_type = monitor.dns_resolve_type; bean.dns_resolve_server = monitor.dns_resolve_server; bean.pushToken = monitor.pushToken; + const checks = monitor.checks; + delete monitor.checks; + await R.store(bean); + await updateMonitorChecks(bean.id, checks); await updateMonitorNotification(bean.id, monitor.notificationIDList); if (bean.active) { @@ -803,7 +823,7 @@ exports.entryPage = "dashboard"; try { checkLogin(socket); - let bean = await R.findOne("monitor", " id = ? ", [ tag.id ]); + let bean = await R.findOne("monitor", " id = ? ", [tag.id]); bean.name = tag.name; bean.color = tag.color; await R.store(bean); @@ -825,7 +845,7 @@ exports.entryPage = "dashboard"; try { checkLogin(socket); - await R.exec("DELETE FROM tag WHERE id = ? ", [ tagID ]); + await R.exec("DELETE FROM tag WHERE id = ? ", [tagID]); callback({ ok: true, @@ -916,7 +936,7 @@ exports.entryPage = "dashboard"; try { checkLogin(socket); - if (! password.newPassword) { + if (!password.newPassword) { throw new Error("Invalid new password"); } @@ -974,7 +994,7 @@ exports.entryPage = "dashboard"; callback({ ok: true, - msg: "Saved" + msg: "Saved", }); sendInfo(socket); @@ -1132,7 +1152,6 @@ exports.entryPage = "dashboard"; // --- End --- let monitor = { - // Define the new variable from earlier here name: monitorListData[i].name, type: monitorListData[i].type, url: monitorListData[i].url, @@ -1140,15 +1159,13 @@ exports.entryPage = "dashboard"; body: monitorListData[i].body, headers: monitorListData[i].headers, interval: monitorListData[i].interval, - retryInterval: retryInterval, hostname: monitorListData[i].hostname, maxretries: monitorListData[i].maxretries, port: monitorListData[i].port, - keyword: monitorListData[i].keyword, ignoreTls: monitorListData[i].ignoreTls, upsideDown: monitorListData[i].upsideDown, maxredirects: monitorListData[i].maxredirects, - accepted_statuscodes: monitorListData[i].accepted_statuscodes, + checks: monitorListData[i].checks, dns_resolve_type: monitorListData[i].dns_resolve_type, dns_resolve_server: monitorListData[i].dns_resolve_server, notificationIDList: {}, @@ -1163,13 +1180,40 @@ exports.entryPage = "dashboard"; let notificationIDList = monitor.notificationIDList; delete monitor.notificationIDList; - monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); - delete monitor.accepted_statuscodes; + // TODO remove this if clause once we no longer expect export files to contain the old accepted_statuscodes and keyword format + if (monitor.type === "http" || monitor.type === "keyword") { + if (monitorListData[i].accepted_statuscodes) { + // old format for checking http status codes + // Convert to new format which uses separate monitor check + monitor.checks = monitor.checks || []; + monitor.checks.push({ + type: "HTTP_STATUS_CODE_SHOULD_EQUAL", + value: monitorListData[i].accepted_statuscodes, + }); + } + + if (monitorListData[i].keyword) { + // old format for checking response contains keyword + // Convert to new format which uses separate monitor check + monitor.checks = monitor.checks || []; + monitor.checks.push({ + type: "RESPONSE_SHOULD_CONTAIN_TEXT", + value: monitorListData[i].keyword, + }); + } + + monitor.type = "http"; + } + + const checks = monitor.checks; + delete monitor.checks; bean.import(monitor); bean.user_id = socket.userID; await R.store(bean); + await updateMonitorChecks(bean.id, checks); + // Only for backup files with the version 1.7.0 or higher, since there was the tag feature implemented if (version17x) { // Only import if the specific monitor has tags assigned @@ -1181,7 +1225,7 @@ exports.entryPage = "dashboard"; ]); let tagId; - if (! tag) { + if (!tag) { // -> If it doesn't exist, create new tag from backup file let beanTag = R.dispense("tag"); beanTag.name = oldTag.name; @@ -1266,7 +1310,7 @@ exports.entryPage = "dashboard"; console.log(`Clear Heartbeats Monitor: ${monitorID} User ID: ${socket.userID}`); await R.exec("DELETE FROM heartbeat WHERE monitor_id = ?", [ - monitorID + monitorID, ]); await sendHeartbeatList(socket, monitorID, true, true); @@ -1370,7 +1414,7 @@ async function checkOwner(userID, monitorID) { userID, ]); - if (! row) { + if (!row) { throw new Error("You do not own this monitor."); } } @@ -1418,7 +1462,7 @@ async function getMonitorJSONList(userID) { } async function initDatabase() { - if (! fs.existsSync(Database.path)) { + if (!fs.existsSync(Database.path)) { console.log("Copying Database"); fs.copyFileSync(Database.templatePath, Database.path); } @@ -1434,7 +1478,7 @@ async function initDatabase() { "jwtSecret", ]); - if (! jwtSecretBean) { + if (!jwtSecretBean) { console.log("JWT secret is not found, generate one."); jwtSecretBean = await initJWTSecret(); console.log("Stored JWT secret into database"); diff --git a/server/util-server.js b/server/util-server.js index 68f59f6..9aaf50d 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -320,6 +320,28 @@ exports.checkLogin = (socket) => { } }; +exports.updateMonitorChecks = async (monitorId, checks) => { + let trx = await R.begin(); + try { + // delete existing checks for monitor + await trx.exec("DELETE FROM monitor_checks WHERE monitor_id = ?", [monitorId]); + + // Replace them with new checks + for (let i = 0; i < (checks || []).length; i++) { + let checkBean = trx.dispense("monitor_checks"); + checkBean.monitor_id = monitorId; + checkBean.type = checks[i].type; + checkBean.value = typeof checks[i].value === "object" ? JSON.stringify(checks[i].value) : checks[i].value; + await trx.store(checkBean); + } + + await trx.commit(); + } catch (err) { + await trx.rollback(); + throw err; + } +}; + exports.startUnitTest = async () => { console.log("Starting unit test..."); const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm"; diff --git a/server/validate-monitor-checks.js b/server/validate-monitor-checks.js new file mode 100644 index 0000000..f9fd1dd --- /dev/null +++ b/server/validate-monitor-checks.js @@ -0,0 +1,107 @@ +const { checkStatusCode } = require("./util-server"); +const { UP } = require("../src/util"); +const get = require("lodash.get"); + +function validateMonitorChecks(res, checks, bean) { + const responseText = typeof data === "string" ? res.data : JSON.stringify(res.data); + + (checks || []).forEach(check => { + switch (check.type) { + case "HTTP_STATUS_CODE_SHOULD_EQUAL": + if (checkStatusCode(res.status, check.value)) { + bean.msg += `, status matches '${check.value}'`; + bean.status = UP; + } else { + throw new Error(bean.msg + ", but status code does not match " + check.value); + } + break; + + case "HTTP_STATUS_CODE_SHOULD_NOT_EQUAL": + if (!checkStatusCode(res.status, check.value)) { + bean.msg += `, status does not match '${check.value}'`; + bean.status = UP; + } else { + throw new Error(bean.msg + ", but status code does match " + check.value); + } + break; + + case "RESPONSE_SHOULD_CONTAIN_TEXT": + if (responseText.includes(check.value)) { + bean.msg += `, response contains '${check.value}'`; + bean.status = UP; + } else { + throw new Error(bean.msg + ", but response does not contain '" + check.value + "'"); + } + break; + + case "RESPONSE_SHOULD_NOT_CONTAIN_TEXT": + if (!responseText.includes(check.value)) { + bean.msg += `, response does not contain '${check.value}'`; + bean.status = UP; + } else { + throw new Error(bean.msg + ", but response does contain '" + check.value + "'"); + } + break; + + case "RESPONSE_SHOULD_MATCH_REGEX": + if (responseText.test(new RegExp(check.value))) { + bean.msg += `, regex '${check.value}' matches`; + bean.status = UP; + } else { + throw new Error(bean.msg + ", but response does not match regex: '" + check.value + "'"); + } + break; + + case "RESPONSE_SHOULD_NOT_MATCH_REGEX": + if (!responseText.test(new RegExp(check.value))) { + bean.msg += `, regex '${check.value}' does not matches`; + bean.status = UP; + } else { + throw new Error(bean.msg + ", but response does match regex: '" + check.value + "'"); + } + break; + + case "RESPONSE_SELECTOR_SHOULD_EQUAL": + if (get(res.data, check.value.selectorPath) === check.value.selectorValue) { + bean.msg += `, response selector equals '${check.value.selectorValue}'`; + bean.status = UP; + } else { + throw new Error(`${bean.msg}, but response selector '${check.value.selectorPath}' does not equal '${check.value.selectorValue}'`); + } + break; + + case "RESPONSE_SELECTOR_SHOULD_NOT_EQUAL": + if (get(res.data, check.value.selectorPath) !== check.value.selectorValue) { + bean.msg += `, response selector does not equal '${check.value.selectorValue}'`; + bean.status = UP; + } else { + throw new Error(`${bean.msg}, but response selector '${check.value.selectorPath}' does equal '${check.value.selectorValue}'`); + } + break; + + case "RESPONSE_SELECTOR_SHOULD_MATCH_REGEX": + if (get(res.data, check.value.selectorPath).test(new RegExp(check.value.selectorValue))) { + bean.msg += `, response selector matches regex '${check.value.selectorValue}'`; + bean.status = UP; + } else { + throw new Error(`${bean.msg}, but response selector '${check.value.selectorPath}' does not match regex '${check.value.selectorValue}'`); + } + break; + + case "RESPONSE_SELECTOR_SHOULD_NOT_MATCH_REGEX": + if (!get(res.data, check.value.selectorPath).test(new RegExp(check.value.selectorValue))) { + bean.msg += `, response selector does not match regex '${check.value.selectorValue}'`; + bean.status = UP; + } else { + throw new Error(`${bean.msg}, but response selector '${check.value.selectorPath}' does match regex '${check.value.selectorValue}'`); + } + break; + + default: + throw new Error(`${bean.msg}, encountered unknown monitor_check.type`); + } + }); + bean.status = UP; +} + +module.exports = validateMonitorChecks; diff --git a/src/components/MonitorCheckEditor.vue b/src/components/MonitorCheckEditor.vue new file mode 100644 index 0000000..afd9ac8 --- /dev/null +++ b/src/components/MonitorCheckEditor.vue @@ -0,0 +1,244 @@ + + + + + + + diff --git a/src/languages/en.js b/src/languages/en.js index 15c3cd0..5c82a6a 100644 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -307,4 +307,22 @@ export default { steamApiKeyDescription: "For monitoring a Steam Game Server you need a Steam Web-API key. You can register your API key here: ", "Current User": "Current User", recent: "Recent", + MonitorCheckTypeShould: "Should", + MonitorCheckTypeShouldNot: "Should not", + MonitorCheckTypeHttpStatusCodeShouldEqual: "equal status code", + MonitorCheckTypeResponseShouldContainText: "contain text", + MonitorCheckTypeResponseShouldMatchRegex: "match regex", + MonitorCheckTypeResponseSelectorShouldEqual: "equal text (JSON selector)", + MonitorCheckTypeResponseSelectorShouldMatchRegex: "match regex (JSON selector)", + MonitorCheckValuePlaceholder: "Value", + MonitorCheckSelectorPathPlaceholder: "Selector, Example: customer.address.street", + MonitorCheckSelectorValuePlaceholder: "Value, Example: First street", + MonitorCheckSelectorPathRegexPlaceholder: "Selector, Example: customer.contactInfo.email", + MonitorCheckSelectorValueRegexPlaceholder: "Regexp, Example: [a-z0-9.]+@gmail\\.com", + MonitorTypeHttps: "HTTP(s)", + MonitorTypeTcpPort: "TCP Port", + MonitorTypePing: "Ping", + MonitorTypeDns: "DNS", + MonitorChecks: "Checks", + AddMonitorCheck: "Add check", }; diff --git a/src/languages/nl-NL.js b/src/languages/nl-NL.js index ff18832..81df826 100644 --- a/src/languages/nl-NL.js +++ b/src/languages/nl-NL.js @@ -204,4 +204,24 @@ export default { PushUrl: "Push URL", HeadersInvalidFormat: "The request headers is geen geldige JSON: ", BodyInvalidFormat: "De request body is geen geldige JSON: ", + BodyPlaceholder: "{\n\t\"id\": 124357,\n\t\"gebruikersnaam\": \"admin\",\n\t\"wachtwoord\": \"mijnAdminWachtwoord\"\n}", + HeadersPlaceholder: "{\n\t\"Authorization\": \"Bearer abc123\",\n\t\"Content-Type\": \"application/json\"\n}", + MonitorCheckTypeShould: "Moet", + MonitorCheckTypeShouldNot: "Mag niet", + MonitorCheckTypeHttpStatusCodeShouldEqual: "gelijk aan status code", + MonitorCheckTypeResponseShouldContainText: "text bevatten", + MonitorCheckTypeResponseShouldMatchRegex: "regex matchen", + MonitorCheckTypeResponseSelectorShouldEqual: "gelijk zijn aan tekst (JSON selectie)", + MonitorCheckTypeResponseSelectorShouldMatchRegex: "regex matchen (JSON selectie)", + MonitorCheckValuePlaceholder: "Waarde", + MonitorCheckSelectorPathPlaceholder: "Selectie, Voorbeeld: klant.adres.straat", + MonitorCheckSelectorValuePlaceholder: "Waarde, Voorbeeld: Hogestraat", + MonitorCheckSelectorPathRegexPlaceholder: "Selectie, Voorbeeld: klant.contactInfo.email", + MonitorCheckSelectorValueRegexPlaceholder: "Reg. Expr., Voorbeeld: [a-z0-9.]+@gmail\\.com", + MonitorTypeHttps: "HTTP(s)", + MonitorTypeTcpPort: "TCP Poort", + MonitorTypePing: "Ping", + MonitorTypeDns: "DNS", + MonitorChecks: "Controles", + AddMonitorCheck: "Controle toevoegen", }; diff --git a/src/pages/Details.vue b/src/pages/Details.vue index d40561f..8365c11 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -6,13 +6,9 @@

- {{ monitor.url }} + {{ monitor.url }} TCP Ping {{ monitor.hostname }}:{{ monitor.port }} Ping: {{ monitor.hostname }} - -
- {{ $t("Keyword") }}: {{ monitor.keyword }} -
[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }}
{{ $t("Last Result") }}: {{ monitor.dns_last_result }} diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 65c3dad..aa02454 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -10,21 +10,18 @@

- -
-
+
@@ -57,17 +54,8 @@
- -
- - -
- {{ $t("keywordDescription") }} -
-
- - +
@@ -139,7 +127,7 @@

{{ $t("Advanced") }}

-
+
- -