@seriousme/opifex 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/closeStale.yml +15 -0
- package/.github/workflows/codeql-analysis.yml +71 -0
- package/.github/workflows/deno-ci.yml +16 -0
- package/.github/workflows/nodejs-ci.yml +23 -0
- package/.github/workflows/publish.yml +16 -0
- package/.vscode/extensions.json +10 -0
- package/.vscode/settings.json +12 -0
- package/LICENSE.txt +21 -0
- package/README.md +34 -0
- package/bin/conformanceServer.ts +70 -0
- package/bin/demoServer.ts +64 -0
- package/bin/mqtt.ts +211 -0
- package/client/client.ts +181 -0
- package/client/context.ts +197 -0
- package/client/deps.ts +36 -0
- package/client/handlers/handleConnack.ts +21 -0
- package/client/handlers/handlePacket.ts +70 -0
- package/client/handlers/handlePuback.ts +10 -0
- package/client/handlers/handlePubcomp.ts +13 -0
- package/client/handlers/handlePublish.ts +32 -0
- package/client/handlers/handlePubrec.ts +24 -0
- package/client/handlers/handlePubrel.ts +26 -0
- package/client/handlers/handleSuback.ts +14 -0
- package/client/handlers/handleUnsuback.ts +11 -0
- package/client/mod.ts +11 -0
- package/client/store/deps.ts +22 -0
- package/client/store/memoryStore.ts +56 -0
- package/client/store/store.ts +29 -0
- package/deno/README.md +64 -0
- package/deno/client.ts +48 -0
- package/deno/server.test.ts +14 -0
- package/deno/server.ts +31 -0
- package/deno/wrapDenoConn.ts +12 -0
- package/deno.json +8 -0
- package/deno.lock +47 -0
- package/dev_utils/dummyConn.ts +95 -0
- package/dev_utils/mod.ts +1 -0
- package/mqttConn/mqttConn.test.ts +110 -0
- package/mqttConn/mqttConn.ts +134 -0
- package/mqttPacket/AuthenticationResult.ts +16 -0
- package/mqttPacket/BitMask.ts +10 -0
- package/mqttPacket/PacketType.ts +21 -0
- package/mqttPacket/connack.test.ts +129 -0
- package/mqttPacket/connack.ts +37 -0
- package/mqttPacket/connect.test.ts +297 -0
- package/mqttPacket/connect.ts +159 -0
- package/mqttPacket/decoder.test.ts +123 -0
- package/mqttPacket/decoder.ts +109 -0
- package/mqttPacket/disconnect.test.ts +50 -0
- package/mqttPacket/disconnect.ts +21 -0
- package/mqttPacket/encoder.test.ts +39 -0
- package/mqttPacket/encoder.ts +72 -0
- package/mqttPacket/length.test.ts +75 -0
- package/mqttPacket/length.ts +57 -0
- package/mqttPacket/mod.ts +151 -0
- package/mqttPacket/pingreq.test.ts +50 -0
- package/mqttPacket/pingreq.ts +22 -0
- package/mqttPacket/pingres.test.ts +50 -0
- package/mqttPacket/pingres.ts +21 -0
- package/mqttPacket/puback.test.ts +55 -0
- package/mqttPacket/puback.ts +28 -0
- package/mqttPacket/pubcomp.test.ts +55 -0
- package/mqttPacket/pubcomp.ts +28 -0
- package/mqttPacket/publish.test.ts +198 -0
- package/mqttPacket/publish.ts +73 -0
- package/mqttPacket/pubrec.test.ts +55 -0
- package/mqttPacket/pubrec.ts +28 -0
- package/mqttPacket/pubrel.test.ts +55 -0
- package/mqttPacket/pubrel.ts +28 -0
- package/mqttPacket/suback.test.ts +49 -0
- package/mqttPacket/suback.ts +41 -0
- package/mqttPacket/subscribe.test.ts +233 -0
- package/mqttPacket/subscribe.ts +69 -0
- package/mqttPacket/types.ts +27 -0
- package/mqttPacket/unsuback.test.ts +55 -0
- package/mqttPacket/unsuback.ts +28 -0
- package/mqttPacket/unsubscribe.test.ts +136 -0
- package/mqttPacket/unsubscribe.ts +48 -0
- package/mqttPacket/validators.ts +20 -0
- package/node/README.md +46 -0
- package/node/client.ts +58 -0
- package/node/server.test.ts +14 -0
- package/node/server.ts +34 -0
- package/node/wrapNodeSocket.ts +54 -0
- package/package.json +35 -0
- package/persistence/deps.ts +12 -0
- package/persistence/memory/memoryPersistence.test.ts +151 -0
- package/persistence/memory/memoryPersistence.ts +156 -0
- package/persistence/persistence.ts +24 -0
- package/persistence/store.ts +14 -0
- package/server/context.ts +157 -0
- package/server/deps.ts +37 -0
- package/server/handlers/handleConnect.ts +73 -0
- package/server/handlers/handleDisconnect.ts +6 -0
- package/server/handlers/handlePacket.ts +78 -0
- package/server/handlers/handlePingreq.ts +8 -0
- package/server/handlers/handlePuback.ts +12 -0
- package/server/handlers/handlePubcomp.ts +12 -0
- package/server/handlers/handlePublish.ts +47 -0
- package/server/handlers/handlePubrec.ts +20 -0
- package/server/handlers/handlePubrel.ts +24 -0
- package/server/handlers/handleSubscribe.ts +47 -0
- package/server/handlers/handleUnsubscribe.ts +17 -0
- package/server/mod.ts +16 -0
- package/server/server.ts +74 -0
- package/server/test/handleConnect.test.ts +126 -0
- package/server/test/test-handlers.ts +52 -0
- package/socket/socket.ts +59 -0
- package/timer/timer.test.ts +48 -0
- package/timer/timer.ts +45 -0
- package/trie/trie.test.ts +147 -0
- package/trie/trie.ts +106 -0
- package/utils/args.ts +86 -0
- package/utils/assert.ts +31 -0
- package/utils/asyncQueue.ts +63 -0
- package/utils/deferred.ts +12 -0
- package/utils/logger.ts +35 -0
- package/utils/mod.ts +7 -0
- package/utils/nextTick.ts +3 -0
- package/utils/platform.ts +4 -0
|
@@ -0,0 +1,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,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,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
|
+
}
|