doover-js 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -5,6 +5,40 @@ const errors_1 = require("../http/errors");
|
|
|
5
5
|
const snowflake_1 = require("../utils/snowflake");
|
|
6
6
|
const RECONNECT_BASE_MS = 1000;
|
|
7
7
|
const RECONNECT_CAP_MS = 30000;
|
|
8
|
+
/** Schemes a WebSocket can be opened on — it maps http(s) to ws(s) itself. */
|
|
9
|
+
const ALLOWED_WSS_PROTOCOLS = new Set(["ws:", "wss:", "http:", "https:"]);
|
|
10
|
+
/**
|
|
11
|
+
* Reject a `dataWssUrl` that cannot address a gateway.
|
|
12
|
+
*
|
|
13
|
+
* An empty or relative value is the dangerous case, because nothing rejects it
|
|
14
|
+
* on its own: the WebSocket constructor resolves a relative URL against the
|
|
15
|
+
* document and maps https to wss, so in a browser `new WebSocket("")` silently
|
|
16
|
+
* points the gateway at whatever page is open. That page answers the upgrade
|
|
17
|
+
* with its own HTML and a 200, the browser reports "the server did not accept
|
|
18
|
+
* the WebSocket handshake", and the client retries against it forever — a loop
|
|
19
|
+
* that looks like a server problem and never names the real cause. It reached
|
|
20
|
+
* a kiosk panel that way, from a client built with `dataWssUrl: ""` because the
|
|
21
|
+
* build it was bundled into defined no env for it.
|
|
22
|
+
*
|
|
23
|
+
* Absolute http(s) is allowed as well as ws(s), since the WebSocket
|
|
24
|
+
* constructor maps those itself and existing configs rely on it.
|
|
25
|
+
*/
|
|
26
|
+
function assertAbsoluteWssUrl(url) {
|
|
27
|
+
if (typeof url !== "string" || url.trim() === "") {
|
|
28
|
+
throw new Error("dataWssUrl is empty, so a gateway connection would resolve against the " +
|
|
29
|
+
"current page rather than a gateway; set it to an absolute ws(s):// URL");
|
|
30
|
+
}
|
|
31
|
+
let parsed;
|
|
32
|
+
try {
|
|
33
|
+
parsed = new URL(url);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new Error(`dataWssUrl must be an absolute ws(s):// URL, not the relative value ${JSON.stringify(url)}`);
|
|
37
|
+
}
|
|
38
|
+
if (!ALLOWED_WSS_PROTOCOLS.has(parsed.protocol)) {
|
|
39
|
+
throw new Error(`dataWssUrl must be ws(s):// or http(s)://, not ${parsed.protocol}//`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
8
42
|
class GatewayClient {
|
|
9
43
|
constructor(config, auth) {
|
|
10
44
|
this.config = config;
|
|
@@ -90,6 +124,10 @@ class GatewayClient {
|
|
|
90
124
|
}
|
|
91
125
|
}
|
|
92
126
|
async openSocket() {
|
|
127
|
+
// Before auth, before any socket: a URL that cannot address a gateway is a
|
|
128
|
+
// configuration fault, and finding out now costs one error instead of an
|
|
129
|
+
// endless handshake failure that reads like a broken server.
|
|
130
|
+
assertAbsoluteWssUrl(this.config.dataWssUrl);
|
|
93
131
|
if (this.auth) {
|
|
94
132
|
await this.auth.ensureReady();
|
|
95
133
|
}
|
|
@@ -393,4 +393,82 @@ class RejectingAuth extends doover_auth_1.DooverAuth {
|
|
|
393
393
|
(0, chai_1.expect)(helpers_1.MockWebSocket.instances.length).to.equal(11);
|
|
394
394
|
randomStub.restore();
|
|
395
395
|
});
|
|
396
|
+
(0, mocha_1.describe)("an unusable dataWssUrl", () => {
|
|
397
|
+
// A relative or empty value is resolved against the document by the
|
|
398
|
+
// WebSocket constructor, so the gateway silently points at whatever page
|
|
399
|
+
// is open, gets that page's HTML and a 200 back, and retries against it
|
|
400
|
+
// forever. Refusing it names the cause once instead.
|
|
401
|
+
(0, mocha_1.it)("refuses an empty one rather than resolving against the page", async () => {
|
|
402
|
+
const client = new gateway_client_1.GatewayClient({
|
|
403
|
+
dataRestUrl: "https://api.example.com",
|
|
404
|
+
controlApiUrl: "https://control.example.com",
|
|
405
|
+
dataWssUrl: "",
|
|
406
|
+
organisationId: null,
|
|
407
|
+
sharing: "none",
|
|
408
|
+
impersonateUserStorageKey: "doover:test",
|
|
409
|
+
});
|
|
410
|
+
await (0, chai_1.expect)(client.connect()).to.be.rejectedWith(/dataWssUrl is empty/);
|
|
411
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances).to.have.length(0);
|
|
412
|
+
});
|
|
413
|
+
(0, mocha_1.it)("refuses a relative one", async () => {
|
|
414
|
+
const client = new gateway_client_1.GatewayClient({
|
|
415
|
+
dataRestUrl: "https://api.example.com",
|
|
416
|
+
controlApiUrl: "https://control.example.com",
|
|
417
|
+
dataWssUrl: "/widget/some_widget?app_key=k",
|
|
418
|
+
organisationId: null,
|
|
419
|
+
sharing: "none",
|
|
420
|
+
impersonateUserStorageKey: "doover:test",
|
|
421
|
+
});
|
|
422
|
+
await (0, chai_1.expect)(client.connect()).to.be.rejectedWith(/not the relative value/);
|
|
423
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances).to.have.length(0);
|
|
424
|
+
});
|
|
425
|
+
(0, mocha_1.it)("refuses a scheme a WebSocket cannot be opened on", async () => {
|
|
426
|
+
const client = new gateway_client_1.GatewayClient({
|
|
427
|
+
dataRestUrl: "https://api.example.com",
|
|
428
|
+
controlApiUrl: "https://control.example.com",
|
|
429
|
+
dataWssUrl: "file:///etc/hosts",
|
|
430
|
+
organisationId: null,
|
|
431
|
+
sharing: "none",
|
|
432
|
+
impersonateUserStorageKey: "doover:test",
|
|
433
|
+
});
|
|
434
|
+
await (0, chai_1.expect)(client.connect()).to.be.rejectedWith(/must be ws\(s\):\/\/ or http\(s\)/);
|
|
435
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances).to.have.length(0);
|
|
436
|
+
});
|
|
437
|
+
(0, mocha_1.it)("reports it through wssError once when a subscribe connects in the background", async () => {
|
|
438
|
+
// `subscribe` is fire-and-forget, so the fault has to surface as an event
|
|
439
|
+
// rather than a rejected promise nobody holds — and only once, because
|
|
440
|
+
// reconnects are scheduled on socket close and no socket was ever opened.
|
|
441
|
+
const client = new gateway_client_1.GatewayClient({
|
|
442
|
+
dataRestUrl: "https://api.example.com",
|
|
443
|
+
controlApiUrl: "https://control.example.com",
|
|
444
|
+
dataWssUrl: "",
|
|
445
|
+
organisationId: null,
|
|
446
|
+
sharing: "none",
|
|
447
|
+
impersonateUserStorageKey: "doover:test",
|
|
448
|
+
});
|
|
449
|
+
const errors = [];
|
|
450
|
+
client.on("wssError", (event) => errors.push(String(event.message)));
|
|
451
|
+
client.subscribe({ agent_id: "111", name: "tag_values" });
|
|
452
|
+
// The rejection travels connect -> openSocket -> catch, which is several
|
|
453
|
+
// microtasks; draining them is not the same as advancing fake timers.
|
|
454
|
+
for (let tick = 0; tick < 5; tick += 1)
|
|
455
|
+
await Promise.resolve();
|
|
456
|
+
(0, chai_1.expect)(errors).to.have.length(1);
|
|
457
|
+
(0, chai_1.expect)(errors[0]).to.match(/dataWssUrl is empty/);
|
|
458
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances).to.have.length(0);
|
|
459
|
+
});
|
|
460
|
+
(0, mocha_1.it)("still accepts an absolute http(s) URL, which WebSocket maps itself", async () => {
|
|
461
|
+
const client = new gateway_client_1.GatewayClient({
|
|
462
|
+
dataRestUrl: "https://api.example.com",
|
|
463
|
+
controlApiUrl: "https://control.example.com",
|
|
464
|
+
dataWssUrl: "https://ws.example.com/gateway",
|
|
465
|
+
organisationId: null,
|
|
466
|
+
sharing: "none",
|
|
467
|
+
impersonateUserStorageKey: "doover:test",
|
|
468
|
+
});
|
|
469
|
+
await client.connect();
|
|
470
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances).to.have.length(1);
|
|
471
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances[0].url).to.equal("https://ws.example.com/gateway");
|
|
472
|
+
});
|
|
473
|
+
});
|
|
396
474
|
});
|