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 +20 -0
- package/README.md +1 -0
- package/config.schema.json +20 -0
- package/package.json +1 -1
- package/src/mqtt.js +36 -18
- package/src/restful.js +43 -0
- package/src/router.js +1 -0
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`. |
|
package/config.schema.json
CHANGED
|
@@ -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
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
|
|
17
|
-
clean:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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,
|