matterbridge 3.3.5-dev-20251025-26d5c31 → 3.3.5-dev-20251029-a0d9d11

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
@@ -30,9 +30,19 @@ Advantages:
30
30
 
31
31
  ## [3.3.5] - 2025-10-??
32
32
 
33
+ ### Added
34
+
35
+ - [thread]: Added get_log_level and set_log_level to BroadcastServer.
36
+ - [frontend]: Added password check to WebSocket.
37
+
33
38
  ### Changed
34
39
 
35
40
  - [package]: Updated dependencies.
41
+ - [frontend]: Bumped `frontend` version to 3.2.4.
42
+
43
+ ### Fixed
44
+
45
+ - [service]: Fixed systemd configuration with local global node_modules.
36
46
 
37
47
  <a href="https://www.buymeacoffee.com/luligugithub">
38
48
  <img src="bmc-button.svg" alt="Buy me a coffee" width="80">
@@ -51,7 +51,7 @@ Create a systemctl configuration file for Matterbridge
51
51
  sudo nano /etc/systemd/system/matterbridge.service
52
52
  ```
53
53
 
54
- Add the following to this file, replacing 5 times (!) USER with your user name (e.g. WorkingDirectory=/home/pi/Matterbridge, User=pi and Group=pi and Environment="NPM_CONFIG_PREFIX=/home/pi/.npm-global"):
54
+ Add the following to this file, replacing 4 times (!) USER with your user name (e.g. WorkingDirectory=/home/pi/Matterbridge, User=pi and Group=pi and Environment="NPM_CONFIG_PREFIX=/home/pi/.npm-global"):
55
55
 
56
56
  ```
57
57
  [Unit]
@@ -171,56 +171,3 @@ save it and run
171
171
  ```bash
172
172
  sudo systemctl restart systemd-journald
173
173
  ```
174
-
175
- ## Verify that with your distro you can run sudo npm install -g matterbridge without the password
176
-
177
- Run the following command to verify if you can install Matterbridge globally without being prompted for a password:
178
-
179
- ```bash
180
- sudo npm install -g matterbridge --omit=dev
181
- ```
182
-
183
- If you are not prompted for a password, no further action is required.
184
-
185
- If that is not the case, open the sudoers file for editing using visudo
186
-
187
- ```bash
188
- sudo visudo
189
- ```
190
-
191
- verify the presence of of a line
192
-
193
- ```
194
- @includedir /etc/sudoers.d
195
- ```
196
-
197
- exit and create a configuration file for sudoers
198
-
199
- ```bash
200
- sudo nano /etc/sudoers.d/matterbridge
201
- ```
202
-
203
- add this line replacing USER with your user name (e.g. radxa ALL=(ALL) NOPASSWD: ALL)
204
-
205
- ```
206
- <USER> ALL=(ALL) NOPASSWD: ALL
207
- ```
208
-
209
- or if you prefers to only give access to npm without password try with (e.g. radxa ALL=(ALL) NOPASSWD: /usr/bin/npm)
210
-
211
- ```
212
- <USER> ALL=(ALL) NOPASSWD: /usr/bin/npm
213
- ```
214
-
215
- save the file and reload the settings with:
216
-
217
- ```bash
218
- sudo chmod 0440 /etc/sudoers.d/matterbridge
219
- sudo visudo -c
220
- ```
221
-
222
- Verify if you can install Matterbridge globally without being prompted for a password:
223
-
224
- ```bash
225
- sudo npm install -g matterbridge --omit=dev
226
- ```
package/README.md CHANGED
@@ -172,6 +172,10 @@ Config editor:
172
172
 
173
173
  [Service configurations](README-SERVICE.md)
174
174
 
175
+ or with local global node_modules
176
+
177
+ [Service configurations with local global node_modules](README-SERVICE-LOCAL.md)
178
+
175
179
  ### Run matterbridge as a system service with launchctl (macOS only)
176
180
 
