@seriousme/opifex 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +72 -4
- package/bin/conformanceServer.ts +1 -1
- package/bin/mqtt.ts +5 -2
- package/client/client.ts +3 -3
- package/client/context.ts +2 -0
- package/client/mod.ts +1 -2
- package/deno/client.ts +4 -3
- package/deno/server.ts +3 -2
- package/deno.json +4 -1
- package/dev_utils/dummyConn.ts +4 -4
- package/dist/bin/conformanceServer.js +52 -0
- package/dist/bin/demoServer.js +29 -57
- package/dist/bin/mqtt.js +148 -187
- package/dist/client/ConnectionState.js +7 -0
- package/dist/client/client.js +125 -0
- package/dist/client/context.js +148 -0
- package/dist/client/deps.js +5 -0
- package/dist/client/handlers/handleConnack.js +18 -0
- package/dist/client/handlers/handlePacket.js +50 -0
- package/dist/client/handlers/handlePuback.js +6 -0
- package/dist/client/handlers/handlePubcomp.js +9 -0
- package/dist/client/handlers/handlePublish.js +28 -0
- package/dist/client/handlers/handlePubrec.js +17 -0
- package/dist/client/handlers/handlePubrel.js +19 -0
- package/dist/client/handlers/handleSuback.js +9 -0
- package/dist/client/handlers/handleUnsuback.js +7 -0
- package/dist/client/mod.js +5 -3
- package/dist/client/store/deps.js +1 -0
- package/dist/client/store/memoryStore.js +36 -0
- package/dist/client/store/store.js +1 -0
- package/dist/mqttConn/mqttConn.js +104 -0
- package/dist/mqttPacket/AuthenticationResult.js +12 -0
- package/dist/mqttPacket/BitMask.js +10 -0
- package/dist/mqttPacket/PacketType.js +18 -0
- package/dist/mqttPacket/connack.js +27 -0
- package/dist/mqttPacket/connect.js +115 -0
- package/dist/mqttPacket/decoder.js +89 -0
- package/dist/mqttPacket/disconnect.js +14 -0
- package/dist/mqttPacket/encoder.js +56 -0
- package/dist/mqttPacket/length.js +43 -0
- package/dist/mqttPacket/mod.js +71 -0
- package/dist/mqttPacket/pingreq.js +15 -0
- package/dist/mqttPacket/pingres.js +14 -0
- package/dist/mqttPacket/puback.js +20 -0
- package/dist/mqttPacket/pubcomp.js +20 -0
- package/dist/mqttPacket/publish.js +51 -0
- package/dist/mqttPacket/pubrec.js +20 -0
- package/dist/mqttPacket/pubrel.js +20 -0
- package/dist/mqttPacket/suback.js +29 -0
- package/dist/mqttPacket/subscribe.js +52 -0
- package/dist/mqttPacket/types.js +1 -0
- package/dist/mqttPacket/unsuback.js +20 -0
- package/dist/mqttPacket/unsubscribe.js +36 -0
- package/dist/mqttPacket/validators.js +15 -0
- package/dist/node/client.js +65 -0
- package/dist/node/server.js +22 -0
- package/dist/node/wrapNodeSocket.js +47 -0
- package/dist/persistence/deps.js +3 -0
- package/dist/persistence/memory/memoryPersistence.js +117 -0
- package/dist/persistence/persistence.js +1 -0
- package/dist/persistence/store.js +1 -0
- package/dist/server/context.js +100 -0
- package/dist/server/deps.js +5 -0
- package/dist/server/handlers/handleConnect.js +46 -0
- package/dist/server/handlers/handleDisconnect.js +4 -0
- package/dist/server/handlers/handlePacket.js +58 -0
- package/dist/server/handlers/handlePingreq.js +6 -0
- package/dist/server/handlers/handlePuback.js +8 -0
- package/dist/server/handlers/handlePubcomp.js +8 -0
- package/dist/server/handlers/handlePublish.js +40 -0
- package/dist/server/handlers/handlePubrec.js +15 -0
- package/dist/server/handlers/handlePubrel.js +18 -0
- package/dist/server/handlers/handleSubscribe.js +33 -0
- package/dist/server/handlers/handleUnsubscribe.js +12 -0
- package/dist/server/mod.js +7 -589
- package/dist/server/server.js +47 -0
- package/dist/socket/socket.js +36 -0
- package/dist/timer/timer.js +40 -0
- package/dist/trie/trie.js +95 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/utils/args.js +61 -0
- package/dist/utils/assert.js +19 -0
- package/dist/utils/asyncQueue.js +56 -0
- package/dist/utils/deferred.js +11 -0
- package/dist/utils/logger.js +29 -0
- package/dist/utils/mod.js +7 -0
- package/dist/utils/nextTick.js +3 -0
- package/dist/utils/platform.js +4 -0
- package/mqttConn/mqttConn.ts +4 -1
- package/node/client.ts +30 -6
- package/node/server.ts +2 -0
- package/node/wrapNodeSocket.ts +1 -1
- package/package.json +16 -3
- package/server/mod.ts +1 -2
- package/socket/socket.ts +3 -2
- package/timer/timer.ts +1 -1
- package/tsconfig.json +3 -0
- package/.vscode/extensions.json +0 -10
- package/.vscode/settings.json +0 -12
- package/deno/wrapDenoConn.ts +0 -12
- package/dist/client-DqLTBVC2.js +0 -541
- package/dist/timer-DDWVNsyG.js +0 -924
- package/dist/wrapNodeSocket-BAJm059T.js +0 -115
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ConnectionState } from "../ConnectionState.js";
|
|
2
|
+
import { handleConnack } from "./handleConnack.js";
|
|
3
|
+
import { handlePublish } from "./handlePublish.js";
|
|
4
|
+
import { handlePuback } from "./handlePuback.js";
|
|
5
|
+
import { handlePubrec } from "./handlePubrec.js";
|
|
6
|
+
import { handlePubrel } from "./handlePubrel.js";
|
|
7
|
+
import { handlePubcomp } from "./handlePubcomp.js";
|
|
8
|
+
import { handleSuback } from "./handleSuback.js";
|
|
9
|
+
import { handleUnsuback } from "./handleUnsuback.js";
|
|
10
|
+
import { logger, PacketNameByType, PacketType } from "../deps.js";
|
|
11
|
+
export async function handlePacket(ctx, packet) {
|
|
12
|
+
logger.debug({ received: JSON.stringify(packet, null, 2) });
|
|
13
|
+
if (ctx.connectionState !== ConnectionState.connected) {
|
|
14
|
+
if (packet.type === PacketType.connack) {
|
|
15
|
+
handleConnack(packet, ctx);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
throw new Error(`Received ${PacketNameByType[packet.type]} packet before connect`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
switch (packet.type) {
|
|
23
|
+
case PacketType.pingres:
|
|
24
|
+
break;
|
|
25
|
+
case PacketType.publish:
|
|
26
|
+
await handlePublish(ctx, packet);
|
|
27
|
+
break;
|
|
28
|
+
case PacketType.puback:
|
|
29
|
+
await handlePuback(ctx, packet);
|
|
30
|
+
break;
|
|
31
|
+
case PacketType.pubrel:
|
|
32
|
+
await handlePubrel(ctx, packet);
|
|
33
|
+
break;
|
|
34
|
+
case PacketType.pubrec:
|
|
35
|
+
await handlePubrec(ctx, packet);
|
|
36
|
+
break;
|
|
37
|
+
case PacketType.pubcomp:
|
|
38
|
+
await handlePubcomp(ctx, packet);
|
|
39
|
+
break;
|
|
40
|
+
case PacketType.suback:
|
|
41
|
+
await handleSuback(ctx, packet);
|
|
42
|
+
break;
|
|
43
|
+
case PacketType.unsuback:
|
|
44
|
+
handleUnsuback(ctx, packet);
|
|
45
|
+
break;
|
|
46
|
+
default:
|
|
47
|
+
throw new Error(`Received unexpected ${PacketNameByType[packet.type]} packet after connect`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The PUBCOMP Packet is the response to a PUBREL Packet.
|
|
2
|
+
// It is the fourth and final packet of the QoS 2 protocol exchange.
|
|
3
|
+
export function handlePubcomp(ctx, packet) {
|
|
4
|
+
const id = packet.id;
|
|
5
|
+
if (ctx.store.pendingAckOutgoing.has(id)) {
|
|
6
|
+
ctx.store.pendingAckOutgoing.delete(id);
|
|
7
|
+
ctx.receivePuback(id);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { PacketType } from "../deps.js";
|
|
2
|
+
// Incoming publish
|
|
3
|
+
export async function handlePublish(ctx, packet) {
|
|
4
|
+
const qos = packet.qos || 0;
|
|
5
|
+
if (qos === 0) {
|
|
6
|
+
ctx.receivePublish(packet);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (packet.id !== undefined) {
|
|
10
|
+
// qos 1
|
|
11
|
+
if (qos === 1) {
|
|
12
|
+
ctx.receivePublish(packet);
|
|
13
|
+
await ctx.send({
|
|
14
|
+
type: PacketType.puback,
|
|
15
|
+
id: packet.id,
|
|
16
|
+
});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// qos 2
|
|
20
|
+
if (ctx.store) {
|
|
21
|
+
ctx.store.pendingIncoming.set(packet.id, packet);
|
|
22
|
+
await ctx.send({
|
|
23
|
+
type: PacketType.pubrec,
|
|
24
|
+
id: packet.id,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PacketType } from "../deps.js";
|
|
2
|
+
// A PUBREC Packet is the response to a PUBLISH Packet with QoS 2.
|
|
3
|
+
// It is the second packet of the QoS 2 protocol exchange.
|
|
4
|
+
// Discard message, Store PUBREC received <Packet Identifier>
|
|
5
|
+
// send PUBREL <Packet Identifier>
|
|
6
|
+
export async function handlePubrec(ctx, packet) {
|
|
7
|
+
const id = packet.id;
|
|
8
|
+
const ack = {
|
|
9
|
+
type: PacketType.pubrel,
|
|
10
|
+
id,
|
|
11
|
+
};
|
|
12
|
+
if (ctx.store.pendingOutgoing.has(id)) {
|
|
13
|
+
ctx.store.pendingAckOutgoing.set(id, ack);
|
|
14
|
+
ctx.store.pendingOutgoing.delete(id);
|
|
15
|
+
await ctx.send(ack);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PacketType } from "../deps.js";
|
|
2
|
+
// A PUBREL Packet is the response to a PUBREC Packet.
|
|
3
|
+
// It is the third packet of the QoS 2 protocol exchange.
|
|
4
|
+
// Method A, Initiate onward delivery of the Application Message then discard message
|
|
5
|
+
// Send PUBCOMP <Packet Identifier>
|
|
6
|
+
export async function handlePubrel(ctx, packet) {
|
|
7
|
+
const id = packet.id;
|
|
8
|
+
if (ctx.store.pendingIncoming.has(id)) {
|
|
9
|
+
const storedPacket = ctx.store.pendingIncoming.get(id);
|
|
10
|
+
if (storedPacket) {
|
|
11
|
+
ctx.receivePublish(storedPacket);
|
|
12
|
+
ctx.store.pendingIncoming.delete(id);
|
|
13
|
+
await ctx.send({
|
|
14
|
+
type: PacketType.pubcomp,
|
|
15
|
+
id,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// A SUBACK Packet is sent by the Server to the Client to confirm receipt
|
|
2
|
+
// and processing of a SUBSCRIBE Packet.
|
|
3
|
+
// A SUBACK Packet contains a list of return codes, that specify the maximum QoS level
|
|
4
|
+
// that was granted in each Subscription that was requested by the SUBSCRIBE.
|
|
5
|
+
export function handleSuback(ctx, packet) {
|
|
6
|
+
const id = packet.id;
|
|
7
|
+
ctx.store.pendingOutgoing.delete(id);
|
|
8
|
+
ctx.receiveSuback(id, packet.returnCodes);
|
|
9
|
+
}
|
package/dist/client/mod.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* This module provides an MQTT Client interface see the /bin folder for examples
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
export { Client, DEFAULT_URL } from "./client.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { assert } from "../../utils/mod.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { assert } from "./deps.js";
|
|
2
|
+
import { maxPacketId, } from "./store.js";
|
|
3
|
+
export class MemoryStore {
|
|
4
|
+
packetId;
|
|
5
|
+
pendingIncoming;
|
|
6
|
+
pendingOutgoing;
|
|
7
|
+
pendingAckOutgoing;
|
|
8
|
+
constructor() {
|
|
9
|
+
this.packetId = 0;
|
|
10
|
+
this.pendingIncoming = new Map();
|
|
11
|
+
this.pendingOutgoing = new Map();
|
|
12
|
+
this.pendingAckOutgoing = new Map();
|
|
13
|
+
}
|
|
14
|
+
nextId() {
|
|
15
|
+
const currentId = this.packetId;
|
|
16
|
+
do {
|
|
17
|
+
this.packetId++;
|
|
18
|
+
if (this.packetId > maxPacketId) {
|
|
19
|
+
this.packetId = 1;
|
|
20
|
+
}
|
|
21
|
+
} while ((this.pendingIncoming.has(this.packetId) ||
|
|
22
|
+
this.pendingOutgoing.has(this.packetId) ||
|
|
23
|
+
this.pendingAckOutgoing.has(this.packetId)) &&
|
|
24
|
+
this.packetId !== currentId);
|
|
25
|
+
assert(this.packetId !== currentId, "No unused packetId available");
|
|
26
|
+
return this.packetId;
|
|
27
|
+
}
|
|
28
|
+
async *pendingOutgoingPackets() {
|
|
29
|
+
for (const [_id, packet] of this.pendingAckOutgoing) {
|
|
30
|
+
yield packet;
|
|
31
|
+
}
|
|
32
|
+
for (const [_id, packet] of this.pendingOutgoing) {
|
|
33
|
+
yield packet;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const maxPacketId = 0xffff;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { decodePayload, encode, getLengthDecoder, } from "../mqttPacket/mod.js";
|
|
2
|
+
import { assert } from "../utils/mod.js";
|
|
3
|
+
import { Conn } from "../socket/socket.js";
|
|
4
|
+
export const MqttConnError = {
|
|
5
|
+
invalidPacket: "Invalid Packet",
|
|
6
|
+
packetTooLarge: "Packet too large",
|
|
7
|
+
UnexpectedEof: "Unexpected EOF",
|
|
8
|
+
};
|
|
9
|
+
async function readByte(conn) {
|
|
10
|
+
const buf = new Uint8Array(1);
|
|
11
|
+
const bytesRead = await conn.read(buf);
|
|
12
|
+
assert(bytesRead !== null, MqttConnError.UnexpectedEof);
|
|
13
|
+
assert(bytesRead !== 0, MqttConnError.UnexpectedEof);
|
|
14
|
+
return buf[0];
|
|
15
|
+
}
|
|
16
|
+
async function readFull(conn, buf) {
|
|
17
|
+
let bytesRead = 0;
|
|
18
|
+
while (bytesRead < buf.length) {
|
|
19
|
+
const read = await conn.read(buf.subarray(bytesRead));
|
|
20
|
+
assert(read !== null, MqttConnError.UnexpectedEof);
|
|
21
|
+
assert(read !== 0, MqttConnError.UnexpectedEof);
|
|
22
|
+
bytesRead += read;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Read MQTT packet
|
|
26
|
+
* @throws `Error` if packet is invalid
|
|
27
|
+
*/
|
|
28
|
+
export async function readPacket(conn, maxPacketSize) {
|
|
29
|
+
// fixed header is 1 byte of type + flags
|
|
30
|
+
// + a maximum of 4 bytes to encode the remaining length
|
|
31
|
+
const decodeLength = getLengthDecoder();
|
|
32
|
+
const firstByte = await readByte(conn);
|
|
33
|
+
let result;
|
|
34
|
+
do {
|
|
35
|
+
const byte = await readByte(conn);
|
|
36
|
+
result = decodeLength(byte);
|
|
37
|
+
} while (!result.done);
|
|
38
|
+
const remainingLength = result.length;
|
|
39
|
+
assert(remainingLength < maxPacketSize - 1, MqttConnError.packetTooLarge);
|
|
40
|
+
const packetBuf = new Uint8Array(remainingLength);
|
|
41
|
+
// read the rest of the packet
|
|
42
|
+
await readFull(conn, packetBuf);
|
|
43
|
+
const packet = decodePayload(firstByte, packetBuf);
|
|
44
|
+
assert(packet !== null, MqttConnError.UnexpectedEof);
|
|
45
|
+
return packet;
|
|
46
|
+
}
|
|
47
|
+
export class MqttConn {
|
|
48
|
+
conn;
|
|
49
|
+
maxPacketSize;
|
|
50
|
+
_reason = undefined;
|
|
51
|
+
_isClosed = false;
|
|
52
|
+
constructor({ conn, maxPacketSize, }) {
|
|
53
|
+
this.conn = new Conn(conn);
|
|
54
|
+
this.maxPacketSize = maxPacketSize || 2 * 1024 * 1024;
|
|
55
|
+
}
|
|
56
|
+
get reason() {
|
|
57
|
+
return this._reason;
|
|
58
|
+
}
|
|
59
|
+
async *[Symbol.asyncIterator]() {
|
|
60
|
+
while (!this._isClosed) {
|
|
61
|
+
try {
|
|
62
|
+
yield await readPacket(this.conn, this.maxPacketSize);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
if (err instanceof Error) {
|
|
66
|
+
if (err.name === "PartialReadError") {
|
|
67
|
+
err.message = MqttConnError.UnexpectedEof;
|
|
68
|
+
}
|
|
69
|
+
this._reason = err.message;
|
|
70
|
+
}
|
|
71
|
+
// packet too large, malformed packet or connection closed
|
|
72
|
+
this.close();
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async send(data) {
|
|
78
|
+
try {
|
|
79
|
+
await this.conn.write(encode(data));
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (err instanceof Error) {
|
|
83
|
+
this._reason = err.message;
|
|
84
|
+
}
|
|
85
|
+
this.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
get isClosed() {
|
|
89
|
+
return this._isClosed;
|
|
90
|
+
}
|
|
91
|
+
close() {
|
|
92
|
+
if (this.isClosed)
|
|
93
|
+
return;
|
|
94
|
+
try {
|
|
95
|
+
this.conn.close();
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
console.error(e);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
this._isClosed = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Possible MQTT authentication results
|
|
3
|
+
*/
|
|
4
|
+
export const AuthenticationResult = {
|
|
5
|
+
ok: 0,
|
|
6
|
+
unacceptableProtocol: 1,
|
|
7
|
+
rejectedUsername: 2,
|
|
8
|
+
serverUnavailable: 3,
|
|
9
|
+
badUsernameOrPassword: 4,
|
|
10
|
+
notAuthorized: 5,
|
|
11
|
+
};
|
|
12
|
+
export const AuthenticationResultByNumber = Object.fromEntries(Object.entries(AuthenticationResult).map(([k, v]) => [v, k]));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const PacketType = {
|
|
2
|
+
reserved: 0,
|
|
3
|
+
connect: 1,
|
|
4
|
+
connack: 2,
|
|
5
|
+
publish: 3,
|
|
6
|
+
puback: 4,
|
|
7
|
+
pubrec: 5,
|
|
8
|
+
pubrel: 6,
|
|
9
|
+
pubcomp: 7,
|
|
10
|
+
subscribe: 8,
|
|
11
|
+
suback: 9,
|
|
12
|
+
unsubscribe: 10,
|
|
13
|
+
unsuback: 11,
|
|
14
|
+
pingreq: 12,
|
|
15
|
+
pingres: 13,
|
|
16
|
+
disconnect: 14,
|
|
17
|
+
};
|
|
18
|
+
export const PacketNameByType = Object.fromEntries(Object.entries(PacketType).map(([k, v]) => [v, k]));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { PacketType } from "./PacketType.js";
|
|
2
|
+
import { BitMask } from "./BitMask.js";
|
|
3
|
+
import { booleanFlag, Decoder, DecoderError } from "./decoder.js";
|
|
4
|
+
import { AuthenticationResultByNumber } from "./AuthenticationResult.js";
|
|
5
|
+
export default {
|
|
6
|
+
encode(packet) {
|
|
7
|
+
const flags = 0;
|
|
8
|
+
return {
|
|
9
|
+
flags,
|
|
10
|
+
bytes: [packet.sessionPresent ? 1 : 0, packet.returnCode || 0],
|
|
11
|
+
};
|
|
12
|
+
},
|
|
13
|
+
decode(buffer, _flags) {
|
|
14
|
+
const decoder = new Decoder(buffer);
|
|
15
|
+
const sessionPresent = booleanFlag(decoder.getByte(), BitMask.bit0);
|
|
16
|
+
const returnCode = decoder.getByte();
|
|
17
|
+
decoder.done();
|
|
18
|
+
if (!AuthenticationResultByNumber[returnCode]) {
|
|
19
|
+
throw new DecoderError("Invalid return code");
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
type: PacketType.connack,
|
|
23
|
+
sessionPresent,
|
|
24
|
+
returnCode,
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { PacketType } from "./PacketType.js";
|
|
2
|
+
import { BitMask } from "./BitMask.js";
|
|
3
|
+
import { Encoder } from "./encoder.js";
|
|
4
|
+
import { booleanFlag, Decoder, DecoderError, hasEmptyFlags, } from "./decoder.js";
|
|
5
|
+
function invalidProtocolName(version, name) {
|
|
6
|
+
if (version === 4 && name !== "MQTT") {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
export default {
|
|
12
|
+
encode(packet) {
|
|
13
|
+
const flags = 0;
|
|
14
|
+
const protocolLevel = 4;
|
|
15
|
+
const protocolName = protocolLevel === 4 ? "MQTT" : "MQIsdp";
|
|
16
|
+
const clientId = packet.clientId || "";
|
|
17
|
+
const usernameFlag = !!packet.username;
|
|
18
|
+
const passwordFlag = !!packet.password;
|
|
19
|
+
const willRetain = !!packet.will?.retain;
|
|
20
|
+
const willQoS = packet.will?.qos || 0;
|
|
21
|
+
const willFlag = !!packet.will;
|
|
22
|
+
const cleanSession = packet.clean !== false;
|
|
23
|
+
const connectFlags = (usernameFlag ? BitMask.bit7 : 0) +
|
|
24
|
+
(passwordFlag ? BitMask.bit6 : 0) +
|
|
25
|
+
(willRetain ? BitMask.bit5 : 0) +
|
|
26
|
+
(willQoS & 2 ? BitMask.bit4 : 0) +
|
|
27
|
+
(willQoS & 1 ? BitMask.bit3 : 0) +
|
|
28
|
+
(willFlag ? BitMask.bit2 : 0) +
|
|
29
|
+
(cleanSession ? BitMask.bit1 : 0);
|
|
30
|
+
const keepAlive = packet.keepAlive || 0;
|
|
31
|
+
const encoder = new Encoder();
|
|
32
|
+
encoder
|
|
33
|
+
.setUtf8String(protocolName)
|
|
34
|
+
.setByte(protocolLevel)
|
|
35
|
+
.setByte(connectFlags)
|
|
36
|
+
.setInt16(keepAlive)
|
|
37
|
+
.setUtf8String(clientId);
|
|
38
|
+
if (packet.will) {
|
|
39
|
+
encoder.setTopic(packet.will.topic).setByteArray(packet.will.payload);
|
|
40
|
+
}
|
|
41
|
+
if (packet.username) {
|
|
42
|
+
encoder.setUtf8String(packet.username);
|
|
43
|
+
}
|
|
44
|
+
if (packet.password) {
|
|
45
|
+
encoder.setByteArray(packet.password);
|
|
46
|
+
}
|
|
47
|
+
return { flags, bytes: encoder.done() };
|
|
48
|
+
},
|
|
49
|
+
decode(buffer, flags) {
|
|
50
|
+
const decoder = new Decoder(buffer);
|
|
51
|
+
const protocolName = decoder.getUtf8String();
|
|
52
|
+
const protocolLevel = decoder.getByte();
|
|
53
|
+
if (invalidProtocolName(protocolLevel, protocolName)) {
|
|
54
|
+
throw new DecoderError("Invalid protocol name");
|
|
55
|
+
}
|
|
56
|
+
const connectFlags = decoder.getByte();
|
|
57
|
+
const usernameFlag = booleanFlag(connectFlags, BitMask.bit7);
|
|
58
|
+
const passwordFlag = booleanFlag(connectFlags, BitMask.bit6);
|
|
59
|
+
const willRetain = booleanFlag(connectFlags, BitMask.bit5);
|
|
60
|
+
const willQoS = (connectFlags & (BitMask.bit4 + BitMask.bit3)) >> 3;
|
|
61
|
+
const willFlag = booleanFlag(connectFlags, BitMask.bit2);
|
|
62
|
+
const cleanSession = booleanFlag(connectFlags, BitMask.bit1);
|
|
63
|
+
const reservedBit = booleanFlag(connectFlags, BitMask.bit0);
|
|
64
|
+
hasEmptyFlags(flags);
|
|
65
|
+
// The Server MUST validate that the reserved flag in the CONNECT Control Packet
|
|
66
|
+
// is set to zero and disconnect the Client if it is not zero [MQTT-3.1.2-3].
|
|
67
|
+
if (reservedBit) {
|
|
68
|
+
throw new DecoderError("Invalid reserved bit");
|
|
69
|
+
}
|
|
70
|
+
if (willQoS !== 0 && willQoS !== 1 && willQoS !== 2) {
|
|
71
|
+
throw new DecoderError("Invalid will qos");
|
|
72
|
+
}
|
|
73
|
+
const keepAlive = decoder.getInt16();
|
|
74
|
+
const clientId = decoder.getUtf8String();
|
|
75
|
+
let willTopic;
|
|
76
|
+
let willPayload;
|
|
77
|
+
if (willFlag) {
|
|
78
|
+
willTopic = decoder.getTopic();
|
|
79
|
+
willPayload = decoder.getByteArray();
|
|
80
|
+
}
|
|
81
|
+
let username;
|
|
82
|
+
let password;
|
|
83
|
+
if (usernameFlag) {
|
|
84
|
+
username = decoder.getUtf8String();
|
|
85
|
+
}
|
|
86
|
+
if (passwordFlag) {
|
|
87
|
+
password = decoder.getByteArray();
|
|
88
|
+
}
|
|
89
|
+
decoder.done();
|
|
90
|
+
if (!willFlag && (willQoS !== 0 || willRetain === true)) {
|
|
91
|
+
throw new DecoderError("Will QoS must be 0 and Will retain to false when Will flag is false");
|
|
92
|
+
}
|
|
93
|
+
if (clientId.length === 0 && cleanSession === false) {
|
|
94
|
+
throw new DecoderError("Clean session must be true if clientID is empty");
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
type: PacketType.connect,
|
|
98
|
+
protocolName: protocolName,
|
|
99
|
+
protocolLevel,
|
|
100
|
+
clientId: clientId,
|
|
101
|
+
username: username ? username : undefined,
|
|
102
|
+
password: password ? password : undefined,
|
|
103
|
+
will: willFlag
|
|
104
|
+
? {
|
|
105
|
+
topic: willTopic || "",
|
|
106
|
+
payload: willPayload || Uint8Array.from([0]),
|
|
107
|
+
retain: willRetain,
|
|
108
|
+
qos: willQoS,
|
|
109
|
+
}
|
|
110
|
+
: undefined,
|
|
111
|
+
clean: cleanSession,
|
|
112
|
+
keepAlive,
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { invalidTopic, invalidTopicFilter } from "./validators.js";
|
|
2
|
+
const utf8Decoder = new TextDecoder("utf-8");
|
|
3
|
+
export function booleanFlag(byte, mask) {
|
|
4
|
+
return !!(byte & mask);
|
|
5
|
+
}
|
|
6
|
+
export function isEmptyBuf(buf) {
|
|
7
|
+
if (buf.length > 0) {
|
|
8
|
+
throw new DecoderError("Packet too long");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function hasEmptyFlags(flags) {
|
|
12
|
+
if (flags !== 0) {
|
|
13
|
+
throw new DecoderError("Invalid fixed header flags");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class DecoderError extends Error {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "DecoderError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class Decoder {
|
|
23
|
+
buf;
|
|
24
|
+
pos;
|
|
25
|
+
len;
|
|
26
|
+
constructor(buf, pos = 0) {
|
|
27
|
+
this.buf = buf;
|
|
28
|
+
this.pos = pos;
|
|
29
|
+
this.len = buf.length;
|
|
30
|
+
}
|
|
31
|
+
checkpos(pos) {
|
|
32
|
+
if (pos > this.len) {
|
|
33
|
+
throw new DecoderError("Packet too short");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
getByte() {
|
|
37
|
+
this.checkpos(this.pos);
|
|
38
|
+
return this.buf[this.pos++];
|
|
39
|
+
}
|
|
40
|
+
getInt16() {
|
|
41
|
+
const msb = this.getByte();
|
|
42
|
+
const lsb = this.getByte();
|
|
43
|
+
return (msb << 8) | lsb;
|
|
44
|
+
}
|
|
45
|
+
getByteArray() {
|
|
46
|
+
const len = this.getInt16();
|
|
47
|
+
const start = this.pos;
|
|
48
|
+
const end = this.pos + len;
|
|
49
|
+
this.pos = end;
|
|
50
|
+
this.checkpos(end);
|
|
51
|
+
return this.buf.subarray(start, end);
|
|
52
|
+
}
|
|
53
|
+
getUtf8String() {
|
|
54
|
+
const str = utf8Decoder.decode(this.getByteArray());
|
|
55
|
+
return str;
|
|
56
|
+
}
|
|
57
|
+
getTopic() {
|
|
58
|
+
const topic = this.getUtf8String();
|
|
59
|
+
if (invalidTopic(topic)) {
|
|
60
|
+
throw new DecoderError("Topic must contain valid UTF-8 and contain more than 1 byte and no wildcards");
|
|
61
|
+
}
|
|
62
|
+
return topic;
|
|
63
|
+
}
|
|
64
|
+
getTopicFilter() {
|
|
65
|
+
const topicFilter = this.getUtf8String();
|
|
66
|
+
if (invalidTopicFilter(topicFilter)) {
|
|
67
|
+
throw new DecoderError("Topicfilter must contain valid UTF-8 and contain more than 1 byte and valid wildcards");
|
|
68
|
+
}
|
|
69
|
+
return topicFilter;
|
|
70
|
+
}
|
|
71
|
+
getRemainder() {
|
|
72
|
+
const start = this.pos;
|
|
73
|
+
const end = this.len;
|
|
74
|
+
this.pos = end;
|
|
75
|
+
return this.buf.subarray(start, end);
|
|
76
|
+
}
|
|
77
|
+
atEnd() {
|
|
78
|
+
if (this.len === this.pos) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
done() {
|
|
84
|
+
if (this.atEnd()) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
throw new DecoderError("Packet too long");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PacketType } from "./PacketType.js";
|
|
2
|
+
import { isEmptyBuf } from "./decoder.js";
|
|
3
|
+
export default {
|
|
4
|
+
encode(_packet) {
|
|
5
|
+
const flags = 0;
|
|
6
|
+
return { flags, bytes: [] };
|
|
7
|
+
},
|
|
8
|
+
decode(buffer, _flags) {
|
|
9
|
+
isEmptyBuf(buffer);
|
|
10
|
+
return {
|
|
11
|
+
type: PacketType.disconnect,
|
|
12
|
+
};
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const utf8Encoder = new TextEncoder();
|
|
2
|
+
import { invalidTopic, invalidTopicFilter } from "./validators.js";
|
|
3
|
+
export class EncoderError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "EncoderError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class Encoder {
|
|
10
|
+
buffer;
|
|
11
|
+
constructor() {
|
|
12
|
+
this.buffer = [];
|
|
13
|
+
}
|
|
14
|
+
setByte(value) {
|
|
15
|
+
this.buffer.push(value);
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
setInt16(value) {
|
|
19
|
+
this.setByte(value >> 8);
|
|
20
|
+
this.setByte(value & 0xff);
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
setByteArray(value) {
|
|
24
|
+
if (value.length > 0xffff) {
|
|
25
|
+
throw new EncoderError("More than 0xffff bytes of data");
|
|
26
|
+
}
|
|
27
|
+
this.setInt16(value.length);
|
|
28
|
+
this.buffer.push(...value);
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
setUtf8String(value) {
|
|
32
|
+
this.setByteArray(utf8Encoder.encode(value));
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
setTopic(value) {
|
|
36
|
+
if (invalidTopic(value)) {
|
|
37
|
+
throw new EncoderError("Topic must contain valid UTF-8 and contain more than 1 byte and no wildcards");
|
|
38
|
+
}
|
|
39
|
+
this.setUtf8String(value);
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
setTopicFilter(value) {
|
|
43
|
+
if (invalidTopicFilter(value)) {
|
|
44
|
+
throw new EncoderError("Topicfilter must contain valid UTF-8 and contain more than 1 byte and valid wildcards");
|
|
45
|
+
}
|
|
46
|
+
this.setUtf8String(value);
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
setRemainder(value) {
|
|
50
|
+
this.buffer.push(...value);
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
done() {
|
|
54
|
+
return this.buffer;
|
|
55
|
+
}
|
|
56
|
+
}
|