Add timeout selection + refine error logic + update tests
This commit is contained in:
parent
a2c81ebf44
commit
20e2cf69e4
@ -31,19 +31,23 @@ class WebSocketMonitorType extends MonitorType {
|
|||||||
async check(monitor, heartbeat, _server) {
|
async check(monitor, heartbeat, _server) {
|
||||||
const [ message, code ] = await this.attemptUpgrade(monitor);
|
const [ message, code ] = await this.attemptUpgrade(monitor);
|
||||||
|
|
||||||
|
if (typeof code !== "undefined") {
|
||||||
|
// If returned status code matches user controlled accepted status code(default 1000), return success
|
||||||
|
if (checkStatusCode(code, JSON.parse(monitor.accepted_statuscodes_json))) {
|
||||||
|
heartbeat.status = UP;
|
||||||
|
heartbeat.msg = message;
|
||||||
|
return; //success at this point
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw an error using friendly name if defined, fallback to generic msg
|
||||||
|
throw new Error(WS_ERR_CODE[code] || `Unexpected status code: ${code}`);
|
||||||
|
}
|
||||||
// If no close code, then an error has occurred, display to user
|
// If no close code, then an error has occurred, display to user
|
||||||
if (typeof code === "undefined") {
|
if (typeof message !== "undefined") {
|
||||||
throw new Error(`${message}`);
|
throw new Error(`${message}`);
|
||||||
}
|
}
|
||||||
// If returned status code matches user controlled accepted status code(default 1000), return success
|
// Throw generic error if nothing is defined, should never happen
|
||||||
if (checkStatusCode(code, JSON.parse(monitor.accepted_statuscodes_json))) {
|
throw new Error("Unknown Websocket Error");
|
||||||
heartbeat.status = UP;
|
|
||||||
heartbeat.msg = message;
|
|
||||||
return; //success at this point
|
|
||||||
}
|
|
||||||
|
|
||||||
// Throw an error using friendly name if defined, fallback to generic msg
|
|
||||||
throw new Error(WS_ERR_CODE[code] || `Unexpected status code: ${code}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,8 +58,8 @@ class WebSocketMonitorType extends MonitorType {
|
|||||||
async attemptUpgrade(monitor) {
|
async attemptUpgrade(monitor) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let ws;
|
let ws;
|
||||||
// If user inputs subprotocol(s), convert to array, then set Sec-WebSocket-Protocol header. Subprotocol Identifier column: https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name
|
// If user inputs subprotocol(s), convert to array, set Sec-WebSocket-Protocol header, timeout in ms. Subprotocol Identifier column: https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name
|
||||||
ws = !monitor.wsSubprotocol ? new WebSocket(monitor.url) : new WebSocket(monitor.url, monitor.wsSubprotocol.replace(/\s/g, "").split(","));
|
ws = !monitor.wsSubprotocol ? new WebSocket(monitor.url, { handshakeTimeout: (monitor?.timeout ?? 0) * 1000 }) : new WebSocket(monitor.url, monitor.wsSubprotocol.replace(/\s/g, "").split(","), { handshakeTimeout: (monitor?.timeout ?? 0) * 1000 });
|
||||||
|
|
||||||
ws.addEventListener("open", (event) => {
|
ws.addEventListener("open", (event) => {
|
||||||
// Immediately close the connection
|
// Immediately close the connection
|
||||||
|
|||||||
@ -702,8 +702,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Timeout: HTTP / JSON query / Keyword / Ping / RabbitMQ / SNMP only -->
|
<!-- Timeout: HTTP / JSON query / Keyword / Ping / RabbitMQ / SNMP / Websocket Upgrade only -->
|
||||||
<div v-if="monitor.type === 'http' || monitor.type === 'json-query' || monitor.type === 'keyword' || monitor.type === 'ping' || monitor.type === 'rabbitmq' || monitor.type === 'snmp'" class="my-3">
|
<div v-if="monitor.type === 'http' || monitor.type === 'json-query' || monitor.type === 'keyword' || monitor.type === 'ping' || monitor.type === 'rabbitmq' || monitor.type === 'snmp' || monitor.type === 'websocket-upgrade'" class="my-3">
|
||||||
<label for="timeout" class="form-label">
|
<label for="timeout" class="form-label">
|
||||||
{{ monitor.type === 'ping' ? $t("pingGlobalTimeoutLabel") : $t("Request Timeout") }}
|
{{ monitor.type === 'ping' ? $t("pingGlobalTimeoutLabel") : $t("Request Timeout") }}
|
||||||
<span v-if="monitor.type !== 'ping'">({{ $t("timeoutAfter", [monitor.timeout || clampTimeout(monitor.interval)]) }})</span>
|
<span v-if="monitor.type !== 'ping'">({{ $t("timeoutAfter", [monitor.timeout || clampTimeout(monitor.interval)]) }})</span>
|
||||||
|
|||||||
@ -3,6 +3,24 @@ const { describe, test } = require("node:test");
|
|||||||
const assert = require("node:assert");
|
const assert = require("node:assert");
|
||||||
const { WebSocketMonitorType } = require("../../server/monitor-types/websocket-upgrade");
|
const { WebSocketMonitorType } = require("../../server/monitor-types/websocket-upgrade");
|
||||||
const { UP, PENDING } = require("../../src/util");
|
const { UP, PENDING } = require("../../src/util");
|
||||||
|
const net = require("node:net");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
function nonCompliantWS(port = 8080) {
|
||||||
|
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(port, () => resolve(srv)));
|
||||||
|
}
|
||||||
|
|
||||||
describe("Websocket Test", {
|
describe("Websocket Test", {
|
||||||
}, () => {
|
}, () => {
|
||||||
@ -12,6 +30,7 @@ describe("Websocket Test", {
|
|||||||
const monitor = {
|
const monitor = {
|
||||||
url: "wss://example.org",
|
url: "wss://example.org",
|
||||||
wsIgnoreSecWebsocketAcceptHeader: false,
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
||||||
|
timeout: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
const heartbeat = {
|
const heartbeat = {
|
||||||
@ -32,6 +51,7 @@ describe("Websocket Test", {
|
|||||||
url: "wss://echo.websocket.org",
|
url: "wss://echo.websocket.org",
|
||||||
wsIgnoreSecWebsocketAcceptHeader: false,
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
||||||
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
||||||
|
timeout: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
const heartbeat = {
|
const heartbeat = {
|
||||||
@ -57,6 +77,7 @@ describe("Websocket Test", {
|
|||||||
url: "ws://localhost:8080",
|
url: "ws://localhost:8080",
|
||||||
wsIgnoreSecWebsocketAcceptHeader: false,
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
||||||
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
||||||
|
timeout: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
const heartbeat = {
|
const heartbeat = {
|
||||||
@ -80,6 +101,7 @@ describe("Websocket Test", {
|
|||||||
url: "wss://echo.websocket.org",
|
url: "wss://echo.websocket.org",
|
||||||
wsIgnoreSecWebsocketAcceptHeader: false,
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
||||||
accepted_statuscodes_json: JSON.stringify([ "1001" ]),
|
accepted_statuscodes_json: JSON.stringify([ "1001" ]),
|
||||||
|
timeout: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
const heartbeat = {
|
const heartbeat = {
|
||||||
@ -93,13 +115,37 @@ describe("Websocket Test", {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Non compliant WS server without IgnoreSecWebsocket", async () => {
|
test("Secure WS Server no status code", async () => {
|
||||||
const websocketMonitor = new WebSocketMonitorType();
|
const websocketMonitor = new WebSocketMonitorType();
|
||||||
|
|
||||||
const monitor = {
|
const monitor = {
|
||||||
url: "wss://c.img-cdn.net/yE4s7KehTFyj/",
|
url: "wss://echo.websocket.org",
|
||||||
|
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("Non compliant WS server without IgnoreSecWebsocket", async (t) => {
|
||||||
|
t.after(() => wss.close());
|
||||||
|
const websocketMonitor = new WebSocketMonitorType();
|
||||||
|
const wss = await nonCompliantWS();
|
||||||
|
|
||||||
|
const monitor = {
|
||||||
|
url: "ws://localhost:8080",
|
||||||
wsIgnoreSecWebsocketAcceptHeader: false,
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
||||||
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
||||||
|
timeout: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
const heartbeat = {
|
const heartbeat = {
|
||||||
@ -113,13 +159,16 @@ describe("Websocket Test", {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Non compliant WS server with IgnoreSecWebsocket", async () => {
|
test("Non compliant WS server with IgnoreSecWebsocket", async (t) => {
|
||||||
|
t.after(() => wss.close());
|
||||||
const websocketMonitor = new WebSocketMonitorType();
|
const websocketMonitor = new WebSocketMonitorType();
|
||||||
|
const wss = await nonCompliantWS();
|
||||||
|
|
||||||
const monitor = {
|
const monitor = {
|
||||||
url: "wss://c.img-cdn.net/yE4s7KehTFyj/",
|
url: "ws://localhost:8080",
|
||||||
wsIgnoreSecWebsocketAcceptHeader: true,
|
wsIgnoreSecWebsocketAcceptHeader: true,
|
||||||
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
||||||
|
timeout: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
const heartbeat = {
|
const heartbeat = {
|
||||||
@ -143,6 +192,7 @@ describe("Websocket Test", {
|
|||||||
url: "wss://echo.websocket.org",
|
url: "wss://echo.websocket.org",
|
||||||
wsIgnoreSecWebsocketAcceptHeader: true,
|
wsIgnoreSecWebsocketAcceptHeader: true,
|
||||||
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
||||||
|
timeout: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
const heartbeat = {
|
const heartbeat = {
|
||||||
@ -166,6 +216,7 @@ describe("Websocket Test", {
|
|||||||
url: "wss://example.org",
|
url: "wss://example.org",
|
||||||
wsIgnoreSecWebsocketAcceptHeader: true,
|
wsIgnoreSecWebsocketAcceptHeader: true,
|
||||||
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
||||||
|
timeout: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
const heartbeat = {
|
const heartbeat = {
|
||||||
@ -187,6 +238,7 @@ describe("Websocket Test", {
|
|||||||
wsIgnoreSecWebsocketAcceptHeader: false,
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
||||||
wsSubprotocol: "ocpp1.6",
|
wsSubprotocol: "ocpp1.6",
|
||||||
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
||||||
|
timeout: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
const heartbeat = {
|
const heartbeat = {
|
||||||
@ -200,14 +252,15 @@ describe("Websocket Test", {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Multiple subprotocols bad input", async () => {
|
test("Multiple subprotocols invalid input", async () => {
|
||||||
const websocketMonitor = new WebSocketMonitorType();
|
const websocketMonitor = new WebSocketMonitorType();
|
||||||
|
|
||||||
const monitor = {
|
const monitor = {
|
||||||
url: "wss://echo.websocket.org",
|
url: "wss://echo.websocket.org",
|
||||||
wsIgnoreSecWebsocketAcceptHeader: false,
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
||||||
wsSubprotocol: " ocpp2.0 , ocpp1.6 , ",
|
wsSubprotocol: " # & ,ocpp2.0 [] , ocpp1.6 , ,, ; ",
|
||||||
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
||||||
|
timeout: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
const heartbeat = {
|
const heartbeat = {
|
||||||
@ -221,6 +274,37 @@ describe("Websocket Test", {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Insecure WS subprotocol multiple spaces", async (t) => {
|
||||||
|
t.after(() => wss.close());
|
||||||
|
const websocketMonitor = new WebSocketMonitorType();
|
||||||
|
const wss = new WebSocketServer({ port: 8080,
|
||||||
|
handleProtocols: (protocols) => {
|
||||||
|
return Array.from(protocols).includes("test") ? "test" : null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const monitor = {
|
||||||
|
url: "ws://localhost:8080",
|
||||||
|
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("Insecure WS support one subprotocol", async (t) => {
|
test("Insecure WS support one subprotocol", async (t) => {
|
||||||
t.after(() => wss.close());
|
t.after(() => wss.close());
|
||||||
const websocketMonitor = new WebSocketMonitorType();
|
const websocketMonitor = new WebSocketMonitorType();
|
||||||
@ -235,6 +319,7 @@ describe("Websocket Test", {
|
|||||||
wsIgnoreSecWebsocketAcceptHeader: false,
|
wsIgnoreSecWebsocketAcceptHeader: false,
|
||||||
wsSubprotocol: "invalid,test",
|
wsSubprotocol: "invalid,test",
|
||||||
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
accepted_statuscodes_json: JSON.stringify([ "1000" ]),
|
||||||
|
timeout: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
const heartbeat = {
|
const heartbeat = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user