From f6ef390c76b282474a9f96984777576a60102e0d Mon Sep 17 00:00:00 2001 From: Nelson Chan Date: Fri, 7 Jan 2022 11:57:24 +0800 Subject: [PATCH 1/8] Fix: Remove Prom. metrics on delete monitor --- server/model/monitor.js | 5 +++++ server/prometheus.js | 10 ++++++++++ server/server.js | 1 + 3 files changed, 16 insertions(+) diff --git a/server/model/monitor.js b/server/model/monitor.js index c4441d63e..5a04b3833 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -468,6 +468,11 @@ class Monitor extends BeanModel { this.isStop = true; } + onDelete() { + let prometheus = new Prometheus(this); + prometheus.remove(); + } + /** * Helper Method: * returns URL object for further usage diff --git a/server/prometheus.js b/server/prometheus.js index 870581d2e..ebcc8fa47 100644 --- a/server/prometheus.js +++ b/server/prometheus.js @@ -84,6 +84,16 @@ class Prometheus { } } + remove() { + try { + monitor_cert_days_remaining.remove(this.monitorLabelValues); + monitor_cert_is_valid.remove(this.monitorLabelValues); + monitor_response_time.remove(this.monitorLabelValues); + monitor_status.remove(this.monitorLabelValues); + } catch (e) { + console.error(e); + } + } } module.exports = { diff --git a/server/server.js b/server/server.js index 868bbd5ef..db985c439 100644 --- a/server/server.js +++ b/server/server.js @@ -733,6 +733,7 @@ exports.entryPage = "dashboard"; if (monitorID in monitorList) { monitorList[monitorID].stop(); + monitorList[monitorID].onDelete(); delete monitorList[monitorID]; } From edd2534a1ba2fbaca5b1c00db533807646d25c7a Mon Sep 17 00:00:00 2001 From: Nelson Chan Date: Fri, 7 Jan 2022 12:26:26 +0800 Subject: [PATCH 2/8] Fix: Clear metrics also on stop and edit --- server/model/monitor.js | 9 +++++++-- server/server.js | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index 5a04b3833..48bd5a4cc 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -466,11 +466,16 @@ class Monitor extends BeanModel { stop() { clearTimeout(this.heartbeatInterval); this.isStop = true; + + this.prometheus().remove(); } onDelete() { - let prometheus = new Prometheus(this); - prometheus.remove(); + this.prometheus().remove(); + } + + prometheus() { + return new Prometheus(this); } /** diff --git a/server/server.js b/server/server.js index db985c439..fbf0a06e0 100644 --- a/server/server.js +++ b/server/server.js @@ -567,6 +567,11 @@ exports.entryPage = "dashboard"; throw new Error("Permission denied."); } + // Reset Prometheus labels + if (monitorList[monitor.id] && monitorList[monitor.id].prometheus) { + monitorList[monitor.id].prometheus().remove(); + } + bean.name = monitor.name; bean.type = monitor.type; bean.url = monitor.url; From 1e92487f30639343b9c0bf0cf138100d5adc4b9e Mon Sep 17 00:00:00 2001 From: Nelson Chan Date: Fri, 7 Jan 2022 12:28:08 +0800 Subject: [PATCH 3/8] Chore: Remove onDelete as unused --- server/model/monitor.js | 4 ---- server/server.js | 1 - 2 files changed, 5 deletions(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index 48bd5a4cc..f99939170 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -470,10 +470,6 @@ class Monitor extends BeanModel { this.prometheus().remove(); } - onDelete() { - this.prometheus().remove(); - } - prometheus() { return new Prometheus(this); } diff --git a/server/server.js b/server/server.js index fbf0a06e0..3e888c92a 100644 --- a/server/server.js +++ b/server/server.js @@ -738,7 +738,6 @@ exports.entryPage = "dashboard"; if (monitorID in monitorList) { monitorList[monitorID].stop(); - monitorList[monitorID].onDelete(); delete monitorList[monitorID]; } From 2e0e35a1ee982ae21452035648c7f707f404498c Mon Sep 17 00:00:00 2001 From: Nelson Chan Date: Fri, 7 Jan 2022 12:34:01 +0800 Subject: [PATCH 4/8] Fix: Fix typo --- server/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/server.js b/server/server.js index 3e888c92a..7db702a9f 100644 --- a/server/server.js +++ b/server/server.js @@ -568,7 +568,7 @@ exports.entryPage = "dashboard"; } // Reset Prometheus labels - if (monitorList[monitor.id] && monitorList[monitor.id].prometheus) { + if (monitorList[monitor.id] && monitorList[monitor.id].prometheus()) { monitorList[monitor.id].prometheus().remove(); } From 1bbd744d0285bcb5bda457165221a3da97bc79d3 Mon Sep 17 00:00:00 2001 From: Nelson Chan Date: Fri, 7 Jan 2022 14:29:42 +0800 Subject: [PATCH 5/8] Chore: Improve syntax --- server/server.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/server.js b/server/server.js index 7db702a9f..821fccc7c 100644 --- a/server/server.js +++ b/server/server.js @@ -568,9 +568,7 @@ exports.entryPage = "dashboard"; } // Reset Prometheus labels - if (monitorList[monitor.id] && monitorList[monitor.id].prometheus()) { - monitorList[monitor.id].prometheus().remove(); - } + monitorList[monitor.id]?.prometheus()?.remove(); bean.name = monitor.name; bean.type = monitor.type; From e11ea7b0618d59dbd5bf13b020b00a4b192b6a70 Mon Sep 17 00:00:00 2001 From: sovushik <30425777+sovushik@users.noreply.github.com> Date: Sat, 26 Mar 2022 10:46:07 +0500 Subject: [PATCH 6/8] Update ru-RU.js Add new string for 1.13.1 --- src/languages/ru-RU.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/languages/ru-RU.js b/src/languages/ru-RU.js index d5bec924e..cfc263e0b 100644 --- a/src/languages/ru-RU.js +++ b/src/languages/ru-RU.js @@ -181,7 +181,7 @@ export default { "Edit Status Page": "Редактировать", "Go to Dashboard": "Панель управления", "Status Page": "Мониторинг", - "Status Pages": "Página de Status", + "Status Pages": "Панель мониторингов", Discard: "Отмена", "Create Incident": "Создать инцидент", "Switch to Dark Theme": "Тёмная тема", @@ -343,4 +343,14 @@ export default { primary: "ОСНОВНОЙ", light: "СВЕТЛЫЙ", dark: "ТЕМНЫЙ", + "New Status Page": "Новый мониторинг", + "Show update if available": "Показывать доступные обновления", + "Also check beta release": "Проверять обновления для бета версий", + "Add New Status Page": "Добавить страницу мониторинга", + "Next": "Далее", + "Accept characters: a-z 0-9 -": "Разрешены символы: a-z 0-9 -", + "Start or end with a-z 0-9 only": "Начало и окончание имени только на символы: a-z 0-9", + "No consecutive dashes --": "Запрещено использовать тире --", + "HTTP Options": "HTTP Опции", + "Basic Auth": "HTTP Авторизация", }; From 61d0a0abce8d699a44b9df3d84c70e05586a8a57 Mon Sep 17 00:00:00 2001 From: DX37 Date: Mon, 28 Mar 2022 21:16:13 +0700 Subject: [PATCH 7/8] update russian translation --- src/languages/ru-RU.js | 64 +++++++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/src/languages/ru-RU.js b/src/languages/ru-RU.js index cfc263e0b..c8212442b 100644 --- a/src/languages/ru-RU.js +++ b/src/languages/ru-RU.js @@ -180,8 +180,8 @@ export default { "Add a monitor": "Добавить монитор", "Edit Status Page": "Редактировать", "Go to Dashboard": "Панель управления", - "Status Page": "Мониторинг", - "Status Pages": "Панель мониторингов", + "Status Page": "Страница статуса", + "Status Pages": "Страницы статуса", Discard: "Отмена", "Create Incident": "Создать инцидент", "Switch to Dark Theme": "Тёмная тема", @@ -311,28 +311,28 @@ export default { "One record": "Одна запись", steamApiKeyDescription: "Для мониторинга игрового сервера Steam вам необходим Web-API ключ Steam. Зарегистрировать его можно здесь: ", "Certificate Chain": "Цепочка сертификатов", - "Valid": "Действительный", + Valid: "Действительный", "Hide Tags": "Скрыть тэги", Title: "Название инцидента:", Content: "Содержание инцидента:", Post: "Опубликовать", - "Cancel": "Отмена", - "Created": "Создано", - "Unpin": "Открепить", + Cancel: "Отмена", + Created: "Создано", + Unpin: "Открепить", "Show Tags": "Показать тэги", - "recent": "Сейчас", + recent: "Сейчас", "3h": "3 часа", "6h": "6 часов", "24h": "24 часа", "1w": "1 неделя", "No monitors available.": "Нет доступных мониторов", "Add one": "Добавить новый", - "Backup": "Резервная копия", - "Security": "Безопасность", + Backup: "Резервная копия", + Security: "Безопасность", "Shrink Database": "Сжать Базу Данных", "Current User": "Текущий пользователь", - "About": "О программе", - "Description": "Описание", + About: "О программе", + Description: "Описание", "Powered by": "Работает на основе скрипта от", shrinkDatabaseDescription: "Включает VACUUM для базы данных SQLite. Если ваша база данных была создана на версии 1.10.0 и более, AUTO_VACUUM уже включен и это действие не требуется.", deleteStatusPageMsg: "Вы действительно хотите удалить эту страницу статуса сервисов?", @@ -343,14 +343,50 @@ export default { primary: "ОСНОВНОЙ", light: "СВЕТЛЫЙ", dark: "ТЕМНЫЙ", - "New Status Page": "Новый мониторинг", + "New Status Page": "Новая страница статуса", "Show update if available": "Показывать доступные обновления", "Also check beta release": "Проверять обновления для бета версий", - "Add New Status Page": "Добавить страницу мониторинга", - "Next": "Далее", + "Add New Status Page": "Добавить страницу статуса", + Next: "Далее", "Accept characters: a-z 0-9 -": "Разрешены символы: a-z 0-9 -", "Start or end with a-z 0-9 only": "Начало и окончание имени только на символы: a-z 0-9", "No consecutive dashes --": "Запрещено использовать тире --", "HTTP Options": "HTTP Опции", "Basic Auth": "HTTP Авторизация", + PushByTechulus: "Push by Techulus", + clicksendsms: "ClickSend SMS", + GoogleChat: "Google Chat (только Google Workspace)", + apiCredentials: "API реквизиты", + Done: "Готово", + Info: "Инфо", + "Steam API Key": "Steam API-Ключ", + "Pick a RR-Type...": "Выберите RR-Тип...", + "Pick Accepted Status Codes...": "Выберите принятые коды состояния...", + Default: "По умолчанию", + "Please input title and content": "Пожалуйста, введите название и содержание", + "Last Updated": "Последнее Обновление", + "Untitled Group": "Группа без названия", + Services: "Сервисы", + serwersms: "SerwerSMS.pl", + serwersmsAPIUser: "API Пользователь (включая префикс webapi_)", + serwersmsAPIPassword: "API Пароль", + serwersmsPhoneNumber: "Номер телефона", + serwersmsSenderName: "SMS Имя Отправителя (регистрированный через пользовательский портал)", + stackfield: "Stackfield", + smtpDkimSettings: "DKIM Настройки", + smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.", + documentation: "документация", + smtpDkimDomain: "Имя Домена", + smtpDkimKeySelector: "Ключ", + smtpDkimPrivateKey: "Приватный ключ", + smtpDkimHashAlgo: "Алгоритм хэша (опционально)", + smtpDkimheaderFieldNames: "Заголовок ключей для подписи (опционально)", + smtpDkimskipFields: "Заколовок ключей не для подписи (опционально)", + gorush: "Gorush", + alerta: "Alerta", + alertaApiEndpoint: "Конечная точка API", + alertaEnvironment: "Среда", + alertaApiKey: "Ключ API", + alertaAlertState: "Состояние алерта", + alertaRecoverState: "Состояние восстановления", }; From 0da6e6b1fb5ee7bc687d491eb3a183061d53b418 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Tue, 29 Mar 2022 17:38:48 +0800 Subject: [PATCH 8/8] Some improvements --- server/auth.js | 4 + server/rate-limiter.js | 10 ++- server/server.js | 120 ++++++++++++++++++--------- server/util-server.js | 27 +++++- src/components/MonitorList.vue | 4 +- src/components/TwoFADialog.vue | 58 ++++++++----- src/components/settings/Security.vue | 24 +++++- src/pages/Settings.vue | 12 ++- 8 files changed, 188 insertions(+), 71 deletions(-) diff --git a/server/auth.js b/server/auth.js index 1196f94d7..c59d65492 100644 --- a/server/auth.js +++ b/server/auth.js @@ -12,6 +12,10 @@ const { loginRateLimiter } = require("./rate-limiter"); * @returns {Promise} */ exports.login = async function (username, password) { + if (typeof username !== "string" || typeof password !== "string") { + return null; + } + let user = await R.findOne("user", " username = ? AND active = 1 ", [ username, ]); diff --git a/server/rate-limiter.js b/server/rate-limiter.js index 0bacc14c7..6422af8d2 100644 --- a/server/rate-limiter.js +++ b/server/rate-limiter.js @@ -34,6 +34,14 @@ const loginRateLimiter = new KumaRateLimiter({ errorMessage: "Too frequently, try again later." }); +const twoFaRateLimiter = new KumaRateLimiter({ + tokensPerInterval: 30, + interval: "minute", + fireImmediately: true, + errorMessage: "Too frequently, try again later." +}); + module.exports = { - loginRateLimiter + loginRateLimiter, + twoFaRateLimiter, }; diff --git a/server/server.js b/server/server.js index 9a5e1028b..cac2bdb63 100644 --- a/server/server.js +++ b/server/server.js @@ -52,7 +52,7 @@ 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, FBSD, errorLog, doubleCheckPassword } = require("./util-server"); debug("Importing Notification"); const { Notification } = require("./notification"); @@ -63,7 +63,7 @@ const Database = require("./database"); debug("Importing Background Jobs"); const { initBackgroundJobs } = require("./jobs"); -const { loginRateLimiter } = require("./rate-limiter"); +const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter"); const { basicAuth } = require("./auth"); const { login } = require("./auth"); @@ -305,6 +305,15 @@ exports.entryPage = "dashboard"; socket.on("login", async (data, callback) => { console.log("Login"); + // Checking + if (typeof callback !== "function") { + return; + } + + if (!data) { + return; + } + // Login Rate Limit if (! await loginRateLimiter.pass(callback)) { return; @@ -363,14 +372,27 @@ exports.entryPage = "dashboard"; }); socket.on("logout", async (callback) => { + // Rate Limit + if (! await loginRateLimiter.pass(callback)) { + return; + } + socket.leave(socket.userID); socket.userID = null; - callback(); + + if (typeof callback === "function") { + callback(); + } }); - socket.on("prepare2FA", async (callback) => { + socket.on("prepare2FA", async (currentPassword, callback) => { try { + if (! await twoFaRateLimiter.pass(callback)) { + return; + } + checkLogin(socket); + await doubleCheckPassword(socket, currentPassword); let user = await R.findOne("user", " id = ? AND active = 1 ", [ socket.userID, @@ -405,14 +427,19 @@ exports.entryPage = "dashboard"; } catch (error) { callback({ ok: false, - msg: "Error while trying to prepare 2FA.", + msg: error.message, }); } }); - socket.on("save2FA", async (callback) => { + socket.on("save2FA", async (currentPassword, callback) => { try { + if (! await twoFaRateLimiter.pass(callback)) { + return; + } + checkLogin(socket); + await doubleCheckPassword(socket, currentPassword); await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [ socket.userID, @@ -425,14 +452,19 @@ exports.entryPage = "dashboard"; } catch (error) { callback({ ok: false, - msg: "Error while trying to change 2FA.", + msg: error.message, }); } }); - socket.on("disable2FA", async (callback) => { + socket.on("disable2FA", async (currentPassword, callback) => { try { + if (! await twoFaRateLimiter.pass(callback)) { + return; + } + checkLogin(socket); + await doubleCheckPassword(socket, currentPassword); await TwoFA.disable2FA(socket.userID); callback({ @@ -442,36 +474,47 @@ exports.entryPage = "dashboard"; } catch (error) { callback({ ok: false, - msg: "Error while trying to change 2FA.", + msg: error.message, }); } }); - socket.on("verifyToken", async (token, callback) => { - let user = await R.findOne("user", " id = ? AND active = 1 ", [ - socket.userID, - ]); + socket.on("verifyToken", async (token, currentPassword, callback) => { + try { + checkLogin(socket); + await doubleCheckPassword(socket, currentPassword); - let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts); + let user = await R.findOne("user", " id = ? AND active = 1 ", [ + socket.userID, + ]); - if (user.twofa_last_token !== token && verify) { - callback({ - ok: true, - valid: true, - }); - } else { + let verify = notp.totp.verify(token, user.twofa_secret, twofa_verification_opts); + + if (user.twofa_last_token !== token && verify) { + callback({ + ok: true, + valid: true, + }); + } else { + callback({ + ok: false, + msg: "Invalid Token.", + valid: false, + }); + } + + } catch (error) { callback({ ok: false, - msg: "Invalid Token.", - valid: false, + msg: error.message, }); } }); socket.on("twoFAStatus", async (callback) => { - checkLogin(socket); - try { + checkLogin(socket); + let user = await R.findOne("user", " id = ? AND active = 1 ", [ socket.userID, ]); @@ -488,9 +531,10 @@ exports.entryPage = "dashboard"; }); } } catch (error) { + console.log(error); callback({ ok: false, - msg: "Error while trying to get 2FA status.", + msg: error.message, }); } }); @@ -936,21 +980,13 @@ exports.entryPage = "dashboard"; throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length."); } - let user = await R.findOne("user", " id = ? AND active = 1 ", [ - socket.userID, - ]); + let user = await doubleCheckPassword(socket, password.currentPassword); + await user.resetPassword(password.newPassword); - if (user && passwordHash.verify(password.currentPassword, user.password)) { - - user.resetPassword(password.newPassword); - - callback({ - ok: true, - msg: "Password has been updated successfully.", - }); - } else { - throw new Error("Incorrect current password"); - } + callback({ + ok: true, + msg: "Password has been updated successfully.", + }); } catch (e) { callback({ @@ -977,10 +1013,14 @@ exports.entryPage = "dashboard"; } }); - socket.on("setSettings", async (data, callback) => { + socket.on("setSettings", async (data, currentPassword, callback) => { try { checkLogin(socket); + if (data.disableAuth) { + await doubleCheckPassword(socket, currentPassword); + } + await setSettings("general", data); exports.entryPage = data.entryPage; diff --git a/server/util-server.js b/server/util-server.js index 2264ebea9..b2c70d92f 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -1,9 +1,8 @@ const tcpp = require("tcp-ping"); const Ping = require("./ping-lite"); const { R } = require("redbean-node"); -const { debug } = require("../src/util"); +const { debug, genSecret } = require("../src/util"); const passwordHash = require("./password-hash"); -const dayjs = require("dayjs"); const { Resolver } = require("dns"); const child_process = require("child_process"); const iconv = require("iconv-lite"); @@ -32,7 +31,7 @@ exports.initJWTSecret = async () => { jwtSecretBean.key = "jwtSecret"; } - jwtSecretBean.value = passwordHash.generate(dayjs() + ""); + jwtSecretBean.value = passwordHash.generate(genSecret()); await R.store(jwtSecretBean); return jwtSecretBean; }; @@ -321,6 +320,28 @@ exports.checkLogin = (socket) => { } }; +/** + * For logged-in users, double-check the password + * @param socket + * @param currentPassword + * @returns {Promise} + */ +exports.doubleCheckPassword = async (socket, currentPassword) => { + if (typeof currentPassword !== "string") { + throw new Error("Wrong data type?"); + } + + let user = await R.findOne("user", " id = ? AND active = 1 ", [ + socket.userID, + ]); + + if (!user || !passwordHash.verify(currentPassword, user.password)) { + throw new Error("Incorrect current password"); + } + + return user; +}; + exports.startUnitTest = async () => { console.log("Starting unit test..."); const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm"; diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue index e38d1810b..6171c0b3a 100644 --- a/src/components/MonitorList.vue +++ b/src/components/MonitorList.vue @@ -9,7 +9,9 @@ - +
+ +
diff --git a/src/components/TwoFADialog.vue b/src/components/TwoFADialog.vue index b7b9668d8..8a773d6b2 100644 --- a/src/components/TwoFADialog.vue +++ b/src/components/TwoFADialog.vue @@ -19,6 +19,19 @@

{{ uri }}

+
+ + +
+ @@ -59,11 +72,11 @@