From 803f0d6219ae84838cc6884c5a9dec379c6bc1ff Mon Sep 17 00:00:00 2001 From: Nelson Chan Date: Wed, 21 Jul 2021 12:09:09 +0800 Subject: [PATCH 1/6] Feat: Add Barebones certificate info display --- server/model/monitor.js | 13 ++++++++++- server/util-server.js | 50 +++++++++++++++++++++++++++++++++++++++++ src/mixins/socket.js | 5 +++++ src/pages/Details.vue | 40 +++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index 133088671..79376c809 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -6,7 +6,7 @@ dayjs.extend(utc) dayjs.extend(timezone) const axios = require("axios"); const {UP, DOWN, PENDING} = require("../util"); -const {tcping, ping} = require("../util-server"); +const {tcping, ping, checkCertificate} = require("../util-server"); const {R} = require("redbean-node"); const {BeanModel} = require("redbean-node/dist/bean-model"); const {Notification} = require("../notification") @@ -79,6 +79,9 @@ class Monitor extends BeanModel { }) bean.msg = `${res.status} - ${res.statusText}` bean.ping = dayjs().valueOf() - startTime; + if (this.url.startsWith("https")) { + Monitor.sendCertInfo(checkCertificate(res), io, this.id, this.user_id); + } if (this.type === "http") { bean.status = UP; @@ -218,6 +221,14 @@ class Monitor extends BeanModel { io.to(userID).emit("avgPing", monitorID, avgPing); } + /** + * + * @param checkCertificateResult : Object return result of checkCertificate + */ + static async sendCertInfo(checkCertificateResult, io, monitorID, userID) { + io.to(userID).emit("certInfo", monitorID, checkCertificateResult); + } + /** * Uptime with calculation * Calculation based on: diff --git a/server/util-server.js b/server/util-server.js index b387f4c7c..e0e255345 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -70,3 +70,53 @@ exports.getSettings = async function (type) { return result; } + + +// ssl-checker by @dyaa +// param: res - response object from axios +// return an object containing the certificate information + +const getDaysBetween = (validFrom, validTo) => + Math.round(Math.abs(+validFrom - +validTo) / 8.64e7); + +const getDaysRemaining = (validFrom, validTo) => { + const daysRemaining = getDaysBetween(validFrom, validTo); + if (new Date(validTo).getTime() < new Date().getTime()) { + return -daysRemaining; + } + return daysRemaining; +}; + +exports.checkCertificate = function (res) { + const { + valid_from, + valid_to, + subjectaltname, + issuer, + fingerprint, + } = res.request.res.socket.getPeerCertificate(false); + + if (!valid_from || !valid_to || !subjectaltname) { + reject(new Error('No certificate')); + return; + } + + const valid = res.request.res.socket.authorized || false; + + const validTo = new Date(valid_to); + + const validFor = subjectaltname + .replace(/DNS:|IP Address:/g, "") + .split(", "); + + const daysRemaining = getDaysRemaining(new Date(), validTo); + + return { + valid, + validFor, + validTo, + daysRemaining, + issuer, + fingerprint, + }; +} \ No newline at end of file diff --git a/src/mixins/socket.js b/src/mixins/socket.js index 0c4d68e15..612bbadd6 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -25,6 +25,7 @@ export default { importantHeartbeatList: { }, avgPingList: { }, uptimeList: { }, + certInfoList: {}, notificationList: [], windowWidth: window.innerWidth, showListMobile: false, @@ -114,6 +115,10 @@ export default { this.uptimeList[`${monitorID}_${type}`] = data }); + socket.on('certInfo', (monitorID, data) => { + this.certInfoList[monitorID] = data + }); + socket.on('importantHeartbeatList', (monitorID, data) => { if (! (monitorID in this.importantHeartbeatList)) { this.importantHeartbeatList[monitorID] = data; diff --git a/src/pages/Details.vue b/src/pages/Details.vue index f8c4879ad..727f0aab4 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -54,6 +54,38 @@ +
+
+
+

Certificate Info

+ + + + + + + + + + + + + + + + + + + + + + + +
Valid: {{ certInfo.valid }}
Valid To: {{ certInfo.validTo ? new Date(certInfo.validTo).toLocaleString() : "" }}
Days Remaining: {{ certInfo.daysRemaining }}
Issuer: {{ certInfo.issuer }}
Fingerprint: {{ certInfo.fingerprint }}
+
+
+
+
@@ -180,6 +212,14 @@ export default { } }, + certInfo() { + if (this.$root.certInfoList[this.monitor.id]) { + return this.$root.certInfoList[this.monitor.id] + } else { + return { } + } + }, + displayedRecords() { const startIndex = this.perPage * (this.page - 1); const endIndex = startIndex + this.perPage; From d0c63ebe3e054a200c25c52f551580c000fca4ba Mon Sep 17 00:00:00 2001 From: Nelson Chan Date: Thu, 22 Jul 2021 16:04:32 +0800 Subject: [PATCH 2/6] Feat: Add database storage for TLS info --- db/patch2.sql | 9 +++++++++ server/database.js | 2 +- server/model/monitor.js | 44 +++++++++++++++++++++++++++++++++-------- src/mixins/socket.js | 14 +++++++++++-- src/pages/Details.vue | 2 +- 5 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 db/patch2.sql diff --git a/db/patch2.sql b/db/patch2.sql new file mode 100644 index 000000000..012d01502 --- /dev/null +++ b/db/patch2.sql @@ -0,0 +1,9 @@ +BEGIN TRANSACTION; + +CREATE TABLE monitor_tls_info ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + monitor_id INTEGER NOT NULL, + info_json TEXT +); + +COMMIT; diff --git a/server/database.js b/server/database.js index 49659e613..eef3587e4 100644 --- a/server/database.js +++ b/server/database.js @@ -8,7 +8,7 @@ class Database { static templatePath = "./db/kuma.db" static path = './data/kuma.db'; - static latestVersion = 1; + static latestVersion = 2; static noReject = true; static async patch() { diff --git a/server/model/monitor.js b/server/model/monitor.js index 79376c809..ae409616a 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -79,8 +79,10 @@ class Monitor extends BeanModel { }) bean.msg = `${res.status} - ${res.statusText}` bean.ping = dayjs().valueOf() - startTime; - if (this.url.startsWith("https")) { - Monitor.sendCertInfo(checkCertificate(res), io, this.id, this.user_id); + + // Check certificate if https is used + if (this.getUrl().protocol === "https:") { + await this.updateTlsInfo(checkCertificate(res)); } if (this.type === "http") { @@ -197,10 +199,35 @@ class Monitor extends BeanModel { clearInterval(this.heartbeatInterval) } + // Helper Method: + // returns URL object for further usage + // returns null if url is invalid + getUrl() { + try { + return new URL(this.url); + } catch (_) { + return null; + } + } + + // Store TLS info to database + async updateTlsInfo(checkCertificateResult) { + let tls_info_bean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ + this.id + ]); + if (tls_info_bean == null) { + tls_info_bean = R.dispense("monitor_tls_info"); + tls_info_bean.monitor_id = this.id; + } + tls_info_bean.info_json = JSON.stringify(checkCertificateResult); + R.store(tls_info_bean); + } + static async sendStats(io, monitorID, userID) { Monitor.sendAvgPing(24, io, monitorID, userID); Monitor.sendUptime(24, io, monitorID, userID); Monitor.sendUptime(24 * 30, io, monitorID, userID); + Monitor.sendCertInfo(io, monitorID, userID); } /** @@ -221,12 +248,13 @@ class Monitor extends BeanModel { io.to(userID).emit("avgPing", monitorID, avgPing); } - /** - * - * @param checkCertificateResult : Object return result of checkCertificate - */ - static async sendCertInfo(checkCertificateResult, io, monitorID, userID) { - io.to(userID).emit("certInfo", monitorID, checkCertificateResult); + static async sendCertInfo(io, monitorID, userID) { + let tls_info = await R.findOne("monitor_tls_info", "monitor_id = ?", [ + monitorID + ]); + if (tls_info != null) { + io.to(userID).emit("certInfo", monitorID, tls_info.info_json); + } } /** diff --git a/src/mixins/socket.js b/src/mixins/socket.js index 612bbadd6..f36a770e3 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -59,7 +59,17 @@ export default { this.$router.push("/setup") }); - socket.on('monitorList', (data) => { + socket.on("monitorList", (data) => { + // Add Helper function + Object.entries(data).forEach(([monitorID, monitor]) => { + monitor.getUrl = () => { + try { + return new URL(monitor.url); + } catch (_) { + return null; + } + }; + }); this.monitorList = data; }); @@ -116,7 +126,7 @@ export default { }); socket.on('certInfo', (monitorID, data) => { - this.certInfoList[monitorID] = data + this.certInfoList[monitorID] = JSON.parse(data) }); socket.on('importantHeartbeatList', (monitorID, data) => { diff --git a/src/pages/Details.vue b/src/pages/Details.vue index 727f0aab4..a2952aa0a 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -54,7 +54,7 @@ -
+

Certificate Info

From 7b8459c73a472145d00118e78bee6dcf06e951be Mon Sep 17 00:00:00 2001 From: Nelson Chan Date: Thu, 22 Jul 2021 16:13:58 +0800 Subject: [PATCH 3/6] Fix: use Optional chaining --- server/model/monitor.js | 2 +- src/pages/Details.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index ae409616a..f523dd4dd 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -81,7 +81,7 @@ class Monitor extends BeanModel { bean.ping = dayjs().valueOf() - startTime; // Check certificate if https is used - if (this.getUrl().protocol === "https:") { + if (this.getUrl()?.protocol === "https:") { await this.updateTlsInfo(checkCertificate(res)); } diff --git a/src/pages/Details.vue b/src/pages/Details.vue index a2952aa0a..6d93df399 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -54,7 +54,7 @@
-
+

Certificate Info

From db26b7d123d0aebb5b9f41b4a5e46f9bd88e8b59 Mon Sep 17 00:00:00 2001 From: Nelson Chan Date: Fri, 23 Jul 2021 11:22:37 +0800 Subject: [PATCH 4/6] Fix: Fix no certificate caused by session reuse --- server/model/monitor.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index f523dd4dd..0fb747da0 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1,4 +1,5 @@ +const https = require('https'); const dayjs = require("dayjs"); const utc = require('dayjs/plugin/utc') var timezone = require('dayjs/plugin/timezone') @@ -11,6 +12,12 @@ const {R} = require("redbean-node"); const {BeanModel} = require("redbean-node/dist/bean-model"); const {Notification} = require("../notification") +// Use Custom agent to disable session reuse +// https://github.com/nodejs/node/issues/3940 +const customAgent = new https.Agent({ + maxCachedSessions: 0 +}); + /** * status: * 0 = DOWN @@ -75,8 +82,9 @@ class Monitor extends BeanModel { if (this.type === "http" || this.type === "keyword") { let startTime = dayjs().valueOf(); let res = await axios.get(this.url, { - headers: { 'User-Agent':'Uptime-Kuma' } - }) + headers: { "User-Agent": "Uptime-Kuma" }, + httpsAgent: customAgent, + }); bean.msg = `${res.status} - ${res.statusText}` bean.ping = dayjs().valueOf() - startTime; From 51ac7a58dc5485cd8dc71af1b6e535ae1ceee7e4 Mon Sep 17 00:00:00 2001 From: Nelson Chan Date: Fri, 23 Jul 2021 11:23:43 +0800 Subject: [PATCH 5/6] Fix: Fix incorrect error handling --- server/util-server.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/util-server.js b/server/util-server.js index e0e255345..f03823d3a 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -97,8 +97,7 @@ exports.checkCertificate = function (res) { } = res.request.res.socket.getPeerCertificate(false); if (!valid_from || !valid_to || !subjectaltname) { - reject(new Error('No certificate')); - return; + throw { message: 'No TLS certificate in response' }; } const valid = res.request.res.socket.authorized || false; From caec93318600f0997f14504a75d0d6801fe94648 Mon Sep 17 00:00:00 2001 From: LouisLam Date: Fri, 23 Jul 2021 12:58:05 +0800 Subject: [PATCH 6/6] prevent unexpected error throw from checkCertificate interrupt the beat --- server/model/monitor.js | 16 ++++++++++++---- server/util.js | 6 ++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index 0fb747da0..9043803b3 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -6,7 +6,7 @@ var timezone = require('dayjs/plugin/timezone') dayjs.extend(utc) dayjs.extend(timezone) const axios = require("axios"); -const {UP, DOWN, PENDING} = require("../util"); +const {debug, UP, DOWN, PENDING} = require("../util"); const {tcping, ping, checkCertificate} = require("../util-server"); const {R} = require("redbean-node"); const {BeanModel} = require("redbean-node/dist/bean-model"); @@ -89,10 +89,18 @@ class Monitor extends BeanModel { bean.ping = dayjs().valueOf() - startTime; // Check certificate if https is used + + let certInfoStartTime = dayjs().valueOf(); if (this.getUrl()?.protocol === "https:") { - await this.updateTlsInfo(checkCertificate(res)); + try { + await this.updateTlsInfo(checkCertificate(res)); + } catch (e) { + console.error(e.message) + } } + debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms") + if (this.type === "http") { bean.status = UP; } else { @@ -207,7 +215,7 @@ class Monitor extends BeanModel { clearInterval(this.heartbeatInterval) } - // Helper Method: + // Helper Method: // returns URL object for further usage // returns null if url is invalid getUrl() { @@ -228,7 +236,7 @@ class Monitor extends BeanModel { tls_info_bean.monitor_id = this.id; } tls_info_bean.info_json = JSON.stringify(checkCertificateResult); - R.store(tls_info_bean); + await R.store(tls_info_bean); } static async sendStats(io, monitorID, userID) { diff --git a/server/util.js b/server/util.js index 33b306b5c..081561bf9 100644 --- a/server/util.js +++ b/server/util.js @@ -18,3 +18,9 @@ exports.ucfirst = function (str) { return firstLetter.toUpperCase() + str.substr(1); } +exports.debug = (msg) => { + if (process.env.NODE_ENV === "development") { + console.log(msg) + } +} +