@seriousme/opifex 1.3.0 → 1.5.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.
@@ -0,0 +1,14 @@
1
+ #!/bin/bash
2
+
3
+ license='LICENSE.txt'
4
+ prefix='2016-'
5
+ current_year=$(date +%Y)
6
+ sed -i -e "s/$prefix\([0-9\]\+\)/$prefix$current_year/" $license
7
+
8
+ if [ "$(git diff $license)" ]; then
9
+ git add $license
10
+ git commit -m "Update license year to $current_year"
11
+ git push
12
+ else
13
+ echo "No changes detected in $license"
14
+ fi
@@ -0,0 +1,13 @@
1
+ name: Update License Year
2
+
3
+ on:
4
+ schedule:
5
+ - cron: "5 12 1 1 *"
6
+
7
+ jobs:
8
+ update_license:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - name: Update license year
13
+ run: ./update_license.sh
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@
4
4
 
5
5
  ### Changed
6
6
 
7
+ ## [v1.5.0] 02-01-2025
8
+
9
+ ### Changed
10
+
11
+ - added client certificate support to mqtts.ts
12
+
13
+ ## [v1.4.0] 02-01-2025
14
+
15
+ ### Changed
16
+
17
+ - added client certificate support (w0otness)
18
+
7
19
  ## [v1.3.0] 19-11-2024
8
20
 
9
21
  ### Changed
package/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Hans Klunder
3
+ Copyright (c) 2023-2025 Hans Klunder
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/bin/mqtt.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  import { DEFAULT_URL } from "../client/mod.ts";
3
3
  import { logger, LogLevel } from "../utils/mod.ts";
4
4
  import { getArgs, parseArgs } from "../utils/mod.ts";
5
- import { getCaCerts, TcpClient } from "../node/client.ts";
5
+ import { getFileData, TcpClient } from "../node/client.ts";
6
+ import type { Args } from "../utils/args.ts";
6
7
 
7
8
  const client = new TcpClient();
8
9
  const encoder = new TextEncoder();
@@ -21,18 +22,30 @@ const ConnectHelp = `
21
22
  -i/--clientId the clientId to connect to the server
22
23
  -U/--username the username to connect to the server
23
24
  -P/--password the password to connect to the server
24
- -c/--certFile the path to a certFile
25
+ -C/--caFile the path to a CA certificate file
26
+ -c/--certFile the path to a certificate file
27
+ -k/--keyFile the path to a key file
25
28
  -n/--noClean try to resume a previous session
26
29
  -h/--help this text
27
30
  `;
28
31
 
