diff --git a/server/model/domain_expiry.js b/server/model/domain_expiry.js index 836c409c4..8df65a63a 100644 --- a/server/model/domain_expiry.js +++ b/server/model/domain_expiry.js @@ -86,19 +86,18 @@ async function getRdapDomainExpiryDate(domain) { */ async function sendDomainNotificationByTargetDays(domain, daysRemaining, targetDays, notificationList) { let sent = false; - log.debug("domain", `Send domain expiry notification for ${targetDays} deadline.`); + log.debug("domain_expiry", `Send domain expiry notification for ${targetDays} deadline.`); for (let notification of notificationList) { try { - log.debug("domain", `Sending to ${notification.name}`); + log.debug("domain_expiry", `Sending to ${notification.name}`); await Notification.send( JSON.parse(notification.config), `Domain name ${domain} will expire in ${daysRemaining} days` ); sent = true; } catch (e) { - log.error("domain", `Cannot send domain notification to ${notification.name}`); - log.error("domain", e); + log.error("domain_expiry", `Cannot send domain notification to ${notification.name}:`, e); } } @@ -152,7 +151,7 @@ class DomainExpiry extends BeanModel { const tld = parseTld(urlTypes.includes(m.type) ? m.url : m.type === "grpc-keyword" ? m.grpcUrl : m.hostname); const rdap = await getRdapServer(tld.publicSuffix); if (!rdap) { - log.warn("domain", `Domain expiry unsupported for '.${tld.publicSuffix}' because its RDAP endpoint is not listed in the IANA database.`); + log.warn("domain_expiry", `Domain expiry unsupported for '.${tld.publicSuffix}' because its RDAP endpoint is not listed in the IANA database.`); return false; } const existing = await DomainExpiry.findByName(tld.domain); @@ -188,7 +187,7 @@ class DomainExpiry extends BeanModel { let expiryDate; if (bean?.lastCheck && getDaysBetween(new Date(bean.lastCheck), new Date()) < 1) { - log.debug("domain", `Domain expiry already checked recently for ${bean.domain}, won't re-check.`); + log.debug("domain_expiry", `Domain expiry already checked recently for ${bean.domain}, won't re-check.`); return bean.expiry; } else if (bean) { expiryDate = await bean.getExpiryDate(); @@ -220,13 +219,13 @@ class DomainExpiry extends BeanModel { if (!notificationList.length > 0) { // fail fast. If no notification is set, all the following checks can be skipped. - log.debug("domain", "No notification, no need to send domain notification"); + log.debug("domain_expiry", "No notification, no need to send domain notification"); return; } const daysRemaining = getDaysRemaining(new Date(), domain.expiry); const lastSent = domain.lastExpiryNotificationSent; - log.debug("domain", `${name} expires in ${daysRemaining} days`); + log.debug("domain_expiry", `${name} expires in ${daysRemaining} days`); let notifyDays = await setting("domainExpiryNotifyDays"); if (notifyDays == null || !Array.isArray(notifyDays)) { diff --git a/server/monitor-types/websocket-upgrade.js b/server/monitor-types/websocket-upgrade.js index 32c7c15da..1ea06928f 100644 --- a/server/monitor-types/websocket-upgrade.js +++ b/server/monitor-types/websocket-upgrade.js @@ -53,7 +53,7 @@ class WebSocketMonitorType extends MonitorType { /** * Uses the ws Node.js library to establish a connection to target server * @param {object} monitor The monitor object for input parameters. - * @returns {[ string, int ]} Array containing a status message and response code + * @returns {Promise<[ string, int ]>} Array containing a status message and response code */ async attemptUpgrade(monitor) { return new Promise((resolve) => { diff --git a/test/backend-test/monitors/test-websocket.js b/test/backend-test/monitors/test-websocket.js index d2923f1e7..1829ce973 100644 --- a/test/backend-test/monitors/test-websocket.js +++ b/test/backend-test/monitors/test-websocket.js @@ -4,13 +4,13 @@ const assert = require("node:assert"); const { WebSocketMonitorType } = require("../../../server/monitor-types/websocket-upgrade"); const { UP, PENDING } = require("../../../src/util"); const net = require("node:net"); +const http = require("node:http"); /** * Simulates non compliant WS Server, doesnt send Sec-WebSocket-Accept header - * @param {number} port Port the server listens on. Defaults to 8080 - * @returns {Promise} Promise that resolves to the created server once listening + * @returns {Promise<{server: net.Server, port: number}>} Promise that resolves to the created server and its port */ -function nonCompliantWS(port = 8080) { +function nonCompliantWS() { const srv = net.createServer((socket) => { socket.once("data", (buf) => { socket.write("HTTP/1.1 101 Switching Protocols\r\n" + @@ -19,16 +19,56 @@ function nonCompliantWS(port = 8080) { socket.destroy(); }); }); - return new Promise((resolve) => srv.listen(port, () => resolve(srv))); + return new Promise((resolve) => { + srv.listen(0, () => { + resolve({ server: srv, + port: srv.address().port }); + }); + }); +} + +/** + * Creates a regular HTTP server (non-WebSocket) for testing WebSocket rejection + * @returns {Promise<{server: http.Server, port: number}>} Promise that resolves to the created server and its port + */ +function httpServer() { + const srv = http.createServer((req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("This is not a WebSocket server"); + }); + return new Promise((resolve) => { + srv.listen(0, () => { + resolve({ server: srv, + port: srv.address().port }); + }); + }); +} + +/** + * Creates a WebSocket server for testing + * @param {object} options Options to pass to WebSocketServer + * @returns {Promise<{server: WebSocketServer, port: number}>} Promise that resolves to the created server and its port + */ +function createWebSocketServer(options = {}) { + return new Promise((resolve) => { + const wss = new WebSocketServer({ port: 0, + ...options }); + wss.on("listening", () => { + resolve({ server: wss, + port: wss.address().port }); + }); + }); } describe("WebSocket Monitor", { }, () => { - test("check() rejects with unexpected server response when connecting to non-WebSocket server", {}, async () => { + test("check() rejects with unexpected server response when connecting to non-WebSocket server", {}, async (t) => { const websocketMonitor = new WebSocketMonitorType(); + const { server: srv, port } = await httpServer(); + t.after(() => srv.close()); const monitor = { - url: "wss://example.org", + url: `ws://localhost:${port}`, wsIgnoreSecWebsocketAcceptHeader: false, timeout: 30, }; @@ -44,11 +84,13 @@ describe("WebSocket Monitor", { ); }); - test("check() sets status to UP when connecting to secure WebSocket server", async () => { + test("check() sets status to UP when connecting to WebSocket server", async (t) => { const websocketMonitor = new WebSocketMonitorType(); + const { server: wss, port } = await createWebSocketServer(); + t.after(() => wss.close()); const monitor = { - url: "wss://echo.websocket.org", + url: `ws://localhost:${port}`, wsIgnoreSecWebsocketAcceptHeader: false, accepted_statuscodes_json: JSON.stringify([ "1000" ]), timeout: 30, @@ -69,12 +111,12 @@ describe("WebSocket Monitor", { }); test("check() sets status to UP when connecting to insecure WebSocket server", async (t) => { - t.after(() => wss.close()); const websocketMonitor = new WebSocketMonitorType(); - const wss = new WebSocketServer({ port: 8080 }); + const { server: wss, port } = await createWebSocketServer(); + t.after(() => wss.close()); const monitor = { - url: "ws://localhost:8080", + url: `ws://localhost:${port}`, wsIgnoreSecWebsocketAcceptHeader: false, accepted_statuscodes_json: JSON.stringify([ "1000" ]), timeout: 30, @@ -94,11 +136,13 @@ describe("WebSocket Monitor", { assert.deepStrictEqual(heartbeat, expected); }); - test("check() rejects when status code does not match expected value", async () => { + test("check() rejects when status code does not match expected value", async (t) => { const websocketMonitor = new WebSocketMonitorType(); + const { server: wss, port } = await createWebSocketServer(); + t.after(() => wss.close()); const monitor = { - url: "wss://echo.websocket.org", + url: `ws://localhost:${port}`, wsIgnoreSecWebsocketAcceptHeader: false, accepted_statuscodes_json: JSON.stringify([ "1001" ]), timeout: 30, @@ -115,11 +159,13 @@ describe("WebSocket Monitor", { ); }); - test("check() rejects when expected status code is empty", async () => { + test("check() rejects when expected status code is empty", async (t) => { const websocketMonitor = new WebSocketMonitorType(); + const { server: wss, port } = await createWebSocketServer(); + t.after(() => wss.close()); const monitor = { - url: "wss://echo.websocket.org", + url: `ws://localhost:${port}`, wsIgnoreSecWebsocketAcceptHeader: false, accepted_statuscodes_json: JSON.stringify([ "" ]), timeout: 30, @@ -137,12 +183,12 @@ describe("WebSocket Monitor", { }); test("check() rejects when Sec-WebSocket-Accept header is invalid", async (t) => { - t.after(() => wss.close()); const websocketMonitor = new WebSocketMonitorType(); - const wss = await nonCompliantWS(); + const { server: wss, port } = await nonCompliantWS(); + t.after(() => wss.close()); const monitor = { - url: "ws://localhost:8080", + url: `ws://localhost:${port}`, wsIgnoreSecWebsocketAcceptHeader: false, accepted_statuscodes_json: JSON.stringify([ "1000" ]), timeout: 30, @@ -160,12 +206,12 @@ describe("WebSocket Monitor", { }); test("check() sets status to UP when ignoring invalid Sec-WebSocket-Accept header", async (t) => { + const websocketMonitor = new WebSocketMonitorType(); + const { server: wss, port } = await nonCompliantWS(); t.after(() => wss.close()); - const websocketMonitor = new WebSocketMonitorType(); - const wss = await nonCompliantWS(); const monitor = { - url: "ws://localhost:8080", + url: `ws://localhost:${port}`, wsIgnoreSecWebsocketAcceptHeader: true, accepted_statuscodes_json: JSON.stringify([ "1000" ]), timeout: 30, @@ -185,11 +231,13 @@ describe("WebSocket Monitor", { assert.deepStrictEqual(heartbeat, expected); }); - test("check() sets status to UP for compliant WebSocket server when ignoring Sec-WebSocket-Accept", async () => { + test("check() sets status to UP for compliant WebSocket server when ignoring Sec-WebSocket-Accept", async (t) => { const websocketMonitor = new WebSocketMonitorType(); + const { server: wss, port } = await createWebSocketServer(); + t.after(() => wss.close()); const monitor = { - url: "wss://echo.websocket.org", + url: `ws://localhost:${port}`, wsIgnoreSecWebsocketAcceptHeader: true, accepted_statuscodes_json: JSON.stringify([ "1000" ]), timeout: 30, @@ -209,11 +257,13 @@ describe("WebSocket Monitor", { assert.deepStrictEqual(heartbeat, expected); }); - test("check() rejects non-WebSocket server even when ignoring Sec-WebSocket-Accept", async () => { + test("check() rejects non-WebSocket server even when ignoring Sec-WebSocket-Accept", async (t) => { const websocketMonitor = new WebSocketMonitorType(); + const { server: srv, port } = await httpServer(); + t.after(() => srv.close()); const monitor = { - url: "wss://example.org", + url: `ws://localhost:${port}`, wsIgnoreSecWebsocketAcceptHeader: true, accepted_statuscodes_json: JSON.stringify([ "1000" ]), timeout: 30, @@ -230,11 +280,18 @@ describe("WebSocket Monitor", { ); }); - test("check() rejects when server does not support requested subprotocol", async () => { + test("check() rejects when server does not support requested subprotocol", async (t) => { const websocketMonitor = new WebSocketMonitorType(); + const { server: wss, port } = await createWebSocketServer({ + handleProtocols: (protocols) => { + // Explicitly reject all subprotocols + return null; + } + }); + t.after(() => wss.close()); const monitor = { - url: "wss://echo.websocket.org", + url: `ws://localhost:${port}`, wsIgnoreSecWebsocketAcceptHeader: false, wsSubprotocol: "ocpp1.6", accepted_statuscodes_json: JSON.stringify([ "1000" ]), @@ -252,11 +309,13 @@ describe("WebSocket Monitor", { ); }); - test("check() rejects when multiple subprotocols contain invalid characters", async () => { + test("check() rejects when multiple subprotocols contain invalid characters", async (t) => { const websocketMonitor = new WebSocketMonitorType(); + const { server: wss, port } = await createWebSocketServer(); + t.after(() => wss.close()); const monitor = { - url: "wss://echo.websocket.org", + url: `ws://localhost:${port}`, wsIgnoreSecWebsocketAcceptHeader: false, wsSubprotocol: " # & ,ocpp2.0 [] , ocpp1.6 , ,, ; ", accepted_statuscodes_json: JSON.stringify([ "1000" ]), @@ -275,16 +334,16 @@ describe("WebSocket Monitor", { }); test("check() sets status to UP when subprotocol with multiple spaces is accepted", async (t) => { - t.after(() => wss.close()); const websocketMonitor = new WebSocketMonitorType(); - const wss = new WebSocketServer({ port: 8080, + const { server: wss, port } = await createWebSocketServer({ handleProtocols: (protocols) => { return Array.from(protocols).includes("test") ? "test" : null; } }); + t.after(() => wss.close()); const monitor = { - url: "ws://localhost:8080", + url: `ws://localhost:${port}`, wsIgnoreSecWebsocketAcceptHeader: false, wsSubprotocol: "invalid , test ", accepted_statuscodes_json: JSON.stringify([ "1000" ]), @@ -306,16 +365,16 @@ describe("WebSocket Monitor", { }); test("check() sets status to UP when server supports requested subprotocol", async (t) => { - t.after(() => wss.close()); const websocketMonitor = new WebSocketMonitorType(); - const wss = new WebSocketServer({ port: 8080, + const { server: wss, port } = await createWebSocketServer({ handleProtocols: (protocols) => { return Array.from(protocols).includes("test") ? "test" : null; } }); + t.after(() => wss.close()); const monitor = { - url: "ws://localhost:8080", + url: `ws://localhost:${port}`, wsIgnoreSecWebsocketAcceptHeader: false, wsSubprotocol: "invalid,test", accepted_statuscodes_json: JSON.stringify([ "1000" ]),