From 893278bd3dc8648f0dc68c7878fe51f644167ea4 Mon Sep 17 00:00:00 2001 From: Nelson Chan <3271800+chakflying@users.noreply.github.com> Date: Sat, 6 Apr 2024 18:43:08 +0800 Subject: [PATCH] Feat: Use keylog event to obtain TLS certificate for better reliability [1.23.X] (#4630) Co-authored-by: Frank Elsinga --- server/model/monitor.js | 35 ++++++++++++++++------------------- server/util-server.js | 19 +++++++++++++------ 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index 40cdd735..cdc9f532 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -512,6 +512,12 @@ class Monitor extends BeanModel { } } + let tlsInfo; + // Store tlsInfo when key material is received + options.httpsAgent.on("keylog", (line, tlsSocket) => { + tlsInfo = checkCertificate(tlsSocket); + }); + log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`); log.debug("monitor", `[${this.name}] Axios Request`); @@ -521,29 +527,20 @@ class Monitor extends BeanModel { bean.msg = `${res.status} - ${res.statusText}`; bean.ping = dayjs().valueOf() - startTime; - // Check certificate if https is used - let certInfoStartTime = dayjs().valueOf(); + // Store certificate and check for expiry if https is used if (this.getUrl()?.protocol === "https:") { - log.debug("monitor", `[${this.name}] Check cert`); - try { - let tlsInfoObject = checkCertificate(res); - tlsInfo = await this.updateTlsInfo(tlsInfoObject); - - if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) { - log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`); - await this.checkCertExpiryNotifications(tlsInfoObject); - } + // No way to listen for the `secureConnection` event, so we do it here + const tlssocket = res.request.res.socket; - } catch (e) { - if (e.message !== "No TLS certificate in response") { - log.error("monitor", "Caught error"); - log.error("monitor", e.message); - } + if (tlssocket) { + tlsInfo.valid = tlssocket.authorized || false; } - } - if (process.env.TIMELOGGER === "1") { - log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms"); + await this.updateTlsInfo(tlsInfo); + if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) { + log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`); + await this.checkCertExpiryNotifications(tlsInfo); + } } if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === this.id) { diff --git a/server/util-server.js b/server/util-server.js index 70588880..37fcc7be 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -716,20 +716,27 @@ const parseCertificateInfo = function (info) { /** * Check if certificate is valid - * @param {Object} res Response object from axios + * @param {tls.TLSSocket} socket TLSSocket, which may or may not be connected * @returns {Object} Object containing certificate information */ -exports.checkCertificate = function (res) { - if (!res.request.res.socket) { - throw new Error("No socket found"); +exports.checkCertificate = function (socket) { + let certInfoStartTime = dayjs().valueOf(); + + // Return null if there is no socket + if (socket === undefined || socket == null) { + return null; } - const info = res.request.res.socket.getPeerCertificate(true); - const valid = res.request.res.socket.authorized || false; + const info = socket.getPeerCertificate(true); + const valid = socket.authorized || false; log.debug("cert", "Parsing Certificate Info"); const parsedInfo = parseCertificateInfo(info); + if (process.env.TIMELOGGER === "1") { + log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms"); + } + return { valid: valid, certInfo: parsedInfo