diff --git a/server/monitor-types/websocket-upgrade.js b/server/monitor-types/websocket-upgrade.js
index 762e8df3b..ed1ba10f2 100644
--- a/server/monitor-types/websocket-upgrade.js
+++ b/server/monitor-types/websocket-upgrade.js
@@ -1,6 +1,26 @@
const { MonitorType } = require("./monitor-type");
const WebSocket = require("ws");
const { UP } = require("../../src/util");
+const { checkStatusCode } = require("../util-server");
+// Define closing error codes https://www.iana.org/assignments/websocket/websocket.xml#close-code-number
+const WS_ERR_CODE = {
+ 1002: "Protocol error",
+ 1003: "Unsupported Data",
+ 1005: "No Status Received",
+ 1006: "Abnormal Closure",
+ 1007: "Invalid frame payload data",
+ 1008: "Policy Violation",
+ 1009: "Message Too Big",
+ 1010: "Mandatory Extension Missing",
+ 1011: "Internal Error",
+ 1012: "Service Restart",
+ 1013: "Try Again Later",
+ 1014: "Bad Gateway",
+ 1015: "TLS Handshake Failed",
+ 3000: "Unauthorized",
+ 3003: "Forbidden",
+ 3008: "Timeout",
+};
class WebSocketMonitorType extends MonitorType {
name = "websocket-upgrade";
@@ -11,24 +31,31 @@ class WebSocketMonitorType extends MonitorType {
async check(monitor, heartbeat, _server) {
const [ message, code ] = await this.attemptUpgrade(monitor);
- if (code === 1000) {
+ // If no close code, then an error has occurred, display to user
+ if (typeof code === "undefined") {
+ throw new Error(`${message}`);
+ }
+ // 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;
- } else {
- throw new Error(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}`);
}
/**
- * Uses the builtin Websocket API to establish a connection to target server
+ * 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
*/
async attemptUpgrade(monitor) {
return new Promise((resolve) => {
let ws;
- //If user selected a subprotocol, sets Sec-WebSocket-Protocol header. 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);
+ // 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
+ ws = !monitor.wsSubprotocol ? new WebSocket(monitor.url) : new WebSocket(monitor.url, monitor.wsSubprotocol.replace(/\s/g, "").split(","));
ws.addEventListener("open", (event) => {
// Immediately close the connection
@@ -36,7 +63,8 @@ class WebSocketMonitorType extends MonitorType {
});
ws.onerror = (error) => {
- // Give user the choice to ignore Sec-WebSocket-Accept header
+ // Give user the choice to ignore Sec-WebSocket-Accept header for non compliant servers
+ // Header in HTTP 101 Switching Protocols response from server, technically already upgraded to WS
if (monitor.wsIgnoreSecWebsocketAcceptHeader && error.message === "Invalid Sec-WebSocket-Accept header") {
resolve([ "101 - OK", 1000 ]);
return;
@@ -46,8 +74,8 @@ class WebSocketMonitorType extends MonitorType {
};
ws.onclose = (event) => {
- // Upgrade success, connection closed successfully
- resolve([ "101 - OK", event.code ]);
+ // Return the close code, if connection didn't close cleanly, return the reason if present
+ resolve([ event.wasClean ? event.code.toString() + " - OK" : event.reason, event.code ]);
};
});
}
diff --git a/src/lang/en.json b/src/lang/en.json
index 727358fdc..e46c6ebbe 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -90,39 +90,9 @@
"upsideDownModeDescription": "Flip the status upside down. If the service is reachable, it is DOWN.",
"ignoreSecWebsocketAcceptHeaderDescription": "Allows the server to not reply with Sec-WebSocket-Accept header, if the websocket upgrade succeeds.",
"Ignore Sec-WebSocket-Accept header": "Ignore {0} header",
- "wsSubprotocolDescription": "For more information on subprotocols, please consult the {documentation}",
- "WebSocket Application Messaging Protocol": "WAMP (The WebSocket Application Messaging Protocol)",
- "Session Initiation Protocol": "WebSocket Transport for SIP (Session Initiation Protocol)",
- "Subprotocol": "Subprotocol",
- "Network API for Notification Channel": "OMA RESTful Network API for Notification Channel",
- "Web Process Control Protocol": "Web Process Control Protocol (WPCP)",
- "Advanced Message Queuing Protocol": "Advanced Message Queuing Protocol (AMQP) 1.0+",
- "jsflow": "jsFlow pubsub/queue Protocol",
- "Reverse Web Process Control": "Reverse Web Process Control Protocol (RWPCP)",
- "Extensible Messaging and Presence Protocol": "WebSocket Transport for the Extensible Messaging and Presence Protocol (XMPP)",
- "Smart Home IP": "SHIP - Smart Home IP",
- "Miele Cloud Connect Protocol": "Miele Cloud Connect Protocol",
- "Push Channel Protocol": "Push Channel Protocol",
- "Message Session Relay Protocol": "WebSocket Transport for MSRP (Message Session Relay Protocol)",
- "Binary Floor Control Protocol": "WebSocket Transport for BFCP (Binary Floor Control Protocol)",
- "Softvelum Low Delay Protocol": "Softvelum Low Delay Protocol",
- "OPC UA Connection Protocol": "OPC UA Connection Protocol",
- "OPC UA JSON Encoding": "OPC UA JSON Encoding",
- "Swindon Web Server Protocol": "Swindon Web Server Protocol (JSON encoding)",
- "Broadband Forum User Services Platform": "USP (Broadband Forum User Services Platform)",
- "Constrained Application Protocol": "Constrained Application Protocol (CoAP)",
- "Softvelum WebSocket signaling protocol": "Softvelum WebSocket Signaling Protocol",
- "Cobra Real Time Messaging Protocol": "Cobra Real Time Messaging Protocol",
- "Declarative Resource Protocol": "Declarative Resource Protocol",
- "BACnet Secure Connect Hub Connection": "BACnet Secure Connect Hub Connection",
- "BACnet Secure Connect Direct Connection": "BACnet Secure Connect Direct Connection",
- "WebSocket Transport for JMAP": "WebSocket Transport for JMAP (JSON Meta Application Protocol)",
- "ITU-T T.140 Real-Time Text": "ITU-T T.140 Real-Time Text",
- "Done.best IoT Protocol": "Done.best IoT Protocol",
- "Collection Update": "The Collection Update Websocket Subprotocol",
- "Text IRC Protocol": "Text IRC Protocol",
- "Binary IRC Protocol": "Binary IRC Protocol",
- "Penguin Statistics Live Protocol v3": "Penguin Statistics Live Protocol v3 (Protobuf encoding)",
+ "wsSubprotocolDescription": "Enter a comma delimited list of subprotocols. For more information on subprotocols, please consult the {documentation}",
+ "wsCodeDescription": "For more information on status codes, please consult {documentation}",
+ "Subprotocol(s)": "Subprotocol(s)",
"maxRedirectDescription": "Maximum number of redirects to follow. Set to 0 to disable redirects.",
"Upside Down Mode": "Upside Down Mode",
"Max. Redirects": "Max. Redirects",
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index 8c7e5cd21..55c551dd6 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -142,73 +142,8 @@
-
-
+
+
{{ $t('documentationOf', ['IANA']) }}
@@ -889,6 +824,36 @@
+
+
+
+
+
+
+
+
+ {{ $t("acceptedStatusCodesDescription") }}
+
+
+
+ {{ 'RFC6455' }}
+
+
+
+
+
@@ -1398,6 +1363,7 @@ export default {
},
hasDomain: false,
acceptedStatusCodeOptions: [],
+ acceptedWebsocketCodeOptions: [],
dnsresolvetypeOptions: [],
kafkaSaslMechanismOptions: [],
ipOrHostnameRegexPattern: hostNameRegexPattern(),
@@ -1770,6 +1736,7 @@ message HealthCheckResponse {
"monitor.type"(newType, oldType) {
if (oldType && this.monitor.type === "websocket-upgrade") {
this.monitor.url = "wss://";
+ this.monitor.accepted_statuscodes = [ "1000" ];
}
if (this.monitor.type === "push") {
if (! this.monitor.pushToken) {
@@ -1877,6 +1844,8 @@ message HealthCheckResponse {
"500-599",
];
+ let acceptedWebsocketCodeOptions = [];
+
let dnsresolvetypeOptions = [
"A",
"AAAA",
@@ -1902,6 +1871,11 @@ message HealthCheckResponse {
acceptedStatusCodeOptions.push(i.toString());
}
+ for (let i = 1000; i <= 4999; i++) {
+ acceptedWebsocketCodeOptions.push(i.toString());
+ }
+
+ this.acceptedWebsocketCodeOptions = acceptedWebsocketCodeOptions;
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
this.dnsresolvetypeOptions = dnsresolvetypeOptions;
this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
diff --git a/test/backend-test/test-websocket.js b/test/backend-test/test-websocket.js
index 33660d134..fba894d44 100644
--- a/test/backend-test/test-websocket.js
+++ b/test/backend-test/test-websocket.js
@@ -6,7 +6,7 @@ const { UP, PENDING } = require("../../src/util");
describe("Websocket Test", {
}, () => {
- test("Non Websocket Server", {}, async () => {
+ test("Non WS Server", {}, async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
@@ -25,12 +25,13 @@ describe("Websocket Test", {
);
});
- test("Secure Websocket", async () => {
+ test("Secure WS", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://echo.websocket.org",
wsIgnoreSecWebsocketAcceptHeader: false,
+ accepted_statuscodes_json: JSON.stringify([ "1000" ]),
};
const heartbeat = {
@@ -39,7 +40,7 @@ describe("Websocket Test", {
};
const expected = {
- msg: "101 - OK",
+ msg: "1000 - OK",
status: UP,
};
@@ -47,7 +48,7 @@ describe("Websocket Test", {
assert.deepStrictEqual(heartbeat, expected);
});
- test("Insecure Websocket", async (t) => {
+ test("Insecure WS", async (t) => {
t.after(() => wss.close());
const websocketMonitor = new WebSocketMonitorType();
const wss = new WebSocketServer({ port: 8080 });
@@ -55,6 +56,7 @@ describe("Websocket Test", {
const monitor = {
url: "ws://localhost:8080",
wsIgnoreSecWebsocketAcceptHeader: false,
+ accepted_statuscodes_json: JSON.stringify([ "1000" ]),
};
const heartbeat = {
@@ -63,7 +65,7 @@ describe("Websocket Test", {
};
const expected = {
- msg: "101 - OK",
+ msg: "1000 - OK",
status: UP,
};
@@ -71,12 +73,33 @@ describe("Websocket Test", {
assert.deepStrictEqual(heartbeat, expected);
});
+ test("Non compliant WS Server wrong status code", async () => {
+ const websocketMonitor = new WebSocketMonitorType();
+
+ const monitor = {
+ url: "wss://echo.websocket.org",
+ wsIgnoreSecWebsocketAcceptHeader: false,
+ accepted_statuscodes_json: JSON.stringify([ "1001" ]),
+ };
+
+ 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 () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://c.img-cdn.net/yE4s7KehTFyj/",
wsIgnoreSecWebsocketAcceptHeader: false,
+ accepted_statuscodes_json: JSON.stringify([ "1000" ]),
};
const heartbeat = {
@@ -96,6 +119,7 @@ describe("Websocket Test", {
const monitor = {
url: "wss://c.img-cdn.net/yE4s7KehTFyj/",
wsIgnoreSecWebsocketAcceptHeader: true,
+ accepted_statuscodes_json: JSON.stringify([ "1000" ]),
};
const heartbeat = {
@@ -104,7 +128,7 @@ describe("Websocket Test", {
};
const expected = {
- msg: "101 - OK",
+ msg: "1000 - OK",
status: UP,
};
@@ -118,6 +142,7 @@ describe("Websocket Test", {
const monitor = {
url: "wss://echo.websocket.org",
wsIgnoreSecWebsocketAcceptHeader: true,
+ accepted_statuscodes_json: JSON.stringify([ "1000" ]),
};
const heartbeat = {
@@ -126,7 +151,7 @@ describe("Websocket Test", {
};
const expected = {
- msg: "101 - OK",
+ msg: "1000 - OK",
status: UP,
};
@@ -140,6 +165,7 @@ describe("Websocket Test", {
const monitor = {
url: "wss://example.org",
wsIgnoreSecWebsocketAcceptHeader: true,
+ accepted_statuscodes_json: JSON.stringify([ "1000" ]),
};
const heartbeat = {
@@ -153,13 +179,14 @@ describe("Websocket Test", {
);
});
- test("Secure Websocket with Subprotocol", async () => {
+ test("Secure WS no support one subprotocol", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://echo.websocket.org",
wsIgnoreSecWebsocketAcceptHeader: false,
wsSubprotocol: "ocpp1.6",
+ accepted_statuscodes_json: JSON.stringify([ "1000" ]),
};
const heartbeat = {
@@ -172,4 +199,55 @@ describe("Websocket Test", {
new Error("Server sent no subprotocol")
);
});
+
+ test("Multiple subprotocols bad input", async () => {
+ const websocketMonitor = new WebSocketMonitorType();
+
+ const monitor = {
+ url: "wss://echo.websocket.org",
+ wsIgnoreSecWebsocketAcceptHeader: false,
+ wsSubprotocol: " ocpp2.0 , ocpp1.6 , ",
+ accepted_statuscodes_json: JSON.stringify([ "1000" ]),
+ };
+
+ const heartbeat = {
+ msg: "",
+ status: PENDING,
+ };
+
+ await assert.rejects(
+ websocketMonitor.check(monitor, heartbeat, {}),
+ new SyntaxError("An invalid or duplicated subprotocol was specified")
+ );
+ });
+
+ test("Insecure WS support one subprotocol", 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" ]),
+ };
+
+ const heartbeat = {
+ msg: "",
+ status: PENDING,
+ };
+
+ const expected = {
+ msg: "1000 - OK",
+ status: UP,
+ };
+
+ await websocketMonitor.check(monitor, heartbeat, {});
+ assert.deepStrictEqual(heartbeat, expected);
+ });
});