@seriousme/opifex 2.1.0 → 2.3.0
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.
- package/README.md +7 -6
- package/bin/mqtt.ts +1 -1
- package/client/client.ts +58 -7
- package/client/context.ts +2 -2
- package/client/deps.ts +3 -3
- package/client/mod.ts +2 -2
- package/connNext/connNext.ts +44 -0
- package/deno/README.md +1 -1
- package/deno/denoConnToNext.ts +104 -0
- package/deno/mod.ts +7 -0
- package/deno/tcpClient.ts +8 -24
- package/deno/tcpServer.test.ts +14 -103
- package/deno/tcpServer.ts +14 -6
- package/deno/tlsServer.test.ts +14 -58
- package/deno/tlsServer.ts +14 -6
- package/deno/wsServer.test.ts +15 -61
- package/deno/wsServer.ts +77 -35
- package/dev_utils/asyncStreamPair.ts +114 -0
- package/dev_utils/asyncStreamToNext.ts +171 -0
- package/dev_utils/connNextPair.test.ts +179 -0
- package/dev_utils/mockConn.ts +115 -29
- package/dev_utils/mockServer.ts +3 -4
- package/dev_utils/mod.ts +1 -2
- package/dev_utils/testClient.ts +64 -0
- package/dev_utils/webStreamToNext.ts +187 -0
- package/dist/bin/mqtt.js +1 -1
- package/dist/bin/mqtt.js.map +1 -1
- package/dist/client/client.d.ts +21 -3
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +43 -5
- package/dist/client/client.js.map +1 -1
- package/dist/client/context.d.ts +2 -2
- package/dist/client/context.d.ts.map +1 -1
- package/dist/client/deps.d.ts +1 -1
- package/dist/client/deps.d.ts.map +1 -1
- package/dist/client/mod.d.ts +1 -1
- package/dist/client/mod.d.ts.map +1 -1
- package/dist/connNext/connNext.d.ts +43 -0
- package/dist/connNext/connNext.d.ts.map +1 -0
- package/dist/connNext/connNext.js +2 -0
- package/dist/connNext/connNext.js.map +1 -0
- package/dist/examples/authHandler.test.js +2 -2
- package/dist/examples/authHandler.test.js.map +1 -1
- package/dist/examples/mqtt+mqtts.test.d.ts +2 -0
- package/dist/examples/mqtt+mqtts.test.d.ts.map +1 -0
- package/dist/examples/mqtt+mqtts.test.js +104 -0
- package/dist/examples/mqtt+mqtts.test.js.map +1 -0
- package/dist/examples/mqtt.test.js +2 -2
- package/dist/examples/mqtt.test.js.map +1 -1
- package/dist/examples/mqtts.test.js +2 -2
- package/dist/examples/mqtts.test.js.map +1 -1
- package/dist/mqttConn/mqttConn.d.ts +5 -6
- package/dist/mqttConn/mqttConn.d.ts.map +1 -1
- package/dist/mqttConn/mqttConn.js +2 -3
- package/dist/mqttConn/mqttConn.js.map +1 -1
- package/dist/mqttPacket/protocolLevels.d.ts +2 -0
- package/dist/mqttPacket/protocolLevels.d.ts.map +1 -1
- package/dist/node/nodeSocketToNext.d.ts +20 -0
- package/dist/node/nodeSocketToNext.d.ts.map +1 -0
- package/dist/node/nodeSocketToNext.js +161 -0
- package/dist/node/nodeSocketToNext.js.map +1 -0
- package/dist/node/tcpClient.d.ts +4 -6
- package/dist/node/tcpClient.d.ts.map +1 -1
- package/dist/node/tcpClient.js +6 -21
- package/dist/node/tcpClient.js.map +1 -1
- package/dist/node/tcpServer.d.ts +8 -4
- package/dist/node/tcpServer.d.ts.map +1 -1
- package/dist/node/tcpServer.js +14 -6
- package/dist/node/tcpServer.js.map +1 -1
- package/dist/node/tlsServer.d.ts +7 -3
- package/dist/node/tlsServer.d.ts.map +1 -1
- package/dist/node/tlsServer.js +13 -5
- package/dist/node/tlsServer.js.map +1 -1
- package/dist/server/context.d.ts +5 -5
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/context.js +4 -1
- package/dist/server/context.js.map +1 -1
- package/dist/server/deps.d.ts +1 -1
- package/dist/server/deps.d.ts.map +1 -1
- package/dist/server/mod.d.ts +1 -1
- package/dist/server/mod.d.ts.map +1 -1
- package/dist/server/server.d.ts +3 -3
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +3 -3
- package/dist/utils/utils.d.ts +1 -2
- package/dist/utils/utils.d.ts.map +1 -1
- package/dist/utils/utils.js +3 -2
- package/dist/utils/utils.js.map +1 -1
- package/dist/web/webSocketToNext.d.ts +3 -0
- package/dist/web/webSocketToNext.d.ts.map +1 -0
- package/dist/web/webSocketToNext.js +160 -0
- package/dist/web/webSocketToNext.js.map +1 -0
- package/dist/web/wsClient.d.ts +8 -3
- package/dist/web/wsClient.d.ts.map +1 -1
- package/dist/web/wsClient.js +20 -6
- package/dist/web/wsClient.js.map +1 -1
- package/dist/web/wsClient.test.js +15 -0
- package/dist/web/wsClient.test.js.map +1 -1
- package/examples/README.md +21 -0
- package/examples/authHandler.test.ts +2 -2
- package/examples/mqtt+mqtts.test.ts +122 -0
- package/examples/mqtt.test.ts +2 -2
- package/examples/mqtts.test.ts +2 -2
- package/examples/webSocket.test.ts +10 -64
- package/examples/webSocketServer.ts +98 -0
- package/mqttConn/mqttConn.test.ts +6 -6
- package/mqttConn/mqttConn.ts +8 -9
- package/mqttPacket/protocolLevels.ts +2 -0
- package/node/README.md +1 -1
- package/node/mod.ts +7 -0
- package/node/{socket-node-stream.ts → nodeSocketToNext.ts} +78 -65
- package/node/tcpClient.ts +13 -27
- package/node/tcpServer.test.ts +12 -58
- package/node/tcpServer.ts +18 -8
- package/node/tlsServer.test.ts +11 -58
- package/node/tlsServer.ts +17 -7
- package/package.json +8 -2
- package/server/context.ts +9 -6
- package/server/deps.ts +1 -1
- package/server/mod.ts +1 -1
- package/server/server.ts +6 -6
- package/utils/utils.ts +3 -2
- package/web/webSocketToNext.ts +183 -0
- package/web/wsClient.test.ts +22 -0
- package/web/wsClient.ts +22 -9
- package/dist/node/wrapNodeSocket.d.ts +0 -12
- package/dist/node/wrapNodeSocket.d.ts.map +0 -1
- package/dist/node/wrapNodeSocket.js +0 -36
- package/dist/node/wrapNodeSocket.js.map +0 -1
- package/dist/socket/socket.d.ts +0 -40
- package/dist/socket/socket.d.ts.map +0 -1
- package/dist/socket/socket.js +0 -94
- package/dist/socket/socket.js.map +0 -1
- package/dist/web/webSocketStream.d.ts +0 -29
- package/dist/web/webSocketStream.d.ts.map +0 -1
- package/dist/web/webSocketStream.js +0 -69
- package/dist/web/webSocketStream.js.map +0 -1
- package/dist/web/wrapWebSocket.d.ts +0 -12
- package/dist/web/wrapWebSocket.d.ts.map +0 -1
- package/dist/web/wrapWebSocket.js +0 -41
- package/dist/web/wrapWebSocket.js.map +0 -1
- package/node/socket-node-stream.test.ts +0 -322
- package/node/wrapNodeSocket.ts +0 -44
- package/socket/socket.test.ts +0 -91
- package/socket/socket.ts +0 -124
- package/web/webSocketStream.ts +0 -97
- package/web/wrapWebSocket.ts +0 -52
package/examples/mqtt.test.ts
CHANGED
|
@@ -30,7 +30,7 @@ test("Test pubSub using TCP client and server using memoryPersistence", async fu
|
|
|
30
30
|
|
|
31
31
|
const client = new TcpClient();
|
|
32
32
|
await client.connect(params);
|
|
33
|
-
logger.info("Client connected to server at", client.url);
|
|
33
|
+
logger.info("Client connected to server at", client.url.toString());
|
|
34
34
|
|
|
35
35
|
const publishSet: { topic: string; qos: QoS }[] = [
|
|
36
36
|
{ topic: "t0@q0", qos: 0 },
|
|
@@ -85,5 +85,5 @@ test("Test pubSub using TCP client and server using memoryPersistence", async fu
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
logger.info("Stop server");
|
|
88
|
-
server.stop();
|
|
88
|
+
await server.stop();
|
|
89
89
|
});
|
package/examples/mqtts.test.ts
CHANGED
|
@@ -41,7 +41,7 @@ test("Test pubSub using TLS client and server and sqlitePersistence", async func
|
|
|
41
41
|
|
|
42
42
|
const client = new TcpClient();
|
|
43
43
|
await client.connect(params);
|
|
44
|
-
logger.info("Client connected to server at", client.url);
|
|
44
|
+
logger.info("Client connected to server at", client.url.toString());
|
|
45
45
|
|
|
46
46
|
const publishSet: { topic: string; qos: QoS }[] = [
|
|
47
47
|
{ topic: "t0@q0", qos: 0 },
|
|
@@ -96,5 +96,5 @@ test("Test pubSub using TLS client and server and sqlitePersistence", async func
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
logger.info("Stop server");
|
|
99
|
-
server.stop();
|
|
99
|
+
await server.stop();
|
|
100
100
|
});
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import type { PublishPacket, QoS } from "@seriousme/opifex/mqttPacket";
|
|
3
|
+
import { logger, LogLevel } from "@seriousme/opifex/utils";
|
|
4
|
+
import { runTest } from "./public/testRunner.js";
|
|
6
5
|
|
|
7
6
|
logger.level(LogLevel.info);
|
|
8
7
|
|
|
@@ -27,68 +26,15 @@ test("Deno: Test pubSub using WebSocket client and server and memoryPersistence"
|
|
|
27
26
|
? "127.0.0.1"
|
|
28
27
|
: server.address;
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
logger.info("Client connected to server at", client.url);
|
|
38
|
-
|
|
39
|
-
const publishSet: { topic: string; qos: QoS }[] = [
|
|
40
|
-
{ topic: "t0@q0", qos: 0 },
|
|
41
|
-
{ topic: "t1@q0", qos: 0 },
|
|
42
|
-
{ topic: "t2@q0", qos: 0 },
|
|
43
|
-
{ topic: "t0@q1", qos: 1 },
|
|
44
|
-
{ topic: "t1@q1", qos: 1 },
|
|
45
|
-
{ topic: "t2@q1", qos: 1 },
|
|
46
|
-
{ topic: "t0@q2", qos: 2 },
|
|
47
|
-
{ topic: "t1@q2", qos: 2 },
|
|
48
|
-
{ topic: "t2@q2", qos: 2 },
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
const subscriptions = publishSet.map((item) => ({
|
|
52
|
-
topicFilter: item.topic,
|
|
53
|
-
qos: item.qos,
|
|
54
|
-
}));
|
|
55
|
-
await client.subscribe({
|
|
56
|
-
subscriptions,
|
|
29
|
+
await runTest({
|
|
30
|
+
WsClient,
|
|
31
|
+
protocol: "ws",
|
|
32
|
+
hostname,
|
|
33
|
+
port: server.port,
|
|
34
|
+
logger: logger.verbose,
|
|
35
|
+
assert,
|
|
57
36
|
});
|
|
58
|
-
|
|
59
|
-
// the IIFE ensures message reception runs in parallel
|
|
60
|
-
logger.info("Start receiving");
|
|
61
|
-
const received: PublishPacket[] = [];
|
|
62
|
-
(async function () {
|
|
63
|
-
for await (const item of client.messages()) {
|
|
64
|
-
logger.verbose("Receiving:", item.topic, "--", item.qos);
|
|
65
|
-
received.push(item);
|
|
66
|
-
}
|
|
67
|
-
})();
|
|
68
|
-
// end of IIFE
|
|
69
|
-
for (const item of publishSet) {
|
|
70
|
-
logger.verbose("Publishing:", item.topic, "--", item.qos);
|
|
71
|
-
await client.publish({
|
|
72
|
-
topic: item.topic,
|
|
73
|
-
qos: item.qos,
|
|
74
|
-
payload: new Uint8Array([0x01]),
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
await delay(100);
|
|
79
|
-
logger.info("Disconnect client");
|
|
80
|
-
await client.disconnect();
|
|
81
|
-
|
|
82
|
-
logger.info("Check completeness");
|
|
83
|
-
for (const item of publishSet) {
|
|
84
|
-
const found = received.find((f) =>
|
|
85
|
-
f.topic == item.topic && f.qos === item.qos
|
|
86
|
-
);
|
|
87
|
-
logger.verbose("Found:", item.topic, "--", item.qos);
|
|
88
|
-
assert(found, `${item.topic} -- ${item.qos}`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
37
|
logger.info("Stop server");
|
|
92
|
-
server.stop();
|
|
38
|
+
await server.stop();
|
|
93
39
|
}
|
|
94
40
|
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* a simple websocket server to verify browser compatibility
|
|
3
|
+
*/
|
|
4
|
+
import { extname, join, resolve } from "node:path";
|
|
5
|
+
import { logger } from "@seriousme/opifex/utils";
|
|
6
|
+
import { MqttServer } from "@seriousme/opifex/server";
|
|
7
|
+
import { mqttOverWS } from "@seriousme/opifex/wsServer";
|
|
8
|
+
import { LogLevel } from "@seriousme/opifex/utils";
|
|
9
|
+
import type { RequestHandler } from "@seriousme/opifex/wsServer";
|
|
10
|
+
|
|
11
|
+
logger.level(LogLevel.info);
|
|
12
|
+
|
|
13
|
+
// Nodejs does not natively support WebSockets so we only continue running Deno
|
|
14
|
+
if (typeof Deno === "undefined") {
|
|
15
|
+
logger.error("Native WebSocket server only supported on Deno");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const currentDir = import.meta.dirname || "./";
|
|
19
|
+
const PUBLIC_DIR = resolve(join(currentDir, "/public"));
|
|
20
|
+
const listenOptions = { port: 8080 };
|
|
21
|
+
const mqttOptions = {};
|
|
22
|
+
const mqttServer = new MqttServer(mqttOptions);
|
|
23
|
+
|
|
24
|
+
const mimeTypes = new Map<string, string>([
|
|
25
|
+
[".html", "text/html; charset=utf-8"],
|
|
26
|
+
[".css", "text/css; charset=utf-8"],
|
|
27
|
+
[".js", "text/javascript; charset=utf-8"],
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function w3cLogger(handler: RequestHandler): RequestHandler {
|
|
31
|
+
return async (req, info) => {
|
|
32
|
+
const res = await handler(req, info);
|
|
33
|
+
const hostname = res.headers.get("upgrade")
|
|
34
|
+
? "unknown"
|
|
35
|
+
: info.remoteAddr.hostname;
|
|
36
|
+
logger.info(W3CLogLine(req, res, hostname));
|
|
37
|
+
return res;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function W3CLogLine(req: Request, res: Response, hostname: string): string {
|
|
42
|
+
const now = new Date();
|
|
43
|
+
|
|
44
|
+
// format date to UTC (YYYY-MM-DD en HH:MM:SS)
|
|
45
|
+
const date = now.toISOString().split("T")[0];
|
|
46
|
+
const time = now.toISOString().split("T")[1]?.substring(0, 8);
|
|
47
|
+
|
|
48
|
+
const url = new URL(req.url);
|
|
49
|
+
const clientIp = hostname ?? "127.0.0.1";
|
|
50
|
+
const method = req.method;
|
|
51
|
+
const uriStem = url.pathname;
|
|
52
|
+
const uriQuery = url.search ? url.search : "-";
|
|
53
|
+
const status = res.status;
|
|
54
|
+
const bytesSent = res.headers.get("content-length") ?? "-";
|
|
55
|
+
|
|
56
|
+
return `${date} ${time} ${clientIp} ${method} ${uriStem} ${uriQuery} ${status} ${bytesSent}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const requestHandler: RequestHandler = async (req) => {
|
|
60
|
+
const url = new URL(req.url);
|
|
61
|
+
|
|
62
|
+
const pathname = url.pathname.endsWith("/")
|
|
63
|
+
? url.pathname + "index.html"
|
|
64
|
+
: url.pathname;
|
|
65
|
+
|
|
66
|
+
const ext = extname(pathname);
|
|
67
|
+
const mimeType = mimeTypes.get(ext) ?? "application/octet-stream";
|
|
68
|
+
|
|
69
|
+
const targetPath = resolve(join(PUBLIC_DIR, pathname));
|
|
70
|
+
logger.debug({ targetPath });
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
if (!targetPath.startsWith(PUBLIC_DIR)) {
|
|
74
|
+
// someone is trying to escape the public folder
|
|
75
|
+
return new Response("403 Forbidden", { status: 403 });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const file = await Deno.readFile(targetPath);
|
|
79
|
+
return new Response(file, {
|
|
80
|
+
status: 200,
|
|
81
|
+
headers: {
|
|
82
|
+
"content-type": mimeType,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (err instanceof Deno.errors.NotFound) {
|
|
87
|
+
return new Response("404 Not Found", { status: 404 });
|
|
88
|
+
}
|
|
89
|
+
return new Response("500 Internal Server Error", { status: 500 });
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const server = Deno.serve(
|
|
94
|
+
listenOptions,
|
|
95
|
+
w3cLogger(mqttOverWS(mqttServer, "/mqtt", requestHandler)),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
await server.finished;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { test } from "node:test";
|
|
3
|
-
import {
|
|
3
|
+
import { createMockConn } from "../dev_utils/mod.ts";
|
|
4
4
|
import { encode, MQTTLevel, PacketType } from "../mqttPacket/mod.ts";
|
|
5
5
|
import { MqttConn, MqttConnError } from "./mqttConn.ts";
|
|
6
6
|
import type { AnyPacket, CodecOpts } from "../mqttPacket/mod.ts";
|
|
@@ -50,7 +50,7 @@ test("MqttConn should act as asyncIterator", async () => {
|
|
|
50
50
|
const publish = encode(publishPacket, codecOptsV4);
|
|
51
51
|
const disconnect = encode(disconnectPacket, codecOptsV4);
|
|
52
52
|
|
|
53
|
-
const conn =
|
|
53
|
+
const conn = createMockConn(
|
|
54
54
|
[connect, publish, disconnect],
|
|
55
55
|
new Uint8Array(),
|
|
56
56
|
);
|
|
@@ -68,7 +68,7 @@ test("MqttConn should act as asyncIterator", async () => {
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
test("MqttConn should close on malformed length", async () => {
|
|
71
|
-
const conn =
|
|
71
|
+
const conn = createMockConn([new Uint8Array([1, 175])], new Uint8Array());
|
|
72
72
|
const mqttConn = new MqttConn({ conn });
|
|
73
73
|
|
|
74
74
|
const packets: AnyPacket[] = [];
|
|
@@ -86,7 +86,7 @@ test("MqttConn should close on failed packets", async () => {
|
|
|
86
86
|
const publish = encode(publishPacket, codecOptsV4);
|
|
87
87
|
const brokenPublish = publish.slice(0, 7);
|
|
88
88
|
|
|
89
|
-
const conn =
|
|
89
|
+
const conn = createMockConn([connect, brokenPublish], new Uint8Array());
|
|
90
90
|
const mqttConn = new MqttConn({ conn });
|
|
91
91
|
|
|
92
92
|
const packets: AnyPacket[] = [];
|
|
@@ -103,7 +103,7 @@ test("MqttConn should close on failed packets", async () => {
|
|
|
103
103
|
test("MqttConn should close on packets too large", async () => {
|
|
104
104
|
const connect = encode(connectPacket, codecOptsV4);
|
|
105
105
|
|
|
106
|
-
const conn =
|
|
106
|
+
const conn = createMockConn([connect], new Uint8Array());
|
|
107
107
|
const mqttConn = new MqttConn({ conn, maxIncomingPacketSize: 20 });
|
|
108
108
|
|
|
109
109
|
const packets: AnyPacket[] = [];
|
|
@@ -119,7 +119,7 @@ test("MqttConn should close on packets too large", async () => {
|
|
|
119
119
|
test("MqttConn should be writable", async () => {
|
|
120
120
|
const connect = encode(connectPacket, codecOptsV4);
|
|
121
121
|
const writer = new Uint8Array(24);
|
|
122
|
-
const conn =
|
|
122
|
+
const conn = createMockConn([connect], writer);
|
|
123
123
|
const mqttConn = new MqttConn({ conn });
|
|
124
124
|
await mqttConn.send(connectPacket);
|
|
125
125
|
assert.deepStrictEqual(writer, connect);
|
package/mqttConn/mqttConn.ts
CHANGED
|
@@ -16,8 +16,7 @@ import {
|
|
|
16
16
|
} from "../mqttPacket/mod.ts";
|
|
17
17
|
|
|
18
18
|
import { assert } from "../utils/mod.ts";
|
|
19
|
-
import type {
|
|
20
|
-
import { Conn } from "../socket/socket.ts";
|
|
19
|
+
import type { ConnNext } from "../connNext/connNext.ts";
|
|
21
20
|
|
|
22
21
|
/**
|
|
23
22
|
* Common MQTT connection error messages
|
|
@@ -45,7 +44,7 @@ export type SendResult =
|
|
|
45
44
|
* @returns Single byte as number
|
|
46
45
|
* @throws Error if EOF reached unexpectedly
|
|
47
46
|
*/
|
|
48
|
-
async function readByte(conn:
|
|
47
|
+
async function readByte(conn: ConnNext): Promise<number> {
|
|
49
48
|
const buff = await conn.read(1);
|
|
50
49
|
assert(buff !== null, MqttConnError.UnexpectedEof);
|
|
51
50
|
assert(buff !== undefined, MqttConnError.UnexpectedEof);
|
|
@@ -59,7 +58,7 @@ async function readByte(conn: Conn): Promise<number> {
|
|
|
59
58
|
* @param buf Buffer to read into
|
|
60
59
|
* @throws Error if EOF reached unexpectedly
|
|
61
60
|
*/
|
|
62
|
-
async function readFull(conn:
|
|
61
|
+
async function readFull(conn: ConnNext, length: number): Promise<Uint8Array> {
|
|
63
62
|
const buff = await conn.read(length);
|
|
64
63
|
assert(buff !== null, MqttConnError.UnexpectedEof);
|
|
65
64
|
assert(buff.byteLength === length, MqttConnError.UnexpectedEof);
|
|
@@ -74,7 +73,7 @@ async function readFull(conn: Conn, length: number): Promise<Uint8Array> {
|
|
|
74
73
|
* @throws Error if packet is invalid or too large
|
|
75
74
|
*/
|
|
76
75
|
export async function readPacket(
|
|
77
|
-
conn:
|
|
76
|
+
conn: ConnNext,
|
|
78
77
|
codecOpts: CodecOpts,
|
|
79
78
|
maxConnectPacketSize: number,
|
|
80
79
|
): Promise<AnyPacket> {
|
|
@@ -113,7 +112,7 @@ export async function readPacket(
|
|
|
113
112
|
*/
|
|
114
113
|
export class MqttConn {
|
|
115
114
|
/** Underlying connection */
|
|
116
|
-
readonly conn:
|
|
115
|
+
readonly conn: ConnNext;
|
|
117
116
|
/** Maximum Connect packet size */
|
|
118
117
|
readonly maxConnectPacketSize: number;
|
|
119
118
|
/** codecOpts */
|
|
@@ -134,10 +133,10 @@ export class MqttConn {
|
|
|
134
133
|
*/
|
|
135
134
|
constructor(
|
|
136
135
|
options:
|
|
137
|
-
& { conn:
|
|
136
|
+
& { conn: ConnNext; maxConnectPacketSize?: number }
|
|
138
137
|
& Partial<CodecOpts>,
|
|
139
138
|
) {
|
|
140
|
-
this.conn =
|
|
139
|
+
this.conn = options.conn;
|
|
141
140
|
this.maxConnectPacketSize = options.maxConnectPacketSize ??
|
|
142
141
|
DEFAULT_MAX_PACKETSIZE;
|
|
143
142
|
this.codecOpts = {
|
|
@@ -154,7 +153,7 @@ export class MqttConn {
|
|
|
154
153
|
return this._reason;
|
|
155
154
|
}
|
|
156
155
|
|
|
157
|
-
/** Get
|
|
156
|
+
/** Get remoteAddress */
|
|
158
157
|
get remoteAddress(): string {
|
|
159
158
|
if (this.conn.remoteAddr?.transport === "tcp") {
|
|
160
159
|
return this.conn.remoteAddr.hostname;
|
|
@@ -14,5 +14,7 @@ export const MQTTLevel = {
|
|
|
14
14
|
v5: 5,
|
|
15
15
|
} as const;
|
|
16
16
|
|
|
17
|
+
/** the MQTT protocol level: 3, 4, 5 or undefined */
|
|
17
18
|
export type ProtocolLevel = (typeof MQTTLevel)[keyof typeof MQTTLevel];
|
|
19
|
+
/** all MQTT protocol levels except 5 */
|
|
18
20
|
export type ProtocolLevelNoV5 = Exclude<ProtocolLevel, 5>;
|
package/node/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Running Opifex server/client on NodeJS
|
|
2
2
|
|
|
3
|
-
These instructions specifify how to run [Opifex](README.md) on NodeJS. To run
|
|
3
|
+
These instructions specifify how to run [Opifex](../README.md) on NodeJS. To run
|
|
4
4
|
Opifex on Deno see the [instructions](../deno/README.md)
|
|
5
5
|
|
|
6
6
|
## Playing around
|
package/node/mod.ts
ADDED
|
@@ -1,59 +1,42 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Socket } from "node:net";
|
|
2
|
+
import type { ConnAddr, ConnNext } from "../connNext/connNext.ts"; // Custom types
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
hostname: string;
|
|
7
|
-
port: number;
|
|
8
|
-
}
|
|
9
|
-
export interface UnixAddr {
|
|
10
|
-
transport: "unix" | "unixpacket";
|
|
11
|
-
path: string;
|
|
12
|
-
}
|
|
13
|
-
export interface VsockAddr {
|
|
14
|
-
transport: "vsock";
|
|
15
|
-
cid: number;
|
|
16
|
-
port: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type SockConn = {
|
|
20
|
-
stream: Duplex;
|
|
21
|
-
close: () => void;
|
|
22
|
-
localAddr?: SockAddr;
|
|
23
|
-
remoteAddr?: SockAddr;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export class Conn {
|
|
27
|
-
private stream: Duplex;
|
|
4
|
+
/** wrapper class */
|
|
5
|
+
export class NodeSocketConnNextWrapper {
|
|
6
|
+
private endPoint: Socket;
|
|
28
7
|
private closed: boolean;
|
|
29
|
-
private closer: () => void;
|
|
30
|
-
public remoteAddr: SockAddr | undefined;
|
|
31
8
|
private leftover: Uint8Array = new Uint8Array(0);
|
|
32
9
|
|
|
33
10
|
// Store bound event listeners so we can cleanly remove them later
|
|
34
|
-
private
|
|
11
|
+
private onClose = () => {
|
|
35
12
|
this.closed = true;
|
|
36
13
|
};
|
|
37
|
-
private
|
|
14
|
+
private onError = () => {
|
|
38
15
|
this.closed = true;
|
|
39
16
|
};
|
|
40
17
|
|
|
41
|
-
|
|
42
|
-
|
|
18
|
+
/** Create a new ConnNext object from a Webstream */
|
|
19
|
+
constructor(endPoint: Socket) {
|
|
20
|
+
this.endPoint = endPoint;
|
|
43
21
|
this.closed = false;
|
|
44
|
-
this.
|
|
45
|
-
this.remoteAddr = sockConn.remoteAddr;
|
|
46
|
-
|
|
47
|
-
// Attach listeners to update internal state on stream termination
|
|
48
|
-
this.stream.on("close", this.onStreamClose);
|
|
49
|
-
this.stream.on("error", this.onStreamError);
|
|
22
|
+
this.endPoint;
|
|
50
23
|
}
|
|
51
24
|
|
|
52
|
-
async read(length
|
|
53
|
-
if (this.closed || length < 0) {
|
|
25
|
+
async read(length?: number): Promise<Uint8Array | null> {
|
|
26
|
+
if (this.closed || (length ?? 0) < 0) {
|
|
54
27
|
return null;
|
|
55
28
|
}
|
|
56
29
|
|
|
30
|
+
// If no specific byte length is requested, read whatever is available
|
|
31
|
+
if (length === undefined) {
|
|
32
|
+
const chunk = this.endPoint.read();
|
|
33
|
+
return chunk
|
|
34
|
+
? Promise.resolve(
|
|
35
|
+
new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength),
|
|
36
|
+
)
|
|
37
|
+
: Promise.resolve(null);
|
|
38
|
+
}
|
|
39
|
+
|
|
57
40
|
if (length === 0) {
|
|
58
41
|
return new Uint8Array(0);
|
|
59
42
|
}
|
|
@@ -71,28 +54,28 @@ export class Conn {
|
|
|
71
54
|
|
|
72
55
|
// 2. Loop until the requested number of bytes is accumulated
|
|
73
56
|
while (bytesAccumulated < length) {
|
|
74
|
-
if (this.closed || !this.
|
|
57
|
+
if (this.closed || !this.endPoint.readable) {
|
|
75
58
|
break;
|
|
76
59
|
}
|
|
77
60
|
|
|
78
|
-
const chunk = this.
|
|
61
|
+
const chunk = this.endPoint.read();
|
|
79
62
|
if (chunk === null) {
|
|
80
63
|
await new Promise<void>((resolve) => {
|
|
81
64
|
const cleanup = () => {
|
|
82
|
-
this.
|
|
83
|
-
this.
|
|
84
|
-
this.
|
|
85
|
-
this.
|
|
65
|
+
this.endPoint.off("readable", onEvent);
|
|
66
|
+
this.endPoint.off("end", onEvent);
|
|
67
|
+
this.endPoint.off("error", onEvent);
|
|
68
|
+
this.endPoint.off("close", onEvent);
|
|
86
69
|
};
|
|
87
70
|
const onEvent = () => {
|
|
88
71
|
cleanup();
|
|
89
72
|
resolve();
|
|
90
73
|
};
|
|
91
74
|
|
|
92
|
-
this.
|
|
93
|
-
this.
|
|
94
|
-
this.
|
|
95
|
-
this.
|
|
75
|
+
this.endPoint.on("readable", onEvent);
|
|
76
|
+
this.endPoint.on("end", onEvent);
|
|
77
|
+
this.endPoint.on("error", onEvent);
|
|
78
|
+
this.endPoint.on("close", onEvent);
|
|
96
79
|
});
|
|
97
80
|
|
|
98
81
|
continue;
|
|
@@ -119,14 +102,14 @@ export class Conn {
|
|
|
119
102
|
return result;
|
|
120
103
|
}
|
|
121
104
|
|
|
122
|
-
write(data: Uint8Array): Promise<
|
|
123
|
-
if (this.closed || !this.
|
|
105
|
+
write(data: Uint8Array): Promise<void> {
|
|
106
|
+
if (this.closed || !this.endPoint.writable) {
|
|
124
107
|
return Promise.reject(new Error("Stream is closed or not writable"));
|
|
125
108
|
}
|
|
126
109
|
|
|
127
110
|
return new Promise((resolve, reject) => {
|
|
128
111
|
// 1. Fire the write command and catch any immediate/async errors via the callback
|
|
129
|
-
const canWriteMore = this.
|
|
112
|
+
const canWriteMore = this.endPoint.write(data, (err) => {
|
|
130
113
|
if (err) {
|
|
131
114
|
reject(err);
|
|
132
115
|
}
|
|
@@ -135,11 +118,11 @@ export class Conn {
|
|
|
135
118
|
// 2. Determine when to resolve based on the return value of write()
|
|
136
119
|
if (canWriteMore) {
|
|
137
120
|
// If the buffer is not full, we can resolve immediately
|
|
138
|
-
resolve(
|
|
121
|
+
resolve();
|
|
139
122
|
} else {
|
|
140
123
|
// If backpressure is triggered, we must wait for the 'drain' event
|
|
141
|
-
this.
|
|
142
|
-
resolve(
|
|
124
|
+
this.endPoint.once("drain", () => {
|
|
125
|
+
resolve();
|
|
143
126
|
});
|
|
144
127
|
}
|
|
145
128
|
});
|
|
@@ -150,25 +133,55 @@ export class Conn {
|
|
|
150
133
|
this.closed = true;
|
|
151
134
|
|
|
152
135
|
// Clean up permanent event listeners to prevent memory leaks
|
|
153
|
-
this.
|
|
154
|
-
this.
|
|
136
|
+
this.endPoint.off("close", this.onClose);
|
|
137
|
+
this.endPoint.off("error", this.onError);
|
|
155
138
|
|
|
156
139
|
// Safely destroy the stream first, then trigger the closer callback
|
|
157
140
|
try {
|
|
158
|
-
this.
|
|
141
|
+
this.endPoint.destroy();
|
|
159
142
|
// deno-coverage-ignore
|
|
160
143
|
} catch (_) {
|
|
161
144
|
// Suppress stream destruction errors
|
|
162
145
|
// deno-coverage-ignore
|
|
163
146
|
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
164
149
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// deno-coverage-ignore
|
|
150
|
+
async *[Symbol.asyncIterator]() {
|
|
151
|
+
while (true) {
|
|
152
|
+
const chunk = await this.read();
|
|
153
|
+
if (chunk === null) {
|
|
154
|
+
break;
|
|
171
155
|
}
|
|
156
|
+
yield chunk;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Convert addresses to custom ConnAddr format
|
|
161
|
+
get localAddr(): ConnAddr | undefined {
|
|
162
|
+
if (this.endPoint.localAddress) {
|
|
163
|
+
return {
|
|
164
|
+
transport: "tcp",
|
|
165
|
+
hostname: this.endPoint.localAddress,
|
|
166
|
+
port: this.endPoint.localPort ?? 0,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
get remoteAddr(): ConnAddr | undefined {
|
|
173
|
+
if (this.endPoint.remoteAddress) {
|
|
174
|
+
return {
|
|
175
|
+
transport: "tcp",
|
|
176
|
+
hostname: this.endPoint.remoteAddress,
|
|
177
|
+
port: this.endPoint.remotePort ?? 0,
|
|
178
|
+
};
|
|
172
179
|
}
|
|
180
|
+
return undefined;
|
|
173
181
|
}
|
|
174
182
|
}
|
|
183
|
+
|
|
184
|
+
export function nodeSocketToNext(socket: Socket): ConnNext {
|
|
185
|
+
const connNext = new NodeSocketConnNextWrapper(socket);
|
|
186
|
+
return connNext;
|
|
187
|
+
}
|
package/node/tcpClient.ts
CHANGED
|
@@ -7,9 +7,9 @@ import { Client } from "../client/client.ts";
|
|
|
7
7
|
import { logger } from "../client/deps.ts";
|
|
8
8
|
import { readFile } from "node:fs/promises";
|
|
9
9
|
import { createConnection } from "node:net";
|
|
10
|
-
import
|
|
11
|
-
import type {
|
|
12
|
-
import {
|
|
10
|
+
import { connect } from "node:tls";
|
|
11
|
+
import type { ConnNext } from "../client/mod.ts";
|
|
12
|
+
import { nodeSocketToNext } from "./nodeSocketToNext.ts";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Fetches data from a file and returns it as a string
|
|
@@ -44,14 +44,17 @@ export class TcpClient extends Client {
|
|
|
44
44
|
* @param hostname the name of the host to connect to
|
|
45
45
|
* @param port the port number
|
|
46
46
|
*/
|
|
47
|
-
protected connectMQTT(
|
|
47
|
+
protected override connectMQTT(
|
|
48
|
+
hostname: string,
|
|
49
|
+
port = 1883,
|
|
50
|
+
): Promise<ConnNext> {
|
|
48
51
|
logger.debug({ hostname, port });
|
|
49
52
|
return new Promise((resolve, reject) => {
|
|
50
53
|
const socket = createConnection(
|
|
51
54
|
{ port, host: hostname },
|
|
52
55
|
() => {
|
|
53
56
|
logger.debug("Connected to server");
|
|
54
|
-
resolve(
|
|
57
|
+
resolve(nodeSocketToNext(socket));
|
|
55
58
|
},
|
|
56
59
|
);
|
|
57
60
|
socket.once("error", (err) => {
|
|
@@ -68,15 +71,15 @@ export class TcpClient extends Client {
|
|
|
68
71
|
* @param caCerts the CA certificates to trust
|
|
69
72
|
* @param cert the clients certificate
|
|
70
73
|
* @param key the clients private key
|
|
71
|
-
* @returns a
|
|
74
|
+
* @returns a ConnNext object
|
|
72
75
|
*/
|
|
73
|
-
protected connectMQTTS(
|
|
76
|
+
protected override connectMQTTS(
|
|
74
77
|
hostname: string,
|
|
75
78
|
port = 8883,
|
|
76
79
|
ca?: string[],
|
|
77
80
|
cert?: string,
|
|
78
81
|
key?: string,
|
|
79
|
-
): Promise<
|
|
82
|
+
): Promise<ConnNext> {
|
|
80
83
|
const opts = {
|
|
81
84
|
host: hostname,
|
|
82
85
|
port,
|
|
@@ -87,9 +90,9 @@ export class TcpClient extends Client {
|
|
|
87
90
|
};
|
|
88
91
|
logger.debug({ hostname, port, ca, cert });
|
|
89
92
|
return new Promise((resolve, reject) => {
|
|
90
|
-
const socket =
|
|
93
|
+
const socket = connect(opts, () => {
|
|
91
94
|
logger.debug("Connected to server");
|
|
92
|
-
resolve(
|
|
95
|
+
resolve(nodeSocketToNext(socket));
|
|
93
96
|
});
|
|
94
97
|
socket.once("error", (err) => {
|
|
95
98
|
logger.debug("Connection failed", err);
|
|
@@ -97,21 +100,4 @@ export class TcpClient extends Client {
|
|
|
97
100
|
});
|
|
98
101
|
});
|
|
99
102
|
}
|
|
100
|
-
|
|
101
|
-
/** createConn is used by the base client class */
|
|
102
|
-
protected override createConn(): Promise<SockConn> {
|
|
103
|
-
const { protocol, hostname, port: portStr } = this.connectUrl;
|
|
104
|
-
const port = Number(portStr);
|
|
105
|
-
const caCerts = this.caCerts;
|
|
106
|
-
const cert = this.cert;
|
|
107
|
-
const key = this.key;
|
|
108
|
-
|
|
109
|
-
if (protocol === "mqtts:") {
|
|
110
|
-
return this.connectMQTTS(hostname, port, caCerts, cert, key);
|
|
111
|
-
}
|
|
112
|
-
if (protocol === "mqtt:") {
|
|
113
|
-
return this.connectMQTT(hostname, port);
|
|
114
|
-
}
|
|
115
|
-
throw new Error(`Unsupported protocol: ${protocol}`);
|
|
116
|
-
}
|
|
117
103
|
}
|