diff --git a/server/model/monitor.js b/server/model/monitor.js index 75af6b813..97926f4c7 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -8,7 +8,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MI PING_COUNT_MIN, PING_COUNT_MAX, PING_COUNT_DEFAULT, PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT } = require("../../src/util"); -const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery, +const { ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal } = require("../util-server"); const { R } = require("redbean-node"); @@ -621,11 +621,6 @@ class Monitor extends BeanModel { } - } else if (this.type === "port") { - bean.ping = await tcping(this.hostname, this.port); - bean.msg = ""; - bean.status = UP; - } else if (this.type === "ping") { bean.ping = await ping(this.hostname, this.ping_count, "", this.ping_numeric, this.packetSize, this.timeout, this.ping_per_request_timeout); bean.msg = ""; diff --git a/server/monitor-types/tcp.js b/server/monitor-types/tcp.js new file mode 100644 index 000000000..f0bc12a01 --- /dev/null +++ b/server/monitor-types/tcp.js @@ -0,0 +1,158 @@ +const { MonitorType } = require("./monitor-type"); +const { UP, DOWN, PING_GLOBAL_TIMEOUT_DEFAULT: TIMEOUT, log } = require("../../src/util"); +const { checkCertificate } = require("../util-server"); +const tls = require("tls"); +const net = require("net"); +const tcpp = require("tcp-ping"); + +/** + * Send TCP request to specified hostname and port + * @param {string} hostname Hostname / address of machine + * @param {number} port TCP port to test + * @returns {Promise} Maximum time in ms rounded to nearest integer + */ +const tcping = (hostname, port) => { + return new Promise((resolve, reject) => { + tcpp.ping( + { + address: hostname, + port: port, + attempts: 1, + }, + (err, data) => { + if (err) { + reject(err); + } + + if (data.results.length >= 1 && data.results[0].err) { + reject(data.results[0].err); + } + + resolve(Math.round(data.max)); + } + ); + }); +}; + +class TCPMonitorType extends MonitorType { + name = "port"; + + /** + * @inheritdoc + */ + async check(monitor, heartbeat, _server) { + try { + const resp = await tcping(monitor.hostname, monitor.port); + heartbeat.ping = resp; + heartbeat.msg = `${resp} ms`; + heartbeat.status = UP; + } catch { + heartbeat.status = DOWN; + heartbeat.msg = "Connection failed"; + return; + } + + let socket_; + + const preTLS = () => + new Promise((resolve, reject) => { + let timeout; + socket_ = net.connect(monitor.port, monitor.hostname); + + const onTimeout = () => { + log.debug(this.name, `[${monitor.name}] Pre-TLS connection timed out`); + reject("Connection timed out"); + }; + + 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}`); + switch (true) { + case response_.includes("start tls") || response_.includes("begin tls"): + timeout && clearTimeout(timeout); + resolve({ socket: socket_ }); + 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; + default: + reject(`Unexpected response: ${response}`); + } + }); + socket_.on("error", error => { + log.debug(this.name, `[${monitor.name}] ${error.toString()}`); + reject(error); + }); + socket_.setTimeout(1000 * TIMEOUT, onTimeout); + timeout = setTimeout(onTimeout, 1000 * TIMEOUT); + }); + + const reuseSocket = monitor.smtpSecurity === "starttls" ? await preTLS() : {}; + + 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) { + heartbeat.status = DOWN; + heartbeat.msg = "Certificate is invalid"; + } + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + heartbeat.status = DOWN; + heartbeat.msg = `TLS Connection failed: ${message}`; + } finally { + if (socket && !socket.destroyed) { + socket.end(); + } + } + } + + if (socket_ && !socket_.destroyed) { + socket_.end(); + } + } +} + +module.exports = { + TCPMonitorType, +}; diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 3c84bf646..b95477664 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -118,6 +118,7 @@ class UptimeKumaServer { UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType(); UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType(); UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType(); + UptimeKumaServer.monitorTypeList["port"] = new TCPMonitorType(); UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType(); UptimeKumaServer.monitorTypeList["redis"] = new RedisMonitorType(); @@ -560,6 +561,7 @@ const { GroupMonitorType } = require("./monitor-types/group"); const { SNMPMonitorType } = require("./monitor-types/snmp"); const { MongodbMonitorType } = require("./monitor-types/mongodb"); const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq"); +const { TCPMonitorType } = require("./monitor-types/tcp.js"); const { ManualMonitorType } = require("./monitor-types/manual"); const { RedisMonitorType } = require("./monitor-types/redis"); const Monitor = require("./model/monitor"); diff --git a/server/util-server.js b/server/util-server.js index 3de6e777f..6365b623c 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -1,4 +1,3 @@ -const tcpp = require("tcp-ping"); const ping = require("@louislam/ping"); const { R } = require("redbean-node"); const { @@ -98,33 +97,6 @@ exports.getOidcTokenClientCredentials = async (tokenEndpoint, clientId, clientSe return await client.grant(grantParams); }; -/** - * Send TCP request to specified hostname and port - * @param {string} hostname Hostname / address of machine - * @param {number} port TCP port to test - * @returns {Promise} Maximum time in ms rounded to nearest integer - */ -exports.tcping = function (hostname, port) { - return new Promise((resolve, reject) => { - tcpp.ping({ - address: hostname, - port: port, - attempts: 1, - }, function (err, data) { - - if (err) { - reject(err); - } - - if (data.results.length >= 1 && data.results[0].err) { - reject(data.results[0].err); - } - - resolve(Math.round(data.max)); - }); - }); -}; - /** * Ping the specified machine * @param {string} destAddr Hostname / IP address of machine to ping diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index b5213ef5e..798c89f17 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -366,6 +366,15 @@ +
+ + +
+
@@ -671,7 +680,7 @@

{{ $t("Advanced") }}

-
+