diff --git a/server/monitor-types/tcp.js b/server/monitor-types/tcp.js index 00ded0217..ca124b400 100644 --- a/server/monitor-types/tcp.js +++ b/server/monitor-types/tcp.js @@ -113,6 +113,16 @@ class TCPMonitorType extends MonitorType { } // Standard TCP check + await this.checkTcp(monitor, heartbeat); + } + + /** + * Standard TCP connectivity check + * @param {object} monitor Monitor object + * @param {object} heartbeat Heartbeat object + * @returns {Promise} + */ + async checkTcp(monitor, heartbeat) { try { const resp = await tcping(monitor.hostname, monitor.port); heartbeat.ping = resp; @@ -124,124 +134,11 @@ class TCPMonitorType extends MonitorType { let socket_; - const preTLS = () => - new Promise((resolve, reject) => { - let dialogTimeout; - let bannerTimeout; - socket_ = net.connect(monitor.port, monitor.hostname); - - const onTimeout = () => { - log.debug(this.name, `[${monitor.name}] Pre-TLS connection timed out`); - doReject("Connection timed out"); - }; - - const onBannerTimeout = () => { - log.debug(this.name, `[${monitor.name}] Pre-TLS timed out waiting for banner`); - // No banner. Could be a XMPP server? - socket_.write(``); - }; - - const doResolve = () => { - dialogTimeout && clearTimeout(dialogTimeout); - bannerTimeout && clearTimeout(bannerTimeout); - resolve({ socket: socket_ }); - }; - - const doReject = (error) => { - dialogTimeout && clearTimeout(dialogTimeout); - bannerTimeout && clearTimeout(bannerTimeout); - socket_.end(); - reject(error); - }; - - socket_.on("connect", () => { - log.debug(this.name, `[${monitor.name}] Pre-TLS connection: ${JSON.stringify(socket_)}`); - }); - - socket_.on("data", data => { - const response = data.toString(); - const response_ = response.toLowerCase(); - log.debug(this.name, `[${monitor.name}] Pre-TLS response: ${response}`); - clearTimeout(bannerTimeout); - switch (true) { - case response_.includes("start tls") || response_.includes("begin tls"): - doResolve(); - break; - case response.startsWith("* OK") || response.match(/CAPABILITY.+STARTTLS/): - socket_.write("a001 STARTTLS\r\n"); - break; - case response.startsWith("220") || response.includes("ESMTP"): - socket_.write(`EHLO ${monitor.hostname}\r\n`); - break; - case response.includes("250-STARTTLS"): - socket_.write("STARTTLS\r\n"); - break; - case response_.includes(""); - break; - case response_.includes(""): - break; - default: - doReject(`Unexpected response: ${response}`); - } - }); - socket_.on("error", error => { - log.debug(this.name, `[${monitor.name}] ${error.toString()}`); - reject(error); - }); - socket_.setTimeout(1000 * TIMEOUT, onTimeout); - dialogTimeout = setTimeout(onTimeout, 1000 * TIMEOUT); - bannerTimeout = setTimeout(onBannerTimeout, 1000 * 1.5); - }); - - const reuseSocket = monitor.smtpSecurity === "starttls" ? await preTLS() : {}; - + // Handle TLS certificate checking for secure/starttls connections if ([ "secure", "starttls" ].includes(monitor.smtpSecurity) && monitor.isEnabledExpiryNotification()) { - let socket = null; - try { - const options = { - host: monitor.hostname, - port: monitor.port, - servername: monitor.hostname, - ...reuseSocket, - }; - - const tlsInfoObject = await new Promise((resolve, reject) => { - socket = tls.connect(options); - - socket.on("secureConnect", () => { - try { - const info = checkCertificate(socket); - resolve(info); - } catch (error) { - reject(error); - } - }); - - socket.on("error", error => { - reject(error); - }); - - socket.setTimeout(1000 * TIMEOUT, () => { - reject(new Error("Connection timed out")); - }); - }); - - await monitor.handleTlsInfo(tlsInfoObject); - if (!tlsInfoObject.valid) { - throw new Error("Certificate is invalid"); - } - } catch (error) { - const message = error instanceof Error ? error.message : "Unknown error"; - throw new Error(`TLS Connection failed: ${message}`); - } finally { - if (socket && !socket.destroyed) { - socket.end(); - } - } + const reuseSocket = monitor.smtpSecurity === "starttls" ? await this.performStartTls(monitor) : {}; + socket_ = reuseSocket.socket; + await this.checkTlsCertificate(monitor, reuseSocket); } if (socket_ && !socket_.destroyed) { @@ -249,6 +146,136 @@ class TCPMonitorType extends MonitorType { } } + /** + * Perform STARTTLS handshake for various protocols (SMTP, IMAP, XMPP) + * @param {object} monitor Monitor object + * @returns {Promise<{socket: net.Socket}>} Object containing the socket + */ + performStartTls(monitor) { + return new Promise((resolve, reject) => { + let dialogTimeout; + let bannerTimeout; + const socket_ = net.connect(monitor.port, monitor.hostname); + + const onTimeout = () => { + log.debug(this.name, `[${monitor.name}] Pre-TLS connection timed out`); + doReject("Connection timed out"); + }; + + const onBannerTimeout = () => { + log.debug(this.name, `[${monitor.name}] Pre-TLS timed out waiting for banner`); + // No banner. Could be a XMPP server? + socket_.write(``); + }; + + const doResolve = () => { + dialogTimeout && clearTimeout(dialogTimeout); + bannerTimeout && clearTimeout(bannerTimeout); + resolve({ socket: socket_ }); + }; + + const doReject = (error) => { + dialogTimeout && clearTimeout(dialogTimeout); + bannerTimeout && clearTimeout(bannerTimeout); + socket_.end(); + reject(error); + }; + + socket_.on("connect", () => { + log.debug(this.name, `[${monitor.name}] Pre-TLS connection: ${JSON.stringify(socket_)}`); + }); + + socket_.on("data", data => { + const response = data.toString(); + const response_ = response.toLowerCase(); + log.debug(this.name, `[${monitor.name}] Pre-TLS response: ${response}`); + clearTimeout(bannerTimeout); + switch (true) { + case response_.includes("start tls") || response_.includes("begin tls"): + doResolve(); + break; + case response.startsWith("* OK") || response.match(/CAPABILITY.+STARTTLS/): + socket_.write("a001 STARTTLS\r\n"); + break; + case response.startsWith("220") || response.includes("ESMTP"): + socket_.write(`EHLO ${monitor.hostname}\r\n`); + break; + case response.includes("250-STARTTLS"): + socket_.write("STARTTLS\r\n"); + break; + case response_.includes(""); + break; + case response_.includes(""): + break; + default: + doReject(`Unexpected response: ${response}`); + } + }); + socket_.on("error", error => { + log.debug(this.name, `[${monitor.name}] ${error.toString()}`); + reject(error); + }); + socket_.setTimeout(1000 * TIMEOUT, onTimeout); + dialogTimeout = setTimeout(onTimeout, 1000 * TIMEOUT); + bannerTimeout = setTimeout(onBannerTimeout, 1000 * 1.5); + }); + } + + /** + * Check TLS certificate validity + * @param {object} monitor Monitor object + * @param {object} reuseSocket Socket to reuse for STARTTLS + * @returns {Promise} + */ + async checkTlsCertificate(monitor, reuseSocket) { + let socket = null; + try { + const options = { + host: monitor.hostname, + port: monitor.port, + servername: monitor.hostname, + ...reuseSocket, + }; + + const tlsInfoObject = await new Promise((resolve, reject) => { + socket = tls.connect(options); + + socket.on("secureConnect", () => { + try { + const info = checkCertificate(socket); + resolve(info); + } catch (error) { + reject(error); + } + }); + + socket.on("error", error => { + reject(error); + }); + + socket.setTimeout(1000 * TIMEOUT, () => { + reject(new Error("Connection timed out")); + }); + }); + + await monitor.handleTlsInfo(tlsInfoObject); + if (!tlsInfoObject.valid) { + throw new Error("Certificate is invalid"); + } + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + throw new Error(`TLS Connection failed: ${message}`); + } finally { + if (socket && !socket.destroyed) { + socket.end(); + } + } + } + /** * Check for expected TLS alert (for mTLS verification) * @param {object} monitor Monitor object @@ -277,12 +304,43 @@ class TCPMonitorType extends MonitorType { } } - const result = await new Promise((resolve, reject) => { + const result = await this.attemptTlsConnection(monitor, options, startTime, timeout); + + heartbeat.ping = result.responseTime; + + // Handle TLS info for certificate expiry monitoring + if (result.tlsInfo && monitor.isEnabledExpiryNotification()) { + await monitor.handleTlsInfo(result.tlsInfo); + } + + // Check if we got the expected alert + if (result.alertName === expectedTlsAlert) { + heartbeat.status = UP; + heartbeat.msg = `TLS alert received as expected: ${result.alertName} (${result.alertNumber})`; + } else if (result.success) { + throw new Error(`Expected TLS alert '${expectedTlsAlert}' but connection succeeded. The server accepted the connection without requiring a client certificate.`); + } else if (result.alertNumber !== null) { + throw new Error(`Expected TLS alert '${expectedTlsAlert}' but received '${result.alertName}' (${result.alertNumber})`); + } else { + throw new Error(`Expected TLS alert '${expectedTlsAlert}' but got unexpected error: ${result.errorMessage}`); + } + } + + /** + * Attempt TLS connection and capture result/alert + * @param {object} monitor Monitor object + * @param {object} options TLS connection options + * @param {number} startTime Connection start timestamp + * @param {number} timeout Connection timeout in ms + * @returns {Promise} Connection result with success, responseTime, tlsInfo, alertNumber, alertName, errorMessage + */ + attemptTlsConnection(monitor, options, startTime, timeout) { + return new Promise((resolve, reject) => { const socket = tls.connect(options); const timeoutId = setTimeout(() => { socket.destroy(); - reject(new Error("Connection timed out")); + reject(new Error("TLS connection timed out")); }, timeout); socket.on("secureConnect", () => { @@ -331,28 +389,9 @@ class TCPMonitorType extends MonitorType { socket.on("timeout", () => { clearTimeout(timeoutId); socket.destroy(); - reject(new Error("Connection timed out")); + reject(new Error("TLS connection timed out")); }); }); - - heartbeat.ping = result.responseTime; - - // Handle TLS info for certificate expiry monitoring - if (result.tlsInfo && monitor.isEnabledExpiryNotification()) { - await monitor.handleTlsInfo(result.tlsInfo); - } - - // Check if we got the expected alert - if (result.alertName === expectedTlsAlert) { - heartbeat.status = UP; - heartbeat.msg = `TLS alert received as expected: ${result.alertName} (${result.alertNumber})`; - } else if (result.success) { - throw new Error(`Expected TLS alert '${expectedTlsAlert}' but connection succeeded`); - } else if (result.alertNumber !== null) { - throw new Error(`Expected TLS alert '${expectedTlsAlert}' but got '${result.alertName}' (${result.alertNumber})`); - } else { - throw new Error(`Expected TLS alert '${expectedTlsAlert}' but got error: ${result.errorMessage}`); - } } } diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 1a22830b0..d60cf4966 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -406,6 +406,7 @@ -
- {{ $t("expectedTlsAlertDescription") }} -
+ + + +