homebridge-openwrt-control 0.3.8 → 0.3.10

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/CHANGELOG.md CHANGED
@@ -9,6 +9,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  - After update to v0.1.0 the plugin need to be reconfigured!
11
11
 
12
+ ## [0.3.10] - (24.09.2026)
13
+
14
+ ### Changes
15
+
16
+ - added: RESTFul request rate limit, 600 requests per minute per client and at most 1000 clients per minute, answered with `429` and `Retry-After`, one log warning per client and minute.
17
+
18
+ ## [0.3.9] - (23.09.2026)
19
+
20
+ ### Changes
21
+
22
+ - fix: MQTT client had `protocolVersion: 5` hardcoded, so brokers that only support MQTT 3.1.1 (e.g. the ioBroker MQTT adapter) accepted the TCP connection, failed the handshake, and got disconnected every second forever, with nothing published and no error logged. Added a `Protocol Version` option (5.0 / 3.1.1) in the MQTT section, and a one-time warning if no successful connection is established within 30 seconds
23
+ - readme update
24
+ - sample config update
25
+
12
26
  ## [0.3.8] - (23.09.2026)
13
27
 
14
28
  ### Added
@@ -16,6 +30,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
30
  - optional RESTful access `Token`. When set, every request (GET and POST) must send the header `Authorization: Bearer <token>`, otherwise the server responds `401`. Leaving it empty keeps the previous behaviour, so existing configs keep working
17
31
  - readme update
18
32
 
33
+ ## [0.3.7] - (13.09.2026)
34
+
35
+ ### Changes
36
+
37
+ - bump dependencies
38
+
19
39
  ## [0.3.6] - (11.05.2026)
20
40
 
21
41
  ## Changes
package/README.md CHANGED
@@ -106,6 +106,7 @@
106
106
  | `mqtt.port` | Here set the `Port` for MQTT Broker, default 1883. |
107
107
  | `mqtt.clientId` | Here optional set the `Client Id` of MQTT Broker. |
108
108
  | `mqtt.prefix` | Here set the `Prefix` for `Topic` or leave empty. |
109
+ | `mqtt.protocolVersion` | Here select the MQTT protocol version, `5.0` (default) or `3.1.1` for brokers that only support 3.1.1 (e.g. ioBroker MQTT adapter). |
109
110
  | `mqtt.auth{}` | MQTT authorization object. |
110
111
  | `mqtt.auth.enable` | Here enable authorization for MQTT Broker. |
111
112
  | `mqtt.auth.user` | Here set the MQTT Broker `Username`. |
@@ -538,6 +538,25 @@
538
538
  "functionBody": "return model.devices[arrayIndices].mqtt.enable == true;"
539
539
  }
540
540
  },
