@seriousme/opifex 1.1.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.
Files changed (120) hide show
  1. package/.github/workflows/closeStale.yml +15 -0
  2. package/.github/workflows/codeql-analysis.yml +71 -0
  3. package/.github/workflows/deno-ci.yml +16 -0
  4. package/.github/workflows/nodejs-ci.yml +23 -0
  5. package/.github/workflows/publish.yml +16 -0
  6. package/.vscode/extensions.json +10 -0
  7. package/.vscode/settings.json +12 -0
  8. package/LICENSE.txt +21 -0
  9. package/README.md +34 -0
  10. package/bin/conformanceServer.ts +70 -0
  11. package/bin/demoServer.ts +64 -0
  12. package/bin/mqtt.ts +211 -0
  13. package/client/client.ts +181 -0
  14. package/client/context.ts +197 -0
  15. package/client/deps.ts +36 -0
  16. package/client/handlers/handleConnack.ts +21 -0
  17. package/client/handlers/handlePacket.ts +70 -0
  18. package/client/handlers/handlePuback.ts +10 -0
  19. package/client/handlers/handlePubcomp.ts +13 -0
  20. package/client/handlers/handlePublish.ts +32 -0
  21. package/client/handlers/handlePubrec.ts +24 -0
  22. package/client/handlers/handlePubrel.ts +26 -0
  23. package/client/handlers/handleSuback.ts +14 -0
  24. package/client/handlers/handleUnsuback.ts +11 -0
  25. package/client/mod.ts +11 -0
  26. package/client/store/deps.ts +22 -0
  27. package/client/store/memoryStore.ts +56 -0
  28. package/client/store/store.ts +29 -0
  29. package/deno/README.md +64 -0
  30. package/deno/client.ts +48 -0
  31. package/deno/server.test.ts +14 -0
  32. package/deno/server.ts +31 -0
  33. package/deno/wrapDenoConn.ts +12 -0
  34. package/deno.json +8 -0
  35. package/deno.lock +47 -0
  36. package/dev_utils/dummyConn.ts +95 -0
  37. package/dev_utils/mod.ts +1 -0
  38. package/mqttConn/mqttConn.test.ts +110 -0
  39. package/mqttConn/mqttConn.ts +134 -0
  40. package/mqttPacket/AuthenticationResult.ts +16 -0
  41. package/mqttPacket/BitMask.ts +10 -0
  42. package/mqttPacket/PacketType.ts +21 -0
  43. package/mqttPacket/connack.test.ts +129 -0
  44. package/mqttPacket/connack.ts +37 -0
  45. package/mqttPacket/connect.test.ts +297 -0
  46. package/mqttPacket/connect.ts +159 -0
  47. package/mqttPacket/decoder.test.ts +123 -0
  48. package/mqttPacket/decoder.ts +109 -0
  49. package/mqttPacket/disconnect.test.ts +50 -0
  50. package/mqttPacket/disconnect.ts +21 -0
  51. package/mqttPacket/encoder.test.ts +39 -0
  52. package/mqttPacket/encoder.ts +72 -0
  53. package/mqttPacket/length.test.ts +75 -0
  54. package/mqttPacket/length.ts +57 -0
  55. package/mqttPacket/mod.ts +151 -0
  56. package/mqttPacket/pingreq.test.ts +50 -0
  57. package/mqttPacket/pingreq.ts +22 -0
  58. package/mqttPacket/pingres.test.ts +50 -0
  59. package/mqttPacket/pingres.ts +21 -0
  60. package/mqttPacket/puback.test.ts +55 -0
  61. package/mqttPacket/puback.ts +28 -0
  62. package/mqttPacket/pubcomp.test.ts +55 -0
  63. package/mqttPacket/pubcomp.ts +28 -0
  64. package/mqttPacket/publish.test.ts +198 -0
  65. package/mqttPacket/publish.ts +73 -0
  66. package/mqttPacket/pubrec.test.ts +55 -0
  67. package/mqttPacket/pubrec.ts +28 -0
  68. package/mqttPacket/pubrel.test.ts +55 -0
  69. package/mqttPacket/pubrel.ts +28 -0
  70. package/mqttPacket/suback.test.ts +49 -0
  71. package/mqttPacket/suback.ts +41 -0
  72. package/mqttPacket/subscribe.test.ts +233 -0
  73. package/mqttPacket/subscribe.ts +69 -0
  74. package/mqttPacket/types.ts +27 -0
  75. package/mqttPacket/unsuback.test.ts +55 -0
  76. package/mqttPacket/unsuback.ts +28 -0
  77. package/mqttPacket/unsubscribe.test.ts +136 -0
  78. package/mqttPacket/unsubscribe.ts +48 -0
  79. package/mqttPacket/validators.ts +20 -0
  80. package/node/README.md +46 -0
  81. package/node/client.ts +58 -0
  82. package/node/server.test.ts +14 -0
  83. package/node/server.ts +34 -0
  84. package/node/wrapNodeSocket.ts +54 -0
  85. package/package.json +35 -0
  86. package/persistence/deps.ts +12 -0
  87. package/persistence/memory/memoryPersistence.test.ts +151 -0
  88. package/persistence/memory/memoryPersistence.ts +156 -0
  89. package/persistence/persistence.ts +24 -0
  90. package/persistence/store.ts +14 -0
  91. package/server/context.ts +157 -0
  92. package/server/deps.ts +37 -0
  93. package/server/handlers/handleConnect.ts +73 -0
  94. package/server/handlers/handleDisconnect.ts +6 -0
  95. package/server/handlers/handlePacket.ts +78 -0
  96. package/server/handlers/handlePingreq.ts +8 -0
  97. package/server/handlers/handlePuback.ts +12 -0
  98. package/server/handlers/handlePubcomp.ts +12 -0
  99. package/server/handlers/handlePublish.ts +47 -0
  100. package/server/handlers/handlePubrec.ts +20 -0
  101. package/server/handlers/handlePubrel.ts +24 -0
  102. package/server/handlers/handleSubscribe.ts +47 -0
  103. package/server/handlers/handleUnsubscribe.ts +17 -0
  104. package/server/mod.ts +16 -0
  105. package/server/server.ts +74 -0
  106. package/server/test/handleConnect.test.ts +126 -0
  107. package/server/test/test-handlers.ts +52 -0
  108. package/socket/socket.ts +59 -0
  109. package/timer/timer.test.ts +48 -0
  110. package/timer/timer.ts +45 -0
  111. package/trie/trie.test.ts +147 -0
  112. package/trie/trie.ts +106 -0
  113. package/utils/args.ts +86 -0
  114. package/utils/assert.ts +31 -0
  115. package/utils/asyncQueue.ts +63 -0
  116. package/utils/deferred.ts +12 -0
  117. package/utils/logger.ts +35 -0
  118. package/utils/mod.ts +7 -0
  119. package/utils/nextTick.ts +3 -0
  120. package/utils/platform.ts +4 -0
