@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.
- package/.github/workflows/closeStale.yml +15 -0
- package/.github/workflows/codeql-analysis.yml +71 -0
- package/.github/workflows/deno-ci.yml +16 -0
- package/.github/workflows/nodejs-ci.yml +23 -0
- package/.github/workflows/publish.yml +16 -0
- package/.vscode/extensions.json +10 -0
- package/.vscode/settings.json +12 -0
- package/LICENSE.txt +21 -0
- package/README.md +34 -0
- package/bin/conformanceServer.ts +70 -0
- package/bin/demoServer.ts +64 -0
- package/bin/mqtt.ts +211 -0
- package/client/client.ts +181 -0
- package/client/context.ts +197 -0
- package/client/deps.ts +36 -0
- package/client/handlers/handleConnack.ts +21 -0
- package/client/handlers/handlePacket.ts +70 -0
- package/client/handlers/handlePuback.ts +10 -0
- package/client/handlers/handlePubcomp.ts +13 -0
- package/client/handlers/handlePublish.ts +32 -0
- package/client/handlers/handlePubrec.ts +24 -0
- package/client/handlers/handlePubrel.ts +26 -0
- package/client/handlers/handleSuback.ts +14 -0
- package/client/handlers/handleUnsuback.ts +11 -0
- package/client/mod.ts +11 -0
- package/client/store/deps.ts +22 -0
- package/client/store/memoryStore.ts +56 -0
- package/client/store/store.ts +29 -0
- package/deno/README.md +64 -0
- package/deno/client.ts +48 -0
- package/deno/server.test.ts +14 -0
- package/deno/server.ts +31 -0
- package/deno/wrapDenoConn.ts +12 -0
- package/deno.json +8 -0
- package/deno.lock +47 -0
- package/dev_utils/dummyConn.ts +95 -0
- package/dev_utils/mod.ts +1 -0
- package/mqttConn/mqttConn.test.ts +110 -0
- package/mqttConn/mqttConn.ts +134 -0
- package/mqttPacket/AuthenticationResult.ts +16 -0
- package/mqttPacket/BitMask.ts +10 -0
- package/mqttPacket/PacketType.ts +21 -0
- package/mqttPacket/connack.test.ts +129 -0
- package/mqttPacket/connack.ts +37 -0
- package/mqttPacket/connect.test.ts +297 -0
- package/mqttPacket/connect.ts +159 -0
- package/mqttPacket/decoder.test.ts +123 -0
- package/mqttPacket/decoder.ts +109 -0
- package/mqttPacket/disconnect.test.ts +50 -0
- package/mqttPacket/disconnect.ts +21 -0
- package/mqttPacket/encoder.test.ts +39 -0
- package/mqttPacket/encoder.ts +72 -0
- package/mqttPacket/length.test.ts +75 -0
- package/mqttPacket/length.ts +57 -0
- package/mqttPacket/mod.ts +151 -0
- package/mqttPacket/pingreq.test.ts +50 -0
- package/mqttPacket/pingreq.ts +22 -0
- package/mqttPacket/pingres.test.ts +50 -0
- package/mqttPacket/pingres.ts +21 -0
- package/mqttPacket/puback.test.ts +55 -0
- package/mqttPacket/puback.ts +28 -0
- package/mqttPacket/pubcomp.test.ts +55 -0
- package/mqttPacket/pubcomp.ts +28 -0
- package/mqttPacket/publish.test.ts +198 -0
- package/mqttPacket/publish.ts +73 -0
- package/mqttPacket/pubrec.test.ts +55 -0
- package/mqttPacket/pubrec.ts +28 -0
- package/mqttPacket/pubrel.test.ts +55 -0
- package/mqttPacket/pubrel.ts +28 -0
- package/mqttPacket/suback.test.ts +49 -0
- package/mqttPacket/suback.ts +41 -0
- package/mqttPacket/subscribe.test.ts +233 -0
- package/mqttPacket/subscribe.ts +69 -0
- package/mqttPacket/types.ts +27 -0
- package/mqttPacket/unsuback.test.ts +55 -0
- package/mqttPacket/unsuback.ts +28 -0
- package/mqttPacket/unsubscribe.test.ts +136 -0
- package/mqttPacket/unsubscribe.ts +48 -0
- package/mqttPacket/validators.ts +20 -0
- package/node/README.md +46 -0
- package/node/client.ts +58 -0
- package/node/server.test.ts +14 -0
- package/node/server.ts +34 -0
- package/node/wrapNodeSocket.ts +54 -0
- package/package.json +35 -0
- package/persistence/deps.ts +12 -0
- package/persistence/memory/memoryPersistence.test.ts +151 -0
- package/persistence/memory/memoryPersistence.ts +156 -0
- package/persistence/persistence.ts +24 -0
- package/persistence/store.ts +14 -0
- package/server/context.ts +157 -0
- package/server/deps.ts +37 -0
- package/server/handlers/handleConnect.ts +73 -0
- package/server/handlers/handleDisconnect.ts +6 -0
- package/server/handlers/handlePacket.ts +78 -0
- package/server/handlers/handlePingreq.ts +8 -0
- package/server/handlers/handlePuback.ts +12 -0
- package/server/handlers/handlePubcomp.ts +12 -0
- package/server/handlers/handlePublish.ts +47 -0
- package/server/handlers/handlePubrec.ts +20 -0
- package/server/handlers/handlePubrel.ts +24 -0
- package/server/handlers/handleSubscribe.ts +47 -0
- package/server/handlers/handleUnsubscribe.ts +17 -0
- package/server/mod.ts +16 -0
- package/server/server.ts +74 -0
- package/server/test/handleConnect.test.ts +126 -0
- package/server/test/test-handlers.ts +52 -0
- package/socket/socket.ts +59 -0
- package/timer/timer.test.ts +48 -0
- package/timer/timer.ts +45 -0
- package/trie/trie.test.ts +147 -0
- package/trie/trie.ts +106 -0
- package/utils/args.ts +86 -0
- package/utils/assert.ts +31 -0
- package/utils/asyncQueue.ts +63 -0
- package/utils/deferred.ts +12 -0
- package/utils/logger.ts +35 -0
- package/utils/mod.ts +7 -0
- package/utils/nextTick.ts +3 -0
- 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
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
|
+
}
|
package/dev_utils/mod.ts
ADDED
|
@@ -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
|
+
);
|