177
181
  [Launchctl configurations](README-MACOS-PLIST.md)
@@ -20,6 +20,13 @@ export class DeviceManager {
20
20
  if (this.server.isWorkerRequest(msg, msg.type) && (msg.dst === 'all' || msg.dst === 'devices')) {
21
21
  this.log.debug(`**Received request message ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
22
22
  switch (msg.type) {
23
+ case 'get_log_level':
24
+ this.server.respond({ ...msg, response: { success: true, logLevel: this.log.logLevel } });
25
+ break;
26
+ case 'set_log_level':
27
+ this.log.logLevel = msg.params.logLevel;
28
+ this.server.respond({ ...msg, response: { success: true, logLevel: this.log.logLevel } });
29
+ break;
23
30
  case 'devices_length':
24
31
  this.server.respond({ ...msg, response: { length: this.length } });
25
32
  break;
package/dist/frontend.js CHANGED
@@ -26,6 +26,7 @@ export class Frontend extends EventEmitter {
26
26
  log;
27
27
  port = 8283;
28
28
  listening = false;
29
+ storedPassword = undefined;
29
30
  expressApp;
30
31
  httpServer;
31
32
  httpsServer;
@@ -46,6 +47,13 @@ export class Frontend extends EventEmitter {
46
47
  if (this.server.isWorkerRequest(msg, msg.type) && (msg.dst === 'all' || msg.dst === 'frontend')) {
47
48
  this.log.debug(`Received broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
48
49
  switch (msg.type) {
50
+ case 'get_log_level':
51
+ this.server.respond({ ...msg, response: { success: true, logLevel: this.log.logLevel } });
52
+ break;
53
+ case 'set_log_level':
54
+ this.log.logLevel = msg.params.logLevel;
55
+ this.server.respond({ ...msg, response: { success: true, logLevel: this.log.logLevel } });
56
+ break;
49
57
  case 'frontend_start':
50
58
  await this.start(msg.params.port);
51
59
  this.server.respond({ ...msg, response: { success: true } });
@@ -54,6 +62,30 @@ export class Frontend extends EventEmitter {
54
62
  await this.stop();
55
63
  this.server.respond({ ...msg, response: { success: true } });
56
64
  break;
65
+ case 'frontend_refreshrequired':
66
+ this.wssSendRefreshRequired(msg.params.changed, { matter: msg.params.matter });
67
+ this.server.respond({ ...msg, response: { success: true } });
68
+ break;
69
+ case 'frontend_restartrequired':
70
+ this.wssSendRestartRequired(msg.params.snackbar, msg.params.fixed);
71
+ this.server.respond({ ...msg, response: { success: true } });
72
+ break;
73
+ case 'frontend_restartnotrequired':
74
+ this.wssSendRestartNotRequired(msg.params.snackbar);
75
+ this.server.respond({ ...msg, response: { success: true } });
76
+ break;
77
+ case 'frontend_updaterequired':
78
+ this.wssSendUpdateRequired(msg.params.devVersion);
79
+ this.server.respond({ ...msg, response: { success: true } });
80
+ break;
81
+ case 'frontend_snackbarmessage':
82
+ this.wssSendSnackbarMessage(msg.params.message, msg.params.timeout, msg.params.severity);
83
+ this.server.respond({ ...msg, response: { success: true } });
84
+ break;
85
+ case 'frontend_attributechanged':
86
+ this.wssSendAttributeChangedMessage(msg.params.plugin, msg.params.serialNumber, msg.params.uniqueId, msg.params.number, msg.params.id, msg.params.cluster, msg.params.attribute, msg.params.value);
87
+ this.server.respond({ ...msg, response: { success: true } });
88
+ break;
57
89
  default:
58
90
  this.log.debug(`Unknown broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}`);
59
91
  }
@@ -61,6 +93,9 @@ export class Frontend extends EventEmitter {
61
93
  if (this.server.isWorkerResponse(msg, msg.type)) {
62
94
  this.log.debug(`Received broadcast response ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
63
95
  switch (msg.type) {
96
+ case 'get_log_level':
97
+ case 'set_log_level':
98
+ break;
64
99
  case 'plugins_install':
65
100
  this.wssSendCloseSnackbarMessage(`Installing package ${msg.response.packageName}...`);
66
101
  if (msg.response.success) {
@@ -93,6 +128,7 @@ export class Frontend extends EventEmitter {
93
128
  }
94
129
  async start(port = 8283) {
95
130
  this.port = port;
131
+ this.storedPassword = await this.matterbridge.nodeContext?.get('password', '');
96
132
  this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
97
133
  const multer = await import('multer');
98
134
  const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
@@ -100,6 +136,47 @@ export class Frontend extends EventEmitter {
100
136
  const express = await import('express');
101
137
  this.expressApp = express.default();
102
138
  this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
139
+ this.log.debug(`Creating WebSocketServer...`);
140
+ const ws = await import('ws');
141
+ this.webSocketServer = new ws.WebSocketServer({ noServer: true });
142
+ this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
143
+ this.webSocketServer.on('connection', (ws, request) => {
144
+ const clientIp = request.socket.remoteAddress;
145
+ let callbackLogLevel = "notice";
146
+ if (this.matterbridge.getLogLevel() === "info" || Logger.level === MatterLogLevel.INFO)
147
+ callbackLogLevel = "info";
148
+ if (this.matterbridge.getLogLevel() === "debug" || Logger.level === MatterLogLevel.DEBUG)
149
+ callbackLogLevel = "debug";
150
+ AnsiLogger.setGlobalCallback(this.wssSendLogMessage.bind(this), callbackLogLevel);
151
+ this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
152
+ this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
153
+ ws.on('message', (message) => {
154
+ this.wsMessageHandler(ws, message);
155
+ });
156
+ ws.on('ping', () => {
157
+ this.log.debug('WebSocket client ping');
158
+ ws.pong();
159
+ });
160
+ ws.on('pong', () => {
161
+ this.log.debug('WebSocket client pong');
162
+ });
163
+ ws.on('close', () => {
164
+ this.log.info('WebSocket client disconnected');
165
+ if (this.webSocketServer?.clients.size === 0) {
166
+ AnsiLogger.setGlobalCallback(undefined);
167
+ this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed');
168
+ }
169
+ });
170
+ ws.on('error', (error) => {
171
+ this.log.error(`WebSocket client error: ${error}`);
172
+ });
173
+ });
174
+ this.webSocketServer.on('close', () => {
175
+ this.log.debug(`WebSocketServer closed`);
176
+ });
177
+ this.webSocketServer.on('error', (ws, error) => {
178
+ this.log.error(`WebSocketServer error: ${error}`);
179
+ });
103
180
  if (!hasParameter('ssl')) {
104
181
  const http = await import('node:http');
105
182
  try {
@@ -128,6 +205,32 @@ export class Frontend extends EventEmitter {
128
205
  this.emit('server_listening', 'http', this.port);
129
206
  });
130
207
  }
208
+ this.httpServer.on('upgrade', async (req, socket, head) => {
209
+ try {
210
+ if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
211
+ socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
212
+ return socket.destroy();
213
+ }
214
+ const url = new URL(req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
215
+ const password = url.searchParams.get('password') ?? '';
216
+ if (password !== this.storedPassword) {
217
+ this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
218
+ socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
219
+ return socket.destroy();
220
+ }
221
+ this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
222
+ this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
223
+ this.webSocketServer?.emit('connection', ws, req);
224
+ });
225
+ }
226
+ catch (err) {
227
+ {
228
+ inspectError(this.log, 'WebSocket upgrade error:', err);
229
+ socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
230
+ socket.destroy();
231
+ }
232
+ }
233
+ });
131
234
  this.httpServer.on('error', (error) => {
132
235
  this.log.error(`Frontend http server error listening on ${this.port}`);
133
236
  switch (error.code) {
@@ -233,6 +336,32 @@ export class Frontend extends EventEmitter {
233
336
  this.emit('server_listening', 'https', this.port);
234
337
  });
235
338
  }
339
+ this.httpsServer.on('upgrade', async (req, socket, head) => {
340
+ try {
341
+ if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
342
+ socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
343
+ return socket.destroy();
344
+ }
345
+ const url = new URL(req.url ?? '/', `https://${req.headers.host || 'localhost'}`);
346
+ const password = url.searchParams.get('password') ?? '';
347
+ if (password !== this.storedPassword) {
348
+ this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
349
+ socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
350
+ return socket.destroy();
351
+ }
352
+ this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
353
+ this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
354
+ this.webSocketServer?.emit('connection', ws, req);
355
+ });
356
+ }
357
+ catch (err) {
358
+ {
359
+ inspectError(this.log, 'WebSocket upgrade error:', err);
360
+ socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
361
+ socket.destroy();
362
+ }
363
+ }
364
+ });
236
365
  this.httpsServer.on('error', (error) => {
237
366
  this.log.error(`Frontend https server error listening on ${this.port}`);
238
367
  switch (error.code) {
@@ -247,50 +376,6 @@ export class Frontend extends EventEmitter {
247
376
  return;
248
377
  });
249
378
  }
250
- const ws = await import('ws');
251
- this.log.debug(`Creating WebSocketServer...`);
252
- this.webSocketServer = new ws.WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
253
- this.webSocketServer.on('connection', (ws, request) => {
254
- const clientIp = request.socket.remoteAddress;
255
- let callbackLogLevel = "notice";
256
- if (this.matterbridge.getLogLevel() === "info" || Logger.level === MatterLogLevel.INFO)
257
- callbackLogLevel = "info";
258
- if (this.matterbridge.getLogLevel() === "debug" || Logger.level === MatterLogLevel.DEBUG)
259
- callbackLogLevel = "debug";
260
- AnsiLogger.setGlobalCallback(this.wssSendLogMessage.bind(this), callbackLogLevel);
261
- this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
262
- this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
263
- ws.on('message', (message) => {
264
- this.wsMessageHandler(ws, message);
265
- });
266
- ws.on('ping', () => {
267
- this.log.debug('WebSocket client ping');
268
- ws.pong();
269
- });
270
- ws.on('pong', () => {
271
- this.log.debug('WebSocket client pong');
272
- });
273
- ws.on('close', () => {
274
- this.log.info('WebSocket client disconnected');
275
- if (this.webSocketServer?.clients.size === 0) {
276
- AnsiLogger.setGlobalCallback(undefined);
277
- this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed');
278
- }
279
- });
280
- ws.on('error', (error) => {
281
- this.log.error(`WebSocket client error: ${error}`);
282
- });
283
- });
284
- this.webSocketServer.on('close', () => {
285
- this.log.debug(`WebSocketServer closed`);
286
- });
287
- this.webSocketServer.on('listening', () => {
288
- this.log.info(`The WebSocketServer is listening`);
289
- this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
290
- });
291
- this.webSocketServer.on('error', (ws, error) => {
292
- this.log.error(`WebSocketServer error: ${error}`);
293
- });
294
379
  cliEmitter.removeAllListeners();
295
380
  cliEmitter.on('uptime', (systemUptime, processUptime) => {
296
381
  this.wssSendUptimeUpdate(systemUptime, processUptime);
@@ -303,25 +388,13 @@ export class Frontend extends EventEmitter {
303
388
  });
304
389
  this.expressApp.post('/api/login', express.json(), async (req, res) => {
305
390
  const { password } = req.body;
306
- this.log.debug('The frontend sent /api/login', password);
307
- if (!this.matterbridge.nodeContext) {
308
- this.log.error('/api/login nodeContext not found');
309
- res.json({ valid: false });
310
- return;
391
+ this.log.debug(`The frontend sent /api/login with password ${password ? '[redacted]' : '(empty)'}`);
392
+ if (this.storedPassword === '' || password === this.storedPassword) {
393
+ this.log.debug('/api/login password valid');
394
+ res.json({ valid: true });
311
395
  }
312
- try {
313
- const storedPassword = await this.matterbridge.nodeContext.get('password', '');
314
- if (storedPassword === '' || password === storedPassword) {
315
- this.log.debug('/api/login password valid');
316
- res.json({ valid: true });
317
- }
318
- else {
319
- this.log.warn('/api/login error wrong password');
320
- res.json({ valid: false });
321
- }
322
- }
323
- catch (error) {
324
- this.log.error('/api/login error getting password');
396
+ else {
397
+ this.log.warn('/api/login error wrong password');
325
398
  res.json({ valid: false });
326
399
  }
327
400
  });
@@ -1480,6 +1553,7 @@ export class Frontend extends EventEmitter {
1480
1553
  case 'setpassword':
1481
1554
  if (isValidString(data.params.value)) {
1482
1555
  await this.matterbridge.nodeContext?.set('password', data.params.value);
1556
+ this.storedPassword = data.params.value;
1483
1557
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1484
1558
  }
1485
1559
  break;
@@ -146,6 +146,9 @@ export class Matterbridge extends EventEmitter {
146
146
  if (this.server.isWorkerResponse(msg, msg.type)) {
147
147
  this.log.debug(`**Received broadcast response ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
148
148
  switch (msg.type) {
149
+ case 'get_log_level':
150
+ case 'set_log_level':
151
+ break;
149
152
  default:
150
153
  this.log.debug(`Unknown broadcast response ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}`);
151
154
  }
@@ -2,6 +2,7 @@ import EventEmitter from 'node:events';
2
2
  import { AnsiLogger, UNDERLINE, UNDERLINEOFF, BLUE, db, er, nf, nt, rs, wr, debugStringify, CYAN } from 'node-ansi-logger';
3
3
  import { plg, typ } from './matterbridgeTypes.js';
4
4
  import { inspectError, logError } from './utils/error.js';
5
+ import { hasParameter } from './utils/commandLine.js';
5
6
  import { BroadcastServer } from './broadcastServer.js';
6
7
  export class PluginManager extends EventEmitter {
7
8
  _plugins = new Map();
@@ -11,7 +12,7 @@ export class PluginManager extends EventEmitter {
11
12
  constructor(matterbridge) {
12
13
  super();
13
14
  this.matterbridge = matterbridge;
14
- this.log = new AnsiLogger({ logName: 'PluginManager', logTimestampFormat: 4, logLevel: matterbridge.log.logLevel });
15
+ this.log = new AnsiLogger({ logName: 'PluginManager', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
15
16
  this.log.debug('Matterbridge plugin manager starting...');
16
17
  this.server = new BroadcastServer('plugins', this.log);
17
18
  this.server.on('broadcast_message', this.msgHandler.bind(this));
@@ -24,6 +25,13 @@ export class PluginManager extends EventEmitter {
24
25
  if (this.server.isWorkerRequest(msg, msg.type) && (msg.dst === 'all' || msg.dst === 'plugins')) {
25
26
  this.log.debug(`**Received request message ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
26
27
  switch (msg.type) {
28
+ case 'get_log_level':
29
+ this.server.respond({ ...msg, response: { success: true, logLevel: this.log.logLevel } });
30
+ break;
31
+ case 'set_log_level':
32
+ this.log.logLevel = msg.params.logLevel;
33
+ this.server.respond({ ...msg, response: { success: true, logLevel: this.log.logLevel } });
34
+ break;
27
35
  case 'plugins_length':
28
36
  this.server.respond({ ...msg, response: { length: this.length } });
29
37
  break;