@@ -0,0 +1,56 @@
1
+ import { assert, type PacketId } from "./deps.ts";
2
+
3
+ import {
4
+ type IStore,
5
+ maxPacketId,
6
+ type PacketStore,
7
+ type PendingAckOutgoing,
8
+ type pendingIncoming,
9
+ type PendingOutgoing,
10
+ type PendingOutgoingPackets,
11
+ } from "./store.ts";
12
+
13
+ export class MemoryStore implements IStore {
14
+ private packetId: PacketId;
15
+
16
+ pendingIncoming: PacketStore<pendingIncoming>;
17
+ pendingOutgoing: PacketStore<PendingOutgoing>;
18
+ pendingAckOutgoing: PacketStore<PendingAckOutgoing>;
19
+
20
+ constructor() {
21
+ this.packetId = 0;
22
+ this.pendingIncoming = new Map();
23
+ this.pendingOutgoing = new Map();
24
+ this.pendingAckOutgoing = new Map();
25
+ }
26
+
27
+ nextId(): PacketId {
28
+ const currentId = this.packetId;
29
+ do {
30
+ this.packetId++;
31
+ if (this.packetId > maxPacketId) {
32
+ this.packetId = 1;
33
+ }
34
+ } while (
35
+ (this.pendingIncoming.has(this.packetId) ||
36
+ this.pendingOutgoing.has(this.packetId) ||
37
+ this.pendingAckOutgoing.has(this.packetId)) &&
38
+ this.packetId !== currentId
39
+ );
40
+ assert(this.packetId !== currentId, "No unused packetId available");
41
+ return this.packetId;
42
+ }
43
+
44
+ async *pendingOutgoingPackets(): AsyncGenerator<
45
+ PendingOutgoingPackets,
46
+ void,
47
+ unknown
48
+ > {
49
+ for (const [_id, packet] of this.pendingAckOutgoing) {
50
+ yield packet;
51
+ }
52
+ for (const [_id, packet] of this.pendingOutgoing) {
53
+ yield packet;
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,29 @@
1
+ import type {
2
+ PacketId,
3
+ PublishPacket,
4
+ PubrelPacket,
5
+ SubscribePacket,
6
+ UnsubscribePacket,
7
+ } from "./deps.ts";
8
+ export const maxPacketId = 0xffff;
9
+
10
+ export type PacketStore<T> = Map<PacketId, T>;
11
+
12
+ export type pendingIncoming = PublishPacket;
13
+ export type PendingOutgoing =
14
+ | PublishPacket
15
+ | SubscribePacket
16
+ | UnsubscribePacket;
17
+ export type PendingAckOutgoing = PubrelPacket;
18
+ export type PendingOutgoingPackets = PendingAckOutgoing | PendingOutgoing;
19
+
20
+ export interface IStore {
21
+ pendingIncoming: PacketStore<pendingIncoming>;
22
+ pendingOutgoing: PacketStore<PendingOutgoing>;
23
+ pendingAckOutgoing: PacketStore<PendingAckOutgoing>;
24
+ pendingOutgoingPackets(): AsyncGenerator<
25
+ PendingOutgoing | PubrelPacket,
26
+ void,
27
+ unknown
28
+ >;
29
+ }
package/deno/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # Running Opifex server/client on Deno
2
+
3
+ These instructions specifify how to run [Opifex](README.md) on Deno. To run
4
+ Opifex on NodeJS see the [instructions](../node/README.md)
5
+
6
+ ## Playing around
7
+
8
+ Make sure you have [Deno](https://deno.land) installed.
9
+
10
+ ### Server
11
+
12
+ ```bash
13
+ deno run https://deno.land/x/opifex/bin/demoServer.ts
14
+ ```
15
+
16
+ On the first invocation Deno will download all dependencies. It will then pop
17
+ the question:
18
+
19
+ ```
20
+ Deno requests net access to ":::1883". Run again with --allow-net to bypass this prompt.
21
+ Allow? [y/n (y = yes allow, n = no deny)]
22
+ ```
23
+
24
+ After you select `yes` you should have a working MQTT server.
25
+
26
+ ### Client
27
+
28
+ ```bash
29
+ deno run https://deno.land/x/opifex/bin/mqtt.ts
30
+ ```
31
+
32
+ On the first invocation Deno will download all dependencies. It will then pop
33
+ the question:
34
+
35
+ ```
36
+ Deno requests net access to "localhost:1883". Run again with --allow-net to bypass this prompt.
37
+ Allow? [y/n (y = yes allow, n = no deny)]
38
+ ```
39
+
40
+ ## Local deployment
41
+
42
+ If you want to use Deno locally then clone the repository, e.g. using:
43
+
44
+ ```bash
45
+ git clone https://github.com/seriousme/opifex.git
46
+ ```
47
+
48
+ and then use:
49
+
50
+ ```bash
51
+ deno run -A bin/demoServer.ts
52
+ ```
53
+
54
+ ```bash
55
+ deno run -A bin/mqtt.ts
56
+ ```
57
+
58
+ ## JSR.io
59
+
60
+ Opifex is also available on [JSR.io](https://jsr.io/@seriousme/opifex).
61
+
62
+ #Platform Native This folder also contains code to run the server using Deno
63
+ sockets instead of NodeJS sockets. You can modify the import statements in the
64
+ demo server/client to use these instead.
package/deno/client.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { Client } from "../client/client.ts";
2
+ import { logger } from "../client/deps.ts";
3
+ import { wrapDenoConn } from "./wrapDenoConn.ts";
4
+ import type { SockConn } from "../client/deps.ts";
5
+
6
+ export async function getCaCerts(filename: string | undefined) {
7
+ if (!filename) {
8
+ return;
9
+ }
10
+ const caCerts = await Deno.readTextFile(filename);
11
+ if (caCerts === "") {
12
+ return;
13
+ }
14
+ return [caCerts];
15
+ }
16
+
17
+ export class TcpClient extends Client {
18
+ protected async connectMQTT(hostname: string, port = 1883) {
19
+ logger.debug({ hostname, port });
20
+ return wrapDenoConn(await Deno.connect({ hostname, port }));
21
+ }
22
+
23
+ protected async connectMQTTS(
24
+ hostname: string,
25
+ port = 8883,
26
+ caCerts?: string[],
27
+ ) {
28
+ logger.debug({ hostname, port, caCerts });
29
+ return wrapDenoConn(await Deno.connectTls({ hostname, port, caCerts }));
30
+ }
31
+
32
+ protected override createConn(
33
+ protocol: string,
34
+ hostname: string,
35
+ port?: number,
36
+ caCerts?: string[],
37
+ ): Promise<SockConn> {
38
+ // if you need to support alternative connection types just
39
+ // overload this method in your subclass
40
+ if (protocol === "mqtts:") {
41
+ return this.connectMQTTS(hostname, port, caCerts);
42
+ }
43
+ if (protocol === "mqtt:") {
44
+ return this.connectMQTT(hostname, port);
45
+ }
46
+ throw `Unsupported protocol: ${protocol}`;
47
+ }
48
+ }
@@ -0,0 +1,14 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { TcpServer } from "./server.ts";
4
+
5
+ test("Deno testServer", () => {
6
+ const server = new TcpServer({ port: 0 }, {});
7
+ server.start();
8
+ assert.deepStrictEqual(
9
+ server.port !== undefined,
10
+ true,
11
+ "server runs a a random port",
12
+ );
13
+ server.stop();
14
+ });
package/deno/server.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { MqttServer } from "../server/mod.ts";
2
+ import type { MqttServerOptions } from "../server/mod.ts";
3
+ import { wrapDenoConn } from "./wrapDenoConn.ts";
4
+
5
+ export class TcpServer {
6
+ private listener: Deno.Listener<Deno.Conn>;
7
+ private mqttServer: MqttServer;
8
+ constructor(
9
+ serverOptions: Deno.TcpListenOptions,
10
+ mqttOptions: MqttServerOptions,
11
+ ) {
12
+ this.listener = Deno.listen(serverOptions);
13
+ this.mqttServer = new MqttServer(mqttOptions);
14
+ }
15
+
16
+ async start() {
17
+ for await (const conn of this.listener) {
18
+ this.mqttServer.serve(wrapDenoConn(conn));
19
+ }
20
+ }
21
+ stop() {
22
+ this.listener.close();
23
+ }
24
+
25
+ get port() {
26
+ if (this.listener.addr.transport === "tcp") {
27
+ return this.listener.addr.port;
28
+ }
29
+ return undefined;
30
+ }
31
+ }
@@ -0,0 +1,12 @@
1
+ import type { SockConn } from "../socket/socket.ts";
2
+
3
+ export function wrapDenoConn(denoConn: Deno.Conn) {
4
+ const conn: SockConn = {
5
+ readable: denoConn.readable,
6
+ writable: denoConn.writable,
7
+ closer: denoConn.close.bind(denoConn),
8
+ localAddr: denoConn.localAddr,
9
+ remoteAddr: denoConn.remoteAddr,
10
+ };
11
+ return conn;
12
+ }
package/deno.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@seriousme/opifex",
3
+ "version": "1.1.0",
4
+ "exports": {
5
+ "./server": "./server/mod.ts",
6
+ "./client": "./client/mod.ts"
7
+ }
8
+ }
package/deno.lock ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "version": "4",
3
+ "specifiers": {
4
+ "jsr:@std/assert@1.0.2": "1.0.2",
5
+ "jsr:@std/internal@^1.0.1": "1.0.1",
6
+ "npm:@types/node@*": "22.5.4",
7
+ "npm:@types/node@^22.8.6": "22.8.6"
8
+ },
9
+ "jsr": {
10
+ "@std/assert@1.0.2": {
11
+ "integrity": "ccacec332958126deaceb5c63ff8b4eaf9f5ed0eac9feccf124110435e59e49c",
12
+ "dependencies": [
13
+ "jsr:@std/internal"
14
+ ]
15
+ },
16
+ "@std/internal@1.0.1": {
17
+ "integrity": "6f8c7544d06a11dd256c8d6ba54b11ed870aac6c5aeafff499892662c57673e6"
18
+ }
19
+ },
20
+ "npm": {
21
+ "@types/node@22.5.4": {
22
+ "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
23
+ "dependencies": [
24
+ "undici-types"
25
+ ]
26
+ },
27
+ "@types/node@22.8.6": {
28
+ "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==",
29
+ "dependencies": [
30
+ "undici-types"
31
+ ]
32
+ },
33
+ "undici-types@6.19.8": {
34
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
35
+ }
36
+ },
37
+ "workspace": {
38
+ "dependencies": [
39
+ "jsr:@std/assert@1.0.2"
40
+ ],
41
+ "packageJson": {
42
+ "dependencies": [
43
+ "npm:@types/node@^22.8.6"
44
+ ]
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,95 @@
1
+ import type { AsyncQueue } from "../utils/mod.ts";
2
+
3
+ class Uint8Writer implements WritableStreamDefaultWriter {
4
+ private buff: Uint8Array;
5
+ private pos: number;
6
+ closed = Promise.resolve(undefined);
7
+ close = () => Promise.resolve();
8
+ abort = () => Promise.resolve();
9
+ ready = Promise.resolve(undefined);
10
+ releaseLock = () => {};
11
+ desiredSize = 20;
12
+
13
+ constructor(
14
+ buff: Uint8Array,
15
+ ) {
16
+ this.buff = buff;
17
+ this.pos = 0;
18
+ }
19
+ write(chunk: Uint8Array): Promise<void> {
20
+ this.buff.set(chunk, this.pos);
21
+ this.pos += chunk.length;
22
+ return Promise.resolve();
23
+ }
24
+ }
25
+
26
+ class Uint8QueuedWriter implements WritableStreamDefaultWriter {
27
+ private queue: AsyncQueue<Uint8Array>;
28
+ closed = Promise.resolve(undefined);
29
+ close = () => Promise.resolve();
30
+ abort = () => Promise.resolve();
31
+ ready = Promise.resolve(undefined);
32
+ releaseLock = () => {};
33
+ desiredSize = 20;
34
+
35
+ constructor(
36
+ queue: AsyncQueue<Uint8Array>,
37
+ ) {
38
+ this.queue = queue;
39
+ }
40
+ write(chunk: Uint8Array): Promise<void> {
41
+ this.queue.push(chunk);
42
+ return Promise.resolve();
43
+ }
44
+ }
45
+
46
+ function ReadableStreamFrom(
47
+ iterable: AsyncIterable<Uint8Array>,
48
+ ): ReadableStream {
49
+ if (!iterable || !(Symbol.asyncIterator in iterable)) {
50
+ throw new TypeError("Argument must be an asyncIterable");
51
+ }
52
+
53
+ return new ReadableStream({
54
+ type: "bytes",
55
+ async pull(controller) {
56
+ const iterator = iterable[Symbol.asyncIterator]();
57
+ const next = await iterator.next();
58
+
59
+ if (next.done) {
60
+ controller.close();
61
+ return;
62
+ }
63
+ controller.enqueue(next.value);
64
+ },
65
+ });
66
+ }
67
+
68
+ export function makeDummySockConn(
69
+ readerBuffs: Uint8Array[],
70
+ writerBuf: Uint8Array,
71
+ closer = () => {},
72
+ ) {
73
+ const readBlob = new Blob(readerBuffs);
74
+ const readable = readBlob.stream();
75
+ const writable = new WritableStream(new Uint8Writer(writerBuf));
76
+ return {
77
+ readable,
78
+ writable,
79
+ closer,
80
+ };
81
+ }
82
+
83
+ export function makeDummyQueueSockConn(
84
+ r: AsyncQueue<Uint8Array>,
85
+ w: AsyncQueue<Uint8Array>,
86
+ closer = () => {},
87
+ ) {
88
+ const readable = ReadableStreamFrom(r);
89
+ const writable = new WritableStream(new Uint8QueuedWriter(w));
90
+ return {
91
+ readable,
92
+ writable,
93
+ closer,
94
+ };
95
+ }
@@ -0,0 +1 @@
1
+ export { makeDummyQueueSockConn, makeDummySockConn } from "./dummyConn.ts";
@@ -0,0 +1,110 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { makeDummySockConn } from "../dev_utils/mod.ts";
4
+ import { type AnyPacket, encode, PacketType } from "../mqttPacket/mod.ts";
5
+ import { MqttConn, MqttConnError } from "./mqttConn.ts";
6
+
7
+ const connectPacket: AnyPacket = {
8
+ type: PacketType.connect,
9
+ protocolName: "MQTT",
10
+ protocolLevel: 4,
11
+ clientId: "testClient",
12
+ clean: true,
13
+ keepAlive: 60,
14
+ username: undefined,
15
+ password: undefined,
16
+ will: undefined,
17
+ };
18
+
19
+ const publishPacket: AnyPacket = {
20
+ type: PacketType.publish,
21
+ topic: "hello",
22
+ payload: new Uint8Array([0x77, 0x6f, 0x72, 0x6c, 0x64]), // "world"
23
+ dup: false,
24
+ retain: false,
25
+ qos: 0,
26
+ id: 0,
27
+ };
28
+
29
+ const disconnectPacket: AnyPacket = {
30
+ type: PacketType.disconnect,
31
+ };
32
+
33
+ test("MqttConn should act as asyncIterator", async () => {
34
+ const connect = encode(connectPacket);
35
+ const publish = encode(publishPacket);
36
+ const disconnect = encode(disconnectPacket);
37
+
38
+ const conn = makeDummySockConn(
39
+ [connect, publish, disconnect],
40
+ new Uint8Array(),
41
+ );
42
+ const mqttConn = new MqttConn({ conn });
43
+
44
+ const packets = [];
45
+ for await (const packet of mqttConn) {
46
+ packets.push(packet);
47
+ }
48
+
49
+ assert.deepStrictEqual(packets.length, 3);
50
+ assert.deepStrictEqual(packets[0], connectPacket);
51
+ assert.deepStrictEqual(packets[1], publishPacket);
52
+ assert.deepStrictEqual(packets[2], disconnectPacket);
53
+ });
54
+
55
+ test("MqttConn should close on malformed length", async () => {
56
+ const conn = makeDummySockConn([new Uint8Array([1, 175])], new Uint8Array());
57
+ const mqttConn = new MqttConn({ conn });
58
+
59
+ const packets = [];
60
+ for await (const packet of mqttConn) {
61
+ packets.push(packet);
62
+ }
63
+
64
+ assert.deepStrictEqual(packets.length, 0);
65
+ assert.deepStrictEqual(mqttConn.isClosed, true);
66
+ assert.deepStrictEqual(mqttConn.reason, MqttConnError.UnexpectedEof);
67
+ });
68
+
69
+ test("MqttConn should close on failed packets", async () => {
70
+ const connect = encode(connectPacket);
71
+ const publish = encode(publishPacket);
72
+ const brokenPublish = publish.slice(0, 7);
73
+
74
+ const conn = makeDummySockConn([connect, brokenPublish], new Uint8Array());
75
+ const mqttConn = new MqttConn({ conn });
76
+
77
+ const packets = [];
78
+ for await (const packet of mqttConn) {
79
+ packets.push(packet);
80
+ }
81
+
82
+ assert.deepStrictEqual(packets.length, 1);
83
+ assert.deepStrictEqual(packets[0], connectPacket);
84
+ assert.deepStrictEqual(mqttConn.isClosed, true);
85
+ assert.deepStrictEqual(mqttConn.reason, MqttConnError.UnexpectedEof);
86
+ });
87
+
88
+ test("MqttConn should close on packets too large", async () => {
89
+ const connect = encode(connectPacket);
90
+
91
+ const conn = makeDummySockConn([connect], new Uint8Array());
92
+ const mqttConn = new MqttConn({ conn, maxPacketSize: 20 });
93
+ const packets = [];
94
+ for await (const packet of mqttConn) {
95
+ packets.push(packet);
96
+ }
97
+
98
+ assert.deepStrictEqual(packets.length, 0);
99
+ assert.deepStrictEqual(mqttConn.isClosed, true);
100
+ assert.deepStrictEqual(mqttConn.reason, MqttConnError.packetTooLarge);
101
+ });
102
+
103
+ test("MqttConn should be writable", async () => {
104
+ const connect = encode(connectPacket);
105
+ const writer = new Uint8Array(24);
106
+ const conn = makeDummySockConn([connect], writer);
107
+ const mqttConn = new MqttConn({ conn });
108
+ await mqttConn.send(connectPacket);
109
+ assert.deepStrictEqual(writer, connect);
110
+ });
@@ -0,0 +1,134 @@
1
+ import {
2
+ type AnyPacket,
3
+ decodePayload,
4
+ encode,
5
+ getLengthDecoder,
6
+ type LengthDecoderResult,
7
+ } from "../mqttPacket/mod.ts";
8
+
9
+ import { assert } from "../utils/mod.ts";
10
+ import type { SockConn } from "../socket/socket.ts";
11
+ import { Conn } from "../socket/socket.ts";
12
+
13
+ export const MqttConnError = {
14
+ invalidPacket: "Invalid Packet",
15
+ packetTooLarge: "Packet too large",
16
+ UnexpectedEof: "Unexpected EOF",
17
+ } as const;
18
+
19
+ export interface IMqttConn extends AsyncIterable<AnyPacket> {
20
+ readonly conn: Conn;
21
+ readonly isClosed: boolean;
22
+ readonly reason: string | undefined;
23
+ [Symbol.asyncIterator](): AsyncIterableIterator<AnyPacket>;
24
+ send(data: AnyPacket): Promise<void>;
25
+ close(): void;
26
+ }
27
+
28
+ async function readByte(conn: Conn): Promise<number> {
29
+ const buf = new Uint8Array(1);
30
+ const bytesRead = await conn.read(buf);
31
+ assert(bytesRead !== null, MqttConnError.UnexpectedEof);
32
+ assert(bytesRead !== 0, MqttConnError.UnexpectedEof);
33
+ return buf[0];
34
+ }
35
+
36
+ async function readFull(conn: Conn, buf: Uint8Array): Promise<void> {
37
+ let bytesRead = 0;
38
+ while (bytesRead < buf.length) {
39
+ const read = await conn.read(buf.subarray(bytesRead));
40
+ assert(read !== null, MqttConnError.UnexpectedEof);
41
+ assert(read !== 0, MqttConnError.UnexpectedEof);
42
+ bytesRead += read;
43
+ }
44
+ }
45
+
46
+ /** Read MQTT packet
47
+ * @throws `Error` if packet is invalid
48
+ */
49
+ export async function readPacket(
50
+ conn: Conn,
51
+ maxPacketSize: number,
52
+ ): Promise<AnyPacket> {
53
+ // fixed header is 1 byte of type + flags
54
+ // + a maximum of 4 bytes to encode the remaining length
55
+ const decodeLength = getLengthDecoder();
56
+ const firstByte = await readByte(conn);
57
+ let result: LengthDecoderResult;
58
+ do {
59
+ const byte = await readByte(conn);
60
+ result = decodeLength(byte);
61
+ } while (!result.done);
62
+
63
+ const remainingLength = result.length;
64
+ assert(remainingLength < maxPacketSize - 1, MqttConnError.packetTooLarge);
65
+ const packetBuf = new Uint8Array(remainingLength);
66
+ // read the rest of the packet
67
+ await readFull(conn, packetBuf);
68
+ const packet = decodePayload(firstByte, packetBuf);
69
+ assert(packet !== null, MqttConnError.UnexpectedEof);
70
+ return packet;
71
+ }
72
+
73
+ export class MqttConn implements IMqttConn {
74
+ readonly conn: Conn;
75
+ private readonly maxPacketSize: number;
76
+ private _reason: string | undefined = undefined;
77
+ private _isClosed = false;
78
+
79
+ constructor({
80
+ conn,
81
+ maxPacketSize,
82
+ }: {
83
+ conn: SockConn;
84
+ maxPacketSize?: number;
85
+ }) {
86
+ this.conn = new Conn(conn);
87
+ this.maxPacketSize = maxPacketSize || 2 * 1024 * 1024;
88
+ }
89
+
90
+ get reason(): string | undefined {
91
+ return this._reason;
92
+ }
93
+
94
+ async *[Symbol.asyncIterator](): AsyncIterableIterator<AnyPacket> {
95
+ while (!this._isClosed) {
96
+ try {
97
+ yield await readPacket(this.conn, this.maxPacketSize);
98
+ } catch (err) {
99
+ if (err instanceof Error) {
100
+ if (err.name === "PartialReadError") {
101
+ err.message = MqttConnError.UnexpectedEof;
102
+ }
103
+ this._reason = err.message;
104
+ }
105
+ // packet too large, malformed packet or connection closed
106
+ this.close();
107
+ break;
108
+ }
109
+ }
110
+ }
111
+
112
+ async send(data: AnyPacket): Promise<void> {
113
+ try {
114
+ await this.conn.write(encode(data));
115
+ } catch {
116
+ this.close();
117
+ }
118
+ }
119
+
120
+ get isClosed(): boolean {
121
+ return this._isClosed;
122
+ }
123
+
124
+ close(): void {
125
+ if (this.isClosed) return;
126
+ try {
127
+ this.conn.close();
128
+ } catch (e) {
129
+ console.error(e);
130
+ } finally {
131
+ this._isClosed = true;
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Possible MQTT authentication results
3
+ */
4
+
5
+ export const AuthenticationResult = {
6
+ ok: 0,
7
+ unacceptableProtocol: 1,
8
+ rejectedUsername: 2,
9
+ serverUnavailable: 3,
10
+ badUsernameOrPassword: 4,
11
+ notAuthorized: 5,
12
+ } as const;
13
+
14
+ export const AuthenticationResultByNumber = Object.fromEntries(
15
+ Object.entries(AuthenticationResult).map(([k, v]) => [v, k]),
16
+ );
@@ -0,0 +1,10 @@
1
+ export const BitMask = {
2
+ bit0: 2 ** 0,
3
+ bit1: 2 ** 1,
4
+ bit2: 2 ** 2,
5
+ bit3: 2 ** 3,
6
+ bit4: 2 ** 4,
7
+ bit5: 2 ** 5,
8
+ bit6: 2 ** 6,
9
+ bit7: 2 ** 7,
10
+ } as const;