@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,15 @@
1
+ name: "Close stale issues"
2
+ on:
3
+ schedule:
4
+ - cron: "5 0 * * *"
5
+
6
+ jobs:
7
+ stale:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/stale@v3
11
+ with:
12
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
13
+ days-before-stale: 30
14
+ days-before-close: 5
15
+ stale-issue-message: This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days'
@@ -0,0 +1,71 @@
1
+ # For most projects, this workflow file will not need changing; you simply need
2
+ # to commit it to your repository.
3
+ #
4
+ # You may wish to alter this file to override the set of languages analyzed,
5
+ # or to provide custom queries or build logic.
6
+ #
7
+ # ******** NOTE ********
8
+ # We have attempted to detect the languages in your repository. Please check
9
+ # the `language` matrix defined below to confirm you have the correct set of
10
+ # supported CodeQL languages.
11
+ #
12
+ name: "CodeQL"
13
+
14
+ on:
15
+ push:
16
+ branches: [master]
17
+ pull_request:
18
+ # The branches below must be a subset of the branches above
19
+ branches: [master]
20
+ schedule:
21
+ - cron: "27 16 * * 3"
22
+
23
+ jobs:
24
+ analyze:
25
+ name: Analyze
26
+ runs-on: ubuntu-latest
27
+ permissions:
28
+ actions: read
29
+ contents: read
30
+ security-events: write
31
+
32
+ strategy:
33
+ fail-fast: false
34
+ matrix:
35
+ language: ["javascript"]
36
+ # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
37
+ # Learn more:
38
+ # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
39
+
40
+ steps:
41
+ - name: Checkout repository
42
+ uses: actions/checkout@v2
43
+
44
+ # Initializes the CodeQL tools for scanning.
45
+ - name: Initialize CodeQL
46
+ uses: github/codeql-action/init@v1
47
+ with:
48
+ languages: ${{ matrix.language }}
49
+ # If you wish to specify custom queries, you can do so here or in a config file.
50
+ # By default, queries listed here will override any specified in a config file.
51
+ # Prefix the list here with "+" to use these queries and those in the config file.
52
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
53
+
54
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55
+ # If this step fails, then you should remove it and run the build manually (see below)
56
+ - name: Autobuild
57
+ uses: github/codeql-action/autobuild@v1
58
+
59
+ # ℹ️ Command-line programs to run using the OS shell.
60
+ # 📚 https://git.io/JvXDl
61
+
62
+ # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63
+ # and modify them (or add more) to build your code if your project
64
+ # uses a compiled language
65
+
66
+ #- run: |
67
+ # make bootstrap
68
+ # make release
69
+
70
+ - name: Perform CodeQL Analysis
71
+ uses: github/codeql-action/analyze@v1
@@ -0,0 +1,16 @@
1
+ # This workflow will test on Deno lastest stable major and canary
2
+ # see https://github.com/marketplace/actions/setup-deno for details
3
+ name: Deno CI
4
+ on: [push, pull_request]
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v4
10
+ - uses: denoland/setup-deno@v2
11
+ with:
12
+ deno-version: vx.x.x # Run with latest stable Deno.
13
+ - run: deno install
14
+ - run: deno fmt --check
15
+ - run: deno lint
16
+ - run: deno test -A
@@ -0,0 +1,23 @@
1
+ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2
+ # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3
+
4
+ name: Node.js CI
5
+
6
+ on: [push, pull_request]
7
+
8
+ permissions: {}
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+
14
+ strategy:
15
+ matrix:
16
+ node-version: ["22", "23"]
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - name: Use Node.js ${{ matrix.node-version }}
20
+ uses: actions/setup-node@v4
21
+ with:
22
+ node-version: ${{ matrix.node-version }}
23
+ - run: npm run test
@@ -0,0 +1,16 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+ id-token: write # The OIDC ID token is used for authentication with JSR.
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - run: npx jsr publish
@@ -0,0 +1,10 @@
1
+ {
2
+ // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
3
+ // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
4
+
5
+ // List of extensions which should be recommended for users of this workspace.
6
+ "recommendations": ["denoland.vscode-deno"],
7
+
8
+ // List of extensions recommended by VS Code that should not be recommended for users of this workspace.
9
+ "unwantedRecommendations": []
10
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "deno.enable": true,
3
+ "deno.suggest.imports.hosts": {
4
+ "https://deno.land": true,
5
+ "https://io": false,
6
+ "https://d": false,
7
+ "https://de": false,
8
+ "https://deno": false,
9
+ "https://deno.": false,
10
+ "https://deno.la": false
11
+ }
12
+ }
package/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Hans Klunder
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ [![Deno CI](https://github.com/seriousme/opifex/actions/workflows/deno-ci.yml/badge.svg)](https://github.com/seriousme/opifex/actions/workflows/deno-ci.yml)
2
+ [![CodeQL](https://github.com/seriousme/opifex/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/seriousme/opifex/actions/workflows/codeql-analysis.yml)
3
+
4
+ # Opifex
5
+
6
+ Opifex aims to provide a MQTT server and MQTT client in Typescript to be used
7
+ with [Deno](https://deno.land) or [NodeJS](https://nodejs.org) It has _no_ third
8
+ party dependencies, it only relies on built in modules.
9
+
10
+ Its a work in progress, only does MQTT 3.1.1 and currently only has memory based
11
+ persistence.
12
+
13
+ There are separate usage instructions for:
14
+
15
+ - [Deno](deno/README.md)
16
+ - [NodeJS](node/README.md)
17
+
18
+ The client and server libs should also work on Bun as they are engine
19
+ independent, but the demo server and client currently do not work on Bun because
20
+ of some socket incompatibilities between Bun and the rest.
21
+
22
+ ## Naming
23
+
24
+ Some MQTT servers have names like:
25
+
26
+ - [Mosquitto](https://en.wikipedia.org/wiki/Mosquito)
27
+ - [Mosca](https://it.wikipedia.org/wiki/Musca_domestica)
28
+ - [Aedes](https://en.wikipedia.org/wiki/Aedes)
29
+
30
+ So to stay with the theme: [Opifex](https://en.wikipedia.org/wiki/Opifex_(fly))
31
+
32
+ # License
33
+
34
+ Licensed under [MIT](LICENSE.txt)
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types
2
+ // this server is intended to be used with https://github.com/eclipse/iottestware
3
+
4
+ import { AuthenticationResult } from "../server/mod.ts";
5
+ import type { Context, TAuthenticationResult, Topic } from "../server/mod.ts";
6
+ import { logger, LogLevel } from "../utils/mod.ts";
7
+ import { getArgs, parseArgs } from "../utils/mod.ts";
8
+ import { TcpServer } from "../deno/server.ts";
9
+
10
+ const utf8Decoder = new TextDecoder();
11
+ const userTable = new Map();
12
+ userTable.set("IoTester_1", "strong_password");
13
+ userTable.set("IoTester_2", "strong_password");
14
+ const strictUsername = new RegExp(/^[a-zA-Z0-9]{0,23}$/);
15
+ const notAuthorizedTable = new Set();
16
+ notAuthorizedTable.add(["123-456-789", "eclipse/iot/tesware/0data"]);
17
+
18
+ function isAuthenticated(
19
+ _ctx: Context,
20
+ clientId: string,
21
+ username: string,
22
+ password: Uint8Array,
23
+ ): TAuthenticationResult {
24
+ const pwd = utf8Decoder.decode(password);
25
+ logger.debug(
26
+ `Verifying authentication of client '${clientId}' with username '${username}' and password '${pwd}'`,
27
+ );
28
+
29
+ if (!userTable.has(username)) {
30
+ if (!strictUsername.test(username)) {
31
+ return AuthenticationResult.badUsernameOrPassword;
32
+ }
33
+ }
34
+ const pass = userTable.get(username);
35
+ if (pwd === pass) {
36
+ return AuthenticationResult.ok;
37
+ }
38
+ return AuthenticationResult.notAuthorized;
39
+ }
40
+
41
+ function isAuthorizedToPublish(ctx: Context, topic: Topic): boolean {
42
+ logger.debug(
43
+ `Checking authorization of client '${ctx.store?.clientId}' to publish on topic '${topic}'`,
44
+ );
45
+ // if (notAuthorizedTable.has([ctx.client,topic])){
46
+ // return false;
47
+ // }
48
+ return true;
49
+ }
50
+ function isAuthorizedToSubscribe(ctx: Context, topic: Topic): boolean {
51
+ logger.debug(
52
+ `Checking authorization of client '${ctx.store?.clientId}' to subscribe to topic '${topic}'`,
53
+ );
54
+ return true;
55
+ }
56
+
57
+ /** start the server **/
58
+ const { _: [portNum] } = parseArgs(getArgs());
59
+ const port = Number(portNum ?? 1883);
60
+ const hostname = "::";
61
+ logger.level(LogLevel.info);
62
+ const tcpServer = new TcpServer({ port, hostname }, {
63
+ handlers: {
64
+ isAuthenticated,
65
+ isAuthorizedToPublish,
66
+ isAuthorizedToSubscribe,
67
+ },
68
+ });
69
+ tcpServer.start();
70
+ logger.info(`Server started on port ${tcpServer.port}`);
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types
2
+ import { TcpServer } from "../node/server.ts";
3
+ import type { Context, TAuthenticationResult, Topic } from "../server/mod.ts";
4
+ import { AuthenticationResult } from "../server/mod.ts";
5
+ import { getArgs, logger, LogLevel, parseArgs } from "../utils/mod.ts";
6
+
7
+ const utf8Decoder = new TextDecoder();
8
+ const userTable = new Map();
9
+ userTable.set("IoTester_1", "strong_password");
10
+ userTable.set("IoTester_2", "strong_password");
11
+ // const strictUsername = new RegExp(/^[a-zA-Z0-9]{0,23}$/);
12
+
13
+ function isAuthenticated(
14
+ _ctx: Context,
15
+ clientId: string,
16
+ username: string,
17
+ password: Uint8Array,
18
+ ): TAuthenticationResult {
19
+ const pwd = utf8Decoder.decode(password);
20
+ logger.info(
21
+ `Verifying authentication of client '${clientId}' with username '${username}' and password '${pwd}'`,
22
+ );
23
+
24
+ return AuthenticationResult.ok;
25
+ // if (!userTable.has(username)) {
26
+ // if (!strictUsername.test(username)) {
27
+ // return AuthenticationResult.badUsernameOrPassword;
28
+ // }
29
+ // }
30
+
31
+ // const pass = userTable.get(username);
32
+ // if (pwd === pass) {
33
+ // return AuthenticationResult.ok;
34
+ // }
35
+ // return AuthenticationResult.badUsernameOrPassword;
36
+ }
37
+
38
+ function isAuthorizedToPublish(ctx: Context, topic: Topic): boolean {
39
+ logger.debug(
40
+ `Checking authorization of client '${ctx.store?.clientId}' to publish on topic '${topic}'`,
41
+ );
42
+ return true;
43
+ }
44
+ function isAuthorizedToSubscribe(ctx: Context, topic: Topic): boolean {
45
+ logger.debug(
46
+ `Checking authorization of client '${ctx.store?.clientId}' to subscribe to topic '${topic}'`,
47
+ );
48
+ return true;
49
+ }
50
+
51
+ /** start the server **/
52
+ const { _: [portNum] } = parseArgs(getArgs());
53
+ const port = Number(portNum ?? 1883);
54
+ const hostname = "::";
55
+ logger.level(LogLevel.info);
56
+ const tcpServer = new TcpServer({ port, hostname }, {
57
+ handlers: {
58
+ isAuthenticated,
59
+ isAuthorizedToPublish,
60
+ isAuthorizedToSubscribe,
61
+ },
62
+ });
63
+ tcpServer.start();
64
+ logger.info(`Server started on port ${tcpServer.port}`);
package/bin/mqtt.ts ADDED
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types
2
+ import { DEFAULT_URL } from "../client/mod.ts";
3
+ import { logger, LogLevel } from "../utils/mod.ts";
4
+ import { getArgs, parseArgs } from "../utils/mod.ts";
5
+ import { getCaCerts, TcpClient } from "../node/client.ts";
6
+
7
+ const client = new TcpClient();
8
+ const encoder = new TextEncoder();
9
+ const decoder = new TextDecoder();
10
+ logger.level(LogLevel.info);
11
+
12
+ const MQTTHelp = `MQTT.ts command line interface, available commands are:
13
+
14
+ * publish publish a message to the broker
15
+ * subscribe subscribe for updates from the broker
16
+
17
+ Launch 'mqtt.ts [command] --help' to know more about the commands.`;
18
+
19
+ const ConnectHelp = `
20
+ -u/--url the URL to connect to: default is ${DEFAULT_URL}
21
+ -i/--clientId the clientId to connect to the server
22
+ -U/--username the username to connect to the server
23
+ -P/--password the password to connect to the server
24
+ -c/--certFile the path to a certFile
25
+ -n/--noClean try to resume a previous session
26
+ -h/--help this text
27
+ `;
28
+
29
+ const connectOpts = {
30
+ string: ["url", "username", "password", "certFile", "clientId"],
31
+ alias: {
32
+ u: "url",
33
+ U: "username",
34
+ P: "password",
35
+ c: "certFile",
36
+ i: "clientId",
37
+ n: "noClean",
38
+ h: "help",
39
+ },
40
+ boolean: ["noClean", "help"],
41
+ default: {
42
+ noClean: false,
43
+ },
44
+ };
45
+
46
+ const SubscribeHelp = `Usage: MQTT.ts subscribe <options>
47
+
48
+ Where options are:
49
+ -t/--topic the topic to use
50
+ -q/--qos the QoS (0/1/2) to use, default is 0
51
+ ${ConnectHelp}
52
+ Example: MQTT.ts subscribe -t hello`;
53
+
54
+ const subscribeOpts = {
55
+ string: ["topic"],
56
+ alias: {
57
+ t: "topic",
58
+ q: "qos",
59
+ },
60
+ default: {
61
+ qos: 0,
62
+ topic: "",
63
+ },
64
+ };
65
+
66
+ function parseQos(qosArg: string | number) {
67
+ const qos = Number(qosArg);
68
+ switch (qos) {
69
+ case 0:
70
+ return 0;
71
+ case 1:
72
+ return 1;
73
+ case 2:
74
+ return 2;
75
+ default:
76
+ break;
77
+ }
78
+ console.log("QoS must be between 0 and 2");
79
+ return 0;
80
+ }
81
+
82
+ async function subscribe() {
83
+ const connectArgs = parseArgs(getArgs(), connectOpts);
84
+ const caCerts = await getCaCerts(connectArgs.certFile);
85
+ const subscribeArgs = parseArgs(getArgs(), subscribeOpts);
86
+ if (connectArgs.help) {
87
+ console.log(SubscribeHelp);
88
+ return;
89
+ }
90
+ if (subscribeArgs.topic === undefined) {
91
+ console.log("Missing `topic`");
92
+ return;
93
+ }
94
+ try {
95
+ await client.connect({
96
+ url: connectArgs.url,
97
+ caCerts,
98
+ options: {
99
+ username: connectArgs.username,
100
+ password: encoder.encode(connectArgs.password),
101
+ clientId: connectArgs.clientId,
102
+ clean: !connectArgs.noClean,
103
+ keepAlive: 60,
104
+ },
105
+ });
106
+ logger.debug("Connected !");
107
+
108
+ client.subscribe({
109
+ subscriptions: [
110
+ {
111
+ topicFilter: subscribeArgs.topic,
112
+ qos: parseQos(subscribeArgs.qos),
113
+ },
114
+ ],
115
+ });
116
+ logger.debug("Subscribed!");
117
+
118
+ for await (const message of client.messages()) {
119
+ console.log(decoder.decode(message.payload));
120
+ }
121
+ } catch (err) {
122
+ if (err instanceof Error) {
123
+ logger.info(err.message);
124
+ }
125
+ }
126
+ }
127
+
128
+ const PublishHelp = `Usage: MQTT.ts publish <options>
129
+
130
+ Where options are:
131
+ -t/--topic the topic to use
132
+ -m/--message the message to send
133
+ -q/--qos the QoS (0/1/2) to use, default is 0
134
+ -r/--retain if the message should be retained, default is false
135
+ ${ConnectHelp}
136
+ Example: MQTT.ts publish -t hello -m world`;
137
+
138
+ const publishOpts = {
139
+ string: ["topic", "message"],
140
+ boolean: ["retain"],
141
+ alias: {
142
+ t: "topic",
143
+ m: "message",
144
+ q: "qos",
145
+ r: "retain",
146
+ },
147
+ default: {
148
+ qos: 0,
149
+ dup: false,
150
+ retain: false,
151
+ topic: undefined,
152
+ message: "",
153
+ },
154
+ };
155
+
156
+ async function publish() {
157
+ const connectArgs = parseArgs(getArgs(), connectOpts);
158
+ const caCerts = await getCaCerts(connectArgs.certFile);
159
+ const publishArgs = parseArgs(getArgs(), publishOpts);
160
+ if (connectArgs.help) {
161
+ console.log(PublishHelp);
162
+ return;
163
+ }
164
+ if (publishArgs.topic === undefined) {
165
+ console.log("Missing `topic`");
166
+ return;
167
+ }
168
+ try {
169
+ await client.connect({
170
+ url: connectArgs.url,
171
+ caCerts,
172
+ options: {
173
+ username: connectArgs.username,
174
+ password: encoder.encode(connectArgs.password),
175
+ clientId: connectArgs.clientId,
176
+ clean: !connectArgs.noClean,
177
+ },
178
+ });
179
+ logger.debug("Connected !");
180
+ await client.publish({
181
+ topic: publishArgs.topic,
182
+ payload: encoder.encode(publishArgs.message),
183
+ retain: publishArgs.retain,
184
+ qos: parseQos(publishArgs.qos),
185
+ });
186
+ logger.debug("Published!");
187
+ client.disconnect();
188
+ logger.debug("Disconnected !");
189
+ } catch (err) {
190
+ if (err instanceof Error) {
191
+ logger.info(err.message);
192
+ }
193
+ }
194
+ }
195
+
196
+ function processArgs() {
197
+ const { _: [cmd] } = parseArgs(getArgs());
198
+ switch (cmd) {
199
+ case "publish":
200
+ publish();
201
+ break;
202
+ case "subscribe":
203
+ subscribe();
204
+ break;
205
+ default:
206
+ console.log(MQTTHelp);
207
+ break;
208
+ }
209
+ }
210
+
211
+ processArgs();