@seriousme/opifex 1.6.2 → 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 +6 -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/tcpServer.test.ts +137 -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/node/tcpServer.js +9 -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/asyncQueue.js +2 -0
- package/dist/utils/nextTick.js +13 -1
- package/node/tcpServer.test.ts +61 -6
- package/node/tcpServer.ts +9 -2
- 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/asyncQueue.ts +2 -1
- package/utils/nextTick.ts +13 -1
package/dist/client/client.js
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { Deferred, logger, MemoryStore, PacketType, } from "./deps.js";
|
|
2
2
|
import { Context } from "./context.js";
|
|
3
|
+
/**
|
|
4
|
+
* Generates a random client ID with the given prefix
|
|
5
|
+
* @param prefix - The prefix to use for the client ID
|
|
6
|
+
* @returns A string containing the prefix followed by a random number
|
|
7
|
+
*/
|
|
3
8
|
function generateClientId(prefix) {
|
|
4
9
|
return `${prefix}-${Math.random().toString().slice(-10)}`;
|
|
5
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Implements exponential backoff sleep with optional randomization
|
|
13
|
+
* based on https://dthain.blogspot.com/2009/02/exponential-backoff-in-distributed.html
|
|
14
|
+
* @param random - Whether to add randomization to the delay
|
|
15
|
+
* @param attempt - The attempt number (used to calculate delay)
|
|
16
|
+
* @returns Promise that resolves after the calculated delay
|
|
17
|
+
*/
|
|
6
18
|
function backOffSleep(random, attempt) {
|
|
7
|
-
// based on https://dthain.blogspot.com/2009/02/exponential-backoff-in-distributed.html
|
|
8
19
|
const factor = 1.5;
|
|
9
20
|
const min = 1000;
|
|
10
21
|
const max = 5000;
|
|
@@ -39,15 +50,32 @@ export class Client {
|
|
|
39
50
|
clientId;
|
|
40
51
|
ctx = new Context(new MemoryStore());
|
|
41
52
|
connectPacket;
|
|
53
|
+
/**
|
|
54
|
+
* Creates a new MQTT client instance
|
|
55
|
+
*/
|
|
42
56
|
constructor() {
|
|
43
57
|
this.clientId = generateClientId(this.clientIdPrefix);
|
|
44
58
|
this.numberOfRetries = DEFAULT_RETRIES;
|
|
45
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Creates a new connection to the MQTT broker
|
|
62
|
+
* @param protocol - The protocol to use (mqtt, mqtts, etc)
|
|
63
|
+
* @param _hostname - The hostname to connect to
|
|
64
|
+
* @param _port - The port to connect to
|
|
65
|
+
* @param _caCerts - Optional CA certificates
|
|
66
|
+
* @param _cert - Optional client certificate
|
|
67
|
+
* @param _key - Optional client key
|
|
68
|
+
* @returns Promise resolving to a SockConn connection
|
|
69
|
+
*/
|
|
46
70
|
createConn(protocol, _hostname, _port, _caCerts, _cert, _key) {
|
|
47
71
|
// if you need to support alternative connection types just
|
|
48
72
|
// overload this method in your subclass
|
|
49
73
|
throw `Unsupported protocol: ${protocol}`;
|
|
50
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Handles the connection process including retries and reconnection
|
|
77
|
+
* @returns Promise that resolves when connection is established or fails
|
|
78
|
+
*/
|
|
51
79
|
async doConnect() {
|
|
52
80
|
if (!this.connectPacket) {
|
|
53
81
|
return;
|
|
@@ -86,6 +114,11 @@ export class Client {
|
|
|
86
114
|
this.ctx.unresolvedConnect?.reject(lastMessage);
|
|
87
115
|
}
|
|
88
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Connects to the MQTT broker
|
|
119
|
+
* @param params - Connection parameters
|
|
120
|
+
* @returns Promise resolving to authentication result
|
|
121
|
+
*/
|
|
89
122
|
connect(params = {}) {
|
|
90
123
|
this.url = params?.url || this.url;
|
|
91
124
|
this.numberOfRetries = params.numberOfRetries || this.numberOfRetries;
|
|
@@ -105,9 +138,18 @@ export class Client {
|
|
|
105
138
|
this.doConnect();
|
|
106
139
|
return deferred.promise;
|
|
107
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Disconnects from the MQTT broker
|
|
143
|
+
* @returns Promise that resolves when disconnected
|
|
144
|
+
*/
|
|
108
145
|
async disconnect() {
|
|
109
146
|
await this.ctx.disconnect();
|
|
110
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Publishes a message to the MQTT broker
|
|
150
|
+
* @param params - Publish parameters including topic and payload
|
|
151
|
+
* @returns Promise that resolves when published
|
|
152
|
+
*/
|
|
111
153
|
async publish(params) {
|
|
112
154
|
const packet = {
|
|
113
155
|
type: PacketType.publish,
|
|
@@ -115,6 +157,11 @@ export class Client {
|
|
|
115
157
|
};
|
|
116
158
|
await this.ctx.publish(packet);
|
|
117
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Subscribes to topics on the MQTT broker
|
|
162
|
+
* @param params - Subscribe parameters including topics
|
|
163
|
+
* @returns Promise that resolves when subscribed
|
|
164
|
+
*/
|
|
118
165
|
async subscribe(params) {
|
|
119
166
|
const packet = {
|
|
120
167
|
type: PacketType.subscribe,
|
|
@@ -123,9 +170,17 @@ export class Client {
|
|
|
123
170
|
};
|
|
124
171
|
await this.ctx.subscribe(packet);
|
|
125
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Gets an async iterator for received messages
|
|
175
|
+
* @returns AsyncGenerator yielding received publish packets
|
|
176
|
+
*/
|
|
126
177
|
async *messages() {
|
|
127
178
|
yield* this.ctx.incoming;
|
|
128
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Closes the message stream
|
|
182
|
+
* @param reason - Optional reason for closing
|
|
183
|
+
*/
|
|
129
184
|
closeMessages(reason) {
|
|
130
185
|
this.ctx.incoming.close(reason);
|
|
131
186
|
}
|
package/dist/client/context.js
CHANGED
|
@@ -125,6 +125,10 @@ export class Context {
|
|
|
125
125
|
}
|
|
126
126
|
return false;
|
|
127
127
|
}
|
|
128
|
+
// just an alias to clarify protocol flow
|
|
129
|
+
receivePubcomp(id) {
|
|
130
|
+
return this.receivePuback(id);
|
|
131
|
+
}
|
|
128
132
|
receiveSuback(id, returnCodes) {
|
|
129
133
|
const unresolvedMap = this.unresolvedSubscribe;
|
|
130
134
|
if (unresolvedMap.has(id)) {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ConnectionState } from "../ConnectionState.js";
|
|
2
2
|
import { AuthenticationResultByNumber } from "../deps.js";
|
|
3
|
+
/**
|
|
4
|
+
* Handles the CONNACK packet received from the MQTT broker
|
|
5
|
+
* @param packet - The CONNACK packet containing the connection acknowledgment
|
|
6
|
+
* @param ctx - The connection context
|
|
7
|
+
* @returns Promise that resolves when handling is complete
|
|
8
|
+
*/
|
|
3
9
|
export async function handleConnack(packet, ctx) {
|
|
4
10
|
if (packet.returnCode === 0) {
|
|
5
11
|
ctx.connectionState = ConnectionState.connected;
|
|
@@ -8,6 +8,13 @@ import { handlePubcomp } from "./handlePubcomp.js";
|
|
|
8
8
|
import { handleSuback } from "./handleSuback.js";
|
|
9
9
|
import { handleUnsuback } from "./handleUnsuback.js";
|
|
10
10
|
import { logger, PacketNameByType, PacketType } from "../deps.js";
|
|
11
|
+
/**
|
|
12
|
+
* Handles incoming MQTT packets based on the connection state and packet type
|
|
13
|
+
* @param ctx - The MQTT client context containing connection state and other information
|
|
14
|
+
* @param packet - The MQTT packet to handle
|
|
15
|
+
* @throws Error if an unexpected packet is received before connection is established
|
|
16
|
+
* @throws Error if an unexpected packet type is received after connection
|
|
17
|
+
*/
|
|
11
18
|
export async function handlePacket(ctx, packet) {
|
|
12
19
|
logger.debug({ received: PacketNameByType[packet.type], packet });
|
|
13
20
|
if (ctx.connectionState !== ConnectionState.connected) {
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Handles PUBACK packet processing for QoS level 1 PUBLISH responses
|
|
3
|
+
* @param ctx - The MQTT client context
|
|
4
|
+
* @param packet - The PUBACK packet received from the broker
|
|
5
|
+
* @description
|
|
6
|
+
* - Removes the packet ID from the list of pending outgoing messages
|
|
7
|
+
* - Completes the corresponding publish operation
|
|
8
|
+
*/
|
|
2
9
|
export function handlePuback(ctx, packet) {
|
|
3
10
|
const id = packet.id;
|
|
4
11
|
ctx.store.pendingOutgoing.delete(id);
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Handles PUBCOMP packets which are the response to PUBREL packets in QoS 2 flow
|
|
3
|
+
* @param ctx - The connection context containing the client state and store
|
|
4
|
+
* @param packet - The PUBCOMP packet received from the broker
|
|
5
|
+
* @description
|
|
6
|
+
* This function processes PUBCOMP packets which are the fourth and final packet
|
|
7
|
+
* in the QoS 2 protocol exchange. It removes the message from pendingAckOutgoing queue
|
|
8
|
+
* Completes the corresponding publish operation
|
|
9
|
+
*/
|
|
3
10
|
export function handlePubcomp(ctx, packet) {
|
|
4
11
|
const id = packet.id;
|
|
5
12
|
if (ctx.store.pendingAckOutgoing.has(id)) {
|
|
6
13
|
ctx.store.pendingAckOutgoing.delete(id);
|
|
7
|
-
ctx.
|
|
14
|
+
ctx.receivePubcomp(id);
|
|
8
15
|
}
|
|
9
16
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { PacketType } from "../deps.js";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Handles incoming MQTT publish packets based on their QoS level
|
|
4
|
+
* @param ctx - The MQTT connection context
|
|
5
|
+
* @param packet - The incoming publish packet to handle
|
|
6
|
+
* @returns Promise that resolves when packet is handled
|
|
7
|
+
*/
|
|
3
8
|
export async function handlePublish(ctx, packet) {
|
|
4
9
|
const qos = packet.qos || 0;
|
|
5
10
|
if (qos === 0) {
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { PacketType } from "../deps.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Handles PUBREC (Publish Received) packets for QoS 2 message flow
|
|
4
|
+
* It is the second packet of the QoS 2 protocol exchange.
|
|
5
|
+
* @param ctx - The connection context containing message stores and send function
|
|
6
|
+
* @param packet - The received PUBREC packet
|
|
7
|
+
* @description
|
|
8
|
+
* When a PUBREC packet is received:
|
|
9
|
+
* 1. Checks if there is a pending outgoing message with matching packet ID
|
|
10
|
+
* 2. Creates a PUBREL packet to acknowledge the PUBREC
|
|
11
|
+
* 3. Add the packet ID to pendingAckOutgoing store
|
|
12
|
+
* 4. Removes the message from pendingOutgoing store
|
|
13
|
+
* 5. Sends the PUBREL packet
|
|
14
|
+
*/
|
|
6
15
|
export async function handlePubrec(ctx, packet) {
|
|
7
16
|
const id = packet.id;
|
|
8
17
|
const ack = {
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { PacketType } from "../deps.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Handles a PUBREL packet in the MQTT QoS 2 flow
|
|
4
|
+
* It is the third packet of the QoS 2 protocol exchange
|
|
5
|
+
* and a response to the PUBREC packet
|
|
6
|
+
*
|
|
7
|
+
* It uses Method A from the MQTT specification
|
|
8
|
+
* @param ctx - The MQTT client context
|
|
9
|
+
* @param packet - The PUBREL packet received
|
|
10
|
+
* @description
|
|
11
|
+
* When a PUBREL packet is received:
|
|
12
|
+
* 1. If there is a matching pending incoming message:
|
|
13
|
+
* - Delivers the stored message to the application
|
|
14
|
+
* - Removes the message from pending incoming store
|
|
15
|
+
* - Sends PUBCOMP packet as acknowledgement
|
|
16
|
+
* 2. If no matching message found, silently ignores
|
|
17
|
+
*/
|
|
6
18
|
export async function handlePubrel(ctx, packet) {
|
|
7
19
|
const id = packet.id;
|
|
8
20
|
if (ctx.store.pendingIncoming.has(id)) {
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Handles SUBACK packets received from the server
|
|
3
|
+
* @param ctx - The MQTT client context
|
|
4
|
+
* @param packet - The SUBACK packet containing subscription confirmations
|
|
5
|
+
* @description
|
|
6
|
+
* - Processes SUBACK packets sent by server to confirm SUBSCRIBE requests
|
|
7
|
+
* - Removes the packet ID from pending outgoing messages
|
|
8
|
+
* - Notifies the client about granted QoS levels for each subscription
|
|
9
|
+
*/
|
|
5
10
|
export function handleSuback(ctx, packet) {
|
|
6
11
|
const id = packet.id;
|
|
7
12
|
ctx.store.pendingOutgoing.delete(id);
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Handles the UNSUBACK packet received from the server
|
|
3
|
+
* @param ctx - The MQTT client context
|
|
4
|
+
* @param packet - The UNSUBACK packet received from the server
|
|
5
|
+
* @description
|
|
6
|
+
* The UNSUBACK Packet is sent by the Server to the Client to confirm receipt of
|
|
7
|
+
* an UNSUBSCRIBE Packet.
|
|
8
|
+
* When received, it removes the pending outgoing request and completes the corresponding
|
|
9
|
+
* unsubscribe operation.
|
|
10
|
+
*/
|
|
3
11
|
export function handleUnsuback(ctx, packet) {
|
|
4
12
|
const id = packet.id;
|
|
5
13
|
ctx.store.pendingOutgoing.delete(id);
|
|
@@ -1,16 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module memoryStore
|
|
3
|
+
*/
|
|
1
4
|
import { assert } from "./deps.js";
|
|
2
5
|
import { maxPacketId, } from "./store.js";
|
|
6
|
+
/**
|
|
7
|
+
* In-memory implementation of the IStore interface for managing MQTT packet storage
|
|
8
|
+
*/
|
|
3
9
|
export class MemoryStore {
|
|
10
|
+
/** Current packet ID counter */
|
|
4
11
|
packetId;
|
|
12
|
+
/** Map of pending incoming packets */
|
|
5
13
|
pendingIncoming;
|
|
14
|
+
/** Map of pending outgoing packets */
|
|
6
15
|
pendingOutgoing;
|
|
16
|
+
/** Map of pending acknowledgment outgoing packets */
|
|
7
17
|
pendingAckOutgoing;
|
|
18
|
+
/**
|
|
19
|
+
* Creates a new MemoryStore instance
|
|
20
|
+
*/
|
|
8
21
|
constructor() {
|
|
9
22
|
this.packetId = 0;
|
|
10
23
|
this.pendingIncoming = new Map();
|
|
11
24
|
this.pendingOutgoing = new Map();
|
|
12
25
|
this.pendingAckOutgoing = new Map();
|
|
13
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Generates the next available packet ID
|
|
29
|
+
* @returns {PacketId} The next available packet ID
|
|
30
|
+
* @throws {Error} If no unused packet ID is available
|
|
31
|
+
*/
|
|
14
32
|
nextId() {
|
|
15
33
|
const currentId = this.packetId;
|
|
16
34
|
do {
|
|
@@ -25,6 +43,10 @@ export class MemoryStore {
|
|
|
25
43
|
assert(this.packetId !== currentId, "No unused packetId available");
|
|
26
44
|
return this.packetId;
|
|
27
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Asynchronously yields all pending outgoing packets
|
|
48
|
+
* @yields {PendingOutgoingPackets} Pending outgoing packets
|
|
49
|
+
*/
|
|
28
50
|
async *pendingOutgoingPackets() {
|
|
29
51
|
for (const [_id, packet] of this.pendingAckOutgoing) {
|
|
30
52
|
yield packet;
|
package/dist/node/tcpServer.js
CHANGED
|
@@ -12,16 +12,23 @@ export class TcpServer {
|
|
|
12
12
|
this.mqttServer = new MqttServer(mqttOptions);
|
|
13
13
|
this.serverOptions = serverOptions;
|
|
14
14
|
}
|
|
15
|
-
start() {
|
|
15
|
+
async start() {
|
|
16
16
|
this.server = createServer((sock) => this.mqttServer.serve(wrapNodeSocket(sock)));
|
|
17
|
+
const isListening = new Promise((resolve) => {
|
|
18
|
+
this.server?.on("listening", () => {
|
|
19
|
+
resolve(true);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
17
22
|
this.server.listen(this.serverOptions.port, this.serverOptions.hostname);
|
|
23
|
+
await isListening;
|
|
24
|
+
return;
|
|
18
25
|
}
|
|
19
26
|
stop() {
|
|
20
27
|
this.server?.close();
|
|
21
28
|
}
|
|
22
29
|
get port() {
|
|
23
30
|
const address = this.server?.address();
|
|
24
|
-
if (typeof address === "object") {
|
|
31
|
+
if (typeof address === "object" && address !== null) {
|
|
25
32
|
return address?.port;
|
|
26
33
|
}
|
|
27
34
|
return this.serverOptions?.port;
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { Writable } from "node:stream";
|
|
2
|
+
/**
|
|
3
|
+
* Closes a Node.js socket if it is not already closed
|
|
4
|
+
* @param sock - The Node.js socket to close
|
|
5
|
+
*/
|
|
2
6
|
function closer(sock) {
|
|
3
7
|
if (!sock.closed) {
|
|
4
8
|
sock.end();
|
|
5
9
|
}
|
|
6
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Wraps a Node.js socket into a standard web streams-based socket connection
|
|
13
|
+
* @param socket - The Node.js socket to wrap
|
|
14
|
+
* @returns A socket connection object with readable/writable streams and close method
|
|
15
|
+
*/
|
|
7
16
|
export function wrapNodeSocket(socket) {
|
|
8
17
|
const readable = new ReadableStream({
|
|
9
18
|
type: "bytes",
|
|
@@ -1,20 +1,35 @@
|
|
|
1
1
|
import { AuthenticationResult, logger, PacketType, Timer, } from "../deps.js";
|
|
2
|
+
/**
|
|
3
|
+
* Checks if the client is authenticated based on the provided credentials
|
|
4
|
+
* @param ctx - The connection context
|
|
5
|
+
* @param packet - The MQTT CONNECT packet
|
|
6
|
+
* @returns Authentication result indicating if the client is authenticated
|
|
7
|
+
*/
|
|
2
8
|
function isAuthenticated(ctx, packet) {
|
|
3
9
|
if (ctx.handlers.isAuthenticated) {
|
|
4
10
|
return ctx.handlers.isAuthenticated(ctx, packet.clientId || "", packet.username || "", packet.password || new Uint8Array(0));
|
|
5
11
|
}
|
|
6
12
|
return AuthenticationResult.ok;
|
|
7
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Validates the CONNECT packet
|
|
16
|
+
* @param ctx - The connection context
|
|
17
|
+
* @param packet - The MQTT CONNECT packet to validate
|
|
18
|
+
* @returns Authentication result indicating if the CONNECT packet is valid
|
|
19
|
+
*/
|
|
8
20
|
function validateConnect(ctx, packet) {
|
|
9
21
|
if (packet.protocolLevel !== 4) {
|
|
10
22
|
return AuthenticationResult.unacceptableProtocol;
|
|
11
23
|
}
|
|
12
24
|
return isAuthenticated(ctx, packet);
|
|
13
25
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Processes the validated CONNECT packet
|
|
28
|
+
* @param packet - The MQTT CONNECT packet
|
|
29
|
+
* @param ctx - The connection context
|
|
30
|
+
* @param clientId - The client ID
|
|
31
|
+
*/
|
|
32
|
+
function processValidatedConnect(returnCode, packet, ctx, clientId) {
|
|
18
33
|
if (returnCode === AuthenticationResult.ok) {
|
|
19
34
|
if (packet.will) {
|
|
20
35
|
ctx.will = {
|
|
@@ -33,8 +48,27 @@ export function handleConnect(ctx, packet) {
|
|
|
33
48
|
ctx.close();
|
|
34
49
|
}, Math.floor(keepAlive * 1500));
|
|
35
50
|
}
|
|
51
|
+
// is this a new session?
|
|
52
|
+
// either because its the first time for the client
|
|
53
|
+
// or it specifically asked for a clean one
|
|
54
|
+
const previousSession = ctx.store?.existingSession;
|
|
55
|
+
// client now has a history
|
|
56
|
+
if (!previousSession && ctx.store) {
|
|
57
|
+
ctx.store.existingSession = true;
|
|
58
|
+
}
|
|
59
|
+
return previousSession || false;
|
|
36
60
|
}
|
|
37
|
-
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Handles the MQTT CONNECT packet
|
|
65
|
+
* @param ctx - The connection context
|
|
66
|
+
* @param packet - The MQTT CONNECT packet to handle
|
|
67
|
+
*/
|
|
68
|
+
export function handleConnect(ctx, packet) {
|
|
69
|
+
const clientId = packet.clientId || `Opifex-${crypto.randomUUID()}`;
|
|
70
|
+
const returnCode = validateConnect(ctx, packet);
|
|
71
|
+
const sessionPresent = processValidatedConnect(returnCode, packet, ctx, clientId);
|
|
38
72
|
ctx.send({
|
|
39
73
|
type: PacketType.connack,
|
|
40
74
|
sessionPresent,
|
|
@@ -10,6 +10,13 @@ import { handleSubscribe } from "./handleSubscribe.js";
|
|
|
10
10
|
import { handleUnsubscribe } from "./handleUnsubscribe.js";
|
|
11
11
|
import { handleDisconnect } from "./handleDisconnect.js";
|
|
12
12
|
import { logger } from "../deps.js";
|
|
13
|
+
/**
|
|
14
|
+
* Handles incoming MQTT packets based on their type and connection state
|
|
15
|
+
* @param ctx - The connection context containing client state and configuration
|
|
16
|
+
* @param packet - The MQTT packet to handle
|
|
17
|
+
* @throws Error if receiving unexpected packet types or packets before connect
|
|
18
|
+
* @returns Promise that resolves when packet handling is complete
|
|
19
|
+
*/
|
|
13
20
|
export async function handlePacket(ctx, packet) {
|
|
14
21
|
logger.debug("handling", PacketNameByType[packet.type]);
|
|
15
22
|
logger.debug(JSON.stringify(packet, null, 2));
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { PacketType } from "../deps.js";
|
|
2
|
+
/**
|
|
3
|
+
* Handles PINGREQ packet by responding with a PINGRESP packet
|
|
4
|
+
* @param ctx - The connection context containing send method
|
|
5
|
+
* @returns Promise that resolves when PINGRESP is sent
|
|
6
|
+
*/
|
|
2
7
|
export async function handlePingreq(ctx) {
|
|
3
8
|
await ctx.send({
|
|
4
9
|
type: PacketType.pingres,
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Handles PUBACK (Publish Acknowledgment) packets in MQTT protocol
|
|
3
|
+
* @param ctx - The connection context containing the client's state and configuration
|
|
4
|
+
* @param packet - The PUBACK packet received from the client
|
|
5
|
+
* @description
|
|
6
|
+
* PUBACK packets are sent in response to PUBLISH packets with QoS level 1.
|
|
7
|
+
* This function removes the original PUBLISH packet from the pending outgoing messages store
|
|
8
|
+
* once acknowledgment is received.
|
|
9
|
+
*/
|
|
2
10
|
export function handlePuback(ctx, packet) {
|
|
3
11
|
// qos 1 only
|
|
4
12
|
const id = packet.id;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Handles PUBCOMP packets which are the response to PUBREL packets in QoS 2 flow
|
|
3
|
+
* @param ctx - The connection context containing the client state and configuration
|
|
4
|
+
* @param packet - The PUBCOMP packet received from the client
|
|
5
|
+
* @description
|
|
6
|
+
* This is the fourth and final packet of the QoS 2 protocol exchange.
|
|
7
|
+
* When received, it removes the message from the pendingAckOutgoing store.
|
|
8
|
+
*/
|
|
3
9
|
export function handlePubcomp(ctx, packet) {
|
|
4
10
|
const id = packet.id;
|
|
5
11
|
if (ctx.store?.pendingAckOutgoing.has(id)) {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { SysPrefix } from "../context.js";
|
|
2
2
|
import { PacketType } from "../deps.js";
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a client is authorized to publish to a given topic
|
|
5
|
+
* @param ctx - The connection context
|
|
6
|
+
* @param topic - The topic to check authorization for
|
|
7
|
+
* @returns boolean indicating if client is authorized to publish
|
|
8
|
+
*/
|
|
3
9
|
function authorizedToPublish(ctx, topic) {
|
|
4
10
|
if (topic.startsWith(SysPrefix)) {
|
|
5
11
|
return false;
|
|
@@ -9,6 +15,13 @@ function authorizedToPublish(ctx, topic) {
|
|
|
9
15
|
}
|
|
10
16
|
return true;
|
|
11
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Handles MQTT PUBLISH packets
|
|
20
|
+
* @param ctx - The connection context
|
|
21
|
+
* @param packet - The PUBLISH packet to process
|
|
22
|
+
* @returns Promise that resolves when packet is processed
|
|
23
|
+
* @throws Error if packet processing fails
|
|
24
|
+
*/
|
|
12
25
|
export async function handlePublish(ctx, packet) {
|
|
13
26
|
if (!authorizedToPublish(ctx, packet.topic)) {
|
|
14
27
|
return;
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { PacketType } from "../deps.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Handles PUBREC (QoS 2 Publish Received) packets
|
|
4
|
+
* @param ctx - The connection context
|
|
5
|
+
* @param packet - The PUBREC packet received from the client
|
|
6
|
+
* @returns Promise that resolves when handling is complete
|
|
7
|
+
* @description
|
|
8
|
+
* For QoS 2 message flow:
|
|
9
|
+
* 1. Discards the original publish message
|
|
10
|
+
* 2. Stores that PUBREC was received for the packet ID
|
|
11
|
+
* 3. Sends PUBREL packet in response
|
|
12
|
+
*/
|
|
5
13
|
export async function handlePubrec(ctx, packet) {
|
|
6
14
|
const id = packet.id;
|
|
7
15
|
if (ctx.store?.pendingOutgoing.has(id)) {
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { PacketType } from "../deps.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Handles PUBREL (QoS 2 publish release) packets
|
|
4
|
+
*
|
|
5
|
+
* @param ctx - The connection context
|
|
6
|
+
* @param packet - The PUBREL packet received from the client
|
|
7
|
+
* @returns Promise that resolves when handling is complete
|
|
8
|
+
* @description
|
|
9
|
+
* For QoS 2 message delivery:
|
|
10
|
+
* 1. Initiates onward delivery of the Application Message
|
|
11
|
+
* 2. Discards the stored message
|
|
12
|
+
* 3. Sends PUBCOMP packet with the Packet Identifier
|
|
13
|
+
*/
|
|
5
14
|
export async function handlePubrel(ctx, packet) {
|
|
6
15
|
const id = packet.id;
|
|
7
16
|
if (ctx.store?.pendingIncoming.has(id)) {
|
|
@@ -1,14 +1,35 @@
|
|
|
1
1
|
import { PacketType, } from "../deps.js";
|
|
2
|
+
/**
|
|
3
|
+
* @constant {number} SubscriptionFailure
|
|
4
|
+
* @description Code indicating a failed subscription attempt
|
|
5
|
+
*/
|
|
2
6
|
const SubscriptionFailure = 0x80;
|
|
7
|
+
/**
|
|
8
|
+
* Checks if a client is authorized to subscribe to a topic
|
|
9
|
+
* @param ctx - The connection context
|
|
10
|
+
* @param topicFilter - The topic filter to check authorization for
|
|
11
|
+
* @returns True if authorized, false otherwise
|
|
12
|
+
*/
|
|
3
13
|
function authorizedToSubscribe(ctx, topicFilter) {
|
|
4
14
|
if (ctx.handlers.isAuthorizedToSubscribe) {
|
|
5
15
|
return ctx.handlers.isAuthorizedToSubscribe(ctx, topicFilter);
|
|
6
16
|
}
|
|
7
17
|
return true;
|
|
8
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* @function handleSubscribe
|
|
21
|
+
* @description Processes an MQTT SUBSCRIBE packet
|
|
22
|
+
* @param {Context} ctx - The connection context
|
|
23
|
+
* @param {SubscribePacket} packet - The SUBSCRIBE packet received from the client
|
|
24
|
+
* @returns {Promise<void>}
|
|
25
|
+
* @throws {Error} If subscription processing fails
|
|
26
|
+
* @remarks The order of return codes in the SUBACK Packet MUST match the order of Topic Filters in the SUBSCRIBE Packet [MQTT-3.9.3-1]
|
|
27
|
+
*/
|
|
9
28
|
export async function handleSubscribe(ctx, packet) {
|
|
10
|
-
|
|
11
|
-
|
|
29
|
+
/*
|
|
30
|
+
* The order of return codes in the SUBACK Packet MUST match the order of
|
|
31
|
+
* Topic Filters in the SUBSCRIBE Packet [MQTT-3.9.3-1].
|
|
32
|
+
*/
|
|
12
33
|
const validSubscriptions = [];
|
|
13
34
|
const returnCodes = packet.subscriptions.map((sub) => {
|
|
14
35
|
if (ctx.store) {
|
|
@@ -19,14 +40,16 @@ export async function handleSubscribe(ctx, packet) {
|
|
|
19
40
|
validSubscriptions.push(sub);
|
|
20
41
|
return sub.qos;
|
|
21
42
|
}
|
|
22
|
-
return SubscriptionFailure;
|
|
43
|
+
return SubscriptionFailure;
|
|
23
44
|
});
|
|
24
45
|
await ctx.send({
|
|
25
46
|
type: PacketType.suback,
|
|
26
47
|
id: packet.id,
|
|
27
48
|
returnCodes: returnCodes,
|
|
28
49
|
});
|
|
29
|
-
|
|
50
|
+
/*
|
|
51
|
+
* send any retained messages that match these subscriptions
|
|
52
|
+
*/
|
|
30
53
|
if (ctx.store) {
|
|
31
54
|
ctx.persistence.handleRetained(ctx.store.clientId);
|
|
32
55
|
}
|