@seriousme/opifex 1.6.1 → 1.6.3
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/dependabot.yml +7 -0
- package/.github/workflows/closeStale.yml +1 -1
- package/.github/workflows/codeql-analysis.yml +4 -4
- package/CHANGELOG.md +12 -0
- package/README.md +6 -7
- package/bin/conformanceServer.ts +67 -16
- package/bin/demoServer.ts +69 -14
- package/bin/mqtt.ts +30 -0
- package/client/ConnectionState.ts +12 -0
- package/client/client.ts +56 -2
- package/client/context.ts +5 -0
- package/client/handlers/handleConnack.ts +6 -0
- package/client/handlers/handlePacket.ts +7 -0
- package/client/handlers/handlePuback.ts +8 -2
- package/client/handlers/handlePubcomp.ts +10 -4
- package/client/handlers/handlePublish.ts +6 -2
- package/client/handlers/handlePubrec.ts +13 -6
- package/client/handlers/handlePubrel.ts +16 -6
- package/client/handlers/handleSuback.ts +9 -6
- package/client/handlers/handleUnsuback.ts +10 -3
- package/client/store/memoryStore.ts +25 -0
- package/client/store/store.ts +39 -0
- package/deno/tcpClient.ts +19 -2
- package/deno/tcpServer.test.ts +137 -2
- package/deno/tcpServer.ts +9 -2
- package/deno.json +2 -1
- package/dist/bin/conformanceServer.js +61 -15
- package/dist/bin/demoServer.js +63 -13
- package/dist/bin/mqtt.js +29 -0
- package/dist/client/ConnectionState.js +9 -0
- package/dist/client/client.js +56 -1
- package/dist/client/context.js +4 -0
- package/dist/client/handlers/handleConnack.js +6 -0
- package/dist/client/handlers/handlePacket.js +7 -0
- package/dist/client/handlers/handlePuback.js +8 -1
- package/dist/client/handlers/handlePubcomp.js +10 -3
- package/dist/client/handlers/handlePublish.js +6 -1
- package/dist/client/handlers/handlePubrec.js +13 -4
- package/dist/client/handlers/handlePubrel.js +16 -4
- package/dist/client/handlers/handleSuback.js +9 -4
- package/dist/client/handlers/handleUnsuback.js +10 -2
- package/dist/client/store/memoryStore.js +22 -0
- package/dist/client/store/store.js +3 -0
- package/dist/mqttPacket/AuthenticationResult.js +3 -0
- package/dist/mqttPacket/mod.js +5 -0
- package/dist/node/tcpClient.js +19 -2
- package/dist/node/tcpServer.js +12 -2
- package/dist/node/wrapNodeSocket.js +9 -0
- package/dist/persistence/memory/memoryPersistence.js +1 -0
- package/dist/server/handlers/handleConnect.js +39 -5
- package/dist/server/handlers/handleDisconnect.js +5 -0
- package/dist/server/handlers/handlePacket.js +7 -0
- package/dist/server/handlers/handlePingreq.js +5 -0
- package/dist/server/handlers/handlePuback.js +9 -1
- package/dist/server/handlers/handlePubcomp.js +8 -2
- package/dist/server/handlers/handlePublish.js +13 -0
- package/dist/server/handlers/handlePubrec.js +11 -3
- package/dist/server/handlers/handlePubrel.js +12 -3
- package/dist/server/handlers/handleSubscribe.js +27 -4
- package/dist/server/handlers/handleUnsubscribe.js +6 -0
- package/dist/utils/args.js +14 -0
- package/dist/utils/assert.js +1 -0
- package/dist/utils/asyncQueue.js +13 -0
- package/dist/utils/deferred.js +3 -0
- package/dist/utils/logger.js +29 -0
- package/dist/utils/mod.js +4 -0
- package/dist/utils/nextTick.js +15 -0
- package/dist/utils/platform.js +3 -0
- package/mqttPacket/AuthenticationResult.ts +3 -1
- package/mqttPacket/connack.ts +4 -0
- package/mqttPacket/connect.ts +3 -0
- package/mqttPacket/disconnect.ts +4 -0
- package/mqttPacket/mod.ts +9 -0
- package/mqttPacket/pingreq.ts +3 -0
- package/mqttPacket/pingres.ts +4 -0
- package/mqttPacket/puback.ts +3 -0
- package/mqttPacket/pubcomp.ts +4 -0
- package/mqttPacket/publish.ts +4 -0
- package/mqttPacket/pubrec.ts +4 -0
- package/mqttPacket/pubrel.ts +4 -0
- package/mqttPacket/suback.ts +4 -0
- package/mqttPacket/subscribe.ts +6 -0
- package/mqttPacket/types.ts +32 -1
- package/mqttPacket/unsuback.ts +4 -0
- package/mqttPacket/unsubscribe.ts +3 -0
- package/node/tcpClient.ts +20 -2
- package/node/tcpServer.test.ts +61 -6
- package/node/tcpServer.ts +17 -4
- package/node/wrapNodeSocket.ts +13 -2
- package/package.json +2 -2
- package/persistence/memory/memoryPersistence.test.ts +2 -1
- package/persistence/memory/memoryPersistence.ts +1 -0
- package/persistence/store.ts +1 -0
- package/server/handlers/handleConnect.ts +49 -6
- package/server/handlers/handleDisconnect.ts +5 -0
- package/server/handlers/handlePacket.ts +7 -0
- package/server/handlers/handlePingreq.ts +6 -0
- package/server/handlers/handlePuback.ts +9 -2
- package/server/handlers/handlePubcomp.ts +8 -3
- package/server/handlers/handlePublish.ts +13 -0
- package/server/handlers/handlePubrec.ts +11 -3
- package/server/handlers/handlePubrel.ts +12 -4
- package/server/handlers/handleSubscribe.ts +28 -5
- package/server/handlers/handleUnsubscribe.ts +6 -0
- package/utils/args.ts +17 -0
- package/utils/assert.ts +1 -0
- package/utils/asyncQueue.ts +13 -1
- package/utils/deferred.ts +4 -0
- package/utils/logger.ts +29 -0
- package/utils/mod.ts +4 -0
- package/utils/nextTick.ts +15 -0
- package/utils/platform.ts +4 -0
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import type { Context } from "../context.ts";
|
|
2
2
|
import { PacketType, type PubrelPacket } from "../deps.ts";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Handles a PUBREL packet in the MQTT QoS 2 flow
|
|
6
|
+
* It is the third packet of the QoS 2 protocol exchange
|
|
7
|
+
* and a response to the PUBREC packet
|
|
8
|
+
*
|
|
9
|
+
* It uses Method A from the MQTT specification
|
|
10
|
+
* @param ctx - The MQTT client context
|
|
11
|
+
* @param packet - The PUBREL packet received
|
|
12
|
+
* @description
|
|
13
|
+
* When a PUBREL packet is received:
|
|
14
|
+
* 1. If there is a matching pending incoming message:
|
|
15
|
+
* - Delivers the stored message to the application
|
|
16
|
+
* - Removes the message from pending incoming store
|
|
17
|
+
* - Sends PUBCOMP packet as acknowledgement
|
|
18
|
+
* 2. If no matching message found, silently ignores
|
|
19
|
+
*/
|
|
10
20
|
export async function handlePubrel(
|
|
11
21
|
ctx: Context,
|
|
12
22
|
packet: PubrelPacket,
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { Context } from "../context.ts";
|
|
2
2
|
import type { SubackPacket } from "../deps.ts";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Handles SUBACK packets received from the server
|
|
6
|
+
* @param ctx - The MQTT client context
|
|
7
|
+
* @param packet - The SUBACK packet containing subscription confirmations
|
|
8
|
+
* @description
|
|
9
|
+
* - Processes SUBACK packets sent by server to confirm SUBSCRIBE requests
|
|
10
|
+
* - Removes the packet ID from pending outgoing messages
|
|
11
|
+
* - Notifies the client about granted QoS levels for each subscription
|
|
12
|
+
*/
|
|
10
13
|
export function handleSuback(ctx: Context, packet: SubackPacket): void {
|
|
11
14
|
const id = packet.id;
|
|
12
15
|
ctx.store.pendingOutgoing.delete(id);
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type { Context } from "../context.ts";
|
|
2
2
|
import type { UnsubackPacket } from "../deps.ts";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Handles the UNSUBACK packet received from the server
|
|
6
|
+
* @param ctx - The MQTT client context
|
|
7
|
+
* @param packet - The UNSUBACK packet received from the server
|
|
8
|
+
* @description
|
|
9
|
+
* The UNSUBACK Packet is sent by the Server to the Client to confirm receipt of
|
|
10
|
+
* an UNSUBSCRIBE Packet.
|
|
11
|
+
* When received, it removes the pending outgoing request and completes the corresponding
|
|
12
|
+
* unsubscribe operation.
|
|
13
|
+
*/
|
|
7
14
|
export function handleUnsuback(ctx: Context, packet: UnsubackPacket): void {
|
|
8
15
|
const id = packet.id;
|
|
9
16
|
ctx.store.pendingOutgoing.delete(id);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module memoryStore
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { assert, type PacketId } from "./deps.ts";
|
|
2
6
|
|
|
3
7
|
import {
|
|
@@ -10,13 +14,25 @@ import {
|
|
|
10
14
|
type PendingOutgoingPackets,
|
|
11
15
|
} from "./store.ts";
|
|
12
16
|
|
|
17
|
+
/**
|
|
18
|
+
* In-memory implementation of the IStore interface for managing MQTT packet storage
|
|
19
|
+
*/
|
|
13
20
|
export class MemoryStore implements IStore {
|
|
21
|
+
/** Current packet ID counter */
|
|
14
22
|
private packetId: PacketId;
|
|
15
23
|
|
|
24
|
+
/** Map of pending incoming packets */
|
|
16
25
|
pendingIncoming: PacketStore<pendingIncoming>;
|
|
26
|
+
|
|
27
|
+
/** Map of pending outgoing packets */
|
|
17
28
|
pendingOutgoing: PacketStore<PendingOutgoing>;
|
|
29
|
+
|
|
30
|
+
/** Map of pending acknowledgment outgoing packets */
|
|
18
31
|
pendingAckOutgoing: PacketStore<PendingAckOutgoing>;
|
|
19
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new MemoryStore instance
|
|
35
|
+
*/
|
|
20
36
|
constructor() {
|
|
21
37
|
this.packetId = 0;
|
|
22
38
|
this.pendingIncoming = new Map();
|
|
@@ -24,6 +40,11 @@ export class MemoryStore implements IStore {
|
|
|
24
40
|
this.pendingAckOutgoing = new Map();
|
|
25
41
|
}
|
|
26
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Generates the next available packet ID
|
|
45
|
+
* @returns {PacketId} The next available packet ID
|
|
46
|
+
* @throws {Error} If no unused packet ID is available
|
|
47
|
+
*/
|
|
27
48
|
nextId(): PacketId {
|
|
28
49
|
const currentId = this.packetId;
|
|
29
50
|
do {
|
|
@@ -41,6 +62,10 @@ export class MemoryStore implements IStore {
|
|
|
41
62
|
return this.packetId;
|
|
42
63
|
}
|
|
43
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Asynchronously yields all pending outgoing packets
|
|
67
|
+
* @yields {PendingOutgoingPackets} Pending outgoing packets
|
|
68
|
+
*/
|
|
44
69
|
async *pendingOutgoingPackets(): AsyncGenerator<
|
|
45
70
|
PendingOutgoingPackets,
|
|
46
71
|
void,
|
package/client/store/store.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module MQTT Store Types
|
|
3
|
+
*/
|
|
1
4
|
import type {
|
|
2
5
|
PacketId,
|
|
3
6
|
PublishPacket,
|
|
@@ -5,22 +8,58 @@ import type {
|
|
|
5
8
|
SubscribePacket,
|
|
6
9
|
UnsubscribePacket,
|
|
7
10
|
} from "./deps.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Maximum value for MQTT packet IDs (65535)
|
|
14
|
+
*/
|
|
8
15
|
export const maxPacketId = 0xffff;
|
|
9
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Generic type for storing MQTT packets by their packet ID
|
|
19
|
+
* @template T The packet type to store
|
|
20
|
+
*/
|
|
10
21
|
export type PacketStore<T> = Map<PacketId, T>;
|
|
11
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Type for incoming PUBLISH packets that are pending processing
|
|
25
|
+
*/
|
|
12
26
|
export type pendingIncoming = PublishPacket;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Type for outgoing packets (PUBLISH, SUBSCRIBE, UNSUBSCRIBE) that are pending sending
|
|
30
|
+
*/
|
|
13
31
|
export type PendingOutgoing =
|
|
14
32
|
| PublishPacket
|
|
15
33
|
| SubscribePacket
|
|
16
34
|
| UnsubscribePacket;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Type for outgoing PUBREL packets that are pending acknowledgement
|
|
38
|
+
*/
|
|
17
39
|
export type PendingAckOutgoing = PubrelPacket;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Union type of all pending outgoing packet types
|
|
43
|
+
*/
|
|
18
44
|
export type PendingOutgoingPackets = PendingAckOutgoing | PendingOutgoing;
|
|
19
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Interface defining the storage requirements for MQTT packet state management
|
|
48
|
+
*/
|
|
20
49
|
export interface IStore {
|
|
50
|
+
/** Store for incoming packets pending processing */
|
|
21
51
|
pendingIncoming: PacketStore<pendingIncoming>;
|
|
52
|
+
|
|
53
|
+
/** Store for outgoing packets pending sending */
|
|
22
54
|
pendingOutgoing: PacketStore<PendingOutgoing>;
|
|
55
|
+
|
|
56
|
+
/** Store for outgoing packets pending acknowledgement */
|
|
23
57
|
pendingAckOutgoing: PacketStore<PendingAckOutgoing>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generator that yields pending outgoing packets
|
|
61
|
+
* @returns AsyncGenerator that yields PendingOutgoing or PubrelPacket packets
|
|
62
|
+
*/
|
|
24
63
|
pendingOutgoingPackets(): AsyncGenerator<
|
|
25
64
|
PendingOutgoing | PubrelPacket,
|
|
26
65
|
void,
|
package/deno/tcpClient.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* This a Deno specific implementation of TCP client sockets
|
|
3
|
+
* it extends the platform agnostic Client class
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
3
6
|
import { Client } from "../client/client.ts";
|
|
4
7
|
import { logger } from "../client/deps.ts";
|
|
5
8
|
import type { SockConn } from "../client/deps.ts";
|
|
6
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @function getFileData
|
|
12
|
+
* @param filename
|
|
13
|
+
* @returns Promise
|
|
14
|
+
*
|
|
15
|
+
* Fetches data from a file and returns it as a string
|
|
16
|
+
* @example
|
|
17
|
+
* const data = await getFileData("data.txt");
|
|
18
|
+
*/
|
|
7
19
|
export async function getFileData(
|
|
8
20
|
filename: string | undefined,
|
|
9
21
|
): Promise<string | undefined> {
|
|
@@ -17,6 +29,11 @@ export async function getFileData(
|
|
|
17
29
|
return data;
|
|
18
30
|
}
|
|
19
31
|
|
|
32
|
+
/*
|
|
33
|
+
* TCPclient extends the Client class to provide TCP based clients
|
|
34
|
+
* it is used by the MQTTclient to connect to the broker
|
|
35
|
+
* see mqtt.ts in the /bin folder as an example
|
|
36
|
+
*/
|
|
20
37
|
export class TcpClient extends Client {
|
|
21
38
|
protected async connectMQTT(
|
|
22
39
|
hostname: string,
|
package/deno/tcpServer.test.ts
CHANGED
|
@@ -1,14 +1,149 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
1
|
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { TcpClient } from "./tcpClient.ts";
|
|
3
4
|
import { TcpServer } from "./tcpServer.ts";
|
|
5
|
+
import { logger, LogLevel } from "../utils/mod.ts";
|
|
6
|
+
import type { PublishPacket, QoS } from "../mqttPacket/mod.ts";
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
logger.level(LogLevel.info);
|
|
9
|
+
export function sleep(ms: number): Promise<unknown> {
|
|
10
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test("Deno Test pubSub using client and server", async function () {
|
|
6
14
|
const server = new TcpServer({ port: 0 }, {});
|
|
7
15
|
server.start();
|
|
16
|
+
|
|
8
17
|
assert.deepStrictEqual(
|
|
9
18
|
server.port !== undefined,
|
|
10
19
|
true,
|
|
11
20
|
"server runs a a random port",
|
|
12
21
|
);
|
|
22
|
+
logger.verbose({ port: server.port, address: server.address });
|
|
23
|
+
|
|
24
|
+
const params = {
|
|
25
|
+
url: new URL(`mqtt://${server.address}:${server.port}`),
|
|
26
|
+
numberOfRetries: 0,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const client = new TcpClient();
|
|
30
|
+
|
|
31
|
+
await client.connect(params);
|
|
32
|
+
assert(true, "Client connected to server");
|
|
33
|
+
|
|
34
|
+
const publishSet: { topic: string; qos: QoS }[] = [
|
|
35
|
+
{ topic: "t0@q0", qos: 0 },
|
|
36
|
+
{ topic: "t1@q0", qos: 0 },
|
|
37
|
+
{ topic: "t2@q0", qos: 0 },
|
|
38
|
+
{ topic: "t0@q1", qos: 1 },
|
|
39
|
+
{ topic: "t1@q1", qos: 1 },
|
|
40
|
+
{ topic: "t2@q1", qos: 1 },
|
|
41
|
+
{ topic: "t0@q2", qos: 2 },
|
|
42
|
+
{ topic: "t1@q2", qos: 2 },
|
|
43
|
+
{ topic: "t2@q2", qos: 2 },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const subscriptions = publishSet.map((item) => ({
|
|
47
|
+
topicFilter: item.topic,
|
|
48
|
+
qos: item.qos,
|
|
49
|
+
}));
|
|
50
|
+
await client.subscribe({
|
|
51
|
+
subscriptions,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// the IIFE ensures message reception runs in parallel
|
|
55
|
+
logger.verbose(`Start receiving`);
|
|
56
|
+
const received: PublishPacket[] = [];
|
|
57
|
+
(async function () {
|
|
58
|
+
for await (const item of client.messages()) {
|
|
59
|
+
logger.verbose(`Receiving: ${item.topic} -- ${item.qos}`);
|
|
60
|
+
received.push(item);
|
|
61
|
+
}
|
|
62
|
+
})();
|
|
63
|
+
// end of IIFE
|
|
64
|
+
for (const item of publishSet) {
|
|
65
|
+
logger.verbose(`Publishing: ${item.topic} -- ${item.qos}`);
|
|
66
|
+
await client.publish({
|
|
67
|
+
topic: item.topic,
|
|
68
|
+
qos: item.qos,
|
|
69
|
+
payload: new Uint8Array([0x01]),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await sleep(100);
|
|
74
|
+
logger.verbose(`Disconnect client`);
|
|
75
|
+
//client.closeMessages();
|
|
76
|
+
await client.disconnect();
|
|
77
|
+
|
|
78
|
+
logger.verbose(`Check completeness`);
|
|
79
|
+
for (const item of publishSet) {
|
|
80
|
+
const found = received.find((f) =>
|
|
81
|
+
f.topic == item.topic && f.qos === item.qos
|
|
82
|
+
);
|
|
83
|
+
logger.verbose(`Found: ${item.topic} -- ${item.qos}`);
|
|
84
|
+
assert(found, `${item.topic} -- ${item.qos}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
logger.verbose(`Stop server`);
|
|
88
|
+
server.stop();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("Deno Test subscription persistence after reconnect", async function () {
|
|
92
|
+
// Start server
|
|
93
|
+
const server = new TcpServer({ port: 0 }, {});
|
|
94
|
+
server.start();
|
|
95
|
+
|
|
96
|
+
const params = {
|
|
97
|
+
url: new URL(`mqtt://${server.address}:${server.port}`),
|
|
98
|
+
numberOfRetries: 0,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const client = new TcpClient();
|
|
102
|
+
const testTopic = "test/topic";
|
|
103
|
+
const received: PublishPacket[] = [];
|
|
104
|
+
|
|
105
|
+
// First connection and subscription
|
|
106
|
+
await client.connect(params);
|
|
107
|
+
await client.subscribe({
|
|
108
|
+
subscriptions: [{
|
|
109
|
+
topicFilter: testTopic,
|
|
110
|
+
qos: 0,
|
|
111
|
+
}],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Start receiving messages
|
|
115
|
+
(async function () {
|
|
116
|
+
for await (const item of client.messages()) {
|
|
117
|
+
received.push(item);
|
|
118
|
+
}
|
|
119
|
+
})();
|
|
120
|
+
|
|
121
|
+
// Disconnect client
|
|
122
|
+
await client.disconnect();
|
|
123
|
+
await sleep(100);
|
|
124
|
+
|
|
125
|
+
// Reconnect client
|
|
126
|
+
await client.connect(params);
|
|
127
|
+
await sleep(100);
|
|
128
|
+
|
|
129
|
+
// Publish test message
|
|
130
|
+
await client.publish({
|
|
131
|
+
topic: testTopic,
|
|
132
|
+
qos: 0,
|
|
133
|
+
payload: new Uint8Array([0x01]),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await sleep(100);
|
|
137
|
+
logger.verbose(`Disconnect client`);
|
|
138
|
+
await client.disconnect();
|
|
139
|
+
|
|
140
|
+
// Verify message was received
|
|
141
|
+
assert.equal(received.length, 1, "Should receive one message");
|
|
142
|
+
assert.equal(
|
|
143
|
+
received[0].topic,
|
|
144
|
+
testTopic,
|
|
145
|
+
"Should receive message on subscribed topic",
|
|
146
|
+
);
|
|
147
|
+
|
|
13
148
|
server.stop();
|
|
14
149
|
});
|
package/deno/tcpServer.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* This module provides a Deno specific implementation of a TCP socket listener
|
|
3
|
+
* it uses the platform agnostic MqttServer class
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
3
7
|
import { MqttServer } from "../server/mod.ts";
|
|
4
8
|
import type { MqttServerOptions } from "../server/mod.ts";
|
|
5
9
|
|
|
10
|
+
/*
|
|
11
|
+
* TCP server that wraps a MqttServer, see demoServer.ts in the /bin folder
|
|
12
|
+
*/
|
|
6
13
|
export class TcpServer {
|
|
7
14
|
private listener: Deno.Listener<Deno.Conn>;
|
|
8
15
|
private mqttServer: MqttServer;
|
package/deno.json
CHANGED
|
@@ -1,45 +1,91 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
2
|
// this server is intended to be used with https://github.com/eclipse/iottestware
|
|
3
|
+
/**
|
|
4
|
+
* MQTT Server implementation with authentication and authorization
|
|
5
|
+
* @module mqttServer
|
|
6
|
+
*/
|
|
7
|
+
import { TcpServer } from "../node/tcpServer.js";
|
|
3
8
|
import { AuthenticationResult } from "../server/mod.js";
|
|
4
9
|
import { getArgs, logger, LogLevel, parseArgs } from "../utils/mod.js";
|
|
5
|
-
|
|
10
|
+
/**
|
|
11
|
+
* UTF-8 decoder for parsing passwords
|
|
12
|
+
*/
|
|
6
13
|
const utf8Decoder = new TextDecoder();
|
|
14
|
+
/**
|
|
15
|
+
* Map storing valid username/password combinations
|
|
16
|
+
*/
|
|
7
17
|
const userTable = new Map();
|
|
8
18
|
userTable.set("IoTester_1", "strong_password");
|
|
9
19
|
userTable.set("IoTester_2", "strong_password");
|
|
20
|
+
/**
|
|
21
|
+
* Regular expression for validating usernames
|
|
22
|
+
* Allows alphanumeric characters up to 23 chars long
|
|
23
|
+
*/
|
|
10
24
|
const strictUsername = new RegExp(/^[a-zA-Z0-9]{0,23}$/);
|
|
11
|
-
|
|
12
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Flag to enable/disable username checking
|
|
27
|
+
* If false, all users are allowed access
|
|
28
|
+
* If true, only users in userTable are allowed access
|
|
29
|
+
*/
|
|
30
|
+
const checkUsername = true;
|
|
31
|
+
/**
|
|
32
|
+
* Authenticates MQTT clients
|
|
33
|
+
* @param {Context} _ctx - Connection context
|
|
34
|
+
* @param {string} clientId - Client identifier
|
|
35
|
+
* @param {string} username - Username to authenticate
|
|
36
|
+
* @param {Uint8Array} password - Password as byte array
|
|
37
|
+
* @returns {TAuthenticationResult} Authentication result
|
|
38
|
+
*/
|
|
13
39
|
function isAuthenticated(_ctx, clientId, username, password) {
|
|
14
40
|
const pwd = utf8Decoder.decode(password);
|
|
15
|
-
logger.
|
|
16
|
-
if (!
|
|
17
|
-
|
|
18
|
-
return AuthenticationResult.badUsernameOrPassword;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
const pass = userTable.get(username);
|
|
22
|
-
if (pwd === pass) {
|
|
41
|
+
logger.info(`Verifying authentication of client '${clientId}' with username '${username}' and password '${pwd}'`);
|
|
42
|
+
if (!checkUsername) {
|
|
43
|
+
// allow all users access
|
|
23
44
|
return AuthenticationResult.ok;
|
|
24
45
|
}
|
|
46
|
+
// is the username valid according to MQTT specification
|
|
47
|
+
if (!strictUsername.test(username)) {
|
|
48
|
+
return AuthenticationResult.badUsernameOrPassword;
|
|
49
|
+
}
|
|
50
|
+
// Does user and password match an entry in userTable
|
|
51
|
+
if (userTable.has(username)) {
|
|
52
|
+
const pass = userTable.get(username);
|
|
53
|
+
if (pwd === pass) {
|
|
54
|
+
return AuthenticationResult.ok;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
25
57
|
return AuthenticationResult.notAuthorized;
|
|
26
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Checks if client is authorized to publish to a topic
|
|
61
|
+
* @param {Context} ctx - Connection context
|
|
62
|
+
* @param {Topic} topic - Topic to publish to
|
|
63
|
+
* @returns {boolean} True if authorized
|
|
64
|
+
*/
|
|
27
65
|
function isAuthorizedToPublish(ctx, topic) {
|
|
28
66
|
logger.debug(`Checking authorization of client '${ctx.store?.clientId}' to publish on topic '${topic}'`);
|
|
29
|
-
// if (notAuthorizedTable.has([ctx.client,topic])){
|
|
30
|
-
// return false;
|
|
31
|
-
// }
|
|
32
67
|
return true;
|
|
33
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Checks if client is authorized to subscribe to a topic
|
|
71
|
+
* @param {Context} ctx - Connection context
|
|
72
|
+
* @param {Topic} topic - Topic to subscribe to
|
|
73
|
+
* @returns {boolean} True if authorized
|
|
74
|
+
*/
|
|
34
75
|
function isAuthorizedToSubscribe(ctx, topic) {
|
|
35
76
|
logger.debug(`Checking authorization of client '${ctx.store?.clientId}' to subscribe to topic '${topic}'`);
|
|
36
77
|
return true;
|
|
37
78
|
}
|
|
38
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Parse command line arguments and start server
|
|
81
|
+
*/
|
|
39
82
|
const { _: [portNum] } = parseArgs(getArgs());
|
|
40
83
|
const port = Number(portNum ?? 1883);
|
|
41
84
|
const hostname = "::";
|
|
42
85
|
logger.level(LogLevel.info);
|
|
86
|
+
/**
|
|
87
|
+
* TCP Server instance with authentication and authorization handlers
|
|
88
|
+
*/
|
|
43
89
|
const tcpServer = new TcpServer({ port, hostname }, {
|
|
44
90
|
handlers: {
|
|
45
91
|
isAuthenticated,
|
package/dist/bin/demoServer.js
CHANGED
|
@@ -1,40 +1,90 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
|
+
/**
|
|
3
|
+
* MQTT Server implementation with authentication and authorization
|
|
4
|
+
* @module mqttServer
|
|
5
|
+
*/
|
|
2
6
|
import { TcpServer } from "../node/tcpServer.js";
|
|
3
7
|
import { AuthenticationResult } from "../server/mod.js";
|
|
4
8
|
import { getArgs, logger, LogLevel, parseArgs } from "../utils/mod.js";
|
|
9
|
+
/**
|
|
10
|
+
* UTF-8 decoder for parsing passwords
|
|
11
|
+
*/
|
|
5
12
|
const utf8Decoder = new TextDecoder();
|
|
13
|
+
/**
|
|
14
|
+
* Map storing valid username/password combinations
|
|
15
|
+
*/
|
|
6
16
|
const userTable = new Map();
|
|
7
17
|
userTable.set("IoTester_1", "strong_password");
|
|
8
18
|
userTable.set("IoTester_2", "strong_password");
|
|
9
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Regular expression for validating usernames
|
|
21
|
+
* Allows alphanumeric characters up to 23 chars long
|
|
22
|
+
*/
|
|
23
|
+
const strictUsername = new RegExp(/^[a-zA-Z0-9]{0,23}$/);
|
|
24
|
+
/**
|
|
25
|
+
* Flag to enable/disable username checking
|
|
26
|
+
* If false, all users are allowed access
|
|
27
|
+
* If true, only users in userTable are allowed access
|
|
28
|
+
*/
|
|
29
|
+
const checkUsername = false;
|
|
30
|
+
/**
|
|
31
|
+
* Authenticates MQTT clients
|
|
32
|
+
* @param {Context} _ctx - Connection context
|
|
33
|
+
* @param {string} clientId - Client identifier
|
|
34
|
+
* @param {string} username - Username to authenticate
|
|
35
|
+
* @param {Uint8Array} password - Password as byte array
|
|
36
|
+
* @returns {TAuthenticationResult} Authentication result
|
|
37
|
+
*/
|
|
10
38
|
function isAuthenticated(_ctx, clientId, username, password) {
|
|
11
39
|
const pwd = utf8Decoder.decode(password);
|
|
12
40
|
logger.info(`Verifying authentication of client '${clientId}' with username '${username}' and password '${pwd}'`);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
41
|
+
if (!checkUsername) {
|
|
42
|
+
// allow all users access
|
|
43
|
+
return AuthenticationResult.ok;
|
|
44
|
+
}
|
|
45
|
+
// is the username valid according to MQTT specification
|
|
46
|
+
if (!strictUsername.test(username)) {
|
|
47
|
+
return AuthenticationResult.badUsernameOrPassword;
|
|
48
|
+
}
|
|
49
|
+
// Does user and password match an entry in userTable
|
|
50
|
+
if (userTable.has(username)) {
|
|
51
|
+
const pass = userTable.get(username);
|
|
52
|
+
if (pwd === pass) {
|
|
53
|
+
return AuthenticationResult.ok;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return AuthenticationResult.notAuthorized;
|
|
24
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Checks if client is authorized to publish to a topic
|
|
60
|
+
* @param {Context} ctx - Connection context
|
|
61
|
+
* @param {Topic} topic - Topic to publish to
|
|
62
|
+
* @returns {boolean} True if authorized
|
|
63
|
+
*/
|
|
25
64
|
function isAuthorizedToPublish(ctx, topic) {
|
|
26
65
|
logger.debug(`Checking authorization of client '${ctx.store?.clientId}' to publish on topic '${topic}'`);
|
|
27
66
|
return true;
|
|
28
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Checks if client is authorized to subscribe to a topic
|
|
70
|
+
* @param {Context} ctx - Connection context
|
|
71
|
+
* @param {Topic} topic - Topic to subscribe to
|
|
72
|
+
* @returns {boolean} True if authorized
|
|
73
|
+
*/
|
|
29
74
|
function isAuthorizedToSubscribe(ctx, topic) {
|
|
30
75
|
logger.debug(`Checking authorization of client '${ctx.store?.clientId}' to subscribe to topic '${topic}'`);
|
|
31
76
|
return true;
|
|
32
77
|
}
|
|
33
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Parse command line arguments and start server
|
|
80
|
+
*/
|
|
34
81
|
const { _: [portNum] } = parseArgs(getArgs());
|
|
35
82
|
const port = Number(portNum ?? 1883);
|
|
36
83
|
const hostname = "::";
|
|
37
84
|
logger.level(LogLevel.info);
|
|
85
|
+
/**
|
|
86
|
+
* TCP Server instance with authentication and authorization handlers
|
|
87
|
+
*/
|
|
38
88
|
const tcpServer = new TcpServer({ port, hostname }, {
|
|
39
89
|
handlers: {
|
|
40
90
|
isAuthenticated,
|
package/dist/bin/mqtt.js
CHANGED
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview MQTT command line interface that provides publish and subscribe functionality
|
|
4
|
+
* @module mqtt-cli
|
|
5
|
+
*
|
|
6
|
+
* @description
|
|
7
|
+
* This module implements a command line interface for MQTT operations:
|
|
8
|
+
* - Publish messages to an MQTT broker
|
|
9
|
+
* - Subscribe to topics on an MQTT broker
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - TLS support with custom certificates
|
|
13
|
+
* - Authentication with username/password
|
|
14
|
+
* - Configurable QoS levels (0-2)
|
|
15
|
+
* - Message retention
|
|
16
|
+
* - Session persistence
|
|
17
|
+
* - Custom client IDs
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* # Publish a message
|
|
21
|
+
* mqtt publish -t topic -m message
|
|
22
|
+
*
|
|
23
|
+
* # Subscribe to a topic
|
|
24
|
+
* mqtt subscribe -t topic
|
|
25
|
+
*
|
|
26
|
+
* @requires node
|
|
27
|
+
* @requires ../client/mod.ts
|
|
28
|
+
* @requires ../node/tcpClient.ts
|
|
29
|
+
* @requires ../utils/mod.ts
|
|
30
|
+
*/
|
|
2
31
|
import { DEFAULT_URL } from "../client/mod.js";
|
|
3
32
|
import { getFileData, TcpClient } from "../node/tcpClient.js";
|
|
4
33
|
import { getArgs, logger, LogLevel, parseArgs } from "../utils/mod.js";
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Object representing possible connection states
|
|
3
|
+
* @readonly
|
|
4
|
+
*/
|
|
1
5
|
export const ConnectionState = {
|
|
6
|
+
/** System is offline and not attempting connection */
|
|
2
7
|
offline: "offline",
|
|
8
|
+
/** System is in the process of establishing a connection */
|
|
3
9
|
connecting: "connecting",
|
|
10
|
+
/** System has an active connection */
|
|
4
11
|
connected: "connected",
|
|
12
|
+
/** System is in the process of disconnecting */
|
|
5
13
|
disconnecting: "disconnecting",
|
|
14
|
+
/** System has been disconnected */
|
|
6
15
|
disconnected: "disconnected",
|
|
7
16
|
};
|