@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
|
@@ -39,11 +39,11 @@ jobs:
|
|
|
39
39
|
|
|
40
40
|
steps:
|
|
41
41
|
- name: Checkout repository
|
|
42
|
-
uses: actions/checkout@
|
|
42
|
+
uses: actions/checkout@v4
|
|
43
43
|
|
|
44
44
|
# Initializes the CodeQL tools for scanning.
|
|
45
45
|
- name: Initialize CodeQL
|
|
46
|
-
uses: github/codeql-action/init@
|
|
46
|
+
uses: github/codeql-action/init@v3
|
|
47
47
|
with:
|
|
48
48
|
languages: ${{ matrix.language }}
|
|
49
49
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
@@ -54,7 +54,7 @@ jobs:
|
|
|
54
54
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
|
55
55
|
# If this step fails, then you should remove it and run the build manually (see below)
|
|
56
56
|
- name: Autobuild
|
|
57
|
-
uses: github/codeql-action/autobuild@
|
|
57
|
+
uses: github/codeql-action/autobuild@v3
|
|
58
58
|
|
|
59
59
|
# ℹ️ Command-line programs to run using the OS shell.
|
|
60
60
|
# 📚 https://git.io/JvXDl
|
|
@@ -68,4 +68,4 @@ jobs:
|
|
|
68
68
|
# make release
|
|
69
69
|
|
|
70
70
|
- name: Perform CodeQL Analysis
|
|
71
|
-
uses: github/codeql-action/analyze@
|
|
71
|
+
uses: github/codeql-action/analyze@v3
|
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
# Opifex
|
|
5
5
|
|
|
6
6
|
Opifex aims to provide a MQTT server and MQTT client in Typescript to be used
|
|
7
|
-
with [Deno](https://deno.land)
|
|
8
|
-
party dependencies, it only relies on
|
|
7
|
+
with [Deno](https://deno.land), [NodeJS](https://nodejs.org) or
|
|
8
|
+
[Bun](https://bun.sh) It has _no_ third party dependencies, it only relies on
|
|
9
|
+
built in modules.
|
|
9
10
|
|
|
10
11
|
Its a work in progress, only does MQTT 3.1.1 and currently only has memory based
|
|
11
12
|
persistence.
|
|
@@ -80,12 +81,10 @@ types of transport (e.g. Unix sockets or websocketstream) then its recommended
|
|
|
80
81
|
to clone and modify the platform specific code in `/node` or `/deno` as well.
|
|
81
82
|
|
|
82
83
|
If you want to port the platform independent client and server libs to another
|
|
83
|
-
platform
|
|
84
|
-
|
|
84
|
+
platform then the platform specific code in `/node` or `/deno` might serve as
|
|
85
|
+
inspiration.
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
are engine independent, but the demo server and client currently do not work on
|
|
88
|
-
Bun because of some socket incompatibilities between Bun and the rest.
|
|
87
|
+
Bun (as of version 1.2) is capable of running the NodeJS version.
|
|
89
88
|
|
|
90
89
|
## Naming
|
|
91
90
|
|
package/bin/conformanceServer.ts
CHANGED
|
@@ -1,19 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
2
|
// this server is intended to be used with https://github.com/eclipse/iottestware
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* MQTT Server implementation with authentication and authorization
|
|
6
|
+
* @module mqttServer
|
|
7
|
+
*/
|
|
8
|
+
import { TcpServer } from "../node/tcpServer.ts";
|
|
5
9
|
import type { Context, TAuthenticationResult, Topic } from "../server/mod.ts";
|
|
10
|
+
import { AuthenticationResult } from "../server/mod.ts";
|
|
6
11
|
import { getArgs, logger, LogLevel, parseArgs } from "../utils/mod.ts";
|
|
7
|
-
import { TcpServer } from "../node/tcpServer.ts";
|
|
8
12
|
|
|
13
|
+
/**
|
|
14
|
+
* UTF-8 decoder for parsing passwords
|
|
15
|
+
*/
|
|
9
16
|
const utf8Decoder = new TextDecoder();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Map storing valid username/password combinations
|
|
20
|
+
*/
|
|
10
21
|
const userTable = new Map();
|
|
11
22
|
userTable.set("IoTester_1", "strong_password");
|
|
12
23
|
userTable.set("IoTester_2", "strong_password");
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Regular expression for validating usernames
|
|
27
|
+
* Allows alphanumeric characters up to 23 chars long
|
|
28
|
+
*/
|
|
13
29
|
const strictUsername = new RegExp(/^[a-zA-Z0-9]{0,23}$/);
|
|
14
|
-
const notAuthorizedTable = new Set();
|
|
15
|
-
notAuthorizedTable.add(["123-456-789", "eclipse/iot/tesware/0data"]);
|
|
16
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Flag to enable/disable username checking
|
|
33
|
+
* If false, all users are allowed access
|
|
34
|
+
* If true, only users in userTable are allowed access
|
|
35
|
+
*/
|
|
36
|
+
const checkUsername = true;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Authenticates MQTT clients
|
|
40
|
+
* @param {Context} _ctx - Connection context
|
|
41
|
+
* @param {string} clientId - Client identifier
|
|
42
|
+
* @param {string} username - Username to authenticate
|
|
43
|
+
* @param {Uint8Array} password - Password as byte array
|
|
44
|
+
* @returns {TAuthenticationResult} Authentication result
|
|
45
|
+
*/
|
|
17
46
|
function isAuthenticated(
|
|
18
47
|
_ctx: Context,
|
|
19
48
|
clientId: string,
|
|
@@ -21,31 +50,47 @@ function isAuthenticated(
|
|
|
21
50
|
password: Uint8Array,
|
|
22
51
|
): TAuthenticationResult {
|
|
23
52
|
const pwd = utf8Decoder.decode(password);
|
|
24
|
-
logger.
|
|
53
|
+
logger.info(
|
|
25
54
|
`Verifying authentication of client '${clientId}' with username '${username}' and password '${pwd}'`,
|
|
26
55
|
);
|
|
27
56
|
|
|
28
|
-
if (!
|
|
29
|
-
|
|
30
|
-
return AuthenticationResult.badUsernameOrPassword;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
const pass = userTable.get(username);
|
|
34
|
-
if (pwd === pass) {
|
|
57
|
+
if (!checkUsername) {
|
|
58
|
+
// allow all users access
|
|
35
59
|
return AuthenticationResult.ok;
|
|
36
60
|
}
|
|
61
|
+
// is the username valid according to MQTT specification
|
|
62
|
+
if (!strictUsername.test(username)) {
|
|
63
|
+
return AuthenticationResult.badUsernameOrPassword;
|
|
64
|
+
}
|
|
65
|
+
// Does user and password match an entry in userTable
|
|
66
|
+
if (userTable.has(username)) {
|
|
67
|
+
const pass = userTable.get(username);
|
|
68
|
+
if (pwd === pass) {
|
|
69
|
+
return AuthenticationResult.ok;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
37
72
|
return AuthenticationResult.notAuthorized;
|
|
38
73
|
}
|
|
39
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Checks if client is authorized to publish to a topic
|
|
77
|
+
* @param {Context} ctx - Connection context
|
|
78
|
+
* @param {Topic} topic - Topic to publish to
|
|
79
|
+
* @returns {boolean} True if authorized
|
|
80
|
+
*/
|
|
40
81
|
function isAuthorizedToPublish(ctx: Context, topic: Topic): boolean {
|
|
41
82
|
logger.debug(
|
|
42
83
|
`Checking authorization of client '${ctx.store?.clientId}' to publish on topic '${topic}'`,
|
|
43
84
|
);
|
|
44
|
-
// if (notAuthorizedTable.has([ctx.client,topic])){
|
|
45
|
-
// return false;
|
|
46
|
-
// }
|
|
47
85
|
return true;
|
|
48
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Checks if client is authorized to subscribe to a topic
|
|
90
|
+
* @param {Context} ctx - Connection context
|
|
91
|
+
* @param {Topic} topic - Topic to subscribe to
|
|
92
|
+
* @returns {boolean} True if authorized
|
|
93
|
+
*/
|
|
49
94
|
function isAuthorizedToSubscribe(ctx: Context, topic: Topic): boolean {
|
|
50
95
|
logger.debug(
|
|
51
96
|
`Checking authorization of client '${ctx.store?.clientId}' to subscribe to topic '${topic}'`,
|
|
@@ -53,11 +98,17 @@ function isAuthorizedToSubscribe(ctx: Context, topic: Topic): boolean {
|
|
|
53
98
|
return true;
|
|
54
99
|
}
|
|
55
100
|
|
|
56
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Parse command line arguments and start server
|
|
103
|
+
*/
|
|
57
104
|
const { _: [portNum] } = parseArgs(getArgs());
|
|
58
105
|
const port = Number(portNum ?? 1883);
|
|
59
106
|
const hostname = "::";
|
|
60
107
|
logger.level(LogLevel.info);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* TCP Server instance with authentication and authorization handlers
|
|
111
|
+
*/
|
|
61
112
|
const tcpServer = new TcpServer({ port, hostname }, {
|
|
62
113
|
handlers: {
|
|
63
114
|
isAuthenticated,
|
package/bin/demoServer.ts
CHANGED
|
@@ -1,15 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MQTT Server implementation with authentication and authorization
|
|
5
|
+
* @module mqttServer
|
|
6
|
+
*/
|
|
2
7
|
import { TcpServer } from "../node/tcpServer.ts";
|
|
3
8
|
import type { Context, TAuthenticationResult, Topic } from "../server/mod.ts";
|
|
4
9
|
import { AuthenticationResult } from "../server/mod.ts";
|
|
5
10
|
import { getArgs, logger, LogLevel, parseArgs } from "../utils/mod.ts";
|
|
6
11
|
|
|
12
|
+
/**
|
|
13
|
+
* UTF-8 decoder for parsing passwords
|
|
14
|
+
*/
|
|
7
15
|
const utf8Decoder = new TextDecoder();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Map storing valid username/password combinations
|
|
19
|
+
*/
|
|
8
20
|
const userTable = new Map();
|
|
9
21
|
userTable.set("IoTester_1", "strong_password");
|
|
10
22
|
userTable.set("IoTester_2", "strong_password");
|
|
11
|
-
// const strictUsername = new RegExp(/^[a-zA-Z0-9]{0,23}$/);
|
|
12
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Regular expression for validating usernames
|
|
26
|
+
* Allows alphanumeric characters up to 23 chars long
|
|
27
|
+
*/
|
|
28
|
+
const strictUsername = new RegExp(/^[a-zA-Z0-9]{0,23}$/);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Flag to enable/disable username checking
|
|
32
|
+
* If false, all users are allowed access
|
|
33
|
+
* If true, only users in userTable are allowed access
|
|
34
|
+
*/
|
|
35
|
+
const checkUsername = false;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Authenticates MQTT clients
|
|
39
|
+
* @param {Context} _ctx - Connection context
|
|
40
|
+
* @param {string} clientId - Client identifier
|
|
41
|
+
* @param {string} username - Username to authenticate
|
|
42
|
+
* @param {Uint8Array} password - Password as byte array
|
|
43
|
+
* @returns {TAuthenticationResult} Authentication result
|
|
44
|
+
*/
|
|
13
45
|
function isAuthenticated(
|
|
14
46
|
_ctx: Context,
|
|
15
47
|
clientId: string,
|
|
@@ -21,26 +53,43 @@ function isAuthenticated(
|
|
|
21
53
|
`Verifying authentication of client '${clientId}' with username '${username}' and password '${pwd}'`,
|
|
22
54
|
);
|
|
23
55
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
56
|
+
if (!checkUsername) {
|
|
57
|
+
// allow all users access
|
|
58
|
+
return AuthenticationResult.ok;
|
|
59
|
+
}
|
|
60
|
+
// is the username valid according to MQTT specification
|
|
61
|
+
if (!strictUsername.test(username)) {
|
|
62
|
+
return AuthenticationResult.badUsernameOrPassword;
|
|
63
|
+
}
|
|
64
|
+
// Does user and password match an entry in userTable
|
|
65
|
+
if (userTable.has(username)) {
|
|
66
|
+
const pass = userTable.get(username);
|
|
67
|
+
if (pwd === pass) {
|
|
68
|
+
return AuthenticationResult.ok;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return AuthenticationResult.notAuthorized;
|
|
36
72
|
}
|
|
37
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Checks if client is authorized to publish to a topic
|
|
76
|
+
* @param {Context} ctx - Connection context
|
|
77
|
+
* @param {Topic} topic - Topic to publish to
|
|
78
|
+
* @returns {boolean} True if authorized
|
|
79
|
+
*/
|
|
38
80
|
function isAuthorizedToPublish(ctx: Context, topic: Topic): boolean {
|
|
39
81
|
logger.debug(
|
|
40
82
|
`Checking authorization of client '${ctx.store?.clientId}' to publish on topic '${topic}'`,
|
|
41
83
|
);
|
|
42
84
|
return true;
|
|
43
85
|
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Checks if client is authorized to subscribe to a topic
|
|
89
|
+
* @param {Context} ctx - Connection context
|
|
90
|
+
* @param {Topic} topic - Topic to subscribe to
|
|
91
|
+
* @returns {boolean} True if authorized
|
|
92
|
+
*/
|
|
44
93
|
function isAuthorizedToSubscribe(ctx: Context, topic: Topic): boolean {
|
|
45
94
|
logger.debug(
|
|
46
95
|
`Checking authorization of client '${ctx.store?.clientId}' to subscribe to topic '${topic}'`,
|
|
@@ -48,11 +97,17 @@ function isAuthorizedToSubscribe(ctx: Context, topic: Topic): boolean {
|
|
|
48
97
|
return true;
|
|
49
98
|
}
|
|
50
99
|
|
|
51
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Parse command line arguments and start server
|
|
102
|
+
*/
|
|
52
103
|
const { _: [portNum] } = parseArgs(getArgs());
|
|
53
104
|
const port = Number(portNum ?? 1883);
|
|
54
105
|
const hostname = "::";
|
|
55
106
|
logger.level(LogLevel.info);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* TCP Server instance with authentication and authorization handlers
|
|
110
|
+
*/
|
|
56
111
|
const tcpServer = new TcpServer({ port, hostname }, {
|
|
57
112
|
handlers: {
|
|
58
113
|
isAuthenticated,
|
package/bin/mqtt.ts
CHANGED
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview MQTT command line interface that provides publish and subscribe functionality
|
|
5
|
+
* @module mqtt-cli
|
|
6
|
+
*
|
|
7
|
+
* @description
|
|
8
|
+
* This module implements a command line interface for MQTT operations:
|
|
9
|
+
* - Publish messages to an MQTT broker
|
|
10
|
+
* - Subscribe to topics on an MQTT broker
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - TLS support with custom certificates
|
|
14
|
+
* - Authentication with username/password
|
|
15
|
+
* - Configurable QoS levels (0-2)
|
|
16
|
+
* - Message retention
|
|
17
|
+
* - Session persistence
|
|
18
|
+
* - Custom client IDs
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* # Publish a message
|
|
22
|
+
* mqtt publish -t topic -m message
|
|
23
|
+
*
|
|
24
|
+
* # Subscribe to a topic
|
|
25
|
+
* mqtt subscribe -t topic
|
|
26
|
+
*
|
|
27
|
+
* @requires node
|
|
28
|
+
* @requires ../client/mod.ts
|
|
29
|
+
* @requires ../node/tcpClient.ts
|
|
30
|
+
* @requires ../utils/mod.ts
|
|
31
|
+
*/
|
|
2
32
|
import { DEFAULT_URL } from "../client/mod.ts";
|
|
3
33
|
import { getFileData, TcpClient } from "../node/tcpClient.ts";
|
|
4
34
|
import { getArgs, logger, LogLevel, parseArgs } from "../utils/mod.ts";
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Object representing possible connection states
|
|
3
|
+
* @readonly
|
|
4
|
+
*/
|
|
1
5
|
export const ConnectionState = {
|
|
6
|
+
/** System is offline and not attempting connection */
|
|
2
7
|
offline: "offline",
|
|
8
|
+
/** System is in the process of establishing a connection */
|
|
3
9
|
connecting: "connecting",
|
|
10
|
+
/** System has an active connection */
|
|
4
11
|
connected: "connected",
|
|
12
|
+
/** System is in the process of disconnecting */
|
|
5
13
|
disconnecting: "disconnecting",
|
|
14
|
+
/** System has been disconnected */
|
|
6
15
|
disconnected: "disconnected",
|
|
7
16
|
} as const;
|
|
8
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @description Type representing the possible values of ConnectionState
|
|
20
|
+
*/
|
|
9
21
|
export type TConnectionState =
|
|
10
22
|
(typeof ConnectionState)[keyof typeof ConnectionState];
|
package/client/client.ts
CHANGED
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
|
|
13
13
|
import { Context } from "./context.ts";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Generates a random client ID with the given prefix
|
|
17
|
+
* @param prefix - The prefix to use for the client ID
|
|
18
|
+
* @returns A string containing the prefix followed by a random number
|
|
19
|
+
*/
|
|
15
20
|
function generateClientId(prefix: string): string {
|
|
16
21
|
return `${prefix}-${Math.random().toString().slice(-10)}`;
|
|
17
22
|
}
|
|
@@ -36,8 +41,14 @@ export type PublishParameters = Omit<PublishPacket, "type" | "id">;
|
|
|
36
41
|
/** SubscribeParameters define how to subscribe to a topic */
|
|
37
42
|
export type SubscribeParameters = Omit<SubscribePacket, "type" | "id">;
|
|
38
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Implements exponential backoff sleep with optional randomization
|
|
46
|
+
* based on https://dthain.blogspot.com/2009/02/exponential-backoff-in-distributed.html
|
|
47
|
+
* @param random - Whether to add randomization to the delay
|
|
48
|
+
* @param attempt - The attempt number (used to calculate delay)
|
|
49
|
+
* @returns Promise that resolves after the calculated delay
|
|
50
|
+
*/
|
|
39
51
|
function backOffSleep(random: boolean, attempt: number): Promise<void> {
|
|
40
|
-
// based on https://dthain.blogspot.com/2009/02/exponential-backoff-in-distributed.html
|
|
41
52
|
const factor = 1.5;
|
|
42
53
|
const min = 1000;
|
|
43
54
|
const max = 5000;
|
|
@@ -64,7 +75,6 @@ const CLIENTID_PREFIX = "opifex"; // on first connect
|
|
|
64
75
|
* override the createConn() method to provide a
|
|
65
76
|
* connection type that is supported by the subclass.
|
|
66
77
|
*/
|
|
67
|
-
|
|
68
78
|
export class Client {
|
|
69
79
|
protected clientIdPrefix = CLIENTID_PREFIX;
|
|
70
80
|
protected numberOfRetries = DEFAULT_RETRIES;
|
|
@@ -78,11 +88,24 @@ export class Client {
|
|
|
78
88
|
private ctx = new Context(new MemoryStore());
|
|
79
89
|
private connectPacket?: ConnectPacket;
|
|
80
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Creates a new MQTT client instance
|
|
93
|
+
*/
|
|
81
94
|
constructor() {
|
|
82
95
|
this.clientId = generateClientId(this.clientIdPrefix);
|
|
83
96
|
this.numberOfRetries = DEFAULT_RETRIES;
|
|
84
97
|
}
|
|
85
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Creates a new connection to the MQTT broker
|
|
101
|
+
* @param protocol - The protocol to use (mqtt, mqtts, etc)
|
|
102
|
+
* @param _hostname - The hostname to connect to
|
|
103
|
+
* @param _port - The port to connect to
|
|
104
|
+
* @param _caCerts - Optional CA certificates
|
|
105
|
+
* @param _cert - Optional client certificate
|
|
106
|
+
* @param _key - Optional client key
|
|
107
|
+
* @returns Promise resolving to a SockConn connection
|
|
108
|
+
*/
|
|
86
109
|
protected createConn(
|
|
87
110
|
protocol: string,
|
|
88
111
|
_hostname: string,
|
|
@@ -96,6 +119,10 @@ export class Client {
|
|
|
96
119
|
throw `Unsupported protocol: ${protocol}`;
|
|
97
120
|
}
|
|
98
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Handles the connection process including retries and reconnection
|
|
124
|
+
* @returns Promise that resolves when connection is established or fails
|
|
125
|
+
*/
|
|
99
126
|
private async doConnect(): Promise<void> {
|
|
100
127
|
if (!this.connectPacket) {
|
|
101
128
|
return;
|
|
@@ -141,6 +168,11 @@ export class Client {
|
|
|
141
168
|
}
|
|
142
169
|
}
|
|
143
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Connects to the MQTT broker
|
|
173
|
+
* @param params - Connection parameters
|
|
174
|
+
* @returns Promise resolving to authentication result
|
|
175
|
+
*/
|
|
144
176
|
connect(params: ConnectParameters = {}): Promise<TAuthenticationResult> {
|
|
145
177
|
this.url = params?.url || this.url;
|
|
146
178
|
this.numberOfRetries = params.numberOfRetries || this.numberOfRetries;
|
|
@@ -164,10 +196,19 @@ export class Client {
|
|
|
164
196
|
return deferred.promise;
|
|
165
197
|
}
|
|
166
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Disconnects from the MQTT broker
|
|
201
|
+
* @returns Promise that resolves when disconnected
|
|
202
|
+
*/
|
|
167
203
|
async disconnect(): Promise<void> {
|
|
168
204
|
await this.ctx.disconnect();
|
|
169
205
|
}
|
|
170
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Publishes a message to the MQTT broker
|
|
209
|
+
* @param params - Publish parameters including topic and payload
|
|
210
|
+
* @returns Promise that resolves when published
|
|
211
|
+
*/
|
|
171
212
|
async publish(params: PublishParameters): Promise<void> {
|
|
172
213
|
const packet: PublishPacket = {
|
|
173
214
|
type: PacketType.publish,
|
|
@@ -176,6 +217,11 @@ export class Client {
|
|
|
176
217
|
await this.ctx.publish(packet);
|
|
177
218
|
}
|
|
178
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Subscribes to topics on the MQTT broker
|
|
222
|
+
* @param params - Subscribe parameters including topics
|
|
223
|
+
* @returns Promise that resolves when subscribed
|
|
224
|
+
*/
|
|
179
225
|
async subscribe(params: SubscribeParameters): Promise<void> {
|
|
180
226
|
const packet: SubscribePacket = {
|
|
181
227
|
type: PacketType.subscribe,
|
|
@@ -185,10 +231,18 @@ export class Client {
|
|
|
185
231
|
await this.ctx.subscribe(packet);
|
|
186
232
|
}
|
|
187
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Gets an async iterator for received messages
|
|
236
|
+
* @returns AsyncGenerator yielding received publish packets
|
|
237
|
+
*/
|
|
188
238
|
async *messages(): AsyncGenerator<PublishPacket, void, unknown> {
|
|
189
239
|
yield* this.ctx.incoming;
|
|
190
240
|
}
|
|
191
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Closes the message stream
|
|
244
|
+
* @param reason - Optional reason for closing
|
|
245
|
+
*/
|
|
192
246
|
closeMessages(reason?: string) {
|
|
193
247
|
this.ctx.incoming.close(reason);
|
|
194
248
|
}
|
package/client/context.ts
CHANGED
|
@@ -166,6 +166,11 @@ export class Context {
|
|
|
166
166
|
return false;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
// just an alias to clarify protocol flow
|
|
170
|
+
receivePubcomp(id: PacketId): boolean {
|
|
171
|
+
return this.receivePuback(id);
|
|
172
|
+
}
|
|
173
|
+
|
|
169
174
|
receiveSuback(id: PacketId, returnCodes: ReturnCodes): boolean {
|
|
170
175
|
const unresolvedMap = this.unresolvedSubscribe;
|
|
171
176
|
if (unresolvedMap.has(id)) {
|
|
@@ -2,6 +2,12 @@ import type { Context } from "../context.ts";
|
|
|
2
2
|
import { ConnectionState } from "../ConnectionState.ts";
|
|
3
3
|
import { AuthenticationResultByNumber, type ConnackPacket } from "../deps.ts";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Handles the CONNACK packet received from the MQTT broker
|
|
7
|
+
* @param packet - The CONNACK packet containing the connection acknowledgment
|
|
8
|
+
* @param ctx - The connection context
|
|
9
|
+
* @returns Promise that resolves when handling is complete
|
|
10
|
+
*/
|
|
5
11
|
export async function handleConnack(packet: ConnackPacket, ctx: Context) {
|
|
6
12
|
if (packet.returnCode === 0) {
|
|
7
13
|
ctx.connectionState = ConnectionState.connected;
|
|
@@ -21,6 +21,13 @@ import type {
|
|
|
21
21
|
UnsubackPacket,
|
|
22
22
|
} from "../deps.ts";
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Handles incoming MQTT packets based on the connection state and packet type
|
|
26
|
+
* @param ctx - The MQTT client context containing connection state and other information
|
|
27
|
+
* @param packet - The MQTT packet to handle
|
|
28
|
+
* @throws Error if an unexpected packet is received before connection is established
|
|
29
|
+
* @throws Error if an unexpected packet type is received after connection
|
|
30
|
+
*/
|
|
24
31
|
export async function handlePacket(
|
|
25
32
|
ctx: Context,
|
|
26
33
|
packet: AnyPacket,
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { Context } from "../context.ts";
|
|
2
2
|
import type { PubackPacket } from "../deps.ts";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Handles PUBACK packet processing for QoS level 1 PUBLISH responses
|
|
6
|
+
* @param ctx - The MQTT client context
|
|
7
|
+
* @param packet - The PUBACK packet received from the broker
|
|
8
|
+
* @description
|
|
9
|
+
* - Removes the packet ID from the list of pending outgoing messages
|
|
10
|
+
* - Completes the corresponding publish operation
|
|
11
|
+
*/
|
|
6
12
|
export function handlePuback(ctx: Context, packet: PubackPacket): void {
|
|
7
13
|
const id = packet.id;
|
|
8
14
|
ctx.store.pendingOutgoing.delete(id);
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import type { Context } from "../context.ts";
|
|
2
2
|
import type { PubcompPacket } from "../deps.ts";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Handles PUBCOMP packets which are the response to PUBREL packets in QoS 2 flow
|
|
6
|
+
* @param ctx - The connection context containing the client state and store
|
|
7
|
+
* @param packet - The PUBCOMP packet received from the broker
|
|
8
|
+
* @description
|
|
9
|
+
* This function processes PUBCOMP packets which are the fourth and final packet
|
|
10
|
+
* in the QoS 2 protocol exchange. It removes the message from pendingAckOutgoing queue
|
|
11
|
+
* Completes the corresponding publish operation
|
|
12
|
+
*/
|
|
7
13
|
export function handlePubcomp(ctx: Context, packet: PubcompPacket): void {
|
|
8
14
|
const id = packet.id;
|
|
9
15
|
if (ctx.store.pendingAckOutgoing.has(id)) {
|
|
10
16
|
ctx.store.pendingAckOutgoing.delete(id);
|
|
11
|
-
ctx.
|
|
17
|
+
ctx.receivePubcomp(id);
|
|
12
18
|
}
|
|
13
19
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { Context } from "../context.ts";
|
|
2
2
|
import { PacketType, type PublishPacket } from "../deps.ts";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Handles incoming MQTT publish packets based on their QoS level
|
|
6
|
+
* @param ctx - The MQTT connection context
|
|
7
|
+
* @param packet - The incoming publish packet to handle
|
|
8
|
+
* @returns Promise that resolves when packet is handled
|
|
9
|
+
*/
|
|
6
10
|
export async function handlePublish(ctx: Context, packet: PublishPacket) {
|
|
7
11
|
const qos = packet.qos || 0;
|
|
8
12
|
if (qos === 0) {
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { Context } from "../context.ts";
|
|
2
2
|
import { PacketType, type PubrecPacket, type PubrelPacket } from "../deps.ts";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Handles PUBREC (Publish Received) packets for QoS 2 message flow
|
|
6
|
+
* It is the second packet of the QoS 2 protocol exchange.
|
|
7
|
+
* @param ctx - The connection context containing message stores and send function
|
|
8
|
+
* @param packet - The received PUBREC packet
|
|
9
|
+
* @description
|
|
10
|
+
* When a PUBREC packet is received:
|
|
11
|
+
* 1. Checks if there is a pending outgoing message with matching packet ID
|
|
12
|
+
* 2. Creates a PUBREL packet to acknowledge the PUBREC
|
|
13
|
+
* 3. Add the packet ID to pendingAckOutgoing store
|
|
14
|
+
* 4. Removes the message from pendingOutgoing store
|
|
15
|
+
* 5. Sends the PUBREL packet
|
|
16
|
+
*/
|
|
10
17
|
export async function handlePubrec(
|
|
11
18
|
ctx: Context,
|
|
12
19
|
packet: PubrecPacket,
|