mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-01-19 21:14:35 +01:00
383 lines
12 KiB
JavaScript
383 lines
12 KiB
JavaScript
const { WebSocketServer } = require("ws");
|
|
const { describe, test } = require("node:test");
|
|
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
|
|
* @returns {Promise<{server: net.Server, port: number}>} Promise that resolves to the created server and its port
|
|
*/
|
|
function nonCompliantWS() {
|
|
const srv = net.createServer((socket) => {
|
|
socket.once("data", (buf) => {
|
|
socket.write(
|
|
"HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n\r\n"
|
|
);
|
|
socket.destroy();
|
|
});
|
|
});
|
|
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 (t) => {
|
|
const websocketMonitor = new WebSocketMonitorType();
|
|
const { server: srv, port } = await httpServer();
|
|
t.after(() => srv.close());
|
|
|
|
const monitor = {
|
|
url: `ws://localhost:${port}`,
|
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
|
timeout: 30,
|
|
};
|
|
|
|
const heartbeat = {
|
|
msg: "",
|
|
status: PENDING,
|
|
};
|
|
|
|
await assert.rejects(
|
|
websocketMonitor.check(monitor, heartbeat, {}),
|
|
new Error("Unexpected server response: 200")
|
|
);
|
|
});
|
|
|
|
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: `ws://localhost:${port}`,
|
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
|
accepted_statuscodes_json: JSON.stringify(["1000"]),
|
|
timeout: 30,
|
|
};
|
|
|
|
const heartbeat = {
|
|
msg: "",
|
|
status: PENDING,
|
|
};
|
|
|
|
const expected = {
|
|
msg: "1000 - OK",
|
|
status: UP,
|
|
};
|
|
|
|
await websocketMonitor.check(monitor, heartbeat, {});
|
|
assert.deepStrictEqual(heartbeat, expected);
|
|
});
|
|
|
|
test("check() sets status to UP when connecting to insecure WebSocket server", async (t) => {
|
|
const websocketMonitor = new WebSocketMonitorType();
|
|
const { server: wss, port } = await createWebSocketServer();
|
|
t.after(() => wss.close());
|
|
|
|
const monitor = {
|
|
url: `ws://localhost:${port}`,
|
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
|
accepted_statuscodes_json: JSON.stringify(["1000"]),
|
|
timeout: 30,
|
|
};
|
|
|
|
const heartbeat = {
|
|
msg: "",
|
|
status: PENDING,
|
|
};
|
|
|
|
const expected = {
|
|
msg: "1000 - OK",
|
|
status: UP,
|
|
};
|
|
|
|
await websocketMonitor.check(monitor, heartbeat, {});
|
|
assert.deepStrictEqual(heartbeat, expected);
|
|
});
|
|
|
|
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: `ws://localhost:${port}`,
|
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
|
accepted_statuscodes_json: JSON.stringify(["1001"]),
|
|
timeout: 30,
|
|
};
|
|
|
|
const heartbeat = {
|
|
msg: "",
|
|
status: PENDING,
|
|
};
|
|
|
|
await assert.rejects(websocketMonitor.check(monitor, heartbeat, {}), new Error("Unexpected status code: 1000"));
|
|
});
|
|
|
|
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: `ws://localhost:${port}`,
|
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
|
accepted_statuscodes_json: JSON.stringify([""]),
|
|
timeout: 30,
|
|
};
|
|
|
|
const heartbeat = {
|
|
msg: "",
|
|
status: PENDING,
|
|
};
|
|
|
|
await assert.rejects(websocketMonitor.check(monitor, heartbeat, {}), new Error("Unexpected status code: 1000"));
|
|
});
|
|
|
|
test("check() rejects when Sec-WebSocket-Accept header is invalid", async (t) => {
|
|
const websocketMonitor = new WebSocketMonitorType();
|
|
const { server: wss, port } = await nonCompliantWS();
|
|
t.after(() => wss.close());
|
|
|
|
const monitor = {
|
|
url: `ws://localhost:${port}`,
|
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
|
accepted_statuscodes_json: JSON.stringify(["1000"]),
|
|
timeout: 30,
|
|
};
|
|
|
|
const heartbeat = {
|
|
msg: "",
|
|
status: PENDING,
|
|
};
|
|
|
|
await assert.rejects(
|
|
websocketMonitor.check(monitor, heartbeat, {}),
|
|
new Error("Invalid Sec-WebSocket-Accept header")
|
|
);
|
|
});
|
|
|
|
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 monitor = {
|
|
url: `ws://localhost:${port}`,
|
|
wsIgnoreSecWebsocketAcceptHeader: true,
|
|
accepted_statuscodes_json: JSON.stringify(["1000"]),
|
|
timeout: 30,
|
|
};
|
|
|
|
const heartbeat = {
|
|
msg: "",
|
|
status: PENDING,
|
|
};
|
|
|
|
const expected = {
|
|
msg: "1000 - OK",
|
|
status: UP,
|
|
};
|
|
|
|
await websocketMonitor.check(monitor, heartbeat, {});
|
|
assert.deepStrictEqual(heartbeat, expected);
|
|
});
|
|
|
|
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: `ws://localhost:${port}`,
|
|
wsIgnoreSecWebsocketAcceptHeader: true,
|
|
accepted_statuscodes_json: JSON.stringify(["1000"]),
|
|
timeout: 30,
|
|
};
|
|
|
|
const heartbeat = {
|
|
msg: "",
|
|
status: PENDING,
|
|
};
|
|
|
|
const expected = {
|
|
msg: "1000 - OK",
|
|
status: UP,
|
|
};
|
|
|
|
await websocketMonitor.check(monitor, heartbeat, {});
|
|
assert.deepStrictEqual(heartbeat, expected);
|
|
});
|
|
|
|
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: `ws://localhost:${port}`,
|
|
wsIgnoreSecWebsocketAcceptHeader: true,
|
|
accepted_statuscodes_json: JSON.stringify(["1000"]),
|
|
timeout: 30,
|
|
};
|
|
|
|
const heartbeat = {
|
|
msg: "",
|
|
status: PENDING,
|
|
};
|
|
|
|
await assert.rejects(
|
|
websocketMonitor.check(monitor, heartbeat, {}),
|
|
new Error("Unexpected server response: 200")
|
|
);
|
|
});
|
|
|
|
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: `ws://localhost:${port}`,
|
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
|
wsSubprotocol: "ocpp1.6",
|
|
accepted_statuscodes_json: JSON.stringify(["1000"]),
|
|
timeout: 30,
|
|
};
|
|
|
|
const heartbeat = {
|
|
msg: "",
|
|
status: PENDING,
|
|
};
|
|
|
|
await assert.rejects(websocketMonitor.check(monitor, heartbeat, {}), new Error("Server sent no subprotocol"));
|
|
});
|
|
|
|
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: `ws://localhost:${port}`,
|
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
|
wsSubprotocol: " # & ,ocpp2.0 [] , ocpp1.6 , ,, ; ",
|
|
accepted_statuscodes_json: JSON.stringify(["1000"]),
|
|
timeout: 30,
|
|
};
|
|
|
|
const heartbeat = {
|
|
msg: "",
|
|
status: PENDING,
|
|
};
|
|
|
|
await assert.rejects(
|
|
websocketMonitor.check(monitor, heartbeat, {}),
|
|
new SyntaxError("An invalid or duplicated subprotocol was specified")
|
|
);
|
|
});
|
|
|
|
test("check() sets status to UP when subprotocol with multiple spaces is accepted", async (t) => {
|
|
const websocketMonitor = new WebSocketMonitorType();
|
|
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:${port}`,
|
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
|
wsSubprotocol: "invalid , test ",
|
|
accepted_statuscodes_json: JSON.stringify(["1000"]),
|
|
timeout: 30,
|
|
};
|
|
|
|
const heartbeat = {
|
|
msg: "",
|
|
status: PENDING,
|
|
};
|
|
|
|
const expected = {
|
|
msg: "1000 - OK",
|
|
status: UP,
|
|
};
|
|
|
|
await websocketMonitor.check(monitor, heartbeat, {});
|
|
assert.deepStrictEqual(heartbeat, expected);
|
|
});
|
|
|
|
test("check() sets status to UP when server supports requested subprotocol", async (t) => {
|
|
const websocketMonitor = new WebSocketMonitorType();
|
|
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:${port}`,
|
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
|
wsSubprotocol: "invalid,test",
|
|
accepted_statuscodes_json: JSON.stringify(["1000"]),
|
|
timeout: 30,
|
|
};
|
|
|
|
const heartbeat = {
|
|
msg: "",
|
|
status: PENDING,
|
|
};
|
|
|
|
const expected = {
|
|
msg: "1000 - OK",
|
|
status: UP,
|
|
};
|
|
|
|
await websocketMonitor.check(monitor, heartbeat, {});
|
|
assert.deepStrictEqual(heartbeat, expected);
|
|
});
|
|
});
|