diff --git a/db/patch-add-radius-monitor.sql b/db/patch-add-radius-monitor.sql new file mode 100644 index 000000000..1fd5b44f4 --- /dev/null +++ b/db/patch-add-radius-monitor.sql @@ -0,0 +1,18 @@ +BEGIN TRANSACTION; + +ALTER TABLE monitor + ADD radius_username VARCHAR(255); + +ALTER TABLE monitor + ADD radius_password VARCHAR(255); + +ALTER TABLE monitor + ADD radius_calling_station_id VARCHAR(50); + +ALTER TABLE monitor + ADD radius_called_station_id VARCHAR(50); + +ALTER TABLE monitor + ADD radius_secret VARCHAR(255); + +COMMIT diff --git a/package-lock.json b/package-lock.json index 778e6bc38..0cf62fa7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "mqtt": "^4.2.8", "mssql": "^8.1.0", "node-cloudflared-tunnel": "~1.0.9", + "node-radius-client": "^1.0.0", "nodemailer": "~6.6.5", "notp": "~2.0.3", "password-hash": "~1.2.2", @@ -8215,6 +8216,12 @@ "readable-stream": "^3.6.0" } }, + "node_modules/hoek": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz", + "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==", + "deprecated": "This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues." + }, "node_modules/homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -8915,6 +8922,17 @@ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "devOptional": true }, + "node_modules/isemail": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", + "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", + "dependencies": { + "punycode": "2.x.x" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -12151,6 +12169,32 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, + "node_modules/node-radius-client": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-radius-client/-/node-radius-client-1.0.0.tgz", + "integrity": "sha512-FkR9cMV5hNoX+kKDUTzuagvEixlLiaEJQ1/ywOdhahsihKrGDhVZmnCvmrCStA589MT3yuC/J2eKc6z68IGdBw==", + "dependencies": { + "joi": "^14.3.1", + "node-radius-utils": "^1.2.0", + "radius": "^1.1.4" + } + }, + "node_modules/node-radius-client/node_modules/joi": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-14.3.1.tgz", + "integrity": "sha512-LQDdM+pkOrpAn4Lp+neNIFV3axv1Vna3j38bisbQhETPMANYRbFJFUyOZcOClYvM/hppMhGWuKSFEK9vjrB+bQ==", + "deprecated": "This module has moved and is now available at @hapi/joi. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.", + "dependencies": { + "hoek": "6.x.x", + "isemail": "3.x.x", + "topo": "3.x.x" + } + }, + "node_modules/node-radius-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/node-radius-utils/-/node-radius-utils-1.2.0.tgz", + "integrity": "sha512-i3Sf6khnenl0aXumo0whAlfPWTaBqHxEnVBBxpu3dZ7q69NkPPv71rvPjlDZ5wkeKCTNNUTECljerS5kcYQxRw==" + }, "node_modules/node-releases": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz", @@ -13429,6 +13473,14 @@ "node": ">=8" } }, + "node_modules/radius": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/radius/-/radius-1.1.4.tgz", + "integrity": "sha512-UWuzdF6xf3NpsXFZZmUEkxtEalDXj8hdmMXgbGzn7vOk6zXNsiIY2I6SJ1euHt7PTQuMoz2qDEJB+AfJDJgQYw==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -15261,6 +15313,15 @@ "node": ">=0.6" } }, + "node_modules/topo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", + "integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==", + "deprecated": "This module has moved and is now available at @hapi/topo. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.", + "dependencies": { + "hoek": "6.x.x" + } + }, "node_modules/toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", @@ -22641,6 +22702,11 @@ "readable-stream": "^3.6.0" } }, + "hoek": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz", + "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==" + }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -23123,6 +23189,14 @@ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "devOptional": true }, + "isemail": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", + "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", + "requires": { + "punycode": "2.x.x" + } + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -25618,6 +25692,33 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, + "node-radius-client": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-radius-client/-/node-radius-client-1.0.0.tgz", + "integrity": "sha512-FkR9cMV5hNoX+kKDUTzuagvEixlLiaEJQ1/ywOdhahsihKrGDhVZmnCvmrCStA589MT3yuC/J2eKc6z68IGdBw==", + "requires": { + "joi": "^14.3.1", + "node-radius-utils": "^1.2.0", + "radius": "^1.1.4" + }, + "dependencies": { + "joi": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-14.3.1.tgz", + "integrity": "sha512-LQDdM+pkOrpAn4Lp+neNIFV3axv1Vna3j38bisbQhETPMANYRbFJFUyOZcOClYvM/hppMhGWuKSFEK9vjrB+bQ==", + "requires": { + "hoek": "6.x.x", + "isemail": "3.x.x", + "topo": "3.x.x" + } + } + } + }, + "node-radius-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/node-radius-utils/-/node-radius-utils-1.2.0.tgz", + "integrity": "sha512-i3Sf6khnenl0aXumo0whAlfPWTaBqHxEnVBBxpu3dZ7q69NkPPv71rvPjlDZ5wkeKCTNNUTECljerS5kcYQxRw==" + }, "node-releases": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz", @@ -26532,6 +26633,11 @@ "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true }, + "radius": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/radius/-/radius-1.1.4.tgz", + "integrity": "sha512-UWuzdF6xf3NpsXFZZmUEkxtEalDXj8hdmMXgbGzn7vOk6zXNsiIY2I6SJ1euHt7PTQuMoz2qDEJB+AfJDJgQYw==" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -27967,6 +28073,14 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, + "topo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", + "integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==", + "requires": { + "hoek": "6.x.x" + } + }, "toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", diff --git a/package.json b/package.json index ea6c5a791..bac451d5e 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "mqtt": "^4.2.8", "mssql": "^8.1.0", "node-cloudflared-tunnel": "~1.0.9", + "node-radius-client": "^1.0.0", "nodemailer": "~6.6.5", "notp": "~2.0.3", "password-hash": "~1.2.2", diff --git a/server/database.js b/server/database.js index e7557fc8d..b1a23a475 100644 --- a/server/database.js +++ b/server/database.js @@ -62,6 +62,7 @@ class Database { "patch-add-clickable-status-page-link.sql": true, "patch-add-sqlserver-monitor.sql": true, "patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] }, + "patch-add-radius-monitor.sql": true, "patch-monitor-add-resend-interval.sql": true, }; @@ -149,6 +150,9 @@ class Database { await R.exec("PRAGMA cache_size = -12000"); await R.exec("PRAGMA auto_vacuum = FULL"); + // Avoid error "SQLITE_BUSY: database is locked" by allowing SQLITE to wait up to 5 seconds to do a write + await R.exec("PRAGMA busy_timeout = 5000"); + // This ensures that an operating system crash or power failure will not corrupt the database. // FULL synchronous is very safe, but it is also slower. // Read more: https://sqlite.org/pragma.html#pragma_synchronous diff --git a/server/model/monitor.js b/server/model/monitor.js index 9e0bfe3c0..f96b4df06 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -7,7 +7,7 @@ dayjs.extend(timezone); const axios = require("axios"); const { Prometheus } = require("../prometheus"); const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util"); -const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm } = require("../util-server"); +const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm, radius } = require("../util-server"); const { R } = require("redbean-node"); const { BeanModel } = require("redbean-node/dist/bean-model"); const { Notification } = require("../notification"); @@ -104,6 +104,11 @@ class Monitor extends BeanModel { authMethod: this.authMethod, authWorkstation: this.authWorkstation, authDomain: this.authDomain, + radiusUsername: this.radiusUsername, + radiusPassword: this.radiusPassword, + radiusCalledStationId: this.radiusCalledStationId, + radiusCallingStationId: this.radiusCallingStationId, + radiusSecret: this.radiusSecret, }; if (includeSensitiveData) { @@ -526,6 +531,30 @@ class Monitor extends BeanModel { bean.msg = ""; bean.status = UP; bean.ping = dayjs().valueOf() - startTime; + } else if (this.type === "radius") { + let startTime = dayjs().valueOf(); + try { + const resp = await radius( + this.hostname, + this.radiusUsername, + this.radiusPassword, + this.radiusCalledStationId, + this.radiusCallingStationId, + this.radiusSecret + ); + if (resp.code) { + bean.msg = resp.code; + } + bean.status = UP; + } catch (error) { + bean.status = DOWN; + if (error.response?.code) { + bean.msg = error.response.code; + } else { + bean.msg = error.message; + } + } + bean.ping = dayjs().valueOf() - startTime; } else { bean.msg = "Unknown Monitor Type"; bean.status = PENDING; diff --git a/server/notification-providers/bark.js b/server/notification-providers/bark.js index 092511d87..3258e7c52 100644 --- a/server/notification-providers/bark.js +++ b/server/notification-providers/bark.js @@ -12,9 +12,7 @@ const { default: axios } = require("axios"); // bark is an APN bridge that sends notifications to Apple devices. -const barkNotificationGroup = "UptimeKuma"; const barkNotificationAvatar = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png"; -const barkNotificationSound = "telegraph"; const successMessage = "Successes!"; class Bark extends NotificationProvider { @@ -50,13 +48,23 @@ class Bark extends NotificationProvider { * @param {string} postUrl URL to append parameters to * @returns {string} */ - appendAdditionalParameters(postUrl) { - // grouping all our notifications - postUrl += "?group=" + barkNotificationGroup; + appendAdditionalParameters(notification, postUrl) { // set icon to uptime kuma icon, 11kb should be fine postUrl += "&icon=" + barkNotificationAvatar; + // grouping all our notifications + if (notification.barkGroup != null) { + postUrl += "&group=" + notification.barkGroup; + } else { + // default name + postUrl += "&group=" + "UptimeKuma"; + } // picked a sound, this should follow system's mute status when arrival - postUrl += "&sound=" + barkNotificationSound; + if (notification.barkSound != null) { + postUrl += "&sound=" + notification.barkSound; + } else { + // default sound + postUrl += "&sound=" + "telegraph"; + } return postUrl; } diff --git a/server/notification-providers/home-assistant.js b/server/notification-providers/home-assistant.js new file mode 100644 index 000000000..285989eeb --- /dev/null +++ b/server/notification-providers/home-assistant.js @@ -0,0 +1,38 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); + +const defaultNotificationService = "notify"; + +class HomeAssistant extends NotificationProvider { + name = "HomeAssistant"; + + async send(notification, message, monitor = null, heartbeat = null) { + const notificationService = notification?.notificationService || defaultNotificationService; + + try { + await axios.post( + `${notification.homeAssistantUrl}/api/services/notify/${notificationService}`, + { + title: "Uptime Kuma", + message, + ...(notificationService !== "persistent_notification" && { data: { + name: monitor?.name, + status: heartbeat?.status, + } }), + }, + { + headers: { + Authorization: `Bearer ${notification.longLivedAccessToken}`, + "Content-Type": "application/json", + }, + } + ); + + return "Sent Successfully."; + } catch (error) { + this.throwGeneralAxiosError(error); + } + } +} + +module.exports = HomeAssistant; diff --git a/server/notification.js b/server/notification.js index ad1c8705a..8093572a1 100644 --- a/server/notification.js +++ b/server/notification.js @@ -12,6 +12,7 @@ const Feishu = require("./notification-providers/feishu"); const GoogleChat = require("./notification-providers/google-chat"); const Gorush = require("./notification-providers/gorush"); const Gotify = require("./notification-providers/gotify"); +const HomeAssistant = require("./notification-providers/home-assistant"); const Line = require("./notification-providers/line"); const LineNotify = require("./notification-providers/linenotify"); const LunaSea = require("./notification-providers/lunasea"); @@ -61,6 +62,7 @@ class Notification { new GoogleChat(), new Gorush(), new Gotify(), + new HomeAssistant(), new Line(), new LineNotify(), new LunaSea(), diff --git a/server/server.js b/server/server.js index 49112a57b..818bd7d12 100644 --- a/server/server.js +++ b/server/server.js @@ -694,6 +694,11 @@ let needSetup = false; bean.authMethod = monitor.authMethod; bean.authWorkstation = monitor.authWorkstation; bean.authDomain = monitor.authDomain; + bean.radiusUsername = monitor.radiusUsername; + bean.radiusPassword = monitor.radiusPassword; + bean.radiusCalledStationId = monitor.radiusCalledStationId; + bean.radiusCallingStationId = monitor.radiusCallingStationId; + bean.radiusSecret = monitor.radiusSecret; await R.store(bean); diff --git a/server/util-server.js b/server/util-server.js index df711cf05..067da6fd5 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -15,6 +15,12 @@ const { Client } = require("pg"); const postgresConParse = require("pg-connection-string").parse; const { NtlmClient } = require("axios-ntlm"); const { Settings } = require("./settings"); +const radiusClient = require("node-radius-client"); +const { + dictionaries: { + rfc2865: { file, attributes }, + }, +} = require("node-radius-utils"); // From ping-lite exports.WIN = /^win/.test(process.platform); @@ -285,6 +291,30 @@ exports.postgresQuery = function (connectionString, query) { }); }; +exports.radius = function ( + hostname, + username, + password, + calledStationId, + callingStationId, + secret, +) { + const client = new radiusClient({ + host: hostname, + dictionaries: [ file ], + }); + + return client.accessRequest({ + secret: secret, + attributes: [ + [ attributes.USER_NAME, username ], + [ attributes.USER_PASSWORD, password ], + [ attributes.CALLING_STATION_ID, callingStationId ], + [ attributes.CALLED_STATION_ID, calledStationId ], + ], + }); +}; + /** * Retrieve value of setting based on key * @param {string} key Key of setting to retrieve diff --git a/src/components/notifications/Bark.vue b/src/components/notifications/Bark.vue index 014450dec..6cac73d36 100644 --- a/src/components/notifications/Bark.vue +++ b/src/components/notifications/Bark.vue @@ -2,9 +2,6 @@
-
-

*{{ $t("Required") }}

-
{{ $t("here") }}
+
+ + +
+
+ + +
diff --git a/src/components/notifications/HomeAssistant.vue b/src/components/notifications/HomeAssistant.vue new file mode 100644 index 000000000..67e370a15 --- /dev/null +++ b/src/components/notifications/HomeAssistant.vue @@ -0,0 +1,40 @@ + diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js index c1b7da4aa..ff523052e 100644 --- a/src/components/notifications/index.js +++ b/src/components/notifications/index.js @@ -10,6 +10,7 @@ import Feishu from "./Feishu.vue"; import GoogleChat from "./GoogleChat.vue"; import Gorush from "./Gorush.vue"; import Gotify from "./Gotify.vue"; +import HomeAssistant from "./HomeAssistant.vue"; import Line from "./Line.vue"; import LineNotify from "./LineNotify.vue"; import LunaSea from "./LunaSea.vue"; @@ -54,6 +55,7 @@ const NotificationFormList = { "GoogleChat": GoogleChat, "gorush": Gorush, "gotify": Gotify, + "HomeAssistant": HomeAssistant, "line": Line, "LineNotify": LineNotify, "lunasea": LunaSea, diff --git a/src/languages/en.js b/src/languages/en.js index fb0f1d87c..3bb025858 100644 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -411,6 +411,8 @@ export default { SignName: "SignName", "Sms template must contain parameters: ": "Sms template must contain parameters: ", "Bark Endpoint": "Bark Endpoint", + "Bark Group": "Bark Group", + "Bark Sound": "Bark Sound", WebHookUrl: "WebHookUrl", SecretKey: "SecretKey", "For safety, must use secret key": "For safety, must use secret key", @@ -470,6 +472,7 @@ export default { "Domain Name Expiry Notification": "Domain Name Expiry Notification", Proxy: "Proxy", "Date Created": "Date Created", + HomeAssistant: "Home Assistant", onebotHttpAddress: "OneBot HTTP Address", onebotMessageType: "OneBot Message Type", onebotGroupMessage: "Group", @@ -482,6 +485,12 @@ export default { "Domain Names": "Domain Names", signedInDisp: "Signed in as {0}", signedInDispDisabled: "Auth Disabled.", + RadiusSecret: "Radius Secret", + RadiusSecretDescription: "Shared Secret between client and server", + RadiusCalledStationId: "Called Station Id", + RadiusCalledStationIdDescription: "Identifier of the called device", + RadiusCallingStationId: "Calling Station Id", + RadiusCallingStationIdDescription: "Identifier of the calling device", "Certificate Expiry Notification": "Certificate Expiry Notification", "API Username": "API Username", "API Key": "API Key", diff --git a/src/languages/zh-CN.js b/src/languages/zh-CN.js index 67077f389..8dbe05f0f 100644 --- a/src/languages/zh-CN.js +++ b/src/languages/zh-CN.js @@ -404,6 +404,8 @@ export default { TemplateCode: "TemplateCode", SignName: "SignName", "Bark Endpoint": "Bark 接入点", + "Bark Group": "Bark 群组", + "Bark Sound": "Bark 铃声", "Device Token": "Apple Device Token", Platform: "平台", iOS: "iOS", diff --git a/src/languages/zh-TW.js b/src/languages/zh-TW.js index be87c540a..3405c02ab 100644 --- a/src/languages/zh-TW.js +++ b/src/languages/zh-TW.js @@ -408,6 +408,8 @@ export default { SignName: "SignName", "Sms template must contain parameters: ": "Sms 範本必須包含參數:", "Bark Endpoint": "Bark 端點", + "Bark Group": "Bark 群組", + "Bark Sound": "Bark 鈴聲", WebHookUrl: "WebHookUrl", SecretKey: "SecretKey", "For safety, must use secret key": "為了安全起見,必須使用秘密金鑰", diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 9fdcbd1cb..99cbeb95f 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -51,6 +51,9 @@ + @@ -87,8 +90,8 @@ - -
+ +
@@ -202,6 +205,36 @@
+ +