From 054d2f7aab566fd56ccfc6df9164b6834bf59a47 Mon Sep 17 00:00:00 2001 From: Jacques ROUSSEL <jacques.roussel@rouaje.com> Date: Tue, 25 Mar 2025 15:41:20 +0100 Subject: [PATCH] feat: Refacto `TCP tls` monitor --- server/model/monitor.js | 44 ------------------ server/monitor-types/tcp.js | 73 +++++++++++++++++++++++++++++ server/uptime-kuma-server.js | 2 + test/backend-test/test-tcp.js | 87 +++++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 44 deletions(-) create mode 100644 server/monitor-types/tcp.js create mode 100644 test/backend-test/test-tcp.js diff --git a/server/model/monitor.js b/server/model/monitor.js index 08f08d868..5377f75f9 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -612,50 +612,6 @@ class Monitor extends BeanModel { } - } else if (this.type === "port") { - bean.ping = await tcping(this.hostname, this.port); - if (this.isEnabledExpiryNotification()) { - const host = this.hostname; - const port = this.port || 443; - try { - const options = { - host, - port, - servername: host, - }; - - // Convert TLS connect to a Promise and await it - const tlsInfoObject = await new Promise((resolve, reject) => { - const socket = tls.connect(options); - - socket.on("secureConnect", () => { - try { - const info = checkCertificate(socket); - socket.end(); - resolve(info); - } catch (error) { - socket.end(); - reject(error); - } - }); - - socket.on("error", (error) => { - reject(error); - }); - - socket.setTimeout(10000, () => { - socket.end(); - reject(new Error("Connection timed out")); - }); - }); - await this.handleTlsInfo(tlsInfoObject); - } catch (error) { - console.log("Retrieve certificate failed"); - } - } - bean.msg = ""; - bean.status = UP; - } else if (this.type === "ping") { bean.ping = await ping(this.hostname, this.packetSize); bean.msg = ""; diff --git a/server/monitor-types/tcp.js b/server/monitor-types/tcp.js new file mode 100644 index 000000000..815be4db2 --- /dev/null +++ b/server/monitor-types/tcp.js @@ -0,0 +1,73 @@ +const { MonitorType } = require("./monitor-type"); +const { UP, DOWN, log, evaluateJsonQuery } = require("../../src/util"); +const { tcping, checkCertificate } = require("../util-server"); +const tls = require("tls"); + +class TCPMonitorType extends MonitorType { + name = "port"; + + /** + * @inheritdoc + */ + async check(monitor, heartbeat, _server) { + try { + heartbeat.ping = await tcping(monitor.hostname, monitor.port); + heartbeat.msg = ""; + heartbeat.status = UP; + } catch (error) { + heartbeat.status = DOWN; + heartbeat.msg = "Connection failed"; + return; + } + + if (monitor.isEnabledExpiryNotification()) { + let socket = null; + try { + const options = { + host: monitor.hostname, + port: monitor.port, + servername: monitor.hostname, + }; + + 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(10000, () => { + reject(new Error("Connection timed out")); + }); + }); + + await monitor.handleTlsInfo(tlsInfoObject); + if (!tlsInfoObject.valid) { + heartbeat.status = DOWN; + heartbeat.msg = "Certificate is invalid"; + } + } catch (error) { + heartbeat.status = DOWN; + heartbeat.msg = "Connection failed"; + } finally { + if (socket && !socket.destroyed) { + socket.end(); + } + } + } + } +} + +module.exports = { + TCPMonitorType, +}; + diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 062f098d7..b0fe4268e 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -116,6 +116,7 @@ class UptimeKumaServer { UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType(); UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType(); UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType(); + UptimeKumaServer.monitorTypeList["port"] = new TCPMonitorType(); // Allow all CORS origins (polling) in development let cors = undefined; @@ -554,4 +555,5 @@ const { MqttMonitorType } = require("./monitor-types/mqtt"); 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 Monitor = require("./model/monitor"); diff --git a/test/backend-test/test-tcp.js b/test/backend-test/test-tcp.js new file mode 100644 index 000000000..fd5d7dae9 --- /dev/null +++ b/test/backend-test/test-tcp.js @@ -0,0 +1,87 @@ +const { describe, test } = require("node:test"); +const assert = require("node:assert"); +const { TCPMonitorType } = require("../../server/monitor-types/tcp"); +const { UP, DOWN, PENDING } = require("../../src/util"); +const net = require("net"); + +describe("TCP Monitor", () => { + async function createTCPServer(port) { + return new Promise((resolve, reject) => { + const server = net.createServer(); + + server.listen(port, () => { + resolve(server); + }); + + server.on("error", (err) => { + reject(err); + }); + }); + } + + test("TCP server is running", async () => { + const port = 12345; + const server = await createTCPServer(port); + + try { + const tcpMonitor = new TCPMonitorType(); + const monitor = { + hostname: "localhost", + port: port, + isEnabledExpiryNotification: () => false + }; + + const heartbeat = { + msg: "", + status: PENDING, + }; + + await tcpMonitor.check(monitor, heartbeat, {}); + + assert.strictEqual(heartbeat.status, UP); + assert.strictEqual(heartbeat.msg, ""); + } finally { + server.close(); + } + }); + + test("TCP server is not running", async () => { + const tcpMonitor = new TCPMonitorType(); + const monitor = { + hostname: "localhost", + port: 54321, + isEnabledExpiryNotification: () => false + }; + + const heartbeat = { + msg: "", + status: PENDING, + }; + + await tcpMonitor.check(monitor, heartbeat, {}); + + assert.strictEqual(heartbeat.status, DOWN); + }); + + test("TCP server with expired or invalid TLS certificate", async (t) => { + const tcpMonitor = new TCPMonitorType(); + const monitor = { + hostname: "expired.badssl.com", + port: 443, + isEnabledExpiryNotification: () => true, + handleTlsInfo: async (tlsInfo) => { + return tlsInfo; + } + }; + + const heartbeat = { + msg: "", + status: PENDING, + }; + + await tcpMonitor.check(monitor, heartbeat, {}); + + assert.strictEqual(heartbeat.status, DOWN); + assert(["Certificate is invalid", "Connection failed"].includes(heartbeat.msg)); + }); +});