29
32
  const connectOpts = {
30
- string: ["url", "username", "password", "certFile", "clientId"],
33
+ string: [
34
+ "url",
35
+ "username",
36
+ "password",
37
+ "caFile",
38
+ "certFile",
39
+ "keyFile",
40
+ "clientId",
41
+ ],
31
42
  alias: {
32
43
  u: "url",
33
44
  U: "username",
34
45
  P: "password",
46
+ C: "caFile",
35
47
  c: "certFile",
48
+ k: "keyFile",
36
49
  i: "clientId",
37
50
  n: "noClean",
38
51
  h: "help",
@@ -80,9 +93,25 @@ function parseQos(qosArg: string | number) {
80
93
  return 0;
81
94
  }
82
95
 
96
+ async function getTLSdata(connectArgs: Args) {
97
+ const caFileData = await getFileData(connectArgs.caFile);
98
+ const caCerts = caFileData ? [caFileData] : undefined;
99
+ const cert = await getFileData(connectArgs.certFile);
100
+ const key = await getFileData(connectArgs.keyFile);
101
+ return {
102
+ caCerts,
103
+ cert,
104
+ key,
105
+ };
106
+ }
107
+
83
108
  async function subscribe() {
84
109
  const connectArgs = parseArgs(getArgs(), connectOpts);
85
- const caCerts = await getCaCerts(connectArgs.certFile);
110
+ const {
111
+ caCerts,
112
+ cert,
113
+ key,
114
+ } = await getTLSdata(connectArgs);
86
115
  const subscribeArgs = parseArgs(getArgs(), subscribeOpts);
87
116
  if (connectArgs.help) {
88
117
  console.log(SubscribeHelp);
@@ -96,6 +125,8 @@ async function subscribe() {
96
125
  await client.connect({
97
126
  url: connectArgs.url,
98
127
  caCerts,
128
+ cert,
129
+ key,
99
130
  options: {
100
131
  username: connectArgs.username,
101
132
  password: encoder.encode(connectArgs.password),
@@ -157,7 +188,11 @@ const publishOpts = {
157
188
 
158
189
  async function publish() {
159
190
  const connectArgs = parseArgs(getArgs(), connectOpts);
160
- const caCerts = await getCaCerts(connectArgs.certFile);
191
+ const {
192
+ caCerts,
193
+ cert,
194
+ key,
195
+ } = await getTLSdata(connectArgs);
161
196
  const publishArgs = parseArgs(getArgs(), publishOpts);
162
197
  if (connectArgs.help) {
163
198
  console.log(PublishHelp);
@@ -171,6 +206,8 @@ async function publish() {
171
206
  await client.connect({
172
207
  url: connectArgs.url,
173
208
  caCerts,
209
+ cert,
210
+ key,
174
211
  options: {
175
212
  username: connectArgs.username,
176
213
  password: encoder.encode(connectArgs.password),
package/client/client.ts CHANGED
@@ -25,6 +25,8 @@ type ConnectOptions = Omit<
25
25
  export type ConnectParameters = {
26
26
  url?: URL;
27
27
  caCerts?: string[];
28
+ cert?: string;
29
+ key?: string;
28
30
  numberOfRetries?: number;
29
31
  options?: ConnectOptions;
30
32
  };
@@ -70,6 +72,8 @@ export class Client {
70
72
  protected keepAlive = DEFAULT_KEEPALIVE;
71
73
  protected autoReconnect = true;
72
74
  private caCerts?: string[];
75
+ private cert?: string;
76
+ private key?: string;
73
77
  private clientId: string;
74
78
  private ctx = new Context(new MemoryStore());
75
79
  private connectPacket?: ConnectPacket;
@@ -84,6 +88,8 @@ export class Client {
84
88
  _hostname: string,
85
89
  _port?: number,
86
90
  _caCerts?: string[],
91
+ _cert?: string,
92
+ _key?: string,
87
93
  ): Promise<SockConn> {
88
94
  // if you need to support alternative connection types just
89
95
  // overload this method in your subclass
@@ -106,6 +112,8 @@ export class Client {
106
112
  this.url.hostname,
107
113
  Number(this.url.port) ?? undefined,
108
114
  this.caCerts,
115
+ this.cert,
116
+ this.key,
109
117
  );
110
118
  // if we get this far we have a connection
111
119
  tryConnect =
@@ -137,6 +145,8 @@ export class Client {
137
145
  this.url = params?.url || this.url;
138
146
  this.numberOfRetries = params.numberOfRetries || this.numberOfRetries;
139
147
  this.caCerts = params?.caCerts;
148
+ this.cert = params?.cert;
149
+ this.key = params?.key;
140
150
  const options = Object.assign(
141
151
  {
142
152
  keepAlive: this.keepAlive,
package/deno/client.ts CHANGED
@@ -4,15 +4,15 @@ import { Client } from "../client/client.ts";
4
4
  import { logger } from "../client/deps.ts";
5
5
  import type { SockConn } from "../client/deps.ts";
6
6
 
7
- export async function getCaCerts(filename: string | undefined) {
7
+ export async function getFileData(filename: string | undefined) {
8
8
  if (!filename) {
9
9
  return;
10
10
  }
11
- const caCerts = await Deno.readTextFile(filename);
12
- if (caCerts === "") {
11
+ const data = await Deno.readTextFile(filename);
12
+ if (data === "") {
13
13
  return;
14
14
  }
15
- return [caCerts];
15
+ return data;
16
16
  }
17
17
 
18
18
  export class TcpClient extends Client {
@@ -25,9 +25,11 @@ export class TcpClient extends Client {
25
25
  hostname: string,
26
26
  port = 8883,
27
27
  caCerts?: string[],
28
+ cert?: string,
29
+ key?: string,
28
30
  ) {
29
- logger.debug({ hostname, port, caCerts });
30
- return await Deno.connectTls({ hostname, port, caCerts });
31
+ logger.debug({ hostname, port, caCerts, cert });
32
+ return await Deno.connectTls({ hostname, port, caCerts, cert, key });
31
33
  }
32
34
 
33
35
  protected override createConn(
@@ -35,11 +37,13 @@ export class TcpClient extends Client {
35
37
  hostname: string,
36
38
  port?: number,
37
39
  caCerts?: string[],
40
+ cert?: string,
41
+ key?: string,
38
42
  ): Promise<SockConn> {
39
43
  // if you need to support alternative connection types just
40
44
  // overload this method in your subclass
41
45
  if (protocol === "mqtts:") {
42
- return this.connectMQTTS(hostname, port, caCerts);
46
+ return this.connectMQTTS(hostname, port, caCerts, cert, key);
43
47
  }
44
48
  if (protocol === "mqtt:") {
45
49
  return this.connectMQTT(hostname, port);
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seriousme/opifex",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "exports": {
5
5
  "./server": "./server/mod.ts",
6
6
  "./client": "./client/mod.ts"
package/deno.lock CHANGED
@@ -2,29 +2,32 @@
2
2
  "version": "4",
3
3
  "specifiers": {
4
4
  "npm:@types/node@*": "22.5.4",
5
- "npm:@types/node@^22.8.6": "22.8.6"
5
+ "npm:@types/node@^22.10.0": "22.10.0"
6
6
  },
7
7
  "npm": {
8
- "@types/node@22.5.4": {
9
- "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
8
+ "@types/node@22.10.0": {
9
+ "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==",
10
10
  "dependencies": [
11
- "undici-types"
11
+ "undici-types@6.20.0"
12
12
  ]
13
13
  },
14
- "@types/node@22.8.6": {
15
- "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==",
14
+ "@types/node@22.5.4": {
15
+ "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
16
16
  "dependencies": [
17
- "undici-types"
17
+ "undici-types@6.19.8"
18
18
  ]
19
19
  },
20
20
  "undici-types@6.19.8": {
21
21
  "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
22
+ },
23
+ "undici-types@6.20.0": {
24
+ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
22
25
  }
23
26
  },
24
27
  "workspace": {
25
28
  "packageJson": {
26
29
  "dependencies": [
27
- "npm:@types/node@^22.8.6"
30
+ "npm:@types/node@^22.10.0"
28
31
  ]
29
32
  }
30
33
  }
package/dist/bin/mqtt.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { DEFAULT_URL } from "../client/mod.js";
3
3
  import { logger, LogLevel } from "../utils/mod.js";
4
4
  import { getArgs, parseArgs } from "../utils/mod.js";
5
- import { getCaCerts, TcpClient } from "../node/client.js";
5
+ import { getFileData, TcpClient } from "../node/client.js";
6
6
  const client = new TcpClient();
7
7
  const encoder = new TextEncoder();
8
8
  const decoder = new TextDecoder();
@@ -18,17 +18,29 @@ const ConnectHelp = `
18
18
  -i/--clientId the clientId to connect to the server
19
19
  -U/--username the username to connect to the server
20
20
  -P/--password the password to connect to the server
21
- -c/--certFile the path to a certFile
21
+ -C/--caFile the path to a CA certificate file
22
+ -c/--certFile the path to a certificate file
23
+ -k/--keyFile the path to a key file
22
24
  -n/--noClean try to resume a previous session
23
25
  -h/--help this text
24
26
  `;
25
27
  const connectOpts = {
26
- string: ["url", "username", "password", "certFile", "clientId"],
28
+ string: [
29
+ "url",
30
+ "username",
31
+ "password",
32
+ "caFile",
33
+ "certFile",
34
+ "keyFile",
35
+ "clientId",
36
+ ],
27
37
  alias: {
28
38
  u: "url",
29
39
  U: "username",
30
40
  P: "password",
41
+ C: "caFile",
31
42
  c: "certFile",
43
+ k: "keyFile",
32
44
  i: "clientId",
33
45
  n: "noClean",
34
46
  h: "help",
@@ -72,9 +84,20 @@ function parseQos(qosArg) {
72
84
  console.log("QoS must be between 0 and 2");
73
85
  return 0;
74
86
  }
87
+ async function getTLSdata(connectArgs) {
88
+ const caFileData = await getFileData(connectArgs.caFile);
89
+ const caCerts = caFileData ? [caFileData] : undefined;
90
+ const cert = await getFileData(connectArgs.certFile);
91
+ const key = await getFileData(connectArgs.keyFile);
92
+ return {
93
+ caCerts,
94
+ cert,
95
+ key,
96
+ };
97
+ }
75
98
  async function subscribe() {
76
99
  const connectArgs = parseArgs(getArgs(), connectOpts);
77
- const caCerts = await getCaCerts(connectArgs.certFile);
100
+ const { caCerts, cert, key, } = await getTLSdata(connectArgs);
78
101
  const subscribeArgs = parseArgs(getArgs(), subscribeOpts);
79
102
  if (connectArgs.help) {
80
103
  console.log(SubscribeHelp);
@@ -88,6 +111,8 @@ async function subscribe() {
88
111
  await client.connect({
89
112
  url: connectArgs.url,
90
113
  caCerts,
114
+ cert,
115
+ key,
91
116
  options: {
92
117
  username: connectArgs.username,
93
118
  password: encoder.encode(connectArgs.password),
@@ -145,7 +170,7 @@ const publishOpts = {
145
170
  };
146
171
  async function publish() {
147
172
  const connectArgs = parseArgs(getArgs(), connectOpts);
148
- const caCerts = await getCaCerts(connectArgs.certFile);
173
+ const { caCerts, cert, key, } = await getTLSdata(connectArgs);
149
174
  const publishArgs = parseArgs(getArgs(), publishOpts);
150
175
  if (connectArgs.help) {
151
176
  console.log(PublishHelp);
@@ -159,6 +184,8 @@ async function publish() {
159
184
  await client.connect({
160
185
  url: connectArgs.url,
161
186
  caCerts,
187
+ cert,
188
+ key,
162
189
  options: {
163
190
  username: connectArgs.username,
164
191
  password: encoder.encode(connectArgs.password),
@@ -34,6 +34,8 @@ export class Client {
34
34
  keepAlive = DEFAULT_KEEPALIVE;
35
35
  autoReconnect = true;
36
36
  caCerts;
37
+ cert;
38
+ key;
37
39
  clientId;
38
40
  ctx = new Context(new MemoryStore());
39
41
  connectPacket;
@@ -41,7 +43,7 @@ export class Client {
41
43
  this.clientId = generateClientId(this.clientIdPrefix);
42
44
  this.numberOfRetries = DEFAULT_RETRIES;
43
45
  }
44
- createConn(protocol, _hostname, _port, _caCerts) {
46
+ createConn(protocol, _hostname, _port, _caCerts, _cert, _key) {
45
47
  // if you need to support alternative connection types just
46
48
  // overload this method in your subclass
47
49
  throw `Unsupported protocol: ${protocol}`;
@@ -57,7 +59,7 @@ export class Client {
57
59
  while (tryConnect) {
58
60
  logger.debug(`${isReconnect ? "re" : ""}connecting`);
59
61
  try {
60
- const conn = await this.createConn(this.url.protocol, this.url.hostname, Number(this.url.port) ?? undefined, this.caCerts);
62
+ const conn = await this.createConn(this.url.protocol, this.url.hostname, Number(this.url.port) ?? undefined, this.caCerts, this.cert, this.key);
61
63
  // if we get this far we have a connection
62
64
  tryConnect =
63
65
  (await this.ctx.handleConnection(conn, this.connectPacket)) &&
@@ -88,6 +90,8 @@ export class Client {
88
90
  this.url = params?.url || this.url;
89
91
  this.numberOfRetries = params.numberOfRetries || this.numberOfRetries;
90
92
  this.caCerts = params?.caCerts;
93
+ this.cert = params?.cert;
94
+ this.key = params?.key;
91
95
  const options = Object.assign({
92
96
  keepAlive: this.keepAlive,
93
97
  clientId: this.clientId,
@@ -6,15 +6,15 @@ import { readFile } from "node:fs/promises";
6
6
  import { createConnection } from "node:net";
7
7
  import * as tls from "node:tls";
8
8
  import { wrapNodeSocket } from "./wrapNodeSocket.js";
9
- export async function getCaCerts(filename) {
9
+ export async function getFileData(filename) {
10
10
  if (!filename) {
11
11
  return;
12
12
  }
13
- const caCerts = await readFile(filename, { encoding: "utf-8" });
14
- if (caCerts === "") {
13
+ const data = await readFile(filename, { encoding: "utf-8" });
14
+ if (data === "") {
15
15
  return;
16
16
  }
17
- return [caCerts];
17
+ return data;
18
18
  }
19
19
  export class TcpClient extends Client {
20
20
  connectMQTT(hostname, port = 1883) {
@@ -25,21 +25,20 @@ export class TcpClient extends Client {
25
25
  resolve(wrapNodeSocket(socket));
26
26
  });
27
27
  socket.once("error", (err) => {
28
- // @ts-ignore the type spec of err is missing err.code
29
28
  logger.debug("Connection failed: ", err);
30
29
  reject(err);
31
30
  });
32
31
  });
33
32
  }
34
- connectMQTTS(hostname, port = 8883, caCerts) {
33
+ connectMQTTS(hostname, port = 8883, ca, cert, key) {
35
34
  const opts = {
36
35
  host: hostname,
37
36
  port,
38
- secureContext: caCerts
39
- ? tls.createSecureContext({ cert: caCerts })
37
+ secureContext: (ca || key || cert)
38
+ ? tls.createSecureContext({ ca, cert, key })
40
39
  : undefined,
41
40
  };
42
- logger.debug({ hostname, port, caCerts });
41
+ logger.debug({ hostname, port, ca, cert });
43
42
  return new Promise((resolve, reject) => {
44
43
  const socket = tls.connect(opts, () => {
45
44
  logger.debug("Connected to server");
@@ -1 +1 @@
1
- {"root":["../bin/conformanceServer.ts","../bin/demoServer.ts","../bin/mqtt.ts"],"version":"5.7.0-beta"}
1
+ {"root":["../bin/conformanceServer.ts","../bin/demoServer.ts","../bin/mqtt.ts"],"version":"5.7.2"}
package/node/client.ts CHANGED
@@ -8,15 +8,15 @@ import * as tls from "node:tls";
8
8
  import type { SockConn } from "../socket/socket.ts";
9
9
  import { wrapNodeSocket } from "./wrapNodeSocket.ts";
10
10
 
11
- export async function getCaCerts(filename: string | undefined) {
11
+ export async function getFileData(filename: string | undefined) {
12
12
  if (!filename) {
13
13
  return;
14
14
  }
15
- const caCerts = await readFile(filename, { encoding: "utf-8" });
16
- if (caCerts === "") {
15
+ const data = await readFile(filename, { encoding: "utf-8" });
16
+ if (data === "") {
17
17
  return;
18
18
  }
19
- return [caCerts];
19
+ return data;
20
20
  }
21
21
 
22
22
  export class TcpClient extends Client {
@@ -31,7 +31,6 @@ export class TcpClient extends Client {
31
31
  },
32
32
  );
33
33
  socket.once("error", (err) => {
34
- // @ts-ignore the type spec of err is missing err.code
35
34
  logger.debug("Connection failed: ", err);
36
35
  reject(err);
37
36
  });
@@ -41,16 +40,18 @@ export class TcpClient extends Client {
41
40
  protected connectMQTTS(
42
41
  hostname: string,
43
42
  port = 8883,
44
- caCerts?: string[],
43
+ ca?: string[],
44
+ cert?: string,
45
+ key?: string,
45
46
  ): Promise<SockConn> {
46
47
  const opts = {
47
48
  host: hostname,
48
49
  port,
49
- secureContext: caCerts
50
- ? tls.createSecureContext({ cert: caCerts })
50
+ secureContext: (ca || key || cert)
51
+ ? tls.createSecureContext({ ca, cert, key })
51
52
  : undefined,
52
53
  };
53
- logger.debug({ hostname, port, caCerts });
54
+ logger.debug({ hostname, port, ca, cert });
54
55
  return new Promise((resolve, reject) => {
55
56
  const socket = tls.connect(opts, () => {
56
57
  logger.debug("Connected to server");
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@seriousme/opifex",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "MQTT client & server for Deno & NodeJS",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "test": "node --experimental-strip-types --test --test-skip-pattern='/^Deno/'",
8
8
  "prebuild": "rm -rf dist",
9
- "build": "npx -p typescript@beta tsc --build",
9
+ "build": "npx -p typescript tsc --build",
10
10
  "postbuild": "deno fmt",
11
11
  "preversion": "npm run test",
12
12
  "version": "npm run build",
@@ -45,6 +45,6 @@
45
45
  },
46
46
  "homepage": "https://github.com/seriousme/opifex#readme",
47
47
  "devDependencies": {
48
- "@types/node": "^22.8.6"
48
+ "@types/node": "^22.10.0"
49
49
  }
50
50
  }