diff --git a/server/model/monitor.js b/server/model/monitor.js index 2ea2f958c..b2ea982c9 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -9,7 +9,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MI PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT } = require("../../src/util"); const { ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, - kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal + kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal, checkCertificateHostname } = require("../util-server"); const { R } = require("redbean-node"); const { BeanModel } = require("redbean-node/dist/bean-model"); @@ -565,6 +565,7 @@ class Monitor extends BeanModel { tlsSocket.once("secureConnect", async () => { tlsInfo = checkCertificate(tlsSocket); tlsInfo.valid = tlsSocket.authorized || false; + tlsInfo.hostnameMatchMonitorUrl = checkCertificateHostname(tlsInfo.certInfo.raw, this.getUrl()?.hostname); await this.handleTlsInfo(tlsInfo); }); @@ -587,6 +588,7 @@ class Monitor extends BeanModel { if (tlsSocket) { tlsInfo = checkCertificate(tlsSocket); tlsInfo.valid = tlsSocket.authorized || false; + tlsInfo.hostnameMatchMonitorUrl = checkCertificateHostname(tlsInfo.certInfo.raw, this.getUrl()?.hostname); await this.handleTlsInfo(tlsInfo); } diff --git a/server/util-server.js b/server/util-server.js index 250f897ba..80110399c 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -636,6 +636,30 @@ exports.checkCertificate = function (socket) { }; }; +/** + * Checks if the certificate is valid for the provided hostname. + * Defaults to true if feature `X509Certificate` is not available, or input is not valid. + * @param {Buffer} certBuffer - The certificate buffer. + * @param {string} hostname - The hostname to compare against. + * @returns {boolean} True if the certificate is valid for the provided hostname, false otherwise. + */ +exports.checkCertificateHostname = function (certBuffer, hostname) { + let X509Certificate; + try { + X509Certificate = require("node:crypto").X509Certificate; + } catch (_) { + // X509Certificate is not available in this version of Node.js + return true; + } + + if (!X509Certificate || !certBuffer || !hostname) { + return true; + } + + let certObject = new X509Certificate(certBuffer); + return certObject.checkHost(hostname) !== undefined; +}; + /** * Check if the provided status code is within the accepted ranges * @param {number} status The status code to check diff --git a/src/icon.js b/src/icon.js index fc50c8a1e..882c0b56c 100644 --- a/src/icon.js +++ b/src/icon.js @@ -12,6 +12,7 @@ import { faArrowUp, faCog, faEdit, + faExclamationTriangle, faEye, faEyeSlash, faList, @@ -60,6 +61,7 @@ library.add( faArrowUp, faCog, faEdit, + faExclamationTriangle, faEye, faEyeSlash, faList, diff --git a/src/lang/en.json b/src/lang/en.json index 968b44336..0d076db00 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -445,6 +445,7 @@ "Query": "Query", "settingsCertificateExpiry": "TLS Certificate Expiry", "certificationExpiryDescription": "HTTPS Monitors trigger notification when TLS certificate expires in:", + "certHostnameMismatch": "Certificate hostname does not match the monitor URL.", "Setup Docker Host": "Set Up Docker Host", "Connection Type": "Connection Type", "Docker Daemon": "Docker Daemon", diff --git a/src/pages/Details.vue b/src/pages/Details.vue index db0a890d0..ca46c19fa 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -294,15 +294,8 @@ />)
- {{ tlsInfo.certInfo.daysRemaining }} - {{ - $tc("day", tlsInfo.certInfo.daysRemaining) - }} + {{ tlsInfo.certInfo.daysRemaining }} {{ $tc("day", tlsInfo.certInfo.daysRemaining) }} +