@seriousme/opifex 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.github/workflows/closeStale.yml +15 -0
  2. package/.github/workflows/codeql-analysis.yml +71 -0
  3. package/.github/workflows/deno-ci.yml +16 -0
  4. package/.github/workflows/nodejs-ci.yml +23 -0
  5. package/.github/workflows/publish.yml +16 -0
  6. package/.vscode/extensions.json +10 -0
  7. package/.vscode/settings.json +12 -0
  8. package/LICENSE.txt +21 -0
  9. package/README.md +34 -0
  10. package/bin/conformanceServer.ts +70 -0
  11. package/bin/demoServer.ts +64 -0
  12. package/bin/mqtt.ts +211 -0
  13. package/client/client.ts +181 -0
  14. package/client/context.ts +197 -0
  15. package/client/deps.ts +36 -0
  16. package/client/handlers/handleConnack.ts +21 -0
  17. package/client/handlers/handlePacket.ts +70 -0
  18. package/client/handlers/handlePuback.ts +10 -0
  19. package/client/handlers/handlePubcomp.ts +13 -0
  20. package/client/handlers/handlePublish.ts +32 -0
  21. package/client/handlers/handlePubrec.ts +24 -0
  22. package/client/handlers/handlePubrel.ts +26 -0
  23. package/client/handlers/handleSuback.ts +14 -0
  24. package/client/handlers/handleUnsuback.ts +11 -0
  25. package/client/mod.ts +11 -0
  26. package/client/store/deps.ts +22 -0
  27. package/client/store/memoryStore.ts +56 -0
  28. package/client/store/store.ts +29 -0
  29. package/deno/README.md +64 -0
  30. package/deno/client.ts +48 -0
  31. package/deno/server.test.ts +14 -0
  32. package/deno/server.ts +31 -0
  33. package/deno/wrapDenoConn.ts +12 -0
  34. package/deno.json +8 -0
  35. package/deno.lock +47 -0
  36. package/dev_utils/dummyConn.ts +95 -0
  37. package/dev_utils/mod.ts +1 -0
  38. package/mqttConn/mqttConn.test.ts +110 -0
  39. package/mqttConn/mqttConn.ts +134 -0
  40. package/mqttPacket/AuthenticationResult.ts +16 -0
  41. package/mqttPacket/BitMask.ts +10 -0
  42. package/mqttPacket/PacketType.ts +21 -0
  43. package/mqttPacket/connack.test.ts +129 -0
  44. package/mqttPacket/connack.ts +37 -0
  45. package/mqttPacket/connect.test.ts +297 -0
  46. package/mqttPacket/connect.ts +159 -0
  47. package/mqttPacket/decoder.test.ts +123 -0
  48. package/mqttPacket/decoder.ts +109 -0
  49. package/mqttPacket/disconnect.test.ts +50 -0
  50. package/mqttPacket/disconnect.ts +21 -0
  51. package/mqttPacket/encoder.test.ts +39 -0
  52. package/mqttPacket/encoder.ts +72 -0
  53. package/mqttPacket/length.test.ts +75 -0
  54. package/mqttPacket/length.ts +57 -0
  55. package/mqttPacket/mod.ts +151 -0
  56. package/mqttPacket/pingreq.test.ts +50 -0
  57. package/mqttPacket/pingreq.ts +22 -0
  58. package/mqttPacket/pingres.test.ts +50 -0
  59. package/mqttPacket/pingres.ts +21 -0
  60. package/mqttPacket/puback.test.ts +55 -0
  61. package/mqttPacket/puback.ts +28 -0
  62. package/mqttPacket/pubcomp.test.ts +55 -0
  63. package/mqttPacket/pubcomp.ts +28 -0
  64. package/mqttPacket/publish.test.ts +198 -0
  65. package/mqttPacket/publish.ts +73 -0
  66. package/mqttPacket/pubrec.test.ts +55 -0
  67. package/mqttPacket/pubrec.ts +28 -0
  68. package/mqttPacket/pubrel.test.ts +55 -0
  69. package/mqttPacket/pubrel.ts +28 -0
  70. package/mqttPacket/suback.test.ts +49 -0
  71. package/mqttPacket/suback.ts +41 -0
  72. package/mqttPacket/subscribe.test.ts +233 -0
  73. package/mqttPacket/subscribe.ts +69 -0
  74. package/mqttPacket/types.ts +27 -0
  75. package/mqttPacket/unsuback.test.ts +55 -0
  76. package/mqttPacket/unsuback.ts +28 -0
  77. package/mqttPacket/unsubscribe.test.ts +136 -0
  78. package/mqttPacket/unsubscribe.ts +48 -0
  79. package/mqttPacket/validators.ts +20 -0
  80. package/node/README.md +46 -0
  81. package/node/client.ts +58 -0
  82. package/node/server.test.ts +14 -0
  83. package/node/server.ts +34 -0
  84. package/node/wrapNodeSocket.ts +54 -0
  85. package/package.json +35 -0
  86. package/persistence/deps.ts +12 -0
  87. package/persistence/memory/memoryPersistence.test.ts +151 -0
  88. package/persistence/memory/memoryPersistence.ts +156 -0
  89. package/persistence/persistence.ts +24 -0
  90. package/persistence/store.ts +14 -0
  91. package/server/context.ts +157 -0
  92. package/server/deps.ts +37 -0
  93. package/server/handlers/handleConnect.ts +73 -0
  94. package/server/handlers/handleDisconnect.ts +6 -0
  95. package/server/handlers/handlePacket.ts +78 -0
  96. package/server/handlers/handlePingreq.ts +8 -0
  97. package/server/handlers/handlePuback.ts +12 -0
  98. package/server/handlers/handlePubcomp.ts +12 -0
  99. package/server/handlers/handlePublish.ts +47 -0
  100. package/server/handlers/handlePubrec.ts +20 -0
  101. package/server/handlers/handlePubrel.ts +24 -0
  102. package/server/handlers/handleSubscribe.ts +47 -0
  103. package/server/handlers/handleUnsubscribe.ts +17 -0
  104. package/server/mod.ts +16 -0
  105. package/server/server.ts +74 -0
  106. package/server/test/handleConnect.test.ts +126 -0
  107. package/server/test/test-handlers.ts +52 -0
  108. package/socket/socket.ts +59 -0
  109. package/timer/timer.test.ts +48 -0
  110. package/timer/timer.ts +45 -0
  111. package/trie/trie.test.ts +147 -0
  112. package/trie/trie.ts +106 -0
  113. package/utils/args.ts +86 -0
  114. package/utils/assert.ts +31 -0
  115. package/utils/asyncQueue.ts +63 -0
  116. package/utils/deferred.ts +12 -0
  117. package/utils/logger.ts +35 -0
  118. package/utils/mod.ts +7 -0
  119. package/utils/nextTick.ts +3 -0
  120. package/utils/platform.ts +4 -0