541
+ "protocolVersion": {
542
+ "title": "Protocol Version",
543
+ "type": "integer",
544
+ "default": 5,
545
+ "oneOf": [
546
+ {
547
+ "title": "5.0",
548
+ "enum": [5]
549
+ },
550
+ {
551
+ "title": "3.1.1",
552
+ "enum": [4]
553
+ }
554
+ ],
555
+ "description": "MQTT protocol version used to connect to the broker. Some brokers (e.g. the ioBroker MQTT adapter) only support 3.1.1 and will silently reject 5.0 connections.",
556
+ "condition": {
557
+ "functionBody": "return model.devices[arrayIndices].mqtt.enable == true;"
558
+ }
559
+ },
541
560
  "auth": {
542
561
  "title": "Authorization",
543
562
  "type": "object",
@@ -805,6 +824,7 @@
805
824
  "devices[].mqtt.port",
806
825
  "devices[].mqtt.clientId",
807
826
  "devices[].mqtt.prefix",
827
+ "devices[].mqtt.protocolVersion",
808
828
  {
809
829
  "key": "devices[].mqtt.auth",
810
830
  "title": "Authorization",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "OpenWrt Control",
3
3
  "name": "homebridge-openwrt-control",
4
- "version": "0.3.8",
4
+ "version": "0.3.10",
5
5
  "description": "Homebridge plugin to control OpenWrt flashed devices.",
6
6
  "license": "MIT",
7
7
  "author": "grzegorz914",
package/src/mqtt.js CHANGED
@@ -8,35 +8,47 @@ class Mqtt extends EventEmitter {
8
8
 
9
9
  const url = `mqtt://${config.host}:${config.port}`;
10
10
  const subscribeTopic = `${config.prefix}/Set`;
11
+ const protocolVersion = config.protocolVersion === 4 ? 4 : 5;
12
+ const isV5 = protocolVersion === 5;
13
+ this.isV5 = isV5;
11
14
 
12
15
  const options = {
13
16
  clientId: config.clientId,
14
17
  username: config.user,
15
18
  password: config.passwd,
16
- protocolVersion: 5,
17
- clean: false,
18
- properties: {
19
- sessionExpiryInterval: 60 * 60,
20
- userProperties: {
21
- source: 'node-client'
19
+ protocolVersion,
20
+ clean: !isV5,
21
+ ...(isV5 ? {
22
+ properties: {
23
+ sessionExpiryInterval: 60 * 60,
24
+ userProperties: {
25
+ source: 'node-client'
26
+ }
22
27
  }
23
- }
28
+ } : {})
24
29
  };
25
30
 
31
+ const startTime = Date.now();
32
+ let hasConnected = false;
33
+ let warnedStalled = false;
34
+
26
35
  this.mqttClient = connect(url, options)
27
36
  .on('connect', async () => {
28
- this.emit('connected', 'MQTT v5 connected.');
37
+ hasConnected = true;
38
+ this.emit('connected', `MQTT v${protocolVersion} connected.`);
29
39
 
30
40
  try {
31
41
  await new Promise((resolve, reject) => {
32
42
  this.mqttClient.subscribe(subscribeTopic,
33
43
  {
34
44
  qos: 1,
35
- properties: {
36
- userProperties: {
37
- type: 'subscription'
45
+ ...(isV5 ? {
46
+ properties: {
47
+ userProperties: {
48
+ type: 'subscription'
49
+ }
38
50
  }
39
- }
51
+ } : {})
40
52
  },
41
53
  (error) => {
42
54
  if (error) return reject(error);
@@ -69,6 +81,10 @@ class Mqtt extends EventEmitter {
69
81
  if (config.logDebug) this.emit('debug', 'MQTT Reconnecting...');
70
82
  })
71
83
  .on('close', () => {
84
+ if (!hasConnected && !warnedStalled && Date.now() - startTime > 30000) {
85
+ warnedStalled = true;
86
+ this.emit('warn', `MQTT has not connected after 30s of retries using protocol v${protocolVersion}. Check broker address, credentials, and whether the broker supports this MQTT protocol version.`);
87
+ }
72
88
  if (config.logDebug) this.emit('debug', 'MQTT Connection closed.');
73
89
  });
74
90
  }
@@ -81,13 +97,15 @@ class Mqtt extends EventEmitter {
81
97
  this.mqttClient.publish(fullTopic, publishMessage,
82
98
  {
83
99
  qos: 1,
84
- properties: {
85
- contentType: 'application/json',
86
- userProperties: {
87
- source: 'node',
88
- action: 'set'
100
+ ...(this.isV5 ? {
101
+ properties: {
102
+ contentType: 'application/json',
103
+ userProperties: {
104
+ source: 'node',
105
+ action: 'set'
106
+ }
89
107
  }
90
- }
108
+ } : {})
91
109
  },
92
110
  (error) => {
93
111
  if (error) {
package/src/restful.js CHANGED
@@ -23,6 +23,49 @@ class RestFul extends EventEmitter {
23
23
  try {
24
24
  const app = express();
25
25
  app.set('json spaces', 2);
26
+
27
+ // Request rate limit per client, protects Homebridge from request floods (CWE-770)
28
+ const rateLimitWindow = 60 * 1000;
29
+ const rateLimitMax = 600;
30
+ const rateLimitClients = 1000;
31
+ const clients = new Map();
32
+ let clientsLimitWarned = 0;
33
+ app.use((req, res, next) => {
34
+ const now = Date.now();
35
+
36
+ // Drop expired windows so the map cannot grow without limit
37
+ if (clients.size > 100) {
38
+ for (const [ip, client] of clients) {
39
+ if (now - client.start >= rateLimitWindow) clients.delete(ip);
40
+ }
41
+ }
42
+
43
+ let client = clients.get(req.ip);
44
+ if (!client || now - client.start >= rateLimitWindow) {
45
+ // Hard limit of tracked addresses, new addresses wait until older windows expire
46
+ if (!client && clients.size >= rateLimitClients) {
47
+ if (now - clientsLimitWarned >= rateLimitWindow) {
48
+ clientsLimitWarned = now;
49
+ if (this.logWarn) this.emit('warn', `RESTFul too many clients (${rateLimitClients}) in one minute, new clients are rejected until the window ends`);
50
+ }
51
+ res.set('Retry-After', String(Math.ceil(rateLimitWindow / 1000)));
52
+ return res.status(429).json({ error: 'RESTFul Too Many Requests' });
53
+ }
54
+ client = { count: 0, start: now, warned: false };
55
+ clients.set(req.ip, client);
56
+ }
57
+
58
+ if (++client.count > rateLimitMax) {
59
+ // Warn once per client and window, a flood must not flood the log too
60
+ if (!client.warned && this.logWarn) this.emit('warn', `RESTFul rate limit ${rateLimitMax} requests per minute exceeded by: ${req.ip}, further requests are rejected until the window ends`);
61
+ client.warned = true;
62
+ res.set('Retry-After', String(Math.ceil((client.start + rateLimitWindow - now) / 1000)));
63
+ return res.status(429).json({ error: 'RESTFul Too Many Requests' });
64
+ }
65
+
66
+ next();
67
+ });
68
+
26
69
  app.use(json());
27
70
 
28
71
  // Optional token auth, when a token is configured every route requires "Authorization: Bearer <token>"
package/src/router.js CHANGED
@@ -207,6 +207,7 @@ class Router extends EventEmitter {
207
207
  port: this.mqtt.port || 1883,
208
208
  clientId: `${this.name}_${Math.random().toString(16).slice(3)}`,
209
209
  prefix: this.mqtt.prefix ? `${this.openWrtInfo.systemInfo.model}/${this.mqtt.prefix}/${this.name}` : `${this.openWrtInfo.systemInfo.model}/${this.name}`,
210
+ protocolVersion: this.mqtt.protocolVersion,
210
211
  user: this.mqtt.auth?.user,
211
212
  passwd: this.mqtt.auth?.passwd,
212
213
  logWarn: this.logWarn,