@@ -0,0 +1,156 @@
1
+ import {
2
+ type ClientId,
3
+ type PacketId,
4
+ type PublishPacket,
5
+ type QoS,
6
+ type Topic,
7
+ type TopicFilter,
8
+ Trie,
9
+ } from "../deps.ts";
10
+
11
+ import type {
12
+ Client,
13
+ Handler,
14
+ IPersistence,
15
+ RetainStore,
16
+ } from "../persistence.ts";
17
+ import type { IStore, PacketStore, SubscriptionStore } from "../store.ts";
18
+ import { assert } from "../../utils/mod.ts";
19
+
20
+ const maxPacketId = 0xffff;
21
+ // const maxQueueLength = 0xffff;
22
+
23
+ type ClientSubscription = {
24
+ clientId: ClientId;
25
+ qos: QoS;
26
+ };
27
+
28
+ export class MemoryStore implements IStore {
29
+ clientId: ClientId;
30
+ private packetId: PacketId;
31
+ pendingIncoming: PacketStore;
32
+ pendingOutgoing: PacketStore;
33
+ pendingAckOutgoing: Set<PacketId>;
34
+ subscriptions: SubscriptionStore;
35
+
36
+ constructor(clientId: ClientId) {
37
+ this.packetId = 0;
38
+ this.pendingIncoming = new Map();
39
+ this.pendingOutgoing = new Map();
40
+ this.pendingAckOutgoing = new Set();
41
+ this.subscriptions = new Map();
42
+ this.clientId = clientId;
43
+ }
44
+
45
+ nextId(): PacketId {
46
+ const currentId = this.packetId;
47
+ do {
48
+ this.packetId++;
49
+ if (this.packetId > maxPacketId) {
50
+ this.packetId = 0;
51
+ }
52
+ } while (
53
+ (this.pendingOutgoing.has(this.packetId) ||
54
+ this.pendingAckOutgoing.has(this.packetId)) &&
55
+ this.packetId !== currentId
56
+ );
57
+ assert(this.packetId !== currentId, "No unused packetId available");
58
+ return this.packetId;
59
+ }
60
+ }
61
+
62
+ export class MemoryPersistence implements IPersistence {
63
+ clientList: Map<ClientId, Client>;
64
+ retained: RetainStore;
65
+ private trie: Trie<ClientSubscription>;
66
+
67
+ constructor() {
68
+ this.clientList = new Map();
69
+ this.retained = new Map();
70
+ this.trie = new Trie(true);
71
+ }
72
+
73
+ registerClient(clientId: ClientId, handler: Handler, clean: boolean): IStore {
74
+ const existingClient = this.clientList.get(clientId);
75
+ const store = !clean && existingClient
76
+ ? existingClient.store
77
+ : new MemoryStore(clientId);
78
+ this.clientList.set(clientId, { store, handler });
79
+ return store;
80
+ }
81
+
82
+ deregisterClient(clientId: ClientId): void {
83
+ const client = this.clientList.get(clientId);
84
+ if (client) {
85
+ this.unsubscribeAll(client.store);
86
+ this.clientList.delete(clientId);
87
+ }
88
+ }
89
+
90
+ subscribe(store: IStore, topicFilter: TopicFilter, qos: QoS): void {
91
+ const clientId = store.clientId;
92
+ if (!store.subscriptions.has(topicFilter)) {
93
+ store.subscriptions.set(topicFilter, qos);
94
+ this.trie.add(topicFilter, { clientId, qos });
95
+ }
96
+ }
97
+
98
+ unsubscribe(store: IStore, topicFilter: TopicFilter): void {
99
+ const clientId = store.clientId;
100
+ const qos = store.subscriptions.get(topicFilter);
101
+ if (qos) {
102
+ store.subscriptions.delete(topicFilter);
103
+ this.trie.remove(topicFilter, { clientId, qos });
104
+ }
105
+ }
106
+
107
+ private unsubscribeAll(store: IStore) {
108
+ for (const [topicFilter, _qos] of store.subscriptions) {
109
+ this.unsubscribe(store, topicFilter);
110
+ }
111
+ }
112
+
113
+ publish(topic: Topic, packet: PublishPacket): void {
114
+ if (packet.retain) {
115
+ this.retained.set(packet.topic, packet);
116
+ if (packet.payload === undefined) {
117
+ this.retained.delete(packet.topic);
118
+ }
119
+ }
120
+
121
+ // dedup clients
122
+ const clients = new Map();
123
+ for (const { clientId, qos } of this.trie.match(topic)) {
124
+ const prevQos = clients.get(clientId);
125
+ if (!prevQos || prevQos < qos) {
126
+ clients.set(clientId, qos);
127
+ }
128
+ }
129
+
130
+ // publish the message to all clients
131
+ for (const [clientId, qos] of clients) {
132
+ const newPacket = Object.assign({}, packet);
133
+ newPacket.retain = false;
134
+ newPacket.qos = qos;
135
+ // logger.debug(`publish ${topic} to client ${clientId}`);
136
+ const client = this.clientList.get(clientId);
137
+ client?.handler(packet);
138
+ }
139
+ }
140
+
141
+ handleRetained(clientId: ClientId): void {
142
+ const retainedTrie: Trie<ClientId> = new Trie();
143
+ const client = this.clientList.get(clientId);
144
+ const store = client?.store;
145
+ if (store) {
146
+ for (const [topicFilter, _qos] of store.subscriptions) {
147
+ retainedTrie.add(topicFilter, clientId);
148
+ }
149
+ for (const [topic, packet] of this.retained) {
150
+ if (retainedTrie.match(topic).length > 0) {
151
+ client?.handler(packet);
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,24 @@
1
+ import type { ClientId, PublishPacket, QoS, Topic } from "./deps.ts";
2
+ import type { IStore } from "./store.ts";
3
+
4
+ export type Handler = (packet: PublishPacket) => void;
5
+
6
+ export type RetainStore = Map<Topic, PublishPacket>;
7
+
8
+ export type Client = { store: IStore; handler: Handler };
9
+
10
+ /**
11
+ * IPersistence is the interface for various types of Persistence
12
+ * to store messages and subscriptions
13
+ * @interface IPersistence
14
+ */
15
+ export interface IPersistence {
16
+ clientList: Map<ClientId, Client>;
17
+ retained: RetainStore;
18
+ registerClient(clientId: ClientId, handler: Handler, clean: boolean): IStore;
19
+ deregisterClient(clientId: ClientId): void;
20
+ publish(topic: Topic, packet: PublishPacket): void;
21
+ subscribe(store: IStore, topic: Topic, qos: QoS): void;
22
+ unsubscribe(store: IStore, topic: Topic): void;
23
+ handleRetained(clientId: ClientId): void;
24
+ }
@@ -0,0 +1,14 @@
1
+ import type { ClientId, PacketId, PublishPacket, QoS, Topic } from "./deps.ts";
2
+
3
+ export type PacketStore = Map<PacketId, PublishPacket>;
4
+
5
+ export type SubscriptionStore = Map<Topic, QoS>;
6
+
7
+ export interface IStore {
8
+ clientId: ClientId;
9
+ pendingIncoming: PacketStore;
10
+ pendingOutgoing: PacketStore;
11
+ pendingAckOutgoing: Set<PacketId>;
12
+ subscriptions: SubscriptionStore;
13
+ nextId(): PacketId;
14
+ }
@@ -0,0 +1,157 @@
1
+ import {
2
+ type AnyPacket,
3
+ type IPersistence,
4
+ type IStore,
5
+ logger,
6
+ MqttConn,
7
+ PacketNameByType,
8
+ PacketType,
9
+ type PublishPacket,
10
+ type SockConn,
11
+ type TAuthenticationResult,
12
+ type Timer,
13
+ type Topic,
14
+ } from "./deps.ts";
15
+
16
+ export type ClientId = string;
17
+
18
+ export const SysPrefix = "$";
19
+
20
+ export const utf8Encoder = new TextEncoder();
21
+
22
+ /**
23
+ * Handlers are hooks that the server will call
24
+ * and let you influence the servers behaviour.
25
+ * The following handlers can be configured:
26
+ * - isAuthenticated()
27
+ * - isAuthorizedToPublish()
28
+ * - isAuthorizedToSubscribe()
29
+ */
30
+ export type Handlers = {
31
+ isAuthenticated?(
32
+ ctx: Context,
33
+ clientId: ClientId,
34
+ username: string,
35
+ password: Uint8Array,
36
+ ): TAuthenticationResult;
37
+ isAuthorizedToPublish?(ctx: Context, topic: Topic): boolean;
38
+ isAuthorizedToSubscribe?(ctx: Context, topic: Topic): boolean;
39
+ };
40
+
41
+ /**
42
+ * The Context class is used to maintain state of a MQTT connection
43
+ * It handles:
44
+ * - connect/disconnect including broadcasting of these events
45
+ * - publish
46
+ * - persistence
47
+ * - the will
48
+ */
49
+
50
+ export class Context {
51
+ connected: boolean;
52
+ conn: SockConn;
53
+ mqttConn: MqttConn;
54
+ persistence: IPersistence;
55
+ handlers: Handlers;
56
+ static clientList: Map<ClientId, Context> = new Map();
57
+ store?: IStore;
58
+ will?: PublishPacket;
59
+ timer?: Timer;
60
+
61
+ constructor(persistence: IPersistence, conn: SockConn, handlers: Handlers) {
62
+ this.persistence = persistence;
63
+ this.connected = false;
64
+ this.conn = conn;
65
+ this.mqttConn = new MqttConn({ conn });
66
+ this.handlers = handlers;
67
+ }
68
+
69
+ async send(packet: AnyPacket): Promise<void> {
70
+ logger.debug("Sending", PacketNameByType[packet.type]);
71
+ logger.debug(JSON.stringify(packet, null, 2));
72
+ await this.mqttConn.send(packet);
73
+ }
74
+
75
+ connect(clientId: string, clean: boolean): void {
76
+ logger.debug("Connecting", clientId);
77
+ const existingSession = Context.clientList.get(clientId);
78
+ if (existingSession) {
79
+ existingSession.close(false);
80
+ }
81
+ this.store = this.persistence.registerClient(
82
+ clientId,
83
+ this.doPublish.bind(this),
84
+ clean,
85
+ );
86
+
87
+ this.connected = true;
88
+ this.broadcast("$SYS/connect/clients", clientId);
89
+ logger.debug("Connected", clientId);
90
+ }
91
+
92
+ doPublish(packet: PublishPacket): void {
93
+ const qos = packet.qos || 0;
94
+ if (qos === 0) {
95
+ packet.id = 0;
96
+ this.send(packet);
97
+ return;
98
+ }
99
+ if (this.store) {
100
+ packet.id = this.store.nextId();
101
+ this.store.pendingOutgoing.set(packet.id, packet);
102
+ this.send(packet);
103
+ }
104
+ }
105
+
106
+ clean(clientId: string) {
107
+ this.persistence.deregisterClient(clientId);
108
+ }
109
+
110
+ close(executewill = true): void {
111
+ if (this.connected) {
112
+ logger.debug(
113
+ `Closing ${this.store?.clientId} while mqttConn is ${
114
+ this.mqttConn.isClosed ? "" : "not "
115
+ }closed`,
116
+ );
117
+ this.connected = false;
118
+ if (typeof this.timer === "object") {
119
+ this.timer.clear();
120
+ }
121
+ if (this.store) {
122
+ this.broadcast("$SYS/disconnect/clients", this.store.clientId);
123
+ }
124
+ if (executewill) {
125
+ this.handleWill();
126
+ }
127
+ }
128
+ if (!this.mqttConn.isClosed) {
129
+ this.mqttConn.close();
130
+ }
131
+ }
132
+
133
+ private handleWill() {
134
+ if (this.will) {
135
+ if (
136
+ !this.will.topic.startsWith(SysPrefix) &&
137
+ this.handlers.isAuthorizedToPublish &&
138
+ this.handlers.isAuthorizedToPublish(this, this.will.topic)
139
+ ) {
140
+ this.persistence.publish(this.will.topic, this.will);
141
+ }
142
+ }
143
+ }
144
+
145
+ broadcast(topic: Topic, payload: string, retain = false): void {
146
+ const packet: PublishPacket = {
147
+ type: PacketType.publish,
148
+ topic,
149
+ retain,
150
+ payload: utf8Encoder.encode(payload),
151
+ };
152
+ if (packet.retain === true) {
153
+ this.persistence.retained.set(packet.topic, packet);
154
+ }
155
+ this.persistence.publish(packet.topic, packet);
156
+ }
157
+ }
package/server/deps.ts ADDED
@@ -0,0 +1,37 @@
1
+ export type { SockConn } from "../socket/socket.ts";
2
+ export { MqttConn } from "../mqttConn/mqttConn.ts";
3
+ export type { IPersistence } from "../persistence/persistence.ts";
4
+ export type { IStore } from "../persistence/store.ts";
5
+ export { MemoryPersistence } from "../persistence/memory/memoryPersistence.ts";
6
+ export type {
7
+ AnyPacket,
8
+ ClientId,
9
+ ConnackPacket,
10
+ ConnectPacket,
11
+ DisconnectPacket,
12
+ PacketId,
13
+ PingreqPacket,
14
+ PingresPacket,
15
+ PubackPacket,
16
+ PubcompPacket,
17
+ PublishPacket,
18
+ PubrecPacket,
19
+ PubrelPacket,
20
+ QoS,
21
+ SubackPacket,
22
+ SubscribePacket,
23
+ Subscription,
24
+ TAuthenticationResult,
25
+ Topic,
26
+ TopicFilter,
27
+ UnsubackPacket,
28
+ UnsubscribePacket,
29
+ } from "../mqttPacket/mod.ts";
30
+
31
+ export {
32
+ AuthenticationResult,
33
+ PacketNameByType,
34
+ PacketType,
35
+ } from "../mqttPacket/mod.ts";
36
+ export { Timer } from "../timer/timer.ts";
37
+ export { logger } from "../utils/mod.ts";
@@ -0,0 +1,73 @@
1
+ import type { Context } from "../context.ts";
2
+ import {
3
+ AuthenticationResult,
4
+ type ConnectPacket,
5
+ logger,
6
+ PacketType,
7
+ type TAuthenticationResult,
8
+ Timer,
9
+ } from "../deps.ts";
10
+
11
+ function isAuthenticated(
12
+ ctx: Context,
13
+ packet: ConnectPacket,
14
+ ): TAuthenticationResult {
15
+ if (ctx.handlers.isAuthenticated) {
16
+ return ctx.handlers.isAuthenticated(
17
+ ctx,
18
+ packet.clientId || "",
19
+ packet.username || "",
20
+ packet.password || new Uint8Array(0),
21
+ );
22
+ }
23
+ return AuthenticationResult.ok;
24
+ }
25
+
26
+ function validateConnect(
27
+ ctx: Context,
28
+ packet: ConnectPacket,
29
+ ): TAuthenticationResult {
30
+ if (packet.protocolLevel !== 4) {
31
+ return AuthenticationResult.unacceptableProtocol;
32
+ }
33
+ return isAuthenticated(ctx, packet);
34
+ }
35
+
36
+ export function handleConnect(ctx: Context, packet: ConnectPacket): void {
37
+ const clientId = packet.clientId || `Opifex-${crypto.randomUUID()}`;
38
+ const returnCode = validateConnect(ctx, packet);
39
+ // connect is ok
40
+ if (returnCode === AuthenticationResult.ok) {
41
+ if (packet.will) {
42
+ ctx.will = {
43
+ type: PacketType.publish,
44
+ qos: packet.will.qos,
45
+ retain: packet.will.retain,
46
+ topic: packet.will.topic,
47
+ payload: packet.will.payload,
48
+ };
49
+ }
50
+
51
+ ctx.connect(clientId, packet.clean || false);
52
+
53
+ const keepAlive = packet.keepAlive || 0;
54
+ if (keepAlive > 0) {
55
+ logger.debug(`Setting keepalive to ${keepAlive * 1500} ms`);
56
+ ctx.timer = new Timer(() => {
57
+ ctx.close();
58
+ }, Math.floor(keepAlive * 1500));
59
+ }
60
+ }
61
+
62
+ const sessionPresent = false;
63
+
64
+ ctx.send({
65
+ type: PacketType.connack,
66
+ sessionPresent,
67
+ returnCode,
68
+ });
69
+
70
+ if (returnCode !== AuthenticationResult.ok) {
71
+ ctx.close();
72
+ }
73
+ }
@@ -0,0 +1,6 @@
1
+ import type { Context } from "../context.ts";
2
+
3
+ export function handleDisconnect(ctx: Context): void {
4
+ ctx.will = undefined;
5
+ ctx.close();
6
+ }
@@ -0,0 +1,78 @@
1
+ import type { Context } from "../context.ts";
2
+ import {
3
+ type AnyPacket,
4
+ PacketNameByType,
5
+ PacketType,
6
+ type PubackPacket,
7
+ type PubcompPacket,
8
+ type PublishPacket,
9
+ type PubrecPacket,
10
+ type PubrelPacket,
11
+ type SubscribePacket,
12
+ type UnsubscribePacket,
13
+ } from "../deps.ts";
14
+ import { handleConnect } from "./handleConnect.ts";
15
+ import { handlePingreq } from "./handlePingreq.ts";
16
+ import { handlePublish } from "./handlePublish.ts";
17
+ import { handlePuback } from "./handlePuback.ts";
18
+ import { handlePubrec } from "./handlePubrec.ts";
19
+ import { handlePubrel } from "./handlePubrel.ts";
20
+ import { handlePubcomp } from "./handlePubcomp.ts";
21
+ import { handleSubscribe } from "./handleSubscribe.ts";
22
+ import { handleUnsubscribe } from "./handleUnsubscribe.ts";
23
+ import { handleDisconnect } from "./handleDisconnect.ts";
24
+ import { logger } from "../deps.ts";
25
+
26
+ export async function handlePacket(
27
+ ctx: Context,
28
+ packet: AnyPacket,
29
+ ): Promise<void> {
30
+ logger.debug("handling", PacketNameByType[packet.type]);
31
+ logger.debug(JSON.stringify(packet, null, 2));
32
+ if (!ctx.connected) {
33
+ if (packet.type === PacketType.connect) {
34
+ handleConnect(ctx, packet);
35
+ } else {
36
+ throw new Error(
37
+ `Received ${PacketNameByType[packet.type]} packet before connect`,
38
+ );
39
+ }
40
+ } else {
41
+ switch (packet.type) {
42
+ case PacketType.pingreq:
43
+ await handlePingreq(ctx);
44
+ break;
45
+ case PacketType.publish:
46
+ await handlePublish(ctx, packet as PublishPacket);
47
+ break;
48
+ case PacketType.puback:
49
+ handlePuback(ctx, packet as PubackPacket);
50
+ break;
51
+ case PacketType.pubrel:
52
+ await handlePubrel(ctx, packet as PubrelPacket);
53
+ break;
54
+ case PacketType.pubrec:
55
+ await handlePubrec(ctx, packet as PubrecPacket);
56
+ break;
57
+ case PacketType.pubcomp:
58
+ handlePubcomp(ctx, packet as PubcompPacket);
59
+ break;
60
+ case PacketType.subscribe:
61
+ await handleSubscribe(ctx, packet as SubscribePacket);
62
+ break;
63
+ case PacketType.unsubscribe:
64
+ await handleUnsubscribe(ctx, packet as UnsubscribePacket);
65
+ break;
66
+ case PacketType.disconnect:
67
+ handleDisconnect(ctx);
68
+ break;
69
+ default:
70
+ throw new Error(
71
+ `Received unexpected ${
72
+ PacketNameByType[packet.type]
73
+ } packet after connect`,
74
+ );
75
+ }
76
+ ctx.timer?.reset();
77
+ }
78
+ }
@@ -0,0 +1,8 @@
1
+ import type { Context } from "../context.ts";
2
+ import { PacketType } from "../deps.ts";
3
+
4
+ export async function handlePingreq(ctx: Context): Promise<void> {
5
+ await ctx.send({
6
+ type: PacketType.pingres,
7
+ });
8
+ }
@@ -0,0 +1,12 @@
1
+ import type { Context } from "../context.ts";
2
+ import type { PubackPacket } from "../deps.ts";
3
+
4
+ // A PUBACK Packet is the response to a PUBLISH Packet with QoS level 1.
5
+
6
+ export function handlePuback(ctx: Context, packet: PubackPacket) {
7
+ // qos 1 only
8
+ const id = packet.id;
9
+ if (ctx.store?.pendingOutgoing.has(id)) {
10
+ ctx.store.pendingOutgoing.delete(id);
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ import type { Context } from "../context.ts";
2
+ import type { PubcompPacket } from "../deps.ts";
3
+
4
+ // The PUBCOMP Packet is the response to a PUBREL Packet.
5
+ // It is the fourth and final packet of the QoS 2 protocol exchange.
6
+
7
+ export function handlePubcomp(ctx: Context, packet: PubcompPacket): void {
8
+ const id = packet.id;
9
+ if (ctx.store?.pendingAckOutgoing.has(id)) {
10
+ ctx.store.pendingAckOutgoing.delete(id);
11
+ }
12
+ }
@@ -0,0 +1,47 @@
1
+ import { type Context, SysPrefix } from "../context.ts";
2
+ import { PacketType, type PublishPacket, type Topic } from "../deps.ts";
3
+
4
+ function authorizedToPublish(ctx: Context, topic: Topic) {
5
+ if (topic.startsWith(SysPrefix)) {
6
+ return false;
7
+ }
8
+ if (ctx.handlers.isAuthorizedToPublish) {
9
+ return ctx.handlers.isAuthorizedToPublish(ctx, topic);
10
+ }
11
+ return true;
12
+ }
13
+
14
+ export async function handlePublish(
15
+ ctx: Context,
16
+ packet: PublishPacket,
17
+ ): Promise<void> {
18
+ if (!authorizedToPublish(ctx, packet.topic)) {
19
+ return;
20
+ }
21
+
22
+ const qos = packet.qos || 0;
23
+ if (qos === 0) {
24
+ ctx.persistence.publish(packet.topic, packet);
25
+ return;
26
+ }
27
+
28
+ if (packet.id !== undefined) {
29
+ // qos 1
30
+ if (qos === 1) {
31
+ ctx.persistence.publish(packet.topic, packet);
32
+ await ctx.send({
33
+ type: PacketType.puback,
34
+ id: packet.id,
35
+ });
36
+ return;
37
+ }
38
+ // qos 2
39
+ if (ctx.store) {
40
+ ctx.store.pendingIncoming.set(packet.id, packet);
41
+ await ctx.send({
42
+ type: PacketType.pubrec,
43
+ id: packet.id,
44
+ });
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,20 @@
1
+ import type { Context } from "../context.ts";
2
+ import { PacketType, type PubrecPacket } from "../deps.ts";
3
+
4
+ // qos 2
5
+ // Discard message, Store PUBREC received <Packet Identifier>
6
+ // send PUBREL <Packet Identifier>
7
+ export async function handlePubrec(
8
+ ctx: Context,
9
+ packet: PubrecPacket,
10
+ ): Promise<void> {
11
+ const id = packet.id;
12
+ if (ctx.store?.pendingOutgoing.has(id)) {
13
+ ctx.store.pendingOutgoing.delete(id);
14
+ ctx.store.pendingAckOutgoing.add(id);
15
+ await ctx.send({
16
+ type: PacketType.pubrel,
17
+ id,
18
+ });
19
+ }
20
+ }
@@ -0,0 +1,24 @@
1
+ import type { Context } from "../context.ts";
2
+ import { PacketType, type PubrelPacket } from "../deps.ts";
3
+
4
+ // qos 2 only
5
+ // Method A, Initiate onward delivery of the Application Message1 then discard message
6
+ // Send PUBCOMP <Packet Identifier>
7
+
8
+ export async function handlePubrel(
9
+ ctx: Context,
10
+ packet: PubrelPacket,
11
+ ): Promise<void> {
12
+ const id = packet.id;
13
+ if (ctx.store?.pendingIncoming.has(id)) {
14
+ const storedPacket = ctx.store.pendingIncoming.get(id);
15
+ if (storedPacket) {
16
+ ctx.persistence.publish(storedPacket.topic, storedPacket);
17
+ ctx.store.pendingIncoming.delete(id);
18
+ await ctx.send({
19
+ type: PacketType.pubcomp,
20
+ id,
21
+ });
22
+ }
23
+ }
24
+ }