nodejs-poolcontroller 8.1.2 → 8.4.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 (106) hide show
  1. package/.eslintrc.json +36 -36
  2. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -84
  3. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -12
  4. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -28
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -8
  6. package/.github/copilot-instructions.md +63 -0
  7. package/.github/workflows/ghcr-publish.yml +67 -0
  8. package/AGENTS.md +597 -0
  9. package/CONTRIBUTING.md +74 -74
  10. package/Changelog +292 -257
  11. package/Dockerfile +62 -19
  12. package/Gruntfile.js +40 -40
  13. package/LICENSE +661 -661
  14. package/README.md +318 -191
  15. package/anslq25/MessagesMock.ts +221 -221
  16. package/anslq25/boards/MockBoardFactory.ts +49 -49
  17. package/anslq25/boards/MockEasyTouchBoard.ts +696 -696
  18. package/anslq25/boards/MockSystemBoard.ts +216 -216
  19. package/anslq25/chemistry/MockChlorinator.ts +98 -98
  20. package/anslq25/pumps/MockPump.ts +83 -83
  21. package/app.ts +115 -115
  22. package/config/Config.ts +57 -7
  23. package/config/VersionCheck.ts +63 -35
  24. package/controller/Constants.ts +809 -805
  25. package/controller/Equipment.ts +2688 -2664
  26. package/controller/Errors.ts +181 -181
  27. package/controller/Lockouts.ts +549 -549
  28. package/controller/State.ts +3738 -3690
  29. package/controller/boards/AquaLinkBoard.ts +1003 -1003
  30. package/controller/boards/BoardFactory.ts +53 -53
  31. package/controller/boards/EasyTouchBoard.ts +3202 -3202
  32. package/controller/boards/IntelliCenterBoard.ts +4393 -3899
  33. package/controller/boards/IntelliComBoard.ts +69 -69
  34. package/controller/boards/IntelliTouchBoard.ts +382 -382
  35. package/controller/boards/NixieBoard.ts +1944 -1929
  36. package/controller/boards/SunTouchBoard.ts +400 -400
  37. package/controller/boards/SystemBoard.ts +5268 -5268
  38. package/controller/comms/Comms.ts +1272 -1214
  39. package/controller/comms/ScreenLogic.ts +1665 -1665
  40. package/controller/comms/messages/Messages.ts +1433 -1243
  41. package/controller/comms/messages/config/ChlorinatorMessage.ts +5 -0
  42. package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
  43. package/controller/comms/messages/config/CircuitMessage.ts +0 -0
  44. package/controller/comms/messages/config/ConfigMessage.ts +6 -0
  45. package/controller/comms/messages/config/CoverMessage.ts +0 -0
  46. package/controller/comms/messages/config/CustomNameMessage.ts +31 -31
  47. package/controller/comms/messages/config/EquipmentMessage.ts +216 -210
  48. package/controller/comms/messages/config/ExternalMessage.ts +96 -10
  49. package/controller/comms/messages/config/FeatureMessage.ts +0 -0
  50. package/controller/comms/messages/config/GeneralMessage.ts +0 -0
  51. package/controller/comms/messages/config/HeaterMessage.ts +0 -0
  52. package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
  53. package/controller/comms/messages/config/OptionsMessage.ts +194 -174
  54. package/controller/comms/messages/config/PumpMessage.ts +0 -0
  55. package/controller/comms/messages/config/RemoteMessage.ts +0 -0
  56. package/controller/comms/messages/config/ScheduleMessage.ts +401 -390
  57. package/controller/comms/messages/config/SecurityMessage.ts +0 -0
  58. package/controller/comms/messages/config/ValveMessage.ts +0 -0
  59. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +0 -0
  60. package/controller/comms/messages/status/EquipmentStateMessage.ts +1158 -822
  61. package/controller/comms/messages/status/HeaterStateMessage.ts +135 -135
  62. package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -448
  63. package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -36
  64. package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
  65. package/controller/comms/messages/status/RegalModbusStateMessage.ts +411 -0
  66. package/controller/comms/messages/status/VersionMessage.ts +103 -41
  67. package/controller/nixie/Nixie.ts +173 -173
  68. package/controller/nixie/NixieEquipment.ts +104 -104
  69. package/controller/nixie/bodies/Body.ts +120 -120
  70. package/controller/nixie/bodies/Filter.ts +135 -135
  71. package/controller/nixie/chemistry/ChemController.ts +2724 -2724
  72. package/controller/nixie/chemistry/ChemDoser.ts +806 -806
  73. package/controller/nixie/chemistry/Chlorinator.ts +367 -367
  74. package/controller/nixie/circuits/Circuit.ts +478 -478
  75. package/controller/nixie/heaters/Heater.ts +834 -834
  76. package/controller/nixie/pumps/Pump.ts +1194 -996
  77. package/controller/nixie/schedules/Schedule.ts +401 -401
  78. package/controller/nixie/valves/Valve.ts +170 -170
  79. package/defaultConfig.json +352 -347
  80. package/docker-compose.yml +32 -0
  81. package/logger/DataLogger.ts +448 -448
  82. package/logger/Logger.ts +448 -436
  83. package/package.json +58 -60
  84. package/sendSocket.js +32 -32
  85. package/tsconfig.json +25 -25
  86. package/types/express-multer.d.ts +32 -0
  87. package/web/Server.ts +1937 -1927
  88. package/web/bindings/aqualinkD.json +559 -559
  89. package/web/bindings/influxDB.json +1066 -1066
  90. package/web/bindings/mqtt.json +721 -721
  91. package/web/bindings/mqttAlt.json +746 -746
  92. package/web/bindings/rulesManager.json +54 -54
  93. package/web/bindings/smartThings-Hubitat.json +31 -31
  94. package/web/bindings/valveRelays.json +20 -20
  95. package/web/bindings/vera.json +25 -25
  96. package/web/interfaces/baseInterface.ts +188 -188
  97. package/web/interfaces/httpInterface.ts +148 -148
  98. package/web/interfaces/influxInterface.ts +283 -283
  99. package/web/interfaces/mqttInterface.ts +695 -695
  100. package/web/interfaces/ruleInterface.ts +101 -87
  101. package/web/services/config/Config.ts +1063 -1053
  102. package/web/services/config/ConfigSocket.ts +0 -0
  103. package/web/services/state/State.ts +0 -0
  104. package/web/services/state/StateSocket.ts +0 -0
  105. package/web/services/utilities/Utilities.ts +233 -233
  106. package/.github/workflows/docker-publish-njsPC-linux.yml +0 -50
package/web/Server.ts CHANGED
@@ -1,1927 +1,1937 @@
1
- /* nodejs-poolController. An application to control pool equipment.
2
- Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3
- Russell Goldin, tagyoureit. russ.goldin@gmail.com
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU Affero General Public License as
7
- published by the Free Software Foundation, either version 3 of the
8
- License, or (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU Affero General Public License for more details.
14
-
15
- You should have received a copy of the GNU Affero General Public License
16
- along with this program. If not, see <http://www.gnu.org/licenses/>.
17
- */
18
- import * as dns from "dns";
19
- import { EventEmitter } from 'events';
20
- import * as fs from "fs";
21
- import * as http from "http";
22
- import * as http2 from "http2";
23
- import * as https from "https";
24
- import * as multicastdns from 'multicast-dns';
25
- import * as ssdp from 'node-ssdp';
26
- import * as os from 'os';
27
- import * as path from "path";
28
- import { RemoteSocket, Server as SocketIoServer, Socket } from "socket.io";
29
- import { io as sockClient } from "socket.io-client";
30
- import { URL } from "url";
31
- import { config } from "../config/Config";
32
- import { conn } from "../controller/comms/Comms";
33
- import { Inbound, Outbound } from "../controller/comms/messages/Messages";
34
- import { Timestamp, utils } from "../controller/Constants";
35
- import { sys } from '../controller/Equipment';
36
- import { state } from "../controller/State";
37
- import { logger } from "../logger/Logger";
38
- import { HttpInterfaceBindings } from './interfaces/httpInterface';
39
- import { InfluxInterfaceBindings } from './interfaces/influxInterface';
40
- import { MqttInterfaceBindings } from './interfaces/mqttInterface';
41
- import { RuleInterfaceBindings } from "./interfaces/ruleInterface";
42
- import { ConfigRoute } from "./services/config/Config";
43
- import { ConfigSocket } from "./services/config/ConfigSocket";
44
- import { StateRoute } from "./services/state/State";
45
- import { StateSocket } from "./services/state/StateSocket";
46
- import { UtilitiesRoute } from "./services/utilities/Utilities";
47
- import express = require('express');
48
- import extend = require("extend");
49
- import { setTimeout as setTimeoutSync } from 'timers';
50
- import { setTimeout } from 'timers/promises';
51
-
52
- // This class serves data and pages for
53
- // external interfaces as well as an internal dashboard.
54
- export class WebServer {
55
- public autoBackup = false;
56
- public lastBackup;
57
- private _servers: ProtoServer[] = [];
58
- private family = 'IPv4';
59
- private _autoBackupTimer: NodeJS.Timeout;
60
- private _httpPort: number;
61
- constructor() { }
62
- public async init() {
63
- try {
64
- let cfg = config.getSection('web');
65
- let srv;
66
- for (let s in cfg.servers) {
67
- let c = cfg.servers[s];
68
- if (typeof c.uuid === 'undefined') {
69
- c.uuid = utils.uuid();
70
- config.setSection(`web.servers.${s}`, c);
71
- }
72
- switch (s) {
73
- case 'http':
74
- srv = new HttpServer(s, s);
75
- if (c.enabled !== false) this._httpPort = c.port;
76
- break;
77
- case 'http2':
78
- srv = new Http2Server(s, s);
79
- if (c.enabled !== false) this._httpPort = c.port;
80
- break;
81
- case 'https':
82
- srv = new HttpsServer(s, s);
83
- if (c.enabled !== false) this._httpPort = c.port;
84
- break;
85
- case 'mdns':
86
- srv = new MdnsServer(s, s);
87
- break;
88
- case 'ssdp':
89
- srv = new SsdpServer(s, s);
90
- break;
91
- }
92
- if (typeof srv !== 'undefined') {
93
- this._servers.push(srv);
94
- await srv.init(c);
95
- srv = undefined;
96
- }
97
- }
98
- this.initInterfaces(cfg.interfaces);
99
-
100
- } catch (err) { logger.error(`Error initializing web server ${err.message}`) }
101
- }
102
- public async initInterfaces(interfaces: any) {
103
- try {
104
- for (let s in interfaces) {
105
- let int;
106
- let c = interfaces[s];
107
- if (typeof c.uuid === 'undefined') {
108
- c.uuid = utils.uuid();
109
- config.setSection(`web.interfaces.${s}`, c);
110
- }
111
- if (!c.enabled) continue;
112
- let type = c.type || 'http';
113
- logger.info(`Init ${type} interface: ${c.name}`);
114
- switch (type) {
115
- case 'rest':
116
- case 'http':
117
- int = new HttpInterfaceServer(c.name, type);
118
- int.init(c);
119
- this._servers.push(int);
120
- break;
121
- case 'rule':
122
- int = new RuleInterfaceServer(c.name, type);
123
- int.init(c);
124
- this._servers.push(int);
125
- break;
126
- case 'influx':
127
- case 'influxdb2':
128
- int = new InfluxInterfaceServer(c.name, type);
129
- int.init(c);
130
- this._servers.push(int);
131
- break;
132
- case 'mqtt':
133
- int = new MqttInterfaceServer(c.name, type);
134
- int.init(c);
135
- this._servers.push(int);
136
- break;
137
- case 'rem':
138
- int = new REMInterfaceServer(c.name, type);
139
- int.init(c);
140
- this._servers.push(int);
141
- break;
142
- }
143
- }
144
- } catch (err) { logger.error(`Error initializing Interface servers ${err.message}`); }
145
- }
146
- public emitToClients(evt: string, ...data: any) {
147
- for (let i = 0; i < this._servers.length; i++) {
148
- this._servers[i].emitToClients(evt, ...data);
149
- }
150
- }
151
- public emitToChannel(channel: string, evt: string, ...data: any) {
152
- for (let i = 0; i < this._servers.length; i++) {
153
- this._servers[i].emitToChannel(channel, evt, ...data);
154
- }
155
- }
156
- public get mdnsServer(): MdnsServer { return this._servers.find(elem => elem instanceof MdnsServer) as MdnsServer; }
157
- public deviceXML() { } // override in SSDP
158
- public async stopAsync() {
159
- try {
160
- // We want to stop all the servers in reverse order so let's pop them out.
161
- for (let s in this._servers) {
162
- try {
163
- let serv = this._servers[s];
164
- if (typeof serv.stopAsync === 'function') {
165
- await serv.stopAsync();
166
- }
167
- this._servers[s] = undefined;
168
- } catch (err) { console.log(`Error stopping server ${s}: ${err.message}`); }
169
- }
170
- } catch (err) { `Error stopping servers` }
171
- }
172
- private getInterface() {
173
- const networkInterfaces = os.networkInterfaces();
174
- // RKS: We need to get the scope-local nic. This has nothing to do with IP4/6 and is not necessarily named en0 or specific to a particular nic. We are
175
- // looking for the first IPv4 interface that has a mac address which will be the scope-local address. However, in the future we can simply use the IPv6 interface
176
- // if that is returned on the local scope but I don't know if the node ssdp server supports it on all platforms.
177
- let fallback; // Use this for WSL adapters.
178
- for (let name in networkInterfaces) {
179
- let nic = networkInterfaces[name];
180
- for (let ndx in nic) {
181
- let addr = nic[ndx];
182
- // All scope-local addresses will have a mac. In a multi-nic scenario we are simply grabbing
183
- // the first one we come across.
184
- if (!addr.internal && addr.mac.indexOf('00:00:00:') < 0 && addr.family === this.family) {
185
- if (!addr.mac.startsWith('00:'))
186
- return addr;
187
- else if (typeof fallback === 'undefined') fallback = addr;
188
- }
189
- }
190
- }
191
- return fallback;
192
- }
193
- public getNetworkInterfaces() {
194
- const networkInterfaces = os.networkInterfaces();
195
- // RKS: We need to get the scope-local nics. This has nothing to do with IP4/6 and is not necessarily named en0 or specific to a particular nic. We are
196
- // looking for the first IPv4 interface that has a mac address which will be the scope-local address. However, in the future we can simply use the IPv6 interface
197
- // if that is returned on the local scope but I don't know if the node ssdp server supports it on all platforms.
198
- let ips = [];
199
- let nics = { physical: [], virtual: [] }
200
- for (let name in networkInterfaces) {
201
- let nic = networkInterfaces[name];
202
- for (let ndx in nic) {
203
- let addr = nic[ndx];
204
- // All scope-local addresses will have a mac. In a multi-nic scenario we are simply grabbing
205
- // the first one we come across.
206
- if (!addr.internal && addr.mac.indexOf('00:00:00:') < 0 && addr.family === this.family) {
207
- if (typeof ips.find((x) => x === addr.address) === 'undefined') {
208
- ips.push(addr.address);
209
- if (!addr.mac.startsWith('00:'))
210
- nics.physical.push(extend(true, { name: name }, addr));
211
- else
212
- nics.virtual.push(extend(true, { name: name }, addr));
213
- }
214
- }
215
- }
216
- }
217
- return nics;
218
- }
219
- public ip() { return typeof this.getInterface() === 'undefined' ? '0.0.0.0' : this.getInterface().address; }
220
- public mac() { return typeof this.getInterface() === 'undefined' ? '00:00:00:00' : this.getInterface().mac; }
221
- public httpPort(): number { return this._httpPort }
222
- public findServer(name: string): ProtoServer { return this._servers.find(elem => elem.name === name); }
223
- public findServersByType(type: string) { return this._servers.filter(elem => elem.type === type); }
224
- public findServerByGuid(uuid: string) { return this._servers.find(elem => elem.uuid === uuid); }
225
- public removeServerByGuid(uuid: string) {
226
- for (let i = 0; i < this._servers.length; i++) {
227
- if (this._servers[i].uuid === uuid) this._servers.splice(i, 1);
228
- }
229
- }
230
- public async updateServerInterface(obj: any): Promise<any> {
231
- let int = config.setInterface(obj);
232
- let srv = this.findServerByGuid(obj.uuid);
233
- // if server is not enabled; stop & remove it from local storage
234
- if (typeof srv !== 'undefined') {
235
- await srv.stopAsync();
236
- this.removeServerByGuid(obj.uuid);
237
- }
238
- // if it's enabled, restart it or initialize it
239
- if (obj.enabled) {
240
- if (typeof srv === 'undefined') {
241
- this.initInterfaces(int);
242
- }
243
- else srv.init(obj);
244
- }
245
- return config.getInterfaceByUuid(obj.uuid);
246
- }
247
- public async initAutoBackup() {
248
- try {
249
- let bu = config.getSection('controller.backups');
250
- this.autoBackup = false;
251
- // These will be returned in reverse order with the newest backup first.
252
- let files = await this.readBackupFiles();
253
- let afiles = files.filter(elem => elem.options.automatic === true);
254
- this.lastBackup = (afiles.length > 0) ? Date.parse(afiles[0].options.backupDate).valueOf() || 0 : 0;
255
- // Set the last backup date.
256
- this.autoBackup = utils.makeBool(bu.automatic);
257
- if (this.autoBackup) {
258
- let nextBackup = this.lastBackup + (bu.interval.days * 86400000) + (bu.interval.hours * 3600000);
259
- logger.info(`Auto-backup initialized Last Backup: ${Timestamp.toISOLocal(new Date(this.lastBackup))} Next Backup: ${Timestamp.toISOLocal(new Date(nextBackup))}`);
260
- }
261
- else
262
- logger.info(`Auto-backup initialized Last Backup: ${Timestamp.toISOLocal(new Date(this.lastBackup))}`);
263
- // Lets wait a good 20 seconds before we auto-backup anything. Now that we are initialized let the OCP have its way with everything.
264
- setTimeoutSync(()=>{this.checkAutoBackup();}, 20000);
265
- }
266
- catch (err) { logger.error(`Error initializing auto-backup: ${err.message}`); }
267
- }
268
- public async stopAutoBackup() {
269
- this.autoBackup = false;
270
- if (typeof this._autoBackupTimer !== 'undefined' || this._autoBackupTimer) clearTimeout(this._autoBackupTimer);
271
- }
272
- public async readBackupFiles(): Promise<BackupFile[]> {
273
- try {
274
- let backupDir = path.join(process.cwd(), 'backups');
275
- let files = fs.readdirSync(backupDir);
276
- let backups = [];
277
- if (typeof files !== 'undefined') {
278
- for (let i = 0; i < files.length; i++) {
279
- let file = files[i];
280
- if (path.extname(file) === '.zip') {
281
- let bf = await BackupFile.fromFile(path.join(backupDir, file));
282
- if (typeof bf !== 'undefined') backups.push(bf);
283
- }
284
- }
285
- }
286
- backups.sort((a, b) => { return Date.parse(b.options.backupDate) - Date.parse(a.options.backupDate) });
287
- return backups;
288
- }
289
- catch (err) { logger.error(`Error reading backup file directory: ${err.message}`); }
290
- }
291
- protected async extractBackupOptions(file: string | Buffer): Promise<{ file: string, options: any }> {
292
- try {
293
- let opts = { file: Buffer.isBuffer(file) ? 'Buffer' : file, options: {} as any };
294
- let jszip = require("jszip");
295
- let buff = Buffer.isBuffer(file) ? file : fs.readFileSync(file);
296
- await jszip.loadAsync(buff).then(async (zip) => {
297
- await zip.file('options.json').async('string').then((data) => {
298
- opts.options = JSON.parse(data);
299
- if (typeof opts.options.backupDate === 'undefined' && typeof file === 'string') {
300
- let name = path.parse(file).name;
301
- if (name.length === 19) {
302
- let date = name.substring(0, 10).replace(/-/g, '/');
303
- let time = name.substring(11).replace(/-/g, ':');
304
- let dt = Date.parse(`${date} ${time}`);
305
- if (!isNaN(dt)) opts.options.backupDate = Timestamp.toISOLocal(new Date(dt));
306
- }
307
- }
308
- });
309
- });
310
- return opts;
311
- } catch (err) { logger.error(`Error extracting backup options from ${file}: ${err.message}`); }
312
- }
313
- public async pruneAutoBackups(keepCount: number) {
314
- try {
315
- // We only automatically prune backups that njsPC put there in the first place so only
316
- // look at auto-backup files.
317
- let files = await this.readBackupFiles();
318
- let afiles = files.filter(elem => elem.options.automatic === true);
319
- if (afiles.length > keepCount) {
320
- // Prune off the oldest backups until we get to our keep count. When we read in the files
321
- // these were sorted newest first.
322
- while (afiles.length > keepCount) {
323
- let afile = afiles.pop();
324
- logger.info(`Pruning auto-backup file: ${afile.filePath}`);
325
- try {
326
- fs.unlinkSync(afile.filePath);
327
- } catch (err) { logger.error(`Error deleting auto-backup file: ${afile.filePath}`); }
328
- }
329
- }
330
- } catch (err) { logger.error(`Error pruning auto-backups: ${err.message}`); }
331
- }
332
- public async backupServer(opts: any): Promise<BackupFile> {
333
- let ret = new BackupFile();
334
- ret.options = extend(true, {}, opts, { version: 1.1, errors: [] });
335
- //{ file: '', options: extend(true, {}, opts, { version: 1.0, errors: [] }) };
336
- let jszip = require("jszip");
337
- function pad(n) { return (n < 10 ? '0' : '') + n; }
338
- let zip = new jszip();
339
- let ts = new Date();
340
- let baseDir = process.cwd();
341
- ret.filename = ts.getFullYear() + '-' + pad(ts.getMonth() + 1) + '-' + pad(ts.getDate()) + '_' + pad(ts.getHours()) + '-' + pad(ts.getMinutes()) + '-' + pad(ts.getSeconds()) + '.zip';
342
- ret.filePath = path.join(baseDir, 'backups', ret.filename);
343
- if (opts.njsPC === true) {
344
- zip.folder('njsPC');
345
- zip.folder('njsPC/data');
346
- // Create the backup file and copy it into it.
347
- zip.file('njsPC/config.json', fs.readFileSync(path.join(baseDir, 'config.json')));
348
- zip.file('njsPC/data/poolConfig.json', fs.readFileSync(path.join(baseDir, 'data', 'poolConfig.json')));
349
- zip.file('njsPC/data/poolState.json', fs.readFileSync(path.join(baseDir, 'data', 'poolState.json')));
350
- }
351
- if (typeof ret.options.servers !== 'undefined' && ret.options.servers.length > 0) {
352
- // Back up all our servers.
353
- for (let i = 0; i < ret.options.servers.length; i++) {
354
- let srv = ret.options.servers[i];
355
- if (typeof srv.errors === 'undefined') srv.errors = [];
356
- if (srv.backup === false) continue;
357
- let server = this.findServerByGuid(srv.uuid) as REMInterfaceServer;
358
- if (typeof server === 'undefined') {
359
- srv.errors.push(`Could not find server ${srv.name} : ${srv.uuid}`);
360
- srv.success = false;
361
- }
362
- else if (!server.isConnected) {
363
- srv.success = false;
364
- srv.errors.push(`Server ${srv.name} : ${srv.uuid} not connected cannot back up`);
365
- }
366
- else {
367
- // Try to get the data from the server.
368
- zip.folder(server.name);
369
- zip.file(`${server.name}/serverConfig.json`, JSON.stringify(server.cfg));
370
- zip.folder(`${server.name}/data`);
371
- try {
372
- let resp = await server.getControllerConfig();
373
- if (typeof resp !== 'undefined') {
374
- if (resp.status.code === 200 && typeof resp.data !== 'undefined') {
375
- let ccfg = JSON.parse(resp.data);
376
- zip.file(`${server.name}/data/controllerConfig.json`, JSON.stringify(ccfg));
377
- srv.success = true;
378
- }
379
- else {
380
- srv.errors.push(`Error getting controller configuration: ${resp.error.message}`);
381
- srv.success = false;
382
- }
383
- }
384
- else {
385
- srv.success = false;
386
- srv.errors.push(`No response from server`);
387
- }
388
- } catch (err) { srv.success = false; srv.errors.push(`Could not obtain server configuration`); }
389
- }
390
- }
391
- }
392
- ret.options.backupDate = Timestamp.toISOLocal(ts);
393
- zip.file('options.json', JSON.stringify(ret.options));
394
- await zip.generateAsync({ type: 'nodebuffer' }).then(content => {
395
- fs.writeFileSync(ret.filePath, content);
396
- this.lastBackup = ts.valueOf();
397
- });
398
- return ret;
399
- }
400
- public async checkAutoBackup() {
401
- if (typeof this._autoBackupTimer !== 'undefined' || this._autoBackupTimer) clearTimeout(this._autoBackupTimer);
402
- this._autoBackupTimer = undefined;
403
- let bu = config.getSection('controller.backups');
404
- if (bu.automatic === true) {
405
- if (typeof this.lastBackup === 'undefined' ||
406
- (this.lastBackup < new Date().valueOf() - (bu.interval.days * 86400000) - (bu.interval.hours * 3600000))) {
407
- bu.name = 'Automatic Backup';
408
- await this.backupServer(bu);
409
- }
410
- }
411
- else this.autoBackup = false;
412
- if (this.autoBackup) {
413
- await this.pruneAutoBackups(bu.keepCount);
414
- let nextBackup = this.lastBackup + (bu.interval.days * 86400000) + (bu.interval.hours * 3600000);
415
- setTimeoutSync(async () => {
416
- try {
417
- await this.checkAutoBackup();
418
- } catch (err) { logger.error(`Error checking auto-backup: ${err.message}`); }
419
- }, Math.max(Math.min(nextBackup - new Date().valueOf(), 2147483647), 60000));
420
- logger.info(`Last auto-backup ${Timestamp.toISOLocal(new Date(this.lastBackup))} Next auto - backup ${Timestamp.toISOLocal(new Date(nextBackup))}`);
421
- }
422
- }
423
- public async validateRestore(opts): Promise<any> {
424
- try {
425
- let stats = { njsPC: {}, servers: [] };
426
- // Step 1: Extract all the files from the zip file.
427
- let rest = await RestoreFile.fromFile(opts.filePath);
428
- // Step 2: Validate the njsPC data against the board. The return
429
- // from here shoudld give a very detailed view of what it is about to do.
430
- if (opts.options.njsPC === true) {
431
- stats.njsPC = await sys.board.system.validateRestore(rest.njsPC);
432
- }
433
- // Step 3: For each REM server we need to validate the restore
434
- // file.
435
- if (typeof opts.options.servers !== 'undefined' && opts.options.servers.length > 0) {
436
- for (let i = 0; i < opts.options.servers.length; i++) {
437
- let s = opts.options.servers[i];
438
- if (s.restore) {
439
- let ctx: any = { server: { uuid: s.uuid, name: s.name, errors: [], warnings: [] } };
440
- // Check to see if the server is on-line.
441
- // First, try by UUID.
442
- let srv = this.findServerByGuid(s.uuid) as REMInterfaceServer;
443
- let cfg = rest.servers.find(elem => elem.uuid === s.uuid);
444
- // Second, try by host
445
- if (typeof srv === 'undefined' && parseFloat(opts.options.version) >= 1.1) {
446
- let srvs = this.findServersByType('rem') as REMInterfaceServer[];
447
- cfg = rest.servers.find(elem => elem.serverConfig.options.host === s.host);
448
- for (let j = 0; j < srvs.length; j++){
449
- if (srvs[j].cfg.options.host === cfg.serverConfig.options.host){
450
- srv = srvs[j];
451
- ctx.server.warnings.push(`REM Server from backup file (${srv.uuid}/${srv.cfg.options.host}) matched to current REM Server (${cfg.uuid}/${cfg.serverConfig.options.host}) by host name or IP and not UUID. UUID in current config.json for REM will be updated.`)
452
- break;
453
- }
454
- }
455
- }
456
- stats.servers.push(ctx);
457
- if (typeof cfg === 'undefined' || typeof cfg.controllerConfig === 'undefined') ctx.server.errors.push(`Server configuration not found in zip file`);
458
- else if (typeof srv === 'undefined') ctx.server.errors.push(`Server ${s.name} is not enabled in njsPC cannot restore.`);
459
- else if (!srv.isConnected) ctx.server.errors.push(`Server ${s.name} is not connected or cannot be found by UUID and cannot restore. If this is a version 1.0 file, update your current REM UUID to match the backup REM UUID.`);
460
- else {
461
- let resp = await srv.validateRestore(cfg.controllerConfig);
462
- if (typeof resp !== 'undefined') {
463
- if (resp.status.code === 200 && typeof resp.data !== 'undefined') {
464
- let cctx = JSON.parse(resp.data);
465
- ctx = extend(true, ctx, cctx);
466
- }
467
- else
468
- ctx.server.errors.push(`Error validating controller configuration: ${resp.error.message}`);
469
- }
470
- else
471
- ctx.server.errors.push(`No response from server`);
472
- }
473
- }
474
-
475
- }
476
- }
477
-
478
- return stats;
479
- } catch (err) { logger.error(`Error validating restore options: ${err.message}`); return Promise.reject(err);}
480
- }
481
- public async restoreServers(opts): Promise<any> {
482
- let stats: { backupOptions?: any, njsPC?: RestoreResults, servers: any[] } = { servers: [] };
483
- try {
484
- // Step 1: Extract all the files from the zip file.
485
- let rest = await RestoreFile.fromFile(opts.filePath);
486
- stats.backupOptions = rest.options;
487
- // Step 2: Validate the njsPC data against the board. The return
488
- // from here shoudld give a very detailed view of what it is about to do.
489
- if (opts.options.njsPC === true) {
490
- logger.info(`Begin Restore njsPC`);
491
- stats.njsPC = await sys.board.system.restore(rest.njsPC);
492
- logger.info(`End Restore njsPC`);
493
- }
494
- // Step 3: For each REM server we need to validate the restore
495
- // file.
496
- if (typeof opts.options.servers !== 'undefined' && opts.options.servers.length > 0) {
497
- for (let i = 0; i < opts.options.servers.length; i++) {
498
- let s = opts.options.servers[i];
499
- if (s.restore) {
500
- // Check to see if the server is on-line.
501
- let srv = this.findServerByGuid(s.uuid) as REMInterfaceServer;
502
- let cfg = rest.servers.find(elem => elem.uuid === s.uuid);
503
- let ctx: any = { server: { uuid: s.uuid, name: s.name, errors: [], warnings: [] } };
504
- if (typeof srv === 'undefined' && parseFloat(opts.options.version) >= 1.1) {
505
- let srvs = this.findServersByType('rem') as REMInterfaceServer[];
506
- cfg = rest.servers.find(elem => elem.serverConfig.options.host === s.host);
507
- for (let j = 0; j < srvs.length; j++){
508
- if (srvs[j].cfg.options.host === cfg.serverConfig.options.host){
509
- srv = srvs[j];
510
- let oldSrvCfg = srv.cfg;
511
- oldSrvCfg.enabled = false;
512
- await this.updateServerInterface(oldSrvCfg); // unload prev server interface
513
- srv.uuid = srv.cfg.uuid = cfg.uuid;
514
- config.setSection('web.interfaces.rem', cfg.serverConfig);
515
- await this.updateServerInterface(cfg.serverConfig); // reset server interface
516
- srv = this.findServerByGuid(s.uuid) as REMInterfaceServer;
517
- logger.info(`Restore REM: Current UUID updated to UUID of backup.`);
518
- break;
519
- }
520
- }
521
- }
522
- stats.servers.push(ctx);
523
- if (!srv.isConnected) await setTimeout(6000); // rem server waits to connect 5s before isConnected will be true. Server.ts#1256 = REMInterfaceServer.init(); What's a better way to do this?
524
- if (typeof cfg === 'undefined' || typeof cfg.controllerConfig === 'undefined') ctx.server.errors.push(`Server configuration not found in zip file`);
525
- else if (typeof srv === 'undefined') ctx.server.errors.push(`Server ${s.name} is not enabled in njsPC cannot restore.`);
526
- else if (!srv.isConnected) ctx.server.errors.push(`Server ${s.name} is not connected cannot restore.`);
527
- else {
528
- let resp = await srv.validateRestore(cfg.controllerConfig);
529
- if (typeof resp !== 'undefined') {
530
- if (resp.status.code === 200 && typeof resp.data !== 'undefined') {
531
- let cctx = JSON.parse(resp.data);
532
- ctx = extend(true, ctx, cctx);
533
- // Ok so now here we are ready to restore the data.
534
- let r = await srv.restoreConfig(cfg.controllerConfig);
535
-
536
- }
537
- else
538
- ctx.server.errors.push(`Error validating controller configuration: ${resp.error.message}`);
539
- }
540
- else
541
- ctx.server.errors.push(`No response from server`);
542
- }
543
- }
544
-
545
- }
546
- }
547
-
548
- return stats;
549
- } catch (err) { logger.error(`Error validating restore options: ${err.message}`); return Promise.reject(err); }
550
- finally {
551
- try {
552
- let baseDir = process.cwd();
553
- let ts = new Date();
554
- function pad(n) { return (n < 10 ? '0' : '') + n; }
555
- let filename = 'restoreLog(' + ts.getFullYear() + '-' + pad(ts.getMonth() + 1) + '-' + pad(ts.getDate()) + '_' + pad(ts.getHours()) + '-' + pad(ts.getMinutes()) + '-' + pad(ts.getSeconds()) + ').log';
556
- let filePath = path.join(baseDir, 'logs', filename);
557
- fs.writeFileSync(filePath, JSON.stringify(stats, undefined, 3));
558
- } catch (err) { logger.error(`Error writing restore log ${err.message}`); }
559
- }
560
- }
561
- }
562
- class ProtoServer {
563
- constructor(name: string, type: string) { this.name = name; this.type = type; }
564
- public name: string;
565
- public type: string;
566
- public uuid: string;
567
- public remoteConnectionId: string;
568
- // base class for all servers.
569
- public isRunning: boolean = false;
570
- public get isConnected() { return this.isRunning; }
571
- public emitToClients(evt: string, ...data: any) { }
572
- public emitToChannel(channel: string, evt: string, ...data: any) { }
573
- public async init(obj: any) { };
574
- public async stopAsync() { }
575
- protected _dev: boolean = process.env.NODE_ENV !== 'production';
576
- // todo: how do we know if the client is using IPv4/IPv6?
577
- }
578
- export class Http2Server extends ProtoServer {
579
- public server: http2.Http2Server;
580
- public app: Express.Application;
581
- public async init(cfg) {
582
- this.uuid = cfg.uuid;
583
- if (cfg.enabled) {
584
- this.app = express();
585
- // TODO: create a key and cert at some time but for now don't fart with it.
586
- }
587
- }
588
- }
589
- interface ClientToServerEvents {
590
- noArg: () => void;
591
- basicEmit: (a: number, b: string, c: number[]) => void;
592
- }
593
-
594
- interface ServerToClientEvents {
595
- withAck: (d: string, cb: (e: number) => void) => void;
596
- [event: string]: (...args: any[]) => void;
597
- }
598
- export class HttpServer extends ProtoServer {
599
- // Http protocol
600
- private static dateTestISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/;
601
- private static dateTextAjax = /^\/Date\((d|-|.*)\)[\/|\\]$/;
602
-
603
- public app: express.Application;
604
- public server: http.Server;
605
- public sockServer: SocketIoServer<ClientToServerEvents, ServerToClientEvents>;
606
- private _sockets: RemoteSocket<ServerToClientEvents, any>[] = [];
607
- public emitToClients(evt: string, ...data: any) {
608
- if (this.isRunning) {
609
- this.sockServer.emit(evt, ...data);
610
- }
611
- }
612
- public emitToChannel(channel: string, evt: string, ...data: any) {
613
- //console.log(`Emitting to channel ${channel} - ${evt}`)
614
- if (this.isRunning) {
615
- this.sockServer.to(channel).emit(evt, ...data);
616
- }
617
- }
618
- public get isConnected() { return typeof this.sockServer !== 'undefined' && this._sockets.length > 0; }
619
- protected initSockets() {
620
- let options = {
621
- allowEIO3: true,
622
- cors: {
623
- origin: true,
624
- methods: ["GET", "POST"],
625
- credentials: true
626
- }
627
- }
628
- this.sockServer = new SocketIoServer(this.server, options);
629
- this.sockServer.on("connection", (sock: Socket) => {
630
- logger.info(`New socket client connected ${sock.id} -- ${sock.client.conn.remoteAddress}`);
631
- this.socketHandler(sock);
632
- sock.emit('controller', state.controllerState);
633
- sock.conn.emit('controller', state.controllerState); // do we need both of these?
634
- //this.sockServer.origins('*:*');
635
- sock.on('connect_error', (err) => {
636
- logger.error('Socket server error %s', err.message);
637
- });
638
- sock.on('reconnect_failed', (err) => {
639
- logger.error('Failed to reconnect with socket %s', err.message);
640
- });
641
- });
642
- this.app.use('/socket.io-client', express.static(path.join(process.cwd(), '/node_modules/socket.io-client/dist/'), { maxAge: '60d' }));
643
- }
644
-
645
- private socketHandler(sock: Socket) {
646
- let self = this;
647
- // this._sockets.push(sock);
648
- setTimeoutSync(async () => {
649
- // refresh socket list with every new socket
650
- self._sockets = await self.sockServer.fetchSockets();
651
- }, 100)
652
-
653
- sock.on('error', (err) => {
654
- logger.error('Error with socket: %s', err);
655
- });
656
- sock.on('close', async (id) => {
657
- logger.info('Socket diconnecting %s', id);
658
- self._sockets = await self.sockServer.fetchSockets();
659
- });
660
- sock.on('echo', (msg) => { sock.emit('echo', msg); });
661
- sock.on('sendOutboundMessage', (mdata) => {
662
- let msg: Outbound = Outbound.create({});
663
- Object.assign(msg, mdata);
664
- msg.calcChecksum();
665
- logger.silly(`sendOutboundMessage ${msg.toLog()}`);
666
- conn.queueSendMessage(msg);
667
- });
668
- sock.on('sendInboundMessage', (mdata) => {
669
- try {
670
-
671
- let msg: Inbound = new Inbound();
672
- msg.direction = mdata.direction;
673
- msg.header = mdata.header;
674
- msg.payload = mdata.payload;
675
- msg.preamble = mdata.preamble;
676
- msg.protocol = mdata.protocol;
677
- msg.term = mdata.term;
678
- if (msg.isValid) msg.process();
679
- }
680
- catch (err){
681
- logger.error(`Error replaying packet: ${err.message}`);
682
- }
683
- });
684
- sock.on('rawbytes', (data:any)=>{
685
- let port = conn.findPortById(0);
686
- port.pushIn(Buffer.from(data));
687
- })
688
- sock.on('sendLogMessages', function (sendMessages: boolean) {
689
- console.log(`sendLogMessages set to ${sendMessages}`);
690
- if (!sendMessages) sock.leave('msgLogger');
691
- else sock.join('msgLogger');
692
- });
693
- sock.on('sendRS485PortStats', function (sendPortStats: boolean) {
694
- console.log(`sendRS485PortStats set to ${sendPortStats}`);
695
- if (!sendPortStats) sock.leave('rs485PortStats');
696
- else sock.join('rs485PortStats');
697
- });
698
- sock.on('sendScreenlogicStats', function (sendScreenlogicStats: boolean) {
699
- console.log(`sendScreenlogicStats set to ${sendScreenlogicStats}`);
700
- if (!sendScreenlogicStats) sock.leave('screenlogicStats');
701
- else sock.join('screenlogicStats');
702
- });
703
- StateSocket.initSockets(sock);
704
- ConfigSocket.initSockets(sock);
705
- }
706
- public async init(cfg) {
707
- try {
708
- this.uuid = cfg.uuid;
709
- if (cfg.enabled) {
710
- this.app = express();
711
-
712
- //this.app.use();
713
- this.server = http.createServer(this.app);
714
- if (cfg.httpsRedirect) {
715
- var cfgHttps = config.getSection('web').server.https;
716
- this.app.get('*', (res: express.Response, req: express.Request) => {
717
- let host = res.get('host');
718
- // Only append a port if there is one declared. This will be the case for urls that have have an implicit port.
719
- host = host.replace(/:\d+$/, typeof cfgHttps.port !== 'undefined' ? ':' + cfgHttps.port : '');
720
- return res.redirect('https://' + host + req.url);
721
- });
722
- }
723
- this.app.use(express.json(
724
- {
725
- reviver: (key, value) => {
726
- if (typeof value === 'string') {
727
- let d = HttpServer.dateTestISO.exec(value);
728
- // By parsing the date and then creating a new date from that we will get
729
- // the date in the proper timezone.
730
- if (d) return new Date(Date.parse(value));
731
- d = HttpServer.dateTextAjax.exec(value);
732
- if (d) {
733
- // Not sure we will be seeing ajax dates but this is
734
- // something that we may see from external sources.
735
- let a = d[1].split(/[-+,.]/);
736
- return new Date(a[0] ? +a[0] : 0 - +a[1]);
737
- }
738
- }
739
- return value;
740
- }
741
- })
742
- );
743
- this.app.use((req, res, next) => {
744
- res.header('Access-Control-Allow-Origin', '*');
745
- res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, api_key, Authorization'); // api_key and Authorization needed for Swagger editor live API document calls
746
- res.header('Access-Control-Allow-Methods', 'OPTIONS, GET, POST, PUT, DELETE');
747
- if ('OPTIONS' === req.method) { res.sendStatus(200); }
748
- else {
749
- if (req.url !== '/upnp.xml') {
750
- logger.info(`[${new Date().toLocaleTimeString()}] ${req.ip} ${req.method} ${req.url} ${typeof req.body === 'undefined' ? '' : JSON.stringify(req.body)}`);
751
- logger.logAPI(`{"dir":"in","proto":"api","requestor":"${req.ip}","method":"${req.method}","path":"${req.url}",${typeof req.body === 'undefined' ? '' : `"body":${JSON.stringify(req.body)},`}"ts":"${Timestamp.toISOLocal(new Date())}"}${os.EOL}`);
752
- }
753
- next();
754
- }
755
- });
756
-
757
-
758
- // Put in a custom replacer so that we can send error messages to the client. If we don't do this the base properties of Error
759
- // are omitted from the output.
760
- this.app.set('json replacer', (key, value) => {
761
- if (value instanceof Error) {
762
- var err = {};
763
- Object.getOwnPropertyNames(value).forEach((prop) => {
764
- if (prop === "level") err[prop] = value[prop].replace(/\x1b\[\d{2}m/g, '') // remove color from level
765
- else err[prop] = value[prop];
766
- });
767
- return err;
768
- }
769
- return value;
770
- });
771
-
772
- ConfigRoute.initRoutes(this.app);
773
- StateRoute.initRoutes(this.app);
774
- UtilitiesRoute.initRoutes(this.app);
775
-
776
- // The socket initialization needs to occur before we start listening. If we don't then
777
- // the headers from the server will not be picked up.
778
- this.initSockets();
779
- this.app.use((error, req, res, next) => {
780
- logger.error(error);
781
- if (!res.headersSent) {
782
- let httpCode = error.httpCode || 500;
783
- res.status(httpCode).send(error);
784
- }
785
- });
786
-
787
- // start our server on port
788
- this.server.listen(cfg.port, cfg.ip, function () {
789
- logger.info('Server is now listening on %s:%s - %s:%s', cfg.ip, cfg.port, webApp.ip(), webApp.httpPort());
790
- });
791
- this.isRunning = true;
792
- }
793
- } catch (err) { logger.error(`Error initializing server ${err.message}`); }
794
- }
795
- public addListenerOnce(event: any, f: (data: any) => void) {
796
- // for (let i = 0; i < this._sockets.length; i++) {
797
- // this._sockets[i].once(event, f);
798
- // }
799
- this.sockServer.once(event, f);
800
- }
801
- }
802
- export class HttpsServer extends HttpServer {
803
- declare server: https.Server;
804
-
805
- public async init(cfg) {
806
- // const auth = require('http-auth');
807
- this.uuid = cfg.uuid;
808
- if (!cfg.enabled) return;
809
- try {
810
- this.app = express();
811
- // Enable Authentication (if configured)
812
- /* if (cfg.authentication === 'basic') {
813
- let basic = auth.basic({
814
- realm: "nodejs-poolController.",
815
- file: path.join(process.cwd(), cfg.authFile)
816
- })
817
- this.app.use(function(req, res, next) {
818
- (auth.connect(basic))(req, res, next);
819
- });
820
- } */
821
- if (cfg.sslKeyFile === '' || cfg.sslCertFile === '' || !fs.existsSync(path.join(process.cwd(), cfg.sslKeyFile)) || !fs.existsSync(path.join(process.cwd(), cfg.sslCertFile))) {
822
- logger.warn(`HTTPS not enabled because key or crt file is missing.`);
823
- return;
824
- }
825
- let opts = {
826
- key: fs.readFileSync(path.join(process.cwd(), cfg.sslKeyFile), 'utf8'),
827
- cert: fs.readFileSync(path.join(process.cwd(), cfg.sslCertFile), 'utf8'),
828
- requestCert: false,
829
- rejectUnauthorized: false
830
- }
831
- this.server = https.createServer(opts, this.app);
832
-
833
- this.app.use(express.json());
834
- this.app.use((req, res, next) => {
835
- res.header('Access-Control-Allow-Origin', '*');
836
- res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, api_key, Authorization'); // api_key and Authorization needed for Swagger editor live API document calls
837
- res.header('Access-Control-Allow-Methods', 'OPTIONS, GET, POST, PUT, DELETE');
838
- if ('OPTIONS' === req.method) { res.sendStatus(200); }
839
- else {
840
- if (!req.url.startsWith('/upnp.xml')) {
841
- logger.info(`[${new Date().toLocaleString()}] ${req.ip} ${req.method} ${req.url} ${typeof req.body === 'undefined' ? '' : JSON.stringify(req.body)}`);
842
- logger.logAPI(`{"dir":"in","proto":"api","requestor":"${req.ip}","method":"${req.method}","path":"${req.url}",${typeof req.body === 'undefined' ? '' : `"body":${JSON.stringify(req.body)},`}"ts":"${Timestamp.toISOLocal(new Date())}"}${os.EOL}`);
843
- }
844
- next();
845
- }
846
- });
847
-
848
-
849
- // Put in a custom replacer so that we can send error messages to the client. If we don't do this the base properties of Error
850
- // are omitted from the output.
851
- this.app.set('json replacer', (key, value) => {
852
- if (value instanceof Error) {
853
- var err = {};
854
- Object.getOwnPropertyNames(value).forEach((prop) => {
855
- if (prop === "level") err[prop] = value[prop].replace(/\x1b\[\d{2}m/g, '') // remove color from level
856
- else err[prop] = value[prop];
857
- });
858
- return err;
859
- }
860
- return value;
861
- });
862
-
863
- ConfigRoute.initRoutes(this.app);
864
- StateRoute.initRoutes(this.app);
865
- UtilitiesRoute.initRoutes(this.app);
866
-
867
- // The socket initialization needs to occur before we start listening. If we don't then
868
- // the headers from the server will not be picked up.
869
- this.initSockets();
870
- this.app.use((error, req, res, next) => {
871
- logger.error(error);
872
- if (!res.headersSent) {
873
- let httpCode = error.httpCode || 500;
874
- res.status(httpCode).send(error);
875
- }
876
- });
877
-
878
- // start our server on port
879
- this.server.listen(cfg.port, cfg.ip, function () {
880
- logger.info('Server is now listening on %s:%s', cfg.ip, cfg.port);
881
- });
882
- this.isRunning = true;
883
- }
884
- catch (err) {
885
- logger.error(`Error starting up https server: ${err}`)
886
- }
887
- }
888
- }
889
- export class SsdpServer extends ProtoServer {
890
- // Simple service discovery protocol
891
- public server: ssdp.Server; //node-ssdp;
892
- public deviceUUID: string;
893
- public upnpPath: string;
894
- public modelName: string;
895
- public modelNumber: string;
896
- public serialNumber: string;
897
- public deviceType = 'urn:schemas-tagyoureit-org:device:PoolController:1';
898
- public async init(cfg) {
899
- this.uuid = cfg.uuid;
900
- if (cfg.enabled) {
901
- let self = this;
902
- logger.info('Starting up SSDP server');
903
- let ver = JSON.parse(fs.readFileSync(path.posix.join(process.cwd(), '/package.json'), 'utf8')).version || '0.0.0';
904
- this.deviceUUID = 'uuid:806f52f4-1f35-4e33-9299-' + webApp.mac().replace(/:/g, '');
905
- this.serialNumber = webApp.mac();
906
- this.modelName = `njsPC v${ver}`;
907
- this.modelNumber = `njsPC${ver.replace(/\./g, '-')}`;
908
- // todo: should probably check if http/https is enabled at this point
909
- //let port = config.getSection('web').servers.http.port || 7777;
910
- this.upnpPath = 'http://' + webApp.ip() + ':' + webApp.httpPort() + '/upnp.xml';
911
- let nics = webApp.getNetworkInterfaces();
912
- let SSDP = ssdp.Server;
913
- if (nics.physical.length + nics.virtual.length > 1) {
914
- // If there are multiple nics (docker...etc) then
915
- // this will bind on all of them.
916
- this.server = new SSDP({
917
- //customLogger: (...args) => console.log.apply(null, args),
918
- logLevel: 'INFO',
919
- udn: this.deviceUUID,
920
- location: {
921
- protocol: 'http://',
922
- port: webApp.httpPort(),
923
- path: '/upnp.xml'
924
- },
925
- explicitSocketBind: true,
926
- sourcePort: 1900
927
- });
928
- }
929
- else {
930
- this.server = new SSDP({
931
- //customLogger: (...args) => console.log.apply(null, args),
932
- logLevel: 'INFO',
933
- udn: this.deviceUUID,
934
- location: this.upnpPath,
935
- sourcePort: 1900
936
- });
937
-
938
-
939
- }
940
- this.server.addUSN('upnp:rootdevice'); // This line will make the server show up in windows.
941
- this.server.addUSN(this.deviceType);
942
- // start the server
943
- this.server.start()
944
- .then(function () {
945
- logger.silly('SSDP/UPnP Server started.');
946
- self.isRunning = true;
947
- });
948
-
949
- this.server.on('error', function (e) {
950
- logger.error('error from SSDP:', e);
951
- });
952
- }
953
- }
954
- public deviceXML(): string {
955
- let ver = sys.appVersion.split('.');
956
- let friendlyName = 'njsPC: unknown model';
957
- if (typeof sys !== 'undefined' && typeof sys.equipment !== 'undefined' && typeof sys.equipment.model !== 'undefined') friendlyName = `${sys.equipment.model}`
958
- let XML = `<?xml version="1.0"?>
959
- <root xmlns="urn:schemas-upnp-org:device-1-0">
960
- <specVersion>
961
- <major>1</major>
962
- <minor>0</minor>
963
- </specVersion>
964
- <device>
965
- <deviceType>${this.deviceType}</deviceType>
966
- <friendlyName>${friendlyName}</friendlyName>
967
- <manufacturer>tagyoureit</manufacturer>
968
- <manufacturerURL>https://github.com/tagyoureit/nodejs-poolController</manufacturerURL>
969
- <presentationURL>http://${webApp.ip()}:${webApp.httpPort()}/state/all</presentationURL>
970
- <appVersion>
971
- <major>${ver[0] || 1}</major>
972
- <minor>${ver[1] || 0}</minor>
973
- <patch>${ver[2] || 0}</patch>
974
- </appVersion>
975
- <modelName>${this.modelName}</modelName>
976
- <modelNumber>${this.modelNumber}</modelNumber>
977
- <modelDescription>An application to control pool equipment.</modelDescription>
978
- <serialNumber>${this.serialNumber}</serialNumber>
979
- <UDN>${this.deviceUUID}::${this.deviceType}</UDN>
980
- <serviceList></serviceList>
981
- <deviceList></deviceList>
982
- </device>
983
- </root>`;
984
- //console.log(XML.match(/<device>[\s|\S]+<appVersion>[\s|\S]+<major>(\d+)<\/major>/)[1]);
985
- //console.log(XML.match(/<device>[\s|\S]+<appVersion>[\s|\S]+<minor>(\d+)<\/minor>/)[1]);
986
- //console.log(XML.match(/<device>[\s|\S]+<appVersion>[\s|\S]+<patch>(\d+)<\/patch>/)[1]);
987
- return XML;
988
- }
989
- public async stopAsync() {
990
- try {
991
- if (typeof this.server !== 'undefined') {
992
- this.server.stop();
993
- logger.info(`Stopped SSDP server: ${this.name}`);
994
- }
995
- } catch (err) { logger.error(`Error stopping SSDP server ${err.message}`); }
996
- }
997
- }
998
- export class MdnsServer extends ProtoServer {
999
- // Multi-cast DNS server
1000
- public server;
1001
- public mdnsEmitter = new EventEmitter();
1002
- private queries = [];
1003
- public async init(cfg) {
1004
- this.uuid = cfg.uuid;
1005
- if (cfg.enabled) {
1006
- logger.info('Starting up MDNS server');
1007
- this.server = multicastdns({ loopback: true });
1008
- var self = this;
1009
-
1010
- // look for responses to queries we send
1011
- // todo: need timeout on queries to remove them in case a bad query is sent
1012
- this.server.on('response', function (responses) {
1013
- self.queries.forEach(function (query) {
1014
- logger.silly(`looking to match on ${query.name}`);
1015
- responses.answers.forEach(answer => {
1016
- if (answer.name === query.name) {
1017
- logger.info(`MDNS: found response: ${answer.name} at ${answer.data}`);
1018
- // need to send response back to client here
1019
- self.mdnsEmitter.emit('mdnsResponse', answer);
1020
- // remove query from list
1021
- self.queries = self.queries.filter((value, index, arr) => {
1022
- if (value.name !== query.name) return arr;
1023
- });
1024
- }
1025
- });
1026
-
1027
- });
1028
- });
1029
-
1030
- // respond to incoming MDNS queries
1031
- this.server.on('query', function (query) {
1032
- query.questions.forEach(question => {
1033
- if (question.name === '_poolcontroller._tcp.local') {
1034
- logger.info(`received mdns query for nodejs_poolController`);
1035
- self.server.respond({
1036
- answers: [
1037
- {
1038
- name: '_poolcontroller._tcp.local',
1039
- type: 'A',
1040
- ttl: 300,
1041
- data: webApp.ip()
1042
- },
1043
- {
1044
- name: '_poolcontroller._tcp.local',
1045
- type: 'SRV',
1046
- data: {
1047
- port: webApp.httpPort().toString(),
1048
- target: '_poolcontroller._tcp.local',
1049
- weight: 0,
1050
- priority: 10
1051
- }
1052
- },
1053
- {
1054
- name: 'model',
1055
- type: 'TXT',
1056
- data: 'njsPC'
1057
- },
1058
- ]
1059
- });
1060
- }
1061
- });
1062
- });
1063
-
1064
- this.isRunning = true;
1065
- }
1066
- }
1067
- public queryMdns(query) {
1068
- // sample query
1069
- // queryMdns({name: '_poolcontroller._tcp.local', type: 'A'});
1070
- if (this.queries.indexOf(query) === -1) {
1071
- this.queries.push(query);
1072
- }
1073
- this.server.query({ questions: [query] });
1074
- }
1075
- public async stopAsync() {
1076
- try {
1077
- if (typeof this.server !== 'undefined')
1078
- await new Promise<void>((resolve, reject) => {
1079
- this.server.destroy((err) => {
1080
- if (err) reject(err);
1081
- else resolve();
1082
- });
1083
- });
1084
- logger.info(`Shut down MDNS Server ${this.name}`);
1085
- } catch (err) { logger.error(`Error shutting down MDNS Server ${this.name}: ${err.message}`); }
1086
- }
1087
- }
1088
- export class HttpInterfaceServer extends ProtoServer {
1089
- public bindingsPath: string;
1090
- public bindings: HttpInterfaceBindings;
1091
- private _fileTime: Date = new Date(0);
1092
- private _isLoading: boolean = false;
1093
- public async init(cfg) {
1094
- this.uuid = cfg.uuid;
1095
- if (cfg.enabled) {
1096
- if (cfg.fileName && this.initBindings(cfg)) this.isRunning = true;
1097
- }
1098
- }
1099
- public loadBindings(cfg): boolean {
1100
- this._isLoading = true;
1101
- if (fs.existsSync(this.bindingsPath)) {
1102
- try {
1103
- let bindings = JSON.parse(fs.readFileSync(this.bindingsPath, 'utf8'));
1104
- let ext = extend(true, {}, typeof cfg.context !== 'undefined' ? cfg.context.options : {}, bindings);
1105
- this.bindings = Object.assign<HttpInterfaceBindings, any>(new HttpInterfaceBindings(cfg), ext);
1106
- this.isRunning = true;
1107
- this._isLoading = false;
1108
- const stats = fs.statSync(this.bindingsPath);
1109
- this._fileTime = stats.mtime;
1110
- return true;
1111
- }
1112
- catch (err) {
1113
- logger.error(`Error reading interface bindings file: ${this.bindingsPath}. ${err}`);
1114
- this.isRunning = false;
1115
- this._isLoading = false;
1116
- }
1117
- }
1118
- return false;
1119
- }
1120
- public initBindings(cfg): boolean {
1121
- let self = this;
1122
- try {
1123
- this.bindingsPath = path.posix.join(process.cwd(), "/web/bindings") + '/' + cfg.fileName;
1124
- let fileTime = new Date(0).valueOf();
1125
- fs.watch(this.bindingsPath, (event, fileName) => {
1126
- if (fileName && event === 'change') {
1127
- if (self._isLoading) return; // Need a debounce here. We will use a semaphore to cause it not to load more than once.
1128
- const stats = fs.statSync(self.bindingsPath);
1129
- if (stats.mtime.valueOf() === self._fileTime.valueOf()) return;
1130
- self.loadBindings(cfg);
1131
- logger.info(`Reloading ${cfg.name || ''} interface config: ${fileName}`);
1132
- }
1133
- });
1134
- this.loadBindings(cfg);
1135
- if (this.bindings.context.mdnsDiscovery) {
1136
- let srv = webApp.mdnsServer;
1137
- let qry = typeof this.bindings.context.mdnsDiscovery === 'string' ? { name: this.bindings.context.mdnsDiscovery, type: 'A' } : this.bindings.context.mdnsDiscovery;
1138
- if (typeof srv !== 'undefined') {
1139
- srv.queryMdns(qry);
1140
- srv.mdnsEmitter.on('mdnsResponse', (response) => {
1141
- let url: URL;
1142
- url = new URL(response);
1143
- this.bindings.context.options.host = url.host;
1144
- this.bindings.context.options.port = url.port || 80;
1145
- });
1146
- }
1147
- }
1148
- return true;
1149
- }
1150
- catch (err) {
1151
- logger.error(`Error initializing interface bindings: ${err}`);
1152
- }
1153
- return false;
1154
- }
1155
- public emitToClients(evt: string, ...data: any) {
1156
- if (this.isRunning) {
1157
- // Take the bindings and map them to the appropriate http GET, PUT, DELETE, and POST.
1158
- this.bindings.bindEvent(evt, ...data);
1159
- }
1160
- }
1161
- public async stopAsync() {
1162
- try {
1163
- logger.info(`${this.name} Interface Server Shut down`);
1164
- }
1165
- catch (err) { }
1166
- }
1167
- }
1168
- export class RuleInterfaceServer extends ProtoServer {
1169
- public bindingsPath: string;
1170
- public bindings: RuleInterfaceBindings;
1171
- private _fileTime: Date = new Date(0);
1172
- private _isLoading: boolean = false;
1173
- public async init(cfg) {
1174
- this.uuid = cfg.uuid;
1175
- if (cfg.enabled) {
1176
- if (cfg.fileName && this.initBindings(cfg)) this.isRunning = true;
1177
- }
1178
- }
1179
- public loadBindings(cfg): boolean {
1180
- this._isLoading = true;
1181
- if (fs.existsSync(this.bindingsPath)) {
1182
- try {
1183
- let bindings = JSON.parse(fs.readFileSync(this.bindingsPath, 'utf8'));
1184
- let ext = extend(true, {}, typeof cfg.context !== 'undefined' ? cfg.context.options : {}, bindings);
1185
- this.bindings = Object.assign<RuleInterfaceBindings, any>(new RuleInterfaceBindings(cfg), ext);
1186
- this.isRunning = true;
1187
- this._isLoading = false;
1188
- const stats = fs.statSync(this.bindingsPath);
1189
- this._fileTime = stats.mtime;
1190
- return true;
1191
- }
1192
- catch (err) {
1193
- logger.error(`Error reading interface bindings file: ${this.bindingsPath}. ${err}`);
1194
- this.isRunning = false;
1195
- this._isLoading = false;
1196
- }
1197
- }
1198
- return false;
1199
- }
1200
- public initBindings(cfg): boolean {
1201
- let self = this;
1202
- try {
1203
- this.bindingsPath = path.posix.join(process.cwd(), "/web/bindings") + '/' + cfg.fileName;
1204
- let fileTime = new Date(0).valueOf();
1205
- fs.watch(this.bindingsPath, (event, fileName) => {
1206
- if (fileName && event === 'change') {
1207
- if (self._isLoading) return; // Need a debounce here. We will use a semaphore to cause it not to load more than once.
1208
- const stats = fs.statSync(self.bindingsPath);
1209
- if (stats.mtime.valueOf() === self._fileTime.valueOf()) return;
1210
- self.loadBindings(cfg);
1211
- logger.info(`Reloading ${cfg.name || ''} interface config: ${fileName}`);
1212
- }
1213
- });
1214
- this.loadBindings(cfg);
1215
- if (this.bindings.context.mdnsDiscovery) {
1216
- let srv = webApp.mdnsServer;
1217
- let qry = typeof this.bindings.context.mdnsDiscovery === 'string' ? { name: this.bindings.context.mdnsDiscovery, type: 'A' } : this.bindings.context.mdnsDiscovery;
1218
- if (typeof srv !== 'undefined') {
1219
- srv.queryMdns(qry);
1220
- srv.mdnsEmitter.on('mdnsResponse', (response) => {
1221
- let url: URL;
1222
- url = new URL(response);
1223
- this.bindings.context.options.host = url.host;
1224
- this.bindings.context.options.port = url.port || 80;
1225
- });
1226
- }
1227
- }
1228
- return true;
1229
- }
1230
- catch (err) {
1231
- logger.error(`Error initializing interface bindings: ${err}`);
1232
- }
1233
- return false;
1234
- }
1235
- public emitToClients(evt: string, ...data: any) {
1236
- if (this.isRunning) {
1237
- // Take the bindings and map them to the appropriate http GET, PUT, DELETE, and POST.
1238
- this.bindings.bindEvent(evt, ...data);
1239
- }
1240
- }
1241
- public async stopAsync() {
1242
- try {
1243
- logger.info(`${this.name} Interface Server Shut down`);
1244
- }
1245
- catch (err) { }
1246
- }
1247
- }
1248
-
1249
- export class InfluxInterfaceServer extends ProtoServer {
1250
- public bindingsPath: string;
1251
- public bindings: InfluxInterfaceBindings;
1252
- private _fileTime: Date = new Date(0);
1253
- private _isLoading: boolean = false;
1254
- public async init(cfg) {
1255
- this.uuid = cfg.uuid;
1256
- if (cfg.enabled) {
1257
- if (cfg.fileName && this.initBindings(cfg)) this.isRunning = true;
1258
- }
1259
- }
1260
- public loadBindings(cfg): boolean {
1261
- this._isLoading = true;
1262
- if (fs.existsSync(this.bindingsPath)) {
1263
- try {
1264
- let bindings = JSON.parse(fs.readFileSync(this.bindingsPath, 'utf8'));
1265
- let ext = extend(true, {}, typeof cfg.context !== 'undefined' ? cfg.context.options : {}, bindings);
1266
- this.bindings = Object.assign<InfluxInterfaceBindings, any>(new InfluxInterfaceBindings(cfg), ext);
1267
- this.isRunning = true;
1268
- this._isLoading = false;
1269
- const stats = fs.statSync(this.bindingsPath);
1270
- this._fileTime = stats.mtime;
1271
- return true;
1272
- }
1273
- catch (err) {
1274
- logger.error(`Error reading interface bindings file: ${this.bindingsPath}. ${err}`);
1275
- this.isRunning = false;
1276
- this._isLoading = false;
1277
- }
1278
- }
1279
- return false;
1280
- }
1281
- public initBindings(cfg): boolean {
1282
- let self = this;
1283
- try {
1284
- this.bindingsPath = path.posix.join(process.cwd(), "/web/bindings") + '/' + cfg.fileName;
1285
- fs.watch(this.bindingsPath, (event, fileName) => {
1286
- if (fileName && event === 'change') {
1287
- if (self._isLoading) return; // Need a debounce here. We will use a semaphore to cause it not to load more than once.
1288
- const stats = fs.statSync(self.bindingsPath);
1289
- if (stats.mtime.valueOf() === self._fileTime.valueOf()) return;
1290
- self.loadBindings(cfg);
1291
- logger.info(`Reloading ${cfg.name || ''} interface config: ${fileName}`);
1292
- }
1293
- });
1294
- this.loadBindings(cfg);
1295
- return true;
1296
- }
1297
- catch (err) {
1298
- logger.error(`Error initializing interface bindings: ${err}`);
1299
- }
1300
- return false;
1301
- }
1302
- public emitToClients(evt: string, ...data: any) {
1303
- if (this.isRunning) {
1304
- // Take the bindings and map them to the appropriate http GET, PUT, DELETE, and POST.
1305
- this.bindings.bindEvent(evt, ...data);
1306
- }
1307
- }
1308
- }
1309
- export class MqttInterfaceServer extends ProtoServer {
1310
- public bindingsPath: string;
1311
- public bindings: MqttInterfaceBindings;
1312
- private _fileTime: Date = new Date(0);
1313
- private _isLoading: boolean = false;
1314
- public get isConnected() { return this.isRunning && this.bindings.events.length > 0; }
1315
- public async init(cfg) {
1316
- this.uuid = cfg.uuid;
1317
- if (cfg.enabled) {
1318
- if (cfg.fileName && this.initBindings(cfg)) this.isRunning = true;
1319
- }
1320
- }
1321
- public loadBindings(cfg): boolean {
1322
- this._isLoading = true;
1323
- if (fs.existsSync(this.bindingsPath)) {
1324
- try {
1325
- let bindings = JSON.parse(fs.readFileSync(this.bindingsPath, 'utf8'));
1326
- let ext = extend(true, {}, typeof cfg.context !== 'undefined' ? cfg.context.options : {}, bindings);
1327
- if (this.bindings && this.bindings.client) {
1328
- // RKS: 05-29-22 - This was actually orphaning the subscriptions and event processors. Instead of simply doing
1329
- // an assign we ned to assign the underlying data and clear the old info out. The reload method takes care of the
1330
- // bindings for us.
1331
- (async () => {
1332
- await this.bindings.reload(ext);
1333
- })();
1334
- }
1335
- else {
1336
- this.bindings = Object.assign<MqttInterfaceBindings, any>(new MqttInterfaceBindings(cfg), ext);
1337
- (async () => {
1338
- await this.bindings.initAsync();
1339
- })();
1340
- }
1341
- this.isRunning = true;
1342
- this._isLoading = false;
1343
- const stats = fs.statSync(this.bindingsPath);
1344
- this._fileTime = stats.mtime;
1345
- return true;
1346
- }
1347
- catch (err) {
1348
- logger.error(`Error reading interface bindings file: ${this.bindingsPath}. ${err}`);
1349
- this.isRunning = false;
1350
- this._isLoading = false;
1351
- }
1352
- }
1353
- return false;
1354
- }
1355
- public initBindings(cfg): boolean {
1356
- let self = this;
1357
- try {
1358
- this.bindingsPath = path.posix.join(process.cwd(), "/web/bindings") + '/' + cfg.fileName;
1359
- let fileTime = new Date(0).valueOf();
1360
- fs.watch(this.bindingsPath, (event, fileName) => {
1361
- if (fileName && event === 'change') {
1362
- if (self._isLoading) return; // Need a debounce here. We will use a semaphore to cause it not to load more than once.
1363
- const stats = fs.statSync(self.bindingsPath);
1364
- if (stats.mtime.valueOf() === self._fileTime.valueOf()) return;
1365
- self.loadBindings(cfg);
1366
- logger.info(`Reloading ${cfg.name || ''} interface config: ${fileName}`);
1367
- }
1368
- });
1369
- this.loadBindings(cfg);
1370
- return true;
1371
- }
1372
- catch (err) {
1373
- logger.error(`Error initializing interface bindings: ${err}`);
1374
- }
1375
- return false;
1376
- }
1377
- public emitToClients(evt: string, ...data: any) {
1378
- if (this.isRunning) {
1379
- // Take the bindings and map them to the appropriate http GET, PUT, DELETE, and POST.
1380
- this.bindings.bindEvent(evt, ...data);
1381
- }
1382
- }
1383
- public async stopAsync() {
1384
- try {
1385
- fs.unwatchFile(this.bindingsPath);
1386
- if (this.bindings) await this.bindings.stopAsync();
1387
- } catch (err) { logger.error(`Error shutting down MQTT Server ${this.name}: ${err.message}`); }
1388
- }
1389
- }
1390
- export class InterfaceServerResponse {
1391
- constructor(statusCode?: number, statusMessage?: string) {
1392
- if (typeof statusCode !== 'undefined') this.status.code = statusCode;
1393
- if (typeof statusMessage !== 'undefined') this.status.message = statusMessage;
1394
- }
1395
- status: { code: number, message: string } = { code: -1, message: '' };
1396
- error: Error;
1397
- data: string;
1398
- obj: any;
1399
- public static createError(err: Error, data?: string, obj?: any) {
1400
- let resp = new InterfaceServerResponse(500, err.message);
1401
- resp.error = err;
1402
- return resp;
1403
- }
1404
- }
1405
- export class REMInterfaceServer extends ProtoServer {
1406
- public async init(cfg) {
1407
- let self = this;
1408
- this.cfg = cfg;
1409
- this.uuid = cfg.uuid;
1410
- if (cfg.enabled) {
1411
- this.initSockets();
1412
- setTimeoutSync(async () => {
1413
- try {
1414
- await self.initConnection();
1415
- }
1416
- catch (err) {
1417
- logger.error(`Error establishing bi-directional Nixie/REM connection: ${err}`)
1418
- }
1419
- }, 5000);
1420
- }
1421
- }
1422
- public async getControllerConfig() : Promise<InterfaceServerResponse> {
1423
- try {
1424
- let response = await this.sendClientRequest('GET', '/config/backup/controller', undefined, 10000);
1425
- return response;
1426
- } catch (err) {
1427
- logger.error(`Error requesting GET /config/backup/controller: ${err.message}`);
1428
- let errorResponse = new InterfaceServerResponse();
1429
- errorResponse.error = new Error(`Error requesting GET /config/backup/controller: ${err.message}`);
1430
- return errorResponse;
1431
- }
1432
- }
1433
- public async validateRestore(cfg): Promise<InterfaceServerResponse> {
1434
- try {
1435
- let response = await this.sendClientRequest('PUT', '/config/restore/validate', cfg, 10000);
1436
- return response;
1437
- } catch (err) {
1438
- logger.error(`Error requesting PUT /config/restore/validate ${err.message}`);
1439
- let errorResponse = new InterfaceServerResponse();
1440
- errorResponse.error = new Error(`Error requesting PUT /config/restore/validate: ${err.message}`);
1441
- return errorResponse;
1442
- }
1443
- }
1444
- public async restoreConfig(cfg): Promise<InterfaceServerResponse> {
1445
- try {
1446
- return await this.sendClientRequest('PUT', '/config/restore/file', cfg, 20000);
1447
- } catch (err) {
1448
- logger.error(`Error requesting PUT /config/restore/file ${err.message}`);
1449
- let errorResponse = new InterfaceServerResponse();
1450
- errorResponse.error = new Error(`Error requesting PUT /config/restore/file: ${err.message}`);
1451
- return errorResponse;
1452
- }
1453
- }
1454
- public async startPacketCapture(): Promise<InterfaceServerResponse> {
1455
- try {
1456
- let response = await this.sendClientRequest('PUT', '/config/packetCapture/start', undefined, 10000);
1457
- return response;
1458
- } catch (err) {
1459
- logger.error(`Error requesting PUT /config/packetCapture/start: ${err.message}`);
1460
- let errorResponse = new InterfaceServerResponse();
1461
- errorResponse.error = new Error(`Error requesting PUT /config/packetCapture/start: ${err.message}`);
1462
- return errorResponse;
1463
- }
1464
- }
1465
- public async stopPacketCapture(): Promise<InterfaceServerResponse> {
1466
- try {
1467
- let response = await this.sendClientRequest('PUT', '/config/packetCapture/stop', undefined, 10000);
1468
- return response;
1469
- } catch (err) {
1470
- logger.error(`Error requesting PUT /config/packetCapture/stop: ${err.message}`);
1471
- let errorResponse = new InterfaceServerResponse();
1472
- errorResponse.error = new Error(`Error requesting PUT /config/packetCapture/stop: ${err.message}`);
1473
- return errorResponse;
1474
- }
1475
- }
1476
- public async getPacketCaptureLog(): Promise<InterfaceServerResponse> {
1477
- try {
1478
- let response = await this.sendClientRequest('GET', '/config/packetCapture/log', undefined, 15000);
1479
- return response;
1480
- } catch (err) {
1481
- logger.error(`Error requesting GET /config/packetCapture/log: ${err.message}`);
1482
- let errorResponse = new InterfaceServerResponse();
1483
- errorResponse.error = new Error(`Error requesting GET /config/packetCapture/log: ${err.message}`);
1484
- return errorResponse;
1485
- }
1486
- }
1487
-
1488
-
1489
- // Static methods to handle the REM server
1490
- public static async startPacketCaptureOnRemServer(): Promise<void> {
1491
- let remServers = webApp.findServersByType('rem') as REMInterfaceServer[];
1492
- logger.info(`Found ${remServers ? remServers.length : 0} REM servers`);
1493
-
1494
- if (remServers && remServers.length > 0) {
1495
- let server = remServers[0]; // Get the single REM server
1496
- logger.info(`Attempting to start packet capture on REM server: ${server.name} (connected: ${server.isConnected})`);
1497
-
1498
- if (server.isConnected) {
1499
- try {
1500
- let response = await server.startPacketCapture();
1501
- logger.info(`Start packet capture response: ${JSON.stringify(response)}`);
1502
-
1503
- if (response && response.status.code === 200) {
1504
- logger.info(`Started packet capture on REM server: ${server.name}`);
1505
- } else {
1506
- logger.warn(`Failed to start packet capture on REM server: ${server.name}. Status: ${response?.status?.code}, Error: ${response?.error?.message}`);
1507
- }
1508
- } catch (err) {
1509
- logger.error(`Error starting packet capture on REM server ${server.name}: ${err.message}`);
1510
- }
1511
- } else {
1512
- logger.warn(`REM server ${server.name} is not connected, cannot start packet capture`);
1513
- }
1514
- } else {
1515
- logger.warn(`No REM servers found or configured`);
1516
- }
1517
- }
1518
-
1519
- public static async stopPacketCaptureOnRemServer(): Promise<any[]> {
1520
- let remServers = webApp.findServersByType('rem') as REMInterfaceServer[];
1521
- let remLogs = [];
1522
-
1523
- logger.info(`Found ${remServers ? remServers.length : 0} REM servers for stop packet capture`);
1524
-
1525
- if (remServers && remServers.length > 0) {
1526
- let server = remServers[0]; // Get the single REM server
1527
- logger.info(`Attempting to stop packet capture on REM server: ${server.name} (connected: ${server.isConnected})`);
1528
-
1529
- if (server.isConnected) {
1530
- try {
1531
- // Stop packet capture
1532
- let stopResponse = await server.stopPacketCapture();
1533
- logger.info(`Stop packet capture response: ${JSON.stringify(stopResponse)}`);
1534
-
1535
- if (stopResponse && stopResponse.status.code === 200) {
1536
- logger.info(`Stopped packet capture on REM server: ${server.name}`);
1537
-
1538
- // Get the log file
1539
- let logResponse = await server.getPacketCaptureLog();
1540
- logger.info(`Get log response: ${JSON.stringify(logResponse)}`);
1541
-
1542
- if (logResponse && logResponse.status.code === 200 && logResponse.data) {
1543
- // Use the actual log file name from the REM response
1544
- logger.info(`Log response obj: ${JSON.stringify(logResponse.obj)}`);
1545
- let logFileName = logResponse.obj && logResponse.obj.logFile ? logResponse.obj.logFile : `rem_${server.name}_packetCapture.log`;
1546
- logger.info(`Using log filename: ${logFileName}`);
1547
- remLogs.push({
1548
- serverName: server.name,
1549
- logData: logResponse.data,
1550
- logFileName: logFileName
1551
- });
1552
- logger.info(`Retrieved packet capture log from REM server: ${server.name}, log size: ${logResponse.data.length} characters, filename: ${logFileName}`);
1553
- } else {
1554
- logger.warn(`Failed to retrieve packet capture log from REM server: ${server.name}. Status: ${logResponse?.status?.code}, Error: ${logResponse?.error?.message}`);
1555
- }
1556
- } else {
1557
- logger.warn(`Failed to stop packet capture on REM server: ${server.name}. Status: ${stopResponse?.status?.code}, Error: ${stopResponse?.error?.message}`);
1558
- }
1559
- } catch (err) {
1560
- logger.error(`Error stopping packet capture on REM server ${server.name}: ${err.message}`);
1561
- }
1562
- } else {
1563
- logger.warn(`REM server ${server.name} is not connected, cannot stop packet capture`);
1564
- }
1565
- } else {
1566
- logger.warn(`No REM servers found or configured for stop packet capture`);
1567
- }
1568
-
1569
- logger.info(`Returning ${remLogs.length} REM logs`);
1570
- return remLogs;
1571
- }
1572
- private async initConnection() {
1573
- try {
1574
- // find HTTP server
1575
- return new Promise<void>(async (resolve, reject) => {
1576
- let self = this;
1577
- // First, send the connection info for njsPC and see if a connection exists.
1578
- let url = '/config/checkconnection/';
1579
- // can & should extend for https/username-password/ssl
1580
- let data: any = { type: "njspc", isActive: true, id: null, name: "njsPC - automatic", protocol: "http:", ipAddress: webApp.ip(), port: config.getSection('web').servers.http.port || 4200, userName: "", password: "", sslKeyFile: "", sslCertFile: "", hostnames: [] }
1581
- if (typeof this.cfg.options !== 'undefined' && this.cfg.options.host !== 'undefined' &&
1582
- this.cfg.options.host.toLowerCase() === 'localhost' || this.cfg.options.host === '127.0.0.1') data.loopback = true;
1583
- logger.info(`Checking REM Connection ${data.name} ${data.ipAddress}:${data.port}`);
1584
- try {
1585
- data.hostnames = await dns.promises.reverse(data.ipAddress);
1586
- } catch (err) { logger.error(`Error getting hostnames for njsPC REM connection`); }
1587
- let result = await this.putApiService(url, data, 5000);
1588
- // If the result code is > 200 we have an issue. (-1 is for timeout)
1589
- if (result.status.code > 200 || result.status.code < 0) return reject(new Error(`initConnection: ${result.error.message}`));
1590
- else {
1591
- this.remoteConnectionId = result.obj.id;
1592
- };
1593
-
1594
- // The passed connection has been setup/verified; now test for emit
1595
- // if this fails, it could be because the remote connection is disabled. We will not
1596
- // automatically re-enable it
1597
- url = '/config/checkemit'
1598
- data = { eventName: "checkemit", property: "result", value: 'success', connectionId: result.obj.id }
1599
- // wait for REM server to finish resetting
1600
- setTimeoutSync(async () => {
1601
- try {
1602
- let _tmr = setTimeoutSync(() => { return reject(new Error(`initConnection: No socket response received. Check REM→njsPC communications.`)) }, 5000);
1603
- let srv: HttpServer = webApp.findServer('http') as HttpServer;
1604
- srv.addListenerOnce('/checkemit', (data: any) => {
1605
- // if we receive the emit, data will work both ways.
1606
- // console.log(data);
1607
- clearTimeout(_tmr);
1608
- logger.info(`${this.name} bi-directional communications established.`)
1609
- resolve();
1610
- });
1611
- result = await self.putApiService(url, data);
1612
- // If the result code is > 200 or -1 we have an issue.
1613
- if (result.status.code > 200 || result.status.code === -1) return reject(new Error(`initConnection: ${result.error.message}`));
1614
- else {
1615
- clearTimeout(_tmr);
1616
- resolve();
1617
- }
1618
- }
1619
- catch (err) {
1620
- logger.error(`initConnection setTimeout error: ${err.message}`);
1621
- reject(new Error(`initConnection setTimeout: ${err.message}`));
1622
- }
1623
- }, 3000);
1624
- });
1625
- }
1626
- catch (err) {
1627
- logger.error(`Error with REM Interface Server initConnection: ${err.message}`);
1628
- }
1629
- }
1630
- public async stopAsync() {
1631
- try {
1632
- if (typeof this.agent !== 'undefined') this.agent.destroy();
1633
- if (typeof this.sockClient !== 'undefined') this.sockClient.destroy();
1634
- logger.info(`Stopped REM Interface Server ${this.name}`);
1635
- } catch (err) { logger.error(`Error closing REM Server ${this.name}: ${err.message}`); }
1636
- }
1637
- public cfg;
1638
- public sockClient;
1639
- protected agent: http.Agent = new http.Agent({ keepAlive: true });
1640
- public get isConnected() { return this.sockClient !== 'undefined' && this.sockClient.connected; };
1641
- private _sockets: RemoteSocket<ServerToClientEvents, any>[] = [];
1642
- private async sendClientRequest(method: string, url: string, data?: any, timeout: number = 10000): Promise<InterfaceServerResponse> {
1643
- try {
1644
-
1645
- let ret = new InterfaceServerResponse();
1646
- let opts = extend(true, { headers: {} }, this.cfg.options);
1647
- if ((typeof opts.hostname === 'undefined' || !opts.hostname) && (typeof opts.host === 'undefined' || !opts.host || opts.host === '*')) {
1648
- ret.error = new Error(`Interface: ${this.cfg.name} has not resolved to a valid host.`)
1649
- logger.warn(ret.error);
1650
- return ret;
1651
- }
1652
- let sbody = typeof data === 'undefined' ? '' : typeof data === 'string' ? data : typeof data === 'object' ? JSON.stringify(data) : data.toString();
1653
- if (typeof sbody !== 'undefined') {
1654
- if (sbody.charAt(0) === '"' && sbody.charAt(sbody.length - 1) === '"') sbody = sbody.substr(1, sbody.length - 2);
1655
- opts.headers["CONTENT-LENGTH"] = Buffer.byteLength(sbody || '');
1656
- }
1657
- opts.path = url;
1658
- opts.method = method || 'GET';
1659
- ret.data = '';
1660
- opts.agent = this.agent;
1661
- logger.verbose(`REM server request initiated. ${opts.method} ${opts.path} ${sbody}`);
1662
- await new Promise<void>((resolve, reject) => {
1663
- let req: http.ClientRequest;
1664
- if (opts.port === 443 || (opts.protocol || '').startsWith('https')) {
1665
- opts.protocol = 'https:';
1666
- req = https.request(opts, (response: http.IncomingMessage) => {
1667
- ret.status.code = response.statusCode;
1668
- ret.status.message = response.statusMessage;
1669
- response.on('error', (err) => { ret.error = err; resolve(); });
1670
- response.on('data', (data) => { ret.data += data; });
1671
- response.on('end', () => { resolve(); });
1672
- });
1673
- }
1674
- else {
1675
- opts.protocol = undefined;
1676
- req = http.request(opts, (response: http.IncomingMessage) => {
1677
- ret.status.code = response.statusCode;
1678
- ret.status.message = response.statusMessage;
1679
- response.on('error', (err) => {
1680
- logger.error(`An error occurred with request: ${err}`);
1681
- ret.error = err; resolve();
1682
- });
1683
- response.on('data', (data) => { ret.data += data; });
1684
- response.on('end', () => { resolve(); });
1685
- });
1686
- }
1687
- req.setTimeout(timeout, () => { reject(new Error(`Request timeout after ${timeout}ms: ${method} ${url}`)); });
1688
- req.on('error', (err, req, res) => {
1689
- logger.error(`Error sending Request: ${opts.method} ${url} ${err.message}`);
1690
- ret.error = err;
1691
- reject(new Error(`Error sending Request: ${opts.method} ${url} ${err.message}`));
1692
- });
1693
- req.on('abort', () => { logger.warn('Request Aborted'); reject(new Error('Request Aborted.')); });
1694
- req.end(sbody);
1695
- }).catch((err) => {
1696
- logger.error(`Error Sending REM Request: ${opts.method} ${url} ${err.message}`);
1697
- ret.error = err;
1698
- });
1699
- logger.verbose(`REM server request returned. ${opts.method} ${opts.path} ${sbody}`);
1700
- if (ret.status.code > 200) {
1701
- // We have an http error so let's parse it up.
1702
- try {
1703
- ret.error = JSON.parse(ret.data);
1704
- } catch (err) { ret.error = new Error(`Unidentified ${ret.status.code} Error: ${ret.status.message}`) }
1705
- ret.data = '';
1706
- }
1707
- else if (ret.status.code === 200 && this.isJSONString(ret.data)) {
1708
- try { ret.obj = JSON.parse(ret.data); }
1709
- catch (err) { }
1710
- }
1711
- logger.debug(`REM server request returned. ${opts.method} ${opts.path} ${sbody} ${JSON.stringify(ret)}`);
1712
- return ret;
1713
- }
1714
- catch (err) {
1715
- logger.error(`Error sending HTTP ${method} command to ${url}: ${err.message}`);
1716
- let errorResponse = new InterfaceServerResponse();
1717
- errorResponse.error = new Error(`Http ${method} Error ${url}:${err.message}`);
1718
- return errorResponse;
1719
- }
1720
- }
1721
- private initSockets() {
1722
- try {
1723
- let self = this;
1724
- let url = `${this.cfg.options.protocol || 'http://'}${this.cfg.options.host}${typeof this.cfg.options.port !== 'undefined' ? ':' + this.cfg.options.port : ''}`;
1725
- logger.info(`Opening ${this.cfg.name} socket on ${url}`);
1726
- //console.log(this.cfg);
1727
- this.sockClient = sockClient(url, extend(true,
1728
- { reconnectionDelay: 2000, reconnection: true, reconnectionDelayMax: 20000, transports: ['websocket'], upgrade: true, }, this.cfg.socket));
1729
- if (typeof this.sockClient === 'undefined') {
1730
- logger.error('Could not Initialize REM Server. Invalid configuration.');
1731
- return;
1732
- }
1733
- //this.sockClient = io.connect(url, { reconnectionDelay: 2000, reconnection: true, reconnectionDelayMax: 20000 });
1734
- //console.log(this.sockClient);
1735
- //console.log(typeof this.sockClient.on);
1736
- this.sockClient.on('connect_error', (err) => { logger.error(`${this.cfg.name} socket connection error: ${err}`); });
1737
- this.sockClient.on('connect_timeout', () => { logger.error(`${this.cfg.name} socket connection timeout`); });
1738
- this.sockClient.on('reconnect', (attempts) => { logger.info(`${this.cfg.name} socket reconnected after ${attempts}`); });
1739
- this.sockClient.on('reconnect_attempt', () => { logger.warn(`${this.cfg.name} socket attempting to reconnect`); });
1740
- this.sockClient.on('reconnecting', (attempts) => { logger.warn(`${this.cfg.name} socket attempting to reconnect: ${attempts}`); });
1741
- this.sockClient.on('reconnect_failed', (err) => { logger.warn(`${this.cfg.name} socket failed to reconnect: ${err}`); });
1742
- this.sockClient.on('close', () => { logger.info(`${this.cfg.name} socket closed`); });
1743
- this.sockClient.on('connect', () => {
1744
- logger.info(`${this.cfg.name} socket connected`);
1745
- this.sockClient.on('i2cDataValues', function (data) {
1746
- //logger.info(`REM Socket i2cDataValues ${JSON.stringify(data)}`);
1747
- });
1748
- });
1749
- this.isRunning = true;
1750
- }
1751
- catch (err) {
1752
- logger.error(`Error Initializing Sockets: ${err.message}`);
1753
- }
1754
- }
1755
- private isJSONString(s: string): boolean {
1756
- if (typeof s !== 'string') return false;
1757
- if (s.startsWith('{') || s.startsWith('[')) return true;
1758
- return false;
1759
- }
1760
- public async getApiService(url: string, data?: any, timeout: number = 3600): Promise<InterfaceServerResponse> {
1761
- // Calls a rest service on the REM to set the state of a connected device.
1762
- let ret = await this.sendClientRequest('GET', url, data, timeout);
1763
- return ret;
1764
- }
1765
- public async putApiService(url: string, data?: any, timeout: number = 3600): Promise<InterfaceServerResponse> {
1766
- // Calls a rest service on the REM to set the state of a connected device.
1767
- let ret = await this.sendClientRequest('PUT', url, data, timeout);
1768
- return ret;
1769
- }
1770
- public async searchApiService(url: string, data?: any, timeout: number = 3600): Promise<InterfaceServerResponse> {
1771
- // Calls a rest service on the REM to set the state of a connected device.
1772
- let ret = await this.sendClientRequest('SEARCH', url, data, timeout);
1773
- return ret;
1774
- }
1775
- public async deleteApiService(url: string, data?: any, timeout: number = 3600): Promise<InterfaceServerResponse> {
1776
- // Calls a rest service on the REM to set the state of a connected device.
1777
- let ret = await this.sendClientRequest('DELETE', url, data, timeout);
1778
- return ret;
1779
- }
1780
- public async getDevices() {
1781
- try {
1782
- let response = await this.sendClientRequest('GET', '/devices/all', undefined, 3000);
1783
- if (response.status.code !== 200) {
1784
- // Let's try again. Sometimes the resolver for calls like this are stupid.
1785
- response = await this.sendClientRequest('GET', '/devices/all', undefined, 10000);
1786
- }
1787
- return (response.status.code === 200) ? JSON.parse(response.data) : [];
1788
- }
1789
- catch (err) {
1790
- logger.error(`getDevices: ${err.message}`);
1791
- return [];
1792
- }
1793
- }
1794
- }
1795
- export class BackupFile {
1796
- public static async fromBuffer(filename: string, buff: Buffer) {
1797
- try {
1798
- let bf = new BackupFile();
1799
- bf.filename = filename;
1800
- bf.filePath = path.join(process.cwd(), 'backups', bf.filename);
1801
- await bf.extractBackupOptions(buff);
1802
- return typeof bf.options !== 'undefined' ? bf : undefined;
1803
- } catch (err) { logger.error(`Error creating buffered backup file: ${filename}`); }
1804
- }
1805
- public static async fromFile(filePath: string) {
1806
- try {
1807
- let bf = new BackupFile();
1808
- bf.filePath = filePath;
1809
- bf.filename = path.parse(filePath).base;
1810
- await bf.extractBackupOptions(filePath);
1811
- return typeof bf.options !== 'undefined' ? bf : undefined;
1812
- } catch (err) { logger.error(`Error creating backup file from file ${filePath}`); }
1813
- }
1814
- public options: any;
1815
- public filename: string;
1816
- public filePath: string;
1817
- public errors = [];
1818
- protected async extractBackupOptions(file: string | Buffer) {
1819
- try {
1820
- let jszip = require("jszip");
1821
- let buff = Buffer.isBuffer(file) ? file : fs.readFileSync(file);
1822
- let zip = await jszip.loadAsync(buff);
1823
- await zip.file('options.json').async('string').then((data) => {
1824
- this.options = JSON.parse(data);
1825
- if (typeof this.options.backupDate === 'undefined' && typeof file === 'string') {
1826
- let name = path.parse(file).name;
1827
- name = name.indexOf('(') !== -1 ? name.substring(0, name.indexOf('(')) : name;
1828
- if (name.length === 19) {
1829
- let date = name.substring(0, 10).replace(/-/g, '/');
1830
- let time = name.substring(11).replace(/-/g, ':');
1831
- let dt = Date.parse(`${date} ${time}`);
1832
- if (!isNaN(dt)) this.options.backupDate = Timestamp.toISOLocal(new Date(dt));
1833
- }
1834
- }
1835
- });
1836
- } catch (err) { this.errors.push(err); logger.error(`Error extracting backup options from ${file}: ${err.message}`); }
1837
- }
1838
- }
1839
- export class RestoreFile {
1840
- public static async fromFile(filePath: string) {
1841
- try {
1842
- let rf = new RestoreFile();
1843
- rf.filePath = filePath;
1844
- rf.filename = path.parse(filePath).base;
1845
- await rf.extractRestoreOptions(filePath);
1846
- return rf;
1847
- } catch (err) { logger.error(`Error created restore file options`); }
1848
- }
1849
- public filename: string;
1850
- public filePath: string;
1851
- public njsPC: { config:any, poolConfig: any, poolState: any };
1852
- public servers: { name: string, uuid: string, serverConfig: any, controllerConfig: any }[] = [];
1853
- public options: any;
1854
- public errors = [];
1855
- protected async extractFile(zip, path): Promise<any> {
1856
- try {
1857
- let obj;
1858
- await zip.file(path).async('string').then((data) => { obj = JSON.parse(data); });
1859
- return obj;
1860
- } catch (err) { logger.error(`Error extracting restore data from ${this.filename}[${path}]: ${err.message}`); }
1861
- }
1862
- protected async extractRestoreOptions(file: string | Buffer) {
1863
- try {
1864
- let jszip = require("jszip");
1865
- let buff = Buffer.isBuffer(file) ? file : fs.readFileSync(file);
1866
- let zip = await jszip.loadAsync(buff);
1867
- this.options = await this.extractFile(zip, 'options.json');
1868
- // Now we need to extract all the servers from the file.
1869
- if (this.options.njsPC) {
1870
- this.njsPC = { config: {}, poolConfig: {}, poolState: {} };
1871
- this.njsPC.config = await this.extractFile(zip, 'njsPC/config.json');
1872
- this.njsPC.poolConfig = await this.extractFile(zip, 'njsPC/data/poolConfig.json');
1873
- this.njsPC.poolState = await this.extractFile(zip, 'njsPC/data/poolState.json');
1874
- }
1875
- if (typeof this.options.servers !== 'undefined') {
1876
- for (let i = 0; i < this.options.servers.length; i++) {
1877
- // Extract each server from the file.
1878
- let srv = this.options.servers[i];
1879
- if (srv.backup && srv.success) {
1880
- this.servers.push({
1881
- name: srv.name,
1882
- uuid: srv.uuid,
1883
- serverConfig: await this.extractFile(zip, `${srv.name}/serverConfig.json`),
1884
- controllerConfig: await this.extractFile(zip, `${srv.name}/data/controllerConfig.json`)
1885
- });
1886
- }
1887
- }
1888
- }
1889
- } catch(err) { this.errors.push(err); logger.error(`Error extracting restore options from ${file}: ${err.message}`); }
1890
- }
1891
- }
1892
- export class RestoreResults {
1893
- public errors = [];
1894
- public warnings = [];
1895
- public success = [];
1896
- public modules: { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number }[] = [];
1897
- protected getModule(name: string): { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number } {
1898
- let mod = this.modules.find(elem => name === elem.name);
1899
- if (typeof mod === 'undefined') {
1900
- mod = { name: name, errors: [], warnings: [], success: [], restored: 0, ignored: 0 };
1901
- this.modules.push(mod);
1902
- }
1903
- return mod;
1904
- }
1905
- public addModuleError(name: string, err: any): { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number } {
1906
- let mod = this.getModule(name);
1907
- mod.errors.push(err);
1908
- mod.ignored++;
1909
- logger.error(`Restore ${name} -> ${err}`);
1910
- return mod;
1911
- }
1912
- public addModuleWarning(name: string, warn: any): { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number } {
1913
- let mod = this.getModule(name);
1914
- mod.warnings.push(warn);
1915
- mod.restored++;
1916
- logger.warn(`Restore ${name} -> ${warn}`);
1917
- return mod;
1918
- }
1919
- public addModuleSuccess(name: string, success: any): { name: string, errors: any[], warnings: any[], success: any[], restored: number, ignored: number } {
1920
- let mod = this.getModule(name);
1921
- mod.success.push(success);
1922
- mod.restored++;
1923
- logger.info(`Restore ${name} -> ${success}`);
1924
- return mod;
1925
- }
1926
- }
1927
- export const webApp = new WebServer();
1
+ /* nodejs-poolController. An application to control pool equipment.
2
+ Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3
+ Russell Goldin, tagyoureit. russ.goldin@gmail.com
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as
7
+ published by the Free Software Foundation, either version 3 of the
8
+ License, or (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+ import * as dns from "dns";
19
+ import { EventEmitter } from 'events';
20
+ import * as fs from "fs";
21
+ import * as http from "http";
22
+ import * as http2 from "http2";
23
+ import * as https from "https";
24
+ import * as multicastdns from 'multicast-dns';
25
+ import * as ssdp from 'node-ssdp';
26
+ import * as os from 'os';
27
+ import * as path from "path";
28
+ import { RemoteSocket, Server as SocketIoServer, Socket } from "socket.io";
29
+ import { io as sockClient } from "socket.io-client";
30
+ import { URL } from "url";
31
+ import { config } from "../config/Config";
32
+ import { conn } from "../controller/comms/Comms";
33
+ import { Inbound, Outbound } from "../controller/comms/messages/Messages";
34
+ import { Timestamp, utils } from "../controller/Constants";
35
+ import { sys } from '../controller/Equipment';
36
+ import { state } from "../controller/State";
37
+ import { logger } from "../logger/Logger";
38
+ import { HttpInterfaceBindings } from './interfaces/httpInterface';
39
+ import { InfluxInterfaceBindings } from './interfaces/influxInterface';
40
+ import { MqttInterfaceBindings } from './interfaces/mqttInterface';
41
+ import { RuleInterfaceBindings } from "./interfaces/ruleInterface";
42
+ import { ConfigRoute } from "./services/config/Config";
43
+ import { ConfigSocket } from "./services/config/ConfigSocket";
44
+ import { StateRoute } from "./services/state/State";
45
+ import { StateSocket } from "./services/state/StateSocket";
46
+ import { UtilitiesRoute } from "./services/utilities/Utilities";
47
+ import express = require('express');
48
+ import extend = require("extend");
49
+ import { setTimeout as setTimeoutSync } from 'timers';
50
+ import { setTimeout } from 'timers/promises';
51
+
52
+ // This class serves data and pages for
53
+ // external interfaces as well as an internal dashboard.
54
+ export class WebServer {
55
+ public autoBackup = false;
56
+ public lastBackup;
57
+ private _servers: ProtoServer[] = [];
58
+ private family = 'IPv4';
59
+ private _autoBackupTimer: NodeJS.Timeout;
60
+ private _httpPort: number;
61
+ constructor() { }
62
+ public async init() {
63
+ try {
64
+ let cfg = config.getSection('web');
65
+ let srv;
66
+ for (let s in cfg.servers) {
67
+ let c = cfg.servers[s];
68
+ if (typeof c.uuid === 'undefined') {
69
+ c.uuid = utils.uuid();
70
+ config.setSection(`web.servers.${s}`, c);
71
+ }
72
+ switch (s) {
73
+ case 'http':
74
+ srv = new HttpServer(s, s);
75
+ if (c.enabled !== false) this._httpPort = c.port;
76
+ break;
77
+ case 'http2':
78
+ srv = new Http2Server(s, s);
79
+ if (c.enabled !== false) this._httpPort = c.port;
80
+ break;
81
+ case 'https':
82
+ srv = new HttpsServer(s, s);
83
+ if (c.enabled !== false) this._httpPort = c.port;
84
+ break;
85
+ case 'mdns':
86
+ srv = new MdnsServer(s, s);
87
+ break;
88
+ case 'ssdp':
89
+ srv = new SsdpServer(s, s);
90
+ break;
91
+ }
92
+ if (typeof srv !== 'undefined') {
93
+ this._servers.push(srv);
94
+ await srv.init(c);
95
+ srv = undefined;
96
+ }
97
+ }
98
+ this.initInterfaces(cfg.interfaces);
99
+
100
+ } catch (err) { logger.error(`Error initializing web server ${err.message}`) }
101
+ }
102
+ public async initInterfaces(interfaces: any) {
103
+ try {
104
+ for (let s in interfaces) {
105
+ let int;
106
+ let c = interfaces[s];
107
+ if (typeof c.uuid === 'undefined') {
108
+ c.uuid = utils.uuid();
109
+ config.setSection(`web.interfaces.${s}`, c);
110
+ }
111
+ if (!c.enabled) continue;
112
+ let type = c.type || 'http';
113
+ logger.info(`Init ${type} interface: ${c.name}`);
114
+ switch (type) {
115
+ case 'rest':
116
+ case 'http':
117
+ int = new HttpInterfaceServer(c.name, type);
118
+ int.init(c);
119
+ this._servers.push(int);
120
+ break;
121
+ case 'rule':
122
+ int = new RuleInterfaceServer(c.name, type);
123
+ int.init(c);
124
+ this._servers.push(int);
125
+ break;
126
+ case 'influx':
127
+ case 'influxdb2':
128
+ int = new InfluxInterfaceServer(c.name, type);
129
+ int.init(c);
130
+ this._servers.push(int);
131
+ break;
132
+ case 'mqtt':
133
+ int = new MqttInterfaceServer(c.name, type);
134
+ int.init(c);
135
+ this._servers.push(int);
136
+ break;
137
+ case 'rem':
138
+ int = new REMInterfaceServer(c.name, type);
139
+ int.init(c);
140
+ this._servers.push(int);
141
+ break;
142
+ }
143
+ }
144
+ } catch (err) { logger.error(`Error initializing Interface servers ${err.message}`); }
145
+ }
146
+ public emitToClients(evt: string, ...data: any) {
147
+ for (let i = 0; i < this._servers.length; i++) {
148
+ this._servers[i].emitToClients(evt, ...data);
149
+ }
150
+ }
151
+ public emitToChannel(channel: string, evt: string, ...data: any) {
152
+ for (let i = 0; i < this._servers.length; i++) {
153
+ this._servers[i].emitToChannel(channel, evt, ...data);
154
+ }
155
+ }
156
+ public get mdnsServer(): MdnsServer { return this._servers.find(elem => elem instanceof MdnsServer) as MdnsServer; }
157
+ public deviceXML() { } // override in SSDP
158
+ public async stopAsync() {
159
+ try {
160
+ // We want to stop all the servers in reverse order so let's pop them out.
161
+ for (let s in this._servers) {
162
+ try {
163
+ let serv = this._servers[s];
164
+ if (typeof serv.stopAsync === 'function') {
165
+ await serv.stopAsync();
166
+ }
167
+ this._servers[s] = undefined;
168
+ } catch (err) { console.log(`Error stopping server ${s}: ${err.message}`); }
169
+ }
170
+ } catch (err) { `Error stopping servers` }
171
+ }
172
+ private getInterface() {
173
+ const networkInterfaces = os.networkInterfaces();
174
+ // RKS: We need to get the scope-local nic. This has nothing to do with IP4/6 and is not necessarily named en0 or specific to a particular nic. We are
175
+ // looking for the first IPv4 interface that has a mac address which will be the scope-local address. However, in the future we can simply use the IPv6 interface
176
+ // if that is returned on the local scope but I don't know if the node ssdp server supports it on all platforms.
177
+ let fallback; // Use this for WSL adapters.
178
+ for (let name in networkInterfaces) {
179
+ let nic = networkInterfaces[name];
180
+ for (let ndx in nic) {
181
+ let addr = nic[ndx];
182
+ // All scope-local addresses will have a mac. In a multi-nic scenario we are simply grabbing
183
+ // the first one we come across.
184
+ if (!addr.internal && addr.mac.indexOf('00:00:00:') < 0 && addr.family === this.family) {
185
+ if (!addr.mac.startsWith('00:'))
186
+ return addr;
187
+ else if (typeof fallback === 'undefined') fallback = addr;
188
+ }
189
+ }
190
+ }
191
+ return fallback;
192
+ }
193
+ public getNetworkInterfaces() {
194
+ const networkInterfaces = os.networkInterfaces();
195
+ // RKS: We need to get the scope-local nics. This has nothing to do with IP4/6 and is not necessarily named en0 or specific to a particular nic. We are
196
+ // looking for the first IPv4 interface that has a mac address which will be the scope-local address. However, in the future we can simply use the IPv6 interface
197
+ // if that is returned on the local scope but I don't know if the node ssdp server supports it on all platforms.
198
+ let ips = [];
199
+ let nics = { physical: [], virtual: [] }
200
+ for (let name in networkInterfaces) {
201
+ let nic = networkInterfaces[name];
202
+ for (let ndx in nic) {
203
+ let addr = nic[ndx];
204
+ // All scope-local addresses will have a mac. In a multi-nic scenario we are simply grabbing
205
+ // the first one we come across.
206
+ if (!addr.internal && addr.mac.indexOf('00:00:00:') < 0 && addr.family === this.family) {
207
+ if (typeof ips.find((x) => x === addr.address) === 'undefined') {
208
+ ips.push(addr.address);
209
+ if (!addr.mac.startsWith('00:'))
210
+ nics.physical.push(extend(true, { name: name }, addr));
211
+ else
212
+ nics.virtual.push(extend(true, { name: name }, addr));
213
+ }
214
+ }
215
+ }
216
+ }
217
+ return nics;
218
+ }
219
+ public ip() { return typeof this.getInterface() === 'undefined' ? '0.0.0.0' : this.getInterface().address; }
220
+ public mac() { return typeof this.getInterface() === 'undefined' ? '00:00:00:00' : this.getInterface().mac; }
221
+ public httpPort(): number { return this._httpPort }
222
+ public findServer(name: string): ProtoServer { return this._servers.find(elem => elem.name === name); }
223
+ public findServersByType(type: string) { return this._servers.filter(elem => elem.type === type); }
224
+ public findServerByGuid(uuid: string) { return this._servers.find(elem => elem.uuid === uuid); }
225
+ public removeServerByGuid(uuid: string) {
226
+ for (let i = 0; i < this._servers.length; i++) {
227
+ if (this._servers[i].uuid === uuid) this._servers.splice(i, 1);
228
+ }
229
+ }
230
+ public async updateServerInterface(obj: any): Promise<any> {
231
+ let int = config.setInterface(obj);
232
+ let srv = this.findServerByGuid(obj.uuid);
233
+ // if server is not enabled; stop & remove it from local storage
234
+ if (typeof srv !== 'undefined') {
235
+ await srv.stopAsync();
236
+ this.removeServerByGuid(obj.uuid);
237
+ }
238
+ // if it's enabled, restart it or initialize it
239
+ if (obj.enabled) {
240
+ if (typeof srv === 'undefined') {
241
+ this.initInterfaces(int);
242
+ }
243
+ else srv.init(obj);
244
+ }
245
+ return config.getInterfaceByUuid(obj.uuid);
246
+ }
247
+ public async initAutoBackup() {
248
+ try {
249
+ let bu = config.getSection('controller.backups');
250
+ this.autoBackup = false;
251
+ // These will be returned in reverse order with the newest backup first.
252
+ let files = await this.readBackupFiles();
253
+ let afiles = files.filter(elem => elem.options.automatic === true);
254
+ this.lastBackup = (afiles.length > 0) ? Date.parse(afiles[0].options.backupDate).valueOf() || 0 : 0;
255
+ // Set the last backup date.
256
+ this.autoBackup = utils.makeBool(bu.automatic);
257
+ if (this.autoBackup) {
258
+ let nextBackup = this.lastBackup + (bu.interval.days * 86400000) + (bu.interval.hours * 3600000);
259
+ logger.info(`Auto-backup initialized Last Backup: ${Timestamp.toISOLocal(new Date(this.lastBackup))} Next Backup: ${Timestamp.toISOLocal(new Date(nextBackup))}`);
260
+ }
261
+ else
262
+ logger.info(`Auto-backup initialized Last Backup: ${Timestamp.toISOLocal(new Date(this.lastBackup))}`);
263
+ // Lets wait a good 20 seconds before we auto-backup anything. Now that we are initialized let the OCP have its way with everything.
264
+ setTimeoutSync(()=>{this.checkAutoBackup();}, 20000);
265
+ }
266
+ catch (err) { logger.error(`Error initializing auto-backup: ${err.message}`); }
267
+ }
268
+ public async stopAutoBackup() {
269
+ this.autoBackup = false;
270
+ if (typeof this._autoBackupTimer !== 'undefined' || this._autoBackupTimer) clearTimeout(this._autoBackupTimer);
271
+ }
272
+ public async readBackupFiles(): Promise<BackupFile[]> {
273
+ try {
274
+ let backupDir = path.join(process.cwd(), 'backups');
275
+ let files = fs.readdirSync(backupDir);
276
+ let backups = [];
277
+ if (typeof files !== 'undefined') {
278
+ for (let i = 0; i < files.length; i++) {
279
+ let file = files[i];
280
+ if (path.extname(file) === '.zip') {
281
+ let bf = await BackupFile.fromFile(path.join(backupDir, file));
282
+ if (typeof bf !== 'undefined') backups.push(bf);
283
+ }
284
+ }
285
+ }
286
+ backups.sort((a, b) => { return Date.parse(b.options.backupDate) - Date.parse(a.options.backupDate) });
287
+ return backups;
288
+ }
289
+ catch (err) { logger.error(`Error reading backup file directory: ${err.message}`); }
290
+ }
291
+ protected async extractBackupOptions(file: string | Buffer): Promise<{ file: string, options: any }> {
292
+ try {
293
+ let opts = { file: Buffer.isBuffer(file) ? 'Buffer' : file, options: {} as any };
294
+ let jszip = require("jszip");
295
+ let buff = Buffer.isBuffer(file) ? file : fs.readFileSync(file);
296
+ await jszip.loadAsync(buff).then(async (zip) => {
297
+ await zip.file('options.json').async('string').then((data) => {
298
+ opts.options = JSON.parse(data);
299
+ if (typeof opts.options.backupDate === 'undefined' && typeof file === 'string') {
300
+ let name = path.parse(file).name;
301
+ if (name.length === 19) {
302
+ let date = name.substring(0, 10).replace(/-/g, '/');
303
+ let time = name.substring(11).replace(/-/g, ':');
304
+ let dt = Date.parse(`${date} ${time}`);
305
+ if (!isNaN(dt)) opts.options.backupDate = Timestamp.toISOLocal(new Date(dt));
306
+ }
307
+ }
308
+ });
309
+ });
310
+ return opts;
311
+ } catch (err) { logger.error(`Error extracting backup options from ${file}: ${err.message}`); }
312
+ }
313
+ public async pruneAutoBackups(keepCount: number) {
314
+ try {
315
+ // We only automatically prune backups that njsPC put there in the first place so only
316
+ // look at auto-backup files.
317
+ let files = await this.readBackupFiles();
318
+ let afiles = files.filter(elem => elem.options.automatic === true);
319
+ if (afiles.length > keepCount) {
320
+ // Prune off the oldest backups until we get to our keep count. When we read in the files
321
+ // these were sorted newest first.
322
+ while (afiles.length > keepCount) {
323
+ let afile = afiles.pop();
324
+ logger.info(`Pruning auto-backup file: ${afile.filePath}`);
325
+ try {
326
+ fs.unlinkSync(afile.filePath);
327
+ } catch (err) { logger.error(`Error deleting auto-backup file: ${afile.filePath}`); }
328
+ }
329
+ }
330
+ } catch (err) { logger.error(`Error pruning auto-backups: ${err.message}`); }
331
+ }
332
+ public async backupServer(opts: any): Promise<BackupFile> {
333
+ let ret = new BackupFile();
334
+ ret.options = extend(true, {}, opts, { version: 1.1, errors: [] });
335
+ //{ file: '', options: extend(true, {}, opts, { version: 1.0, errors: [] }) };
336
+ let jszip = require("jszip");
337
+ function pad(n) { return (n < 10 ? '0' : '') + n; }
338
+ let zip = new jszip();
339
+ let ts = new Date();
340
+ let baseDir = process.cwd();
341
+ ret.filename = ts.getFullYear() + '-' + pad(ts.getMonth() + 1) + '-' + pad(ts.getDate()) + '_' + pad(ts.getHours()) + '-' + pad(ts.getMinutes()) + '-' + pad(ts.getSeconds()) + '.zip';
342
+ ret.filePath = path.join(baseDir, 'backups', ret.filename);
343
+ if (opts.njsPC === true) {
344
+ zip.folder('njsPC');
345
+ zip.folder('njsPC/data');
346
+ // Create the backup file and copy it into it.
347
+ zip.file('njsPC/config.json', fs.readFileSync(path.join(baseDir, 'config.json')));
348
+ zip.file('njsPC/data/poolConfig.json', fs.readFileSync(path.join(baseDir, 'data', 'poolConfig.json')));
349
+ zip.file('njsPC/data/poolState.json', fs.readFileSync(path.join(baseDir, 'data', 'poolState.json')));
350
+ }
351
+ if (typeof ret.options.servers !== 'undefined' && ret.options.servers.length > 0) {
352
+ // Back up all our servers.
353
+ for (let i = 0; i < ret.options.servers.length; i++) {
354
+ let srv = ret.options.servers[i];
355
+ if (typeof srv.errors === 'undefined') srv.errors = [];
356
+ if (srv.backup === false) continue;
357
+ let server = this.findServerByGuid(srv.uuid) as REMInterfaceServer;
358
+ if (typeof server === 'undefined') {
359
+ srv.errors.push(`Could not find server ${srv.name} : ${srv.uuid}`);
360
+ srv.success = false;
361
+ }
362
+ else if (!server.isConnected) {
363
+ srv.success = false;
364
+ srv.errors.push(`Server ${srv.name} : ${srv.uuid} not connected cannot back up`);
365
+ }
366
+ else {
367
+ // Try to get the data from the server.
368
+ zip.folder(server.name);
369
+ zip.file(`${server.name}/serverConfig.json`, JSON.stringify(server.cfg));
370
+ zip.folder(`${server.name}/data`);
371
+ try {
372
+ let resp = await server.getControllerConfig();
373
+ if (typeof resp !== 'undefined') {
374
+ if (resp.status.code === 200 && typeof resp.data !== 'undefined') {
375
+ let ccfg = JSON.parse(resp.data);
376
+ zip.file(`${server.name}/data/controllerConfig.json`, JSON.stringify(ccfg));
377
+ srv.success = true;
378
+ }
379
+ else {
380
+ srv.errors.push(`Error getting controller configuration: ${resp.error.message}`);
381
+ srv.success = false;
382
+ }
383
+ }
384
+ else {
385
+ srv.success = false;
386
+ srv.errors.push(`No response from server`);
387
+ }
388
+ } catch (err) { srv.success = false; srv.errors.push(`Could not obtain server configuration`); }
389
+ }
390
+ }
391
+ }
392
+ ret.options.backupDate = Timestamp.toISOLocal(ts);
393
+ zip.file('options.json', JSON.stringify(ret.options));
394
+ await zip.generateAsync({ type: 'nodebuffer' }).then(content => {
395
+ fs.writeFileSync(ret.filePath, content);
396
+ this.lastBackup = ts.valueOf();
397
+ });
398
+ return ret;
399
+ }
400
+ public async checkAutoBackup() {
401
+ if (typeof this._autoBackupTimer !== 'undefined' || this._autoBackupTimer) clearTimeout(this._autoBackupTimer);
402
+ this._autoBackupTimer = undefined;
403
+ let bu = config.getSection('controller.backups');
404
+ if (bu.automatic === true) {
405
+ if (typeof this.lastBackup === 'undefined' ||
406
+ (this.lastBackup < new Date().valueOf() - (bu.interval.days * 86400000) - (bu.interval.hours * 3600000))) {
407
+ bu.name = 'Automatic Backup';
408
+ await this.backupServer(bu);
409
+ }
410
+ }
411
+ else this.autoBackup = false;
412
+ if (this.autoBackup) {
413
+ await this.pruneAutoBackups(bu.keepCount);
414
+ let nextBackup = this.lastBackup + (bu.interval.days * 86400000) + (bu.interval.hours * 3600000);
415
+ setTimeoutSync(async () => {
416
+ try {
417
+ await this.checkAutoBackup();
418
+ } catch (err) { logger.error(`Error checking auto-backup: ${err.message}`); }
419
+ }, Math.max(Math.min(nextBackup - new Date().valueOf(), 2147483647), 60000));
420
+ logger.info(`Last auto-backup ${Timestamp.toISOLocal(new Date(this.lastBackup))} Next auto - backup ${Timestamp.toISOLocal(new Date(nextBackup))}`);
421
+ }
422
+ }
423
+ public async validateRestore(opts): Promise<any> {
424
+ try {
425
+ let stats = { njsPC: {}, servers: [] };
426
+ // Step 1: Extract all the files from the zip file.
427
+ let rest = await RestoreFile.fromFile(opts.filePath);
428
+ // Step 2: Validate the njsPC data against the board. The return
429
+ // from here shoudld give a very detailed view of what it is about to do.
430
+ if (opts.options.njsPC === true) {
431
+ stats.njsPC = await sys.board.system.validateRestore(rest.njsPC);
432
+ }
433
+ // Step 3: For each REM server we need to validate the restore
434
+ // file.
435
+ if (typeof opts.options.servers !== 'undefined' && opts.options.servers.length > 0) {
436
+ for (let i = 0; i < opts.options.servers.length; i++) {
437
+ let s = opts.options.servers[i];
438
+ if (s.restore) {
439
+ let ctx: any = { server: { uuid: s.uuid, name: s.name, errors: [], warnings: [] } };
440
+ // Check to see if the server is on-line.
441
+ // First, try by UUID.
442
+ let srv = this.findServerByGuid(s.uuid) as REMInterfaceServer;
443
+ let cfg = rest.servers.find(elem => elem.uuid === s.uuid);
444
+ // Second, try by host
445
+ if (typeof srv === 'undefined' && parseFloat(opts.options.version) >= 1.1) {
446
+ let srvs = this.findServersByType('rem') as REMInterfaceServer[];
447
+ cfg = rest.servers.find(elem => elem.serverConfig.options.host === s.host);
448
+ for (let j = 0; j < srvs.length; j++){
449
+ if (srvs[j].cfg.options.host === cfg.serverConfig.options.host){
450
+ srv = srvs[j];
451
+ ctx.server.warnings.push(`REM Server from backup file (${srv.uuid}/${srv.cfg.options.host}) matched to current REM Server (${cfg.uuid}/${cfg.serverConfig.options.host}) by host name or IP and not UUID. UUID in current config.json for REM will be updated.`)
452
+ break;
453
+ }
454
+ }
455
+ }
456
+ stats.servers.push(ctx);
457
+ if (typeof cfg === 'undefined' || typeof cfg.controllerConfig === 'undefined') ctx.server.errors.push(`Server configuration not found in zip file`);
458
+ else if (typeof srv === 'undefined') ctx.server.errors.push(`Server ${s.name} is not enabled in njsPC cannot restore.`);
459
+ else if (!srv.isConnected) ctx.server.errors.push(`Server ${s.name} is not connected or cannot be found by UUID and cannot restore. If this is a version 1.0 file, update your current REM UUID to match the backup REM UUID.`);
460
+ else {
461
+ let resp = await srv.validateRestore(cfg.controllerConfig);
462
+ if (typeof resp !== 'undefined') {
463
+ if (resp.status.code === 200 && typeof resp.data !== 'undefined') {
464
+ let cctx = JSON.parse(resp.data);
465
+ ctx = extend(true, ctx, cctx);
466
+ }
467
+ else
468
+ ctx.server.errors.push(`Error validating controller configuration: ${resp.error.message}`);
469
+ }
470
+ else
471
+ ctx.server.errors.push(`No response from server`);
472
+ }
473
+ }
474
+
475
+ }
476
+ }
477
+
478
+ return stats;
479
+ } catch (err) { logger.error(`Error validating restore options: ${err.message}`); return Promise.reject(err);}
480
+ }
481
+ public async restoreServers(opts): Promise<any> {
482
+ let stats: { backupOptions?: any, njsPC?: RestoreResults, servers: any[] } = { servers: [] };
483
+ try {
484
+ // Step 1: Extract all the files from the zip file.
485
+ let rest = await RestoreFile.fromFile(opts.filePath);
486
+ stats.backupOptions = rest.options;
487
+ // Step 2: Validate the njsPC data against the board. The return
488
+ // from here shoudld give a very detailed view of what it is about to do.
489
+ if (opts.options.njsPC === true) {
490
+ logger.info(`Begin Restore njsPC`);
491
+ stats.njsPC = await sys.board.system.restore(rest.njsPC);
492
+ logger.info(`End Restore njsPC`);
493
+ }
494
+ // Step 3: For each REM server we need to validate the restore
495
+ // file.
496
+ if (typeof opts.options.servers !== 'undefined' && opts.options.servers.length > 0) {
497
+ for (let i = 0; i < opts.options.servers.length; i++) {
498
+ let s = opts.options.servers[i];
499
+ if (s.restore) {
500
+ // Check to see if the server is on-line.
501
+ let srv = this.findServerByGuid(s.uuid) as REMInterfaceServer;
502
+ let cfg = rest.servers.find(elem => elem.uuid === s.uuid);
503
+ let ctx: any = { server: { uuid: s.uuid, name: s.name, errors: [], warnings: [] } };
504
+ if (typeof srv === 'undefined' && parseFloat(opts.options.version) >= 1.1) {
505
+ let srvs = this.findServersByType('rem') as REMInterfaceServer[];
506
+ cfg = rest.servers.find(elem => elem.serverConfig.options.host === s.host);
507
+ for (let j = 0; j < srvs.length; j++){
508
+ if (srvs[j].cfg.options.host === cfg.serverConfig.options.host){
509
+ srv = srvs[j];
510
+ let oldSrvCfg = srv.cfg;
511
+ oldSrvCfg.enabled = false;
512
+ await this.updateServerInterface(oldSrvCfg); // unload prev server interface
513
+ srv.uuid = srv.cfg.uuid = cfg.uuid;
514
+ config.setSection('web.interfaces.rem', cfg.serverConfig);
515
+ await this.updateServerInterface(cfg.serverConfig); // reset server interface
516
+ srv = this.findServerByGuid(s.uuid) as REMInterfaceServer;
517
+ logger.info(`Restore REM: Current UUID updated to UUID of backup.`);
518
+ break;
519
+ }
520
+ }
521
+ }
522
+ stats.servers.push(ctx);
523
+ if (!srv.isConnected) await setTimeout(6000); // rem server waits to connect 5s before isConnected will be true. Server.ts#1256 = REMInterfaceServer.init(); What's a better way to do this?
524
+ if (typeof cfg === 'undefined' || typeof cfg.controllerConfig === 'undefined') ctx.server.errors.push(`Server configuration not found in zip file`);
525
+ else if (typeof srv === 'undefined') ctx.server.errors.push(`Server ${s.name} is not enabled in njsPC cannot restore.`);
526
+ else if (!srv.isConnected) ctx.server.errors.push(`Server ${s.name} is not connected cannot restore.`);
527
+ else {
528
+ let resp = await srv.validateRestore(cfg.controllerConfig);
529
+ if (typeof resp !== 'undefined') {
530
+ if (resp.status.code === 200 && typeof resp.data !== 'undefined') {
531
+ let cctx = JSON.parse(resp.data);
532
+ ctx = extend(true, ctx, cctx);
533
+ // Ok so now here we are ready to restore the data.
534
+ let r = await srv.restoreConfig(cfg.controllerConfig);
535
+
536
+ }
537
+ else
538
+ ctx.server.errors.push(`Error validating controller configuration: ${resp.error.message}`);
539
+ }
540
+ else
541
+ ctx.server.errors.push(`No response from server`);
542
+ }
543
+ }
544
+
545
+ }
546
+ }
547
+
548
+ return stats;
549
+ } catch (err) { logger.error(`Error validating restore options: ${err.message}`); return Promise.reject(err); }
550
+ finally {
551
+ try {
552
+ let baseDir = process.cwd();
553
+ let ts = new Date();
554
+ function pad(n) { return (n < 10 ? '0' : '') + n; }
555
+ let filename = 'restoreLog(' + ts.getFullYear() + '-' + pad(ts.getMonth() + 1) + '-' + pad(ts.getDate()) + '_' + pad(ts.getHours()) + '-' + pad(ts.getMinutes()) + '-' + pad(ts.getSeconds()) + ').log';
556
+ let filePath = path.join(baseDir, 'logs', filename);
557
+ fs.writeFileSync(filePath, JSON.stringify(stats, undefined, 3));
558
+ } catch (err) { logger.error(`Error writing restore log ${err.message}`); }
559
+ }
560
+ }
561
+ }
562
+ class ProtoServer {
563
+ constructor(name: string, type: string) { this.name = name; this.type = type; }
564
+ public name: string;
565
+ public type: string;
566
+ public uuid: string;
567
+ public remoteConnectionId: string;
568
+ // base class for all servers.
569
+ public isRunning: boolean = false;
570
+ public get isConnected() { return this.isRunning; }
571
+ public emitToClients(evt: string, ...data: any) { }
572
+ public emitToChannel(channel: string, evt: string, ...data: any) { }
573
+ public async init(obj: any) { };
574
+ public async stopAsync() { }
575
+ protected _dev: boolean = process.env.NODE_ENV !== 'production';
576
+ // todo: how do we know if the client is using IPv4/IPv6?
577
+ }
578
+ export class Http2Server extends ProtoServer {
579
+ public server: http2.Http2Server;
580
+ public app: Express.Application;
581
+ public async init(cfg) {
582
+ this.uuid = cfg.uuid;
583
+ if (cfg.enabled) {
584
+ this.app = express();
585
+ // TODO: create a key and cert at some time but for now don't fart with it.
586
+ }
587
+ }
588
+ }
589
+ interface ClientToServerEvents {
590
+ noArg: () => void;
591
+ basicEmit: (a: number, b: string, c: number[]) => void;
592
+ }
593
+
594
+ interface ServerToClientEvents {
595
+ withAck: (d: string, cb: (e: number) => void) => void;
596
+ [event: string]: (...args: any[]) => void;
597
+ }
598
+ export class HttpServer extends ProtoServer {
599
+ // Http protocol
600
+ private static dateTestISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/;
601
+ private static dateTextAjax = /^\/Date\((d|-|.*)\)[\/|\\]$/;
602
+
603
+ public app: express.Application;
604
+ public server: http.Server;
605
+ public sockServer: SocketIoServer<ClientToServerEvents, ServerToClientEvents>;
606
+ private _sockets: RemoteSocket<ServerToClientEvents, any>[] = [];
607
+ public emitToClients(evt: string, ...data: any) {
608
+ if (this.isRunning) {
609
+ this.sockServer.emit(evt, ...data);
610
+ }
611
+ }
612
+ public emitToChannel(channel: string, evt: string, ...data: any) {
613
+ //console.log(`Emitting to channel ${channel} - ${evt}`)
614
+ if (this.isRunning) {
615
+ this.sockServer.to(channel).emit(evt, ...data);
616
+ }
617
+ }
618
+ public get isConnected() { return typeof this.sockServer !== 'undefined' && this._sockets.length > 0; }
619
+ protected initSockets() {
620
+ let options = {
621
+ allowEIO3: true,
622
+ cors: {
623
+ origin: true,
624
+ methods: ["GET", "POST"],
625
+ credentials: true
626
+ }
627
+ }
628
+ this.sockServer = new SocketIoServer(this.server, options);
629
+ this.sockServer.on("connection", (sock: Socket) => {
630
+ logger.info(`New socket client connected ${sock.id} -- ${sock.client.conn.remoteAddress}`);
631
+ this.socketHandler(sock);
632
+ sock.emit('controller', state.controllerState);
633
+ sock.conn.emit('controller', state.controllerState); // do we need both of these?
634
+ //this.sockServer.origins('*:*');
635
+ sock.on('connect_error', (err) => {
636
+ logger.error('Socket server error %s', err.message);
637
+ });
638
+ sock.on('reconnect_failed', (err) => {
639
+ logger.error('Failed to reconnect with socket %s', err.message);
640
+ });
641
+ });
642
+ this.app.use('/socket.io-client', express.static(path.join(process.cwd(), '/node_modules/socket.io-client/dist/'), { maxAge: '60d' }));
643
+ }
644
+
645
+ private replayInboundMessage(mdata: any) {
646
+ try {
647
+ let msg: Inbound = new Inbound();
648
+ msg.direction = mdata.direction;
649
+ msg.header = mdata.header;
650
+ msg.payload = mdata.payload;
651
+ msg.preamble = mdata.preamble;
652
+ msg.protocol = mdata.protocol;
653
+ msg.term = mdata.term;
654
+ if (msg.isValid) msg.process();
655
+ }
656
+ catch (err) {
657
+ logger.error(`Error replaying packet: ${err.message}`);
658
+ }
659
+ }
660
+
661
+ private socketHandler(sock: Socket) {
662
+ let self = this;
663
+ // this._sockets.push(sock);
664
+ setTimeoutSync(async () => {
665
+ // refresh socket list with every new socket
666
+ self._sockets = await self.sockServer.fetchSockets();
667
+ }, 100)
668
+
669
+ sock.on('error', (err) => {
670
+ logger.error('Error with socket: %s', err);
671
+ });
672
+ sock.on('close', async (id) => {
673
+ logger.info('Socket diconnecting %s', id);
674
+ self._sockets = await self.sockServer.fetchSockets();
675
+ });
676
+ sock.on('echo', (msg) => { sock.emit('echo', msg); });
677
+ sock.on('sendOutboundMessage', (mdata) => {
678
+ let msg: Outbound = Outbound.create({});
679
+ Object.assign(msg, mdata);
680
+ msg.calcChecksum();
681
+ logger.silly(`sendOutboundMessage ${msg.toLog()}`);
682
+ conn.queueSendMessage(msg);
683
+ });
684
+ sock.on('sendInboundMessage', (mdata) => {
685
+ this.replayInboundMessage(mdata);
686
+ });
687
+ sock.on('rawbytes', (data:any)=>{
688
+ let port = conn.findPortById(0);
689
+ port.pushIn(Buffer.from(data));
690
+ })
691
+ sock.on('sendLogMessages', function (sendMessages: boolean) {
692
+ console.log(`sendLogMessages set to ${sendMessages}`);
693
+ if (!sendMessages) sock.leave('msgLogger');
694
+ else sock.join('msgLogger');
695
+ });
696
+ sock.on('sendRS485PortStats', function (sendPortStats: boolean) {
697
+ console.log(`sendRS485PortStats set to ${sendPortStats}`);
698
+ if (!sendPortStats) sock.leave('rs485PortStats');
699
+ else sock.join('rs485PortStats');
700
+ });
701
+ sock.on('sendScreenlogicStats', function (sendScreenlogicStats: boolean) {
702
+ console.log(`sendScreenlogicStats set to ${sendScreenlogicStats}`);
703
+ if (!sendScreenlogicStats) sock.leave('screenlogicStats');
704
+ else sock.join('screenlogicStats');
705
+ });
706
+ StateSocket.initSockets(sock);
707
+ ConfigSocket.initSockets(sock);
708
+ }
709
+ public async init(cfg) {
710
+ try {
711
+ this.uuid = cfg.uuid;
712
+ if (cfg.enabled) {
713
+ this.app = express();
714
+
715
+ //this.app.use();
716
+ this.server = http.createServer(this.app);
717
+ if (cfg.httpsRedirect) {
718
+ var cfgHttps = config.getSection('web').server.https;
719
+ this.app.get('*', (res: express.Response, req: express.Request) => {
720
+ let host = res.get('host');
721
+ // Only append a port if there is one declared. This will be the case for urls that have have an implicit port.
722
+ host = host.replace(/:\d+$/, typeof cfgHttps.port !== 'undefined' ? ':' + cfgHttps.port : '');
723
+ return res.redirect('https://' + host + req.url);
724
+ });
725
+ }
726
+ this.app.use(express.json(
727
+ {
728
+ reviver: (key, value) => {
729
+ if (typeof value === 'string') {
730
+ let d = HttpServer.dateTestISO.exec(value);
731
+ // By parsing the date and then creating a new date from that we will get
732
+ // the date in the proper timezone.
733
+ if (d) return new Date(Date.parse(value));
734
+ d = HttpServer.dateTextAjax.exec(value);
735
+ if (d) {
736
+ // Not sure we will be seeing ajax dates but this is
737
+ // something that we may see from external sources.
738
+ let a = d[1].split(/[-+,.]/);
739
+ return new Date(a[0] ? +a[0] : 0 - +a[1]);
740
+ }
741
+ }
742
+ return value;
743
+ }
744
+ })
745
+ );
746
+ this.app.use((req, res, next) => {
747
+ res.header('Access-Control-Allow-Origin', '*');
748
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, api_key, Authorization'); // api_key and Authorization needed for Swagger editor live API document calls
749
+ res.header('Access-Control-Allow-Methods', 'OPTIONS, GET, POST, PUT, DELETE');
750
+ if ('OPTIONS' === req.method) { res.sendStatus(200); }
751
+ else {
752
+ if (req.url !== '/upnp.xml') {
753
+ logger.info(`[${new Date().toLocaleTimeString()}] ${req.ip} ${req.method} ${req.url} ${typeof req.body === 'undefined' ? '' : JSON.stringify(req.body)}`);
754
+ logger.logAPI(`{"dir":"in","proto":"api","requestor":"${req.ip}","method":"${req.method}","path":"${req.url}",${typeof req.body === 'undefined' ? '' : `"body":${JSON.stringify(req.body)},`}"ts":"${Timestamp.toISOLocal(new Date())}"}${os.EOL}`);
755
+ }
756
+ next();
757
+ }
758
+ });
759
+
760
+ // Minimal inbound replay endpoint for local testing/tools.
761
+ // Expected JSON shape: { direction, protocol, preamble, header, payload, term }
762
+ this.app.post('/replay/inbound', (req, res) => {
763
+ this.replayInboundMessage(req.body);
764
+ res.status(200).send('Inbound message replayed.');
765
+ });
766
+
767
+
768
+ // Put in a custom replacer so that we can send error messages to the client. If we don't do this the base properties of Error
769
+ // are omitted from the output.
770
+ this.app.set('json replacer', (key, value) => {
771
+ if (value instanceof Error) {
772
+ var err = {};
773
+ Object.getOwnPropertyNames(value).forEach((prop) => {
774
+ if (prop === "level") err[prop] = value[prop].replace(/\x1b\[\d{2}m/g, '') // remove color from level
775
+ else err[prop] = value[prop];
776
+ });
777
+ return err;
778
+ }
779
+ return value;
780
+ });
781
+
782
+ ConfigRoute.initRoutes(this.app);
783
+ StateRoute.initRoutes(this.app);
784
+ UtilitiesRoute.initRoutes(this.app);
785
+
786
+ // The socket initialization needs to occur before we start listening. If we don't then
787
+ // the headers from the server will not be picked up.
788
+ this.initSockets();
789
+ this.app.use((error, req, res, next) => {
790
+ logger.error(error);
791
+ if (!res.headersSent) {
792
+ let httpCode = error.httpCode || 500;
793
+ res.status(httpCode).send(error);
794
+ }
795
+ });
796
+
797
+ // start our server on port
798
+ this.server.listen(cfg.port, cfg.ip, function () {
799
+ logger.info('Server is now listening on %s:%s - %s:%s', cfg.ip, cfg.port, webApp.ip(), webApp.httpPort());
800
+ });
801
+ this.isRunning = true;
802
+ }
803
+ } catch (err) { logger.error(`Error initializing server ${err.message}`); }
804
+ }
805
+ public addListenerOnce(event: any, f: (data: any) => void) {
806
+ // for (let i = 0; i < this._sockets.length; i++) {
807
+ // this._sockets[i].once(event, f);
808
+ // }
809
+ this.sockServer.once(event, f);
810
+ }
811
+ }
812
+ export class HttpsServer extends HttpServer {
813
+ declare server: https.Server;
814
+
815
+ public async init(cfg) {
816
+ // const auth = require('http-auth');
817
+ this.uuid = cfg.uuid;
818
+ if (!cfg.enabled) return;
819
+ try {
820
+ this.app = express();
821
+ // Enable Authentication (if configured)
822
+ /* if (cfg.authentication === 'basic') {
823
+ let basic = auth.basic({
824
+ realm: "nodejs-poolController.",
825
+ file: path.join(process.cwd(), cfg.authFile)
826
+ })
827
+ this.app.use(function(req, res, next) {
828
+ (auth.connect(basic))(req, res, next);
829
+ });
830
+ } */
831
+ if (cfg.sslKeyFile === '' || cfg.sslCertFile === '' || !fs.existsSync(path.join(process.cwd(), cfg.sslKeyFile)) || !fs.existsSync(path.join(process.cwd(), cfg.sslCertFile))) {
832
+ logger.warn(`HTTPS not enabled because key or crt file is missing.`);
833
+ return;
834
+ }
835
+ let opts = {
836
+ key: fs.readFileSync(path.join(process.cwd(), cfg.sslKeyFile), 'utf8'),
837
+ cert: fs.readFileSync(path.join(process.cwd(), cfg.sslCertFile), 'utf8'),
838
+ requestCert: false,
839
+ rejectUnauthorized: false
840
+ }
841
+ this.server = https.createServer(opts, this.app);
842
+
843
+ this.app.use(express.json());
844
+ this.app.use((req, res, next) => {
845
+ res.header('Access-Control-Allow-Origin', '*');
846
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, api_key, Authorization'); // api_key and Authorization needed for Swagger editor live API document calls
847
+ res.header('Access-Control-Allow-Methods', 'OPTIONS, GET, POST, PUT, DELETE');
848
+ if ('OPTIONS' === req.method) { res.sendStatus(200); }
849
+ else {
850
+ if (!req.url.startsWith('/upnp.xml')) {
851
+ logger.info(`[${new Date().toLocaleString()}] ${req.ip} ${req.method} ${req.url} ${typeof req.body === 'undefined' ? '' : JSON.stringify(req.body)}`);
852
+ logger.logAPI(`{"dir":"in","proto":"api","requestor":"${req.ip}","method":"${req.method}","path":"${req.url}",${typeof req.body === 'undefined' ? '' : `"body":${JSON.stringify(req.body)},`}"ts":"${Timestamp.toISOLocal(new Date())}"}${os.EOL}`);
853
+ }
854
+ next();
855
+ }
856
+ });
857
+
858
+
859
+ // Put in a custom replacer so that we can send error messages to the client. If we don't do this the base properties of Error
860
+ // are omitted from the output.
861
+ this.app.set('json replacer', (key, value) => {
862
+ if (value instanceof Error) {
863
+ var err = {};
864
+ Object.getOwnPropertyNames(value).forEach((prop) => {
865
+ if (prop === "level") err[prop] = value[prop].replace(/\x1b\[\d{2}m/g, '') // remove color from level
866
+ else err[prop] = value[prop];
867
+ });
868
+ return err;
869
+ }
870
+ return value;
871
+ });
872
+
873
+ ConfigRoute.initRoutes(this.app);
874
+ StateRoute.initRoutes(this.app);
875
+ UtilitiesRoute.initRoutes(this.app);
876
+
877
+ // The socket initialization needs to occur before we start listening. If we don't then
878
+ // the headers from the server will not be picked up.
879
+ this.initSockets();
880
+ this.app.use((error, req, res, next) => {
881
+ logger.error(error);
882
+ if (!res.headersSent) {
883
+ let httpCode = error.httpCode || 500;
884
+ res.status(httpCode).send(error);
885
+ }
886
+ });
887
+
888
+ // start our server on port
889
+ this.server.listen(cfg.port, cfg.ip, function () {
890
+ logger.info('Server is now listening on %s:%s', cfg.ip, cfg.port);
891
+ });
892
+ this.isRunning = true;
893
+ }
894
+ catch (err) {
895
+ logger.error(`Error starting up https server: ${err}`)
896
+ }
897
+ }
898
+ }
899
+ export class SsdpServer extends ProtoServer {
900
+ // Simple service discovery protocol
901
+ public server: ssdp.Server; //node-ssdp;
902
+ public deviceUUID: string;
903
+ public upnpPath: string;
904
+ public modelName: string;
905
+ public modelNumber: string;
906
+ public serialNumber: string;
907
+ public deviceType = 'urn:schemas-tagyoureit-org:device:PoolController:1';
908
+ public async init(cfg) {
909
+ this.uuid = cfg.uuid;
910
+ if (cfg.enabled) {
911
+ let self = this;
912
+ logger.info('Starting up SSDP server');
913
+ let ver = JSON.parse(fs.readFileSync(path.posix.join(process.cwd(), '/package.json'), 'utf8')).version || '0.0.0';
914
+ this.deviceUUID = 'uuid:806f52f4-1f35-4e33-9299-' + webApp.mac().replace(/:/g, '');
915
+ this.serialNumber = webApp.mac();
916
+ this.modelName = `njsPC v${ver}`;
917
+ this.modelNumber = `njsPC${ver.replace(/\./g, '-')}`;
918
+ // todo: should probably check if http/https is enabled at this point
919
+ //let port = config.getSection('web').servers.http.port || 7777;
920
+ this.upnpPath = 'http://' + webApp.ip() + ':' + webApp.httpPort() + '/upnp.xml';
921
+ let nics = webApp.getNetworkInterfaces();
922
+ let SSDP = ssdp.Server;
923
+ if (nics.physical.length + nics.virtual.length > 1) {
924
+ // If there are multiple nics (docker...etc) then
925
+ // this will bind on all of them.
926
+ this.server = new SSDP({
927
+ //customLogger: (...args) => console.log.apply(null, args),
928
+ logLevel: 'INFO',
929
+ udn: this.deviceUUID,
930
+ location: {
931
+ protocol: 'http://',
932
+ port: webApp.httpPort(),
933
+ path: '/upnp.xml'
934
+ },
935
+ explicitSocketBind: true,
936
+ sourcePort: 1900
937
+ });
938
+ }
939
+ else {
940
+ this.server = new SSDP({
941
+ //customLogger: (...args) => console.log.apply(null, args),
942
+ logLevel: 'INFO',
943
+ udn: this.deviceUUID,
944
+ location: this.upnpPath,
945
+ sourcePort: 1900
946
+ });
947
+
948
+
949
+ }
950
+ this.server.addUSN('upnp:rootdevice'); // This line will make the server show up in windows.
951
+ this.server.addUSN(this.deviceType);
952
+ // start the server
953
+ this.server.start()
954
+ .then(function () {
955
+ logger.silly('SSDP/UPnP Server started.');
956
+ self.isRunning = true;
957
+ });
958
+
959
+ this.server.on('error', function (e) {
960
+ logger.error('error from SSDP:', e);
961
+ });
962
+ }
963
+ }
964
+ public deviceXML(): string {
965
+ let ver = sys.appVersion.split('.');
966
+ let friendlyName = 'njsPC: unknown model';
967
+ if (typeof sys !== 'undefined' && typeof sys.equipment !== 'undefined' && typeof sys.equipment.model !== 'undefined') friendlyName = `${sys.equipment.model}`
968
+ let XML = `<?xml version="1.0"?>
969
+ <root xmlns="urn:schemas-upnp-org:device-1-0">
970
+ <specVersion>
971
+ <major>1</major>
972
+ <minor>0</minor>
973
+ </specVersion>
974
+ <device>
975
+ <deviceType>${this.deviceType}</deviceType>
976
+ <friendlyName>${friendlyName}</friendlyName>
977
+ <manufacturer>tagyoureit</manufacturer>
978
+ <manufacturerURL>https://github.com/tagyoureit/nodejs-poolController</manufacturerURL>
979
+ <presentationURL>http://${webApp.ip()}:${webApp.httpPort()}/state/all</presentationURL>
980
+ <appVersion>
981
+ <major>${ver[0] || 1}</major>
982
+ <minor>${ver[1] || 0}</minor>
983
+ <patch>${ver[2] || 0}</patch>
984
+ </appVersion>
985
+ <modelName>${this.modelName}</modelName>
986
+ <modelNumber>${this.modelNumber}</modelNumber>
987
+ <modelDescription>An application to control pool equipment.</modelDescription>
988
+ <serialNumber>${this.serialNumber}</serialNumber>
989
+ <UDN>${this.deviceUUID}::${this.deviceType}</UDN>
990
+ <serviceList></serviceList>
991
+ <deviceList></deviceList>
992
+ </device>
993
+ </root>`;
994
+ //console.log(XML.match(/<device>[\s|\S]+<appVersion>[\s|\S]+<major>(\d+)<\/major>/)[1]);
995
+ //console.log(XML.match(/<device>[\s|\S]+<appVersion>[\s|\S]+<minor>(\d+)<\/minor>/)[1]);
996
+ //console.log(XML.match(/<device>[\s|\S]+<appVersion>[\s|\S]+<patch>(\d+)<\/patch>/)[1]);
997
+ return XML;
998
+ }
999
+ public async stopAsync() {
1000
+ try {
1001
+ if (typeof this.server !== 'undefined') {
1002
+ this.server.stop();
1003
+ logger.info(`Stopped SSDP server: ${this.name}`);
1004
+ }
1005
+ } catch (err) { logger.error(`Error stopping SSDP server ${err.message}`); }
1006
+ }
1007
+ }
1008
+ export class MdnsServer extends ProtoServer {
1009
+ // Multi-cast DNS server
1010
+ public server;
1011
+ public mdnsEmitter = new EventEmitter();
1012
+ private queries = [];
1013
+ public async init(cfg) {
1014
+ this.uuid = cfg.uuid;
1015
+ if (cfg.enabled) {
1016
+ logger.info('Starting up MDNS server');
1017
+ this.server = multicastdns({ loopback: true });
1018
+ var self = this;
1019
+
1020
+ // look for responses to queries we send
1021
+ // todo: need timeout on queries to remove them in case a bad query is sent
1022
+ this.server.on('response', function (responses) {
1023
+ self.queries.forEach(function (query) {
1024
+ logger.silly(`looking to match on ${query.name}`);
1025
+ responses.answers.forEach(answer => {
1026
+ if (answer.name === query.name) {
1027
+ logger.info(`MDNS: found response: ${answer.name} at ${answer.data}`);
1028
+ // need to send response back to client here
1029
+ self.mdnsEmitter.emit('mdnsResponse', answer);
1030
+ // remove query from list
1031
+ self.queries = self.queries.filter((value, index, arr) => {
1032
+ if (value.name !== query.name) return arr;
1033
+ });
1034
+ }
1035
+ });
1036
+
1037
+ });
1038
+ });
1039
+
1040
+ // respond to incoming MDNS queries
1041
+ this.server.on('query', function (query) {
1042
+ query.questions.forEach(question => {
1043
+ if (question.name === '_poolcontroller._tcp.local') {
1044
+ logger.info(`received mdns query for nodejs_poolController`);
1045
+ self.server.respond({
1046
+ answers: [
1047
+ {
1048
+ name: '_poolcontroller._tcp.local',
1049
+ type: 'A',
1050
+ ttl: 300,
1051
+ data: webApp.ip()
1052
+ },
1053
+ {
1054
+ name: '_poolcontroller._tcp.local',
1055
+ type: 'SRV',
1056
+ data: {
1057
+ port: webApp.httpPort().toString(),
1058
+ target: '_poolcontroller._tcp.local',
1059
+ weight: 0,
1060
+ priority: 10
1061
+ }
1062
+ },
1063
+ {
1064
+ name: 'model',
1065
+ type: 'TXT',
1066
+ data: 'njsPC'
1067
+ },
1068
+ ]
1069
+ });
1070
+ }
1071
+ });
1072
+ });
1073
+
1074
+ this.isRunning = true;
1075
+ }
1076
+ }
1077
+ public queryMdns(query) {
1078
+ // sample query
1079
+ // queryMdns({name: '_poolcontroller._tcp.local', type: 'A'});
1080
+ if (this.queries.indexOf(query) === -1) {
1081
+ this.queries.push(query);
1082
+ }
1083
+ this.server.query({ questions: [query] });
1084
+ }
1085
+ public async stopAsync() {
1086
+ try {
1087
+ if (typeof this.server !== 'undefined')
1088
+ await new Promise<void>((resolve, reject) => {
1089
+ this.server.destroy((err) => {
1090
+ if (err) reject(err);
1091
+ else resolve();
1092
+ });
1093
+ });
1094
+ logger.info(`Shut down MDNS Server ${this.name}`);
1095
+ } catch (err) { logger.error(`Error shutting down MDNS Server ${this.name}: ${err.message}`); }
1096
+ }
1097
+ }
1098
+ export class HttpInterfaceServer extends ProtoServer {
1099
+ public bindingsPath: string;
1100
+ public bindings: HttpInterfaceBindings;
1101
+ private _fileTime: Date = new Date(0);
1102
+ private _isLoading: boolean = false;
1103
+ public async init(cfg) {
1104
+ this.uuid = cfg.uuid;
1105
+ if (cfg.enabled) {
1106
+ if (cfg.fileName && this.initBindings(cfg)) this.isRunning = true;
1107
+ }
1108
+ }
1109
+ public loadBindings(cfg): boolean {
1110
+ this._isLoading = true;
1111
+ if (fs.existsSync(this.bindingsPath)) {
1112
+ try {
1113
+ let bindings = JSON.parse(fs.readFileSync(this.bindingsPath, 'utf8'));
1114
+ let ext = extend(true, {}, typeof cfg.context !== 'undefined' ? cfg.context.options : {}, bindings);
1115
+ this.bindings = Object.assign<HttpInterfaceBindings, any>(new HttpInterfaceBindings(cfg), ext);
1116
+ this.isRunning = true;
1117
+ this._isLoading = false;
1118
+ const stats = fs.statSync(this.bindingsPath);
1119
+ this._fileTime = stats.mtime;
1120
+ return true;
1121
+ }
1122
+ catch (err) {
1123
+ logger.error(`Error reading interface bindings file: ${this.bindingsPath}. ${err}`);
1124
+ this.isRunning = false;
1125
+ this._isLoading = false;
1126
+ }
1127
+ }
1128
+ return false;
1129
+ }
1130
+ public initBindings(cfg): boolean {
1131
+ let self = this;
1132
+ try {
1133
+ this.bindingsPath = path.posix.join(process.cwd(), "/web/bindings") + '/' + cfg.fileName;
1134
+ let fileTime = new Date(0).valueOf();
1135
+ fs.watch(this.bindingsPath, (event, fileName) => {
1136
+ if (fileName && event === 'change') {
1137
+ if (self._isLoading) return; // Need a debounce here. We will use a semaphore to cause it not to load more than once.
1138
+ const stats = fs.statSync(self.bindingsPath);
1139
+ if (stats.mtime.valueOf() === self._fileTime.valueOf()) return;
1140
+ self.loadBindings(cfg);
1141
+ logger.info(`Reloading ${cfg.name || ''} interface config: ${fileName}`);
1142
+ }
1143
+ });
1144
+ this.loadBindings(cfg);
1145
+ if (this.bindings.context.mdnsDiscovery) {
1146
+ let srv = webApp.mdnsServer;
1147
+ let qry = typeof this.bindings.context.mdnsDiscovery === 'string' ? { name: this.bindings.context.mdnsDiscovery, type: 'A' } : this.bindings.context.mdnsDiscovery;
1148
+ if (typeof srv !== 'undefined') {
1149
+ srv.queryMdns(qry);
1150
+ srv.mdnsEmitter.on('mdnsResponse', (response) => {
1151
+ let url: URL;
1152
+ url = new URL(response);
1153
+ this.bindings.context.options.host = url.host;
1154
+ this.bindings.context.options.port = url.port || 80;
1155
+ });
1156
+ }
1157
+ }
1158
+ return true;
1159
+ }
1160
+ catch (err) {
1161
+ logger.error(`Error initializing interface bindings: ${err}`);
1162
+ }
1163
+ return false;
1164
+ }
1165
+ public emitToClients(evt: string, ...data: any) {
1166
+ if (this.isRunning) {
1167
+ // Take the bindings and map them to the appropriate http GET, PUT, DELETE, and POST.
1168
+ this.bindings.bindEvent(evt, ...data);
1169
+ }
1170
+ }
1171
+ public async stopAsync() {
1172
+ try {
1173
+ logger.info(`${this.name} Interface Server Shut down`);
1174
+ }
1175
+ catch (err) { }
1176
+ }
1177
+ }
1178
+ export class RuleInterfaceServer extends ProtoServer {
1179
+ public bindingsPath: string;
1180
+ public bindings: RuleInterfaceBindings;
1181
+ private _fileTime: Date = new Date(0);
1182
+ private _isLoading: boolean = false;
1183
+ public async init(cfg) {
1184
+ this.uuid = cfg.uuid;
1185
+ if (cfg.enabled) {
1186
+ if (cfg.fileName && this.initBindings(cfg)) this.isRunning = true;
1187
+ }
1188
+ }
1189
+ public loadBindings(cfg): boolean {
1190
+ this._isLoading = true;
1191
+ if (fs.existsSync(this.bindingsPath)) {
1192
+ try {
1193
+ let bindings = JSON.parse(fs.readFileSync(this.bindingsPath, 'utf8'));
1194
+ let ext = extend(true, {}, typeof cfg.context !== 'undefined' ? cfg.context.options : {}, bindings);
1195
+ this.bindings = Object.assign<RuleInterfaceBindings, any>(new RuleInterfaceBindings(cfg), ext);
1196
+ this.isRunning = true;
1197
+ this._isLoading = false;
1198
+ const stats = fs.statSync(this.bindingsPath);
1199
+ this._fileTime = stats.mtime;
1200
+ return true;
1201
+ }
1202
+ catch (err) {
1203
+ logger.error(`Error reading interface bindings file: ${this.bindingsPath}. ${err}`);
1204
+ this.isRunning = false;
1205
+ this._isLoading = false;
1206
+ }
1207
+ }
1208
+ return false;
1209
+ }
1210
+ public initBindings(cfg): boolean {
1211
+ let self = this;
1212
+ try {
1213
+ this.bindingsPath = path.posix.join(process.cwd(), "/web/bindings") + '/' + cfg.fileName;
1214
+ let fileTime = new Date(0).valueOf();
1215
+ fs.watch(this.bindingsPath, (event, fileName) => {
1216
+ if (fileName && event === 'change') {
1217
+ if (self._isLoading) return; // Need a debounce here. We will use a semaphore to cause it not to load more than once.
1218
+ const stats = fs.statSync(self.bindingsPath);
1219
+ if (stats.mtime.valueOf() === self._fileTime.valueOf()) return;
1220
+ self.loadBindings(cfg);
1221
+ logger.info(`Reloading ${cfg.name || ''} interface config: ${fileName}`);
1222
+ }
1223
+ });
1224
+ this.loadBindings(cfg);
1225
+ if (this.bindings.context.mdnsDiscovery) {
1226
+ let srv = webApp.mdnsServer;
1227
+ let qry = typeof this.bindings.context.mdnsDiscovery === 'string' ? { name: this.bindings.context.mdnsDiscovery, type: 'A' } : this.bindings.context.mdnsDiscovery;
1228
+ if (typeof srv !== 'undefined') {
1229
+ srv.queryMdns(qry);
1230
+ srv.mdnsEmitter.on('mdnsResponse', (response) => {
1231
+ let url: URL;
1232
+ url = new URL(response);
1233
+ this.bindings.context.options.host = url.host;
1234
+ this.bindings.context.options.port = url.port || 80;
1235
+ });
1236
+ }
1237
+ }
1238
+ return true;
1239
+ }
1240
+ catch (err) {
1241
+ logger.error(`Error initializing interface bindings: ${err}`);
1242
+ }
1243
+ return false;
1244
+ }
1245
+ public emitToClients(evt: string, ...data: any) {
1246
+ if (this.isRunning) {
1247
+ // Take the bindings and map them to the appropriate http GET, PUT, DELETE, and POST.
1248
+ this.bindings.bindEvent(evt, ...data);
1249
+ }
1250
+ }
1251
+ public async stopAsync() {
1252
+ try {
1253
+ logger.info(`${this.name} Interface Server Shut down`);
1254
+ }
1255
+ catch (err) { }
1256
+ }
1257
+ }
1258
+
1259
+ export class InfluxInterfaceServer extends ProtoServer {
1260
+ public bindingsPath: string;
1261
+ public bindings: InfluxInterfaceBindings;
1262
+ private _fileTime: Date = new Date(0);
1263
+ private _isLoading: boolean = false;
1264
+ public async init(cfg) {
1265
+ this.uuid = cfg.uuid;
1266
+ if (cfg.enabled) {
1267
+ if (cfg.fileName && this.initBindings(cfg)) this.isRunning = true;
1268
+ }
1269
+ }
1270
+ public loadBindings(cfg): boolean {
1271
+ this._isLoading = true;
1272
+ if (fs.existsSync(this.bindingsPath)) {
1273
+ try {
1274
+ let bindings = JSON.parse(fs.readFileSync(this.bindingsPath, 'utf8'));
1275
+ let ext = extend(true, {}, typeof cfg.context !== 'undefined' ? cfg.context.options : {}, bindings);
1276
+ this.bindings = Object.assign<InfluxInterfaceBindings, any>(new InfluxInterfaceBindings(cfg), ext);
1277
+ this.isRunning = true;
1278
+ this._isLoading = false;
1279
+ const stats = fs.statSync(this.bindingsPath);
1280
+ this._fileTime = stats.mtime;
1281
+ return true;
1282
+ }
1283
+ catch (err) {
1284
+ logger.error(`Error reading interface bindings file: ${this.bindingsPath}. ${err}`);
1285
+ this.isRunning = false;
1286
+ this._isLoading = false;
1287
+ }
1288
+ }
1289
+ return false;
1290
+ }
1291
+ public initBindings(cfg): boolean {
1292
+ let self = this;
1293
+ try {
1294
+ this.bindingsPath = path.posix.join(process.cwd(), "/web/bindings") + '/' + cfg.fileName;
1295
+ fs.watch(this.bindingsPath, (event, fileName) => {
1296
+ if (fileName && event === 'change') {
1297
+ if (self._isLoading) return; // Need a debounce here. We will use a semaphore to cause it not to load more than once.
1298
+ const stats = fs.statSync(self.bindingsPath);
1299
+ if (stats.mtime.valueOf() === self._fileTime.valueOf()) return;
1300
+ self.loadBindings(cfg);
1301
+ logger.info(`Reloading ${cfg.name || ''} interface config: ${fileName}`);
1302
+ }
1303
+ });
1304
+ this.loadBindings(cfg);
1305
+ return true;
1306
+ }
1307
+ catch (err) {
1308
+ logger.error(`Error initializing interface bindings: ${err}`);
1309
+ }
1310
+ return false;
1311
+ }
1312
+ public emitToClients(evt: string, ...data: any) {
1313
+ if (this.isRunning) {
1314
+ // Take the bindings and map them to the appropriate http GET, PUT, DELETE, and POST.
1315
+ this.bindings.bindEvent(evt, ...data);
1316
+ }
1317
+ }
1318
+ }
1319
+ export class MqttInterfaceServer extends ProtoServer {
1320
+ public bindingsPath: string;
1321
+ public bindings: MqttInterfaceBindings;
1322
+ private _fileTime: Date = new Date(0);
1323
+ private _isLoading: boolean = false;
1324
+ public get isConnected() { return this.isRunning && this.bindings.events.length > 0; }
1325
+ public async init(cfg) {
1326
+ this.uuid = cfg.uuid;
1327
+ if (cfg.enabled) {
1328
+ if (cfg.fileName && this.initBindings(cfg)) this.isRunning = true;
1329
+ }
1330
+ }
1331
+ public loadBindings(cfg): boolean {
1332
+ this._isLoading = true;
1333
+ if (fs.existsSync(this.bindingsPath)) {
1334
+ try {
1335
+ let bindings = JSON.parse(fs.readFileSync(this.bindingsPath, 'utf8'));
1336
+ let ext = extend(true, {}, typeof cfg.context !== 'undefined' ? cfg.context.options : {}, bindings);
1337
+ if (this.bindings && this.bindings.client) {
1338
+ // RKS: 05-29-22 - This was actually orphaning the subscriptions and event processors. Instead of simply doing
1339
+ // an assign we ned to assign the underlying data and clear the old info out. The reload method takes care of the
1340
+ // bindings for us.
1341
+ (async () => {
1342
+ await this.bindings.reload(ext);
1343
+ })();
1344
+ }
1345
+ else {
1346
+ this.bindings = Object.assign<MqttInterfaceBindings, any>(new MqttInterfaceBindings(cfg), ext);
1347
+ (async () => {
1348
+ await this.bindings.initAsync();
1349
+ })();
1350
+ }
1351
+ this.isRunning = true;
1352
+ this._isLoading = false;
1353
+ const stats = fs.statSync(this.bindingsPath);
1354
+ this._fileTime = stats.mtime;
1355
+ return true;
1356
+ }
1357
+ catch (err) {
1358
+ logger.error(`Error reading interface bindings file: ${this.bindingsPath}. ${err}`);
1359
+ this.isRunning = false;
1360
+ this._isLoading = false;
1361
+ }
1362
+ }
1363
+ return false;
1364
+ }
1365
+ public initBindings(cfg): boolean {
1366
+ let self = this;
1367
+ try {
1368
+ this.bindingsPath = path.posix.join(process.cwd(), "/web/bindings") + '/' + cfg.fileName;
1369
+ let fileTime = new Date(0).valueOf();
1370
+ fs.watch(this.bindingsPath, (event, fileName) => {
1371
+ if (fileName && event === 'change') {
1372
+ if (self._isLoading) return; // Need a debounce here. We will use a semaphore to cause it not to load more than once.
1373
+ const stats = fs.statSync(self.bindingsPath);
1374
+ if (stats.mtime.valueOf() === self._fileTime.valueOf()) return;
1375
+ self.loadBindings(cfg);
1376
+ logger.info(`Reloading ${cfg.name || ''} interface config: ${fileName}`);
1377
+ }
1378
+ });
1379
+ this.loadBindings(cfg);
1380
+ return true;
1381
+ }
1382
+ catch (err) {
1383
+ logger.error(`Error initializing interface bindings: ${err}`);
1384
+ }
1385
+ return false;
1386
+ }
1387
+ public emitToClients(evt: string, ...data: any) {
1388
+ if (this.isRunning) {
1389
+ // Take the bindings and map them to the appropriate http GET, PUT, DELETE, and POST.
1390
+ this.bindings.bindEvent(evt, ...data);
1391
+ }
1392
+ }
1393
+ public async stopAsync() {
1394
+ try {
1395
+ fs.unwatchFile(this.bindingsPath);
1396
+ if (this.bindings) await this.bindings.stopAsync();
1397
+ } catch (err) { logger.error(`Error shutting down MQTT Server ${this.name}: ${err.message}`); }
1398
+ }
1399
+ }
1400
+ export class InterfaceServerResponse {
1401
+ constructor(statusCode?: number, statusMessage?: string) {
1402
+ if (typeof statusCode !== 'undefined') this.status.code = statusCode;
1403
+ if (typeof statusMessage !== 'undefined') this.status.message = statusMessage;
1404
+ }
1405
+ status: { code: number, message: string } = { code: -1, message: '' };
1406
+ error: Error;
1407
+ data: string;
1408
+ obj: any;
1409
+ public static createError(err: Error, data?: string, obj?: any) {
1410
+ let resp = new InterfaceServerResponse(500, err.message);
1411
+ resp.error = err;
1412
+ return resp;
1413
+ }
1414
+ }
1415
+ export class REMInterfaceServer extends ProtoServer {
1416
+ public async init(cfg) {
1417
+ let self = this;
1418
+ this.cfg = cfg;
1419
+ this.uuid = cfg.uuid;
1420
+ if (cfg.enabled) {
1421
+ this.initSockets();
1422
+ setTimeoutSync(async () => {
1423
+ try {
1424
+ await self.initConnection();
1425
+ }
1426
+ catch (err) {
1427
+ logger.error(`Error establishing bi-directional Nixie/REM connection: ${err}`)
1428
+ }
1429
+ }, 5000);
1430
+ }
1431
+ }
1432
+ public async getControllerConfig() : Promise<InterfaceServerResponse> {
1433
+ try {
1434
+ let response = await this.sendClientRequest('GET', '/config/backup/controller', undefined, 10000);
1435
+ return response;
1436
+ } catch (err) {
1437
+ logger.error(`Error requesting GET /config/backup/controller: ${err.message}`);
1438
+ let errorResponse = new InterfaceServerResponse();
1439
+ errorResponse.error = new Error(`Error requesting GET /config/backup/controller: ${err.message}`);
1440
+ return errorResponse;
1441
+ }
1442
+ }
1443
+ public async validateRestore(cfg): Promise<InterfaceServerResponse> {
1444
+ try {
1445
+ let response = await this.sendClientRequest('PUT', '/config/restore/validate', cfg, 10000);
1446
+ return response;
1447
+ } catch (err) {
1448
+ logger.error(`Error requesting PUT /config/restore/validate ${err.message}`);
1449
+ let errorResponse = new InterfaceServerResponse();
1450
+ errorResponse.error = new Error(`Error requesting PUT /config/restore/validate: ${err.message}`);
1451
+ return errorResponse;
1452
+ }
1453
+ }
1454
+ public async restoreConfig(cfg): Promise<InterfaceServerResponse> {
1455
+ try {
1456
+ return await this.sendClientRequest('PUT', '/config/restore/file', cfg, 20000);
1457
+ } catch (err) {
1458
+ logger.error(`Error requesting PUT /config/restore/file ${err.message}`);
1459
+ let errorResponse = new InterfaceServerResponse();
1460
+ errorResponse.error = new Error(`Error requesting PUT /config/restore/file: ${err.message}`);
1461
+ return errorResponse;
1462
+ }
1463
+ }
1464
+ public async startPacketCapture(): Promise<InterfaceServerResponse> {
1465
+ try {
1466
+ let response = await this.sendClientRequest('PUT', '/config/packetCapture/start', undefined, 10000);
1467
+ return response;
1468
+ } catch (err) {
1469
+ logger.error(`Error requesting PUT /config/packetCapture/start: ${err.message}`);
1470
+ let errorResponse = new InterfaceServerResponse();
1471
+ errorResponse.error = new Error(`Error requesting PUT /config/packetCapture/start: ${err.message}`);
1472
+ return errorResponse;
1473
+ }
1474
+ }
1475
+ public async stopPacketCapture(): Promise<InterfaceServerResponse> {
1476
+ try {
1477
+ let response = await this.sendClientRequest('PUT', '/config/packetCapture/stop', undefined, 10000);
1478
+ return response;
1479
+ } catch (err) {
1480
+ logger.error(`Error requesting PUT /config/packetCapture/stop: ${err.message}`);
1481
+ let errorResponse = new InterfaceServerResponse();
1482
+ errorResponse.error = new Error(`Error requesting PUT /config/packetCapture/stop: ${err.message}`);
1483
+ return errorResponse;
1484
+ }
1485
+ }
1486
+ public async getPacketCaptureLog(): Promise<InterfaceServerResponse> {
1487
+ try {
1488
+ let response = await this.sendClientRequest('GET', '/config/packetCapture/log', undefined, 15000);
1489
+ return response;
1490
+ } catch (err) {
1491
+ logger.error(`Error requesting GET /config/packetCapture/log: ${err.message}`);
1492
+ let errorResponse = new InterfaceServerResponse();
1493
+ errorResponse.error = new Error(`Error requesting GET /config/packetCapture/log: ${err.message}`);
1494
+ return errorResponse;
1495
+ }
1496
+ }
1497
+
1498
+
1499
+ // Static methods to handle the REM server
1500
+ public static async startPacketCaptureOnRemServer(): Promise<void> {
1501
+ let remServers = webApp.findServersByType('rem') as REMInterfaceServer[];
1502
+ logger.info(`Found ${remServers ? remServers.length : 0} REM servers`);
1503
+
1504
+ if (remServers && remServers.length > 0) {
1505
+ let server = remServers[0]; // Get the single REM server
1506
+ logger.info(`Attempting to start packet capture on REM server: ${server.name} (connected: ${server.isConnected})`);
1507
+
1508
+ if (server.isConnected) {
1509
+ try {
1510
+ let response = await server.startPacketCapture();
1511
+ logger.info(`Start packet capture response: ${JSON.stringify(response)}`);
1512
+
1513
+ if (response && response.status.code === 200) {
1514
+ logger.info(`Started packet capture on REM server: ${server.name}`);
1515
+ } else {
1516
+ logger.warn(`Failed to start packet capture on REM server: ${server.name}. Status: ${response?.status?.code}, Error: ${response?.error?.message}`);
1517
+ }
1518
+ } catch (err) {
1519
+ logger.error(`Error starting packet capture on REM server ${server.name}: ${err.message}`);
1520
+ }
1521
+ } else {
1522
+ logger.warn(`REM server ${server.name} is not connected, cannot start packet capture`);
1523
+ }
1524
+ } else {
1525
+ logger.warn(`No REM servers found or configured`);
1526
+ }
1527
+ }
1528
+
1529
+ public static async stopPacketCaptureOnRemServer(): Promise<any[]> {
1530
+ let remServers = webApp.findServersByType('rem') as REMInterfaceServer[];
1531
+ let remLogs = [];
1532
+
1533
+ logger.info(`Found ${remServers ? remServers.length : 0} REM servers for stop packet capture`);
1534
+
1535
+ if (remServers && remServers.length > 0) {
1536
+ let server = remServers[0]; // Get the single REM server
1537
+ logger.info(`Attempting to stop packet capture on REM server: ${server.name} (connected: ${server.isConnected})`);
1538
+
1539
+ if (server.isConnected) {
1540
+ try {
1541
+ // Stop packet capture
1542
+ let stopResponse = await server.stopPacketCapture();
1543
+ logger.info(`Stop packet capture response: ${JSON.stringify(stopResponse)}`);
1544
+
1545
+ if (stopResponse && stopResponse.status.code === 200) {
1546
+ logger.info(`Stopped packet capture on REM server: ${server.name}`);
1547
+
1548
+ // Get the log file
1549
+ let logResponse = await server.getPacketCaptureLog();
1550
+ logger.info(`Get log response: ${JSON.stringify(logResponse)}`);
1551
+
1552
+ if (logResponse && logResponse.status.code === 200 && logResponse.data) {
1553
+ // Use the actual log file name from the REM response
1554
+ logger.info(`Log response obj: ${JSON.stringify(logResponse.obj)}`);
1555
+ let logFileName = logResponse.obj && logResponse.obj.logFile ? logResponse.obj.logFile : `rem_${server.name}_packetCapture.log`;
1556
+ logger.info(`Using log filename: ${logFileName}`);
1557
+ remLogs.push({
1558
+ serverName: server.name,
1559
+ logData: logResponse.data,
1560
+ logFileName: logFileName
1561
+ });
1562
+ logger.info(`Retrieved packet capture log from REM server: ${server.name}, log size: ${logResponse.data.length} characters, filename: ${logFileName}`);
1563
+ } else {
1564
+ logger.warn(`Failed to retrieve packet capture log from REM server: ${server.name}. Status: ${logResponse?.status?.code}, Error: ${logResponse?.error?.message}`);
1565
+ }
1566
+ } else {
1567
+ logger.warn(`Failed to stop packet capture on REM server: ${server.name}. Status: ${stopResponse?.status?.code}, Error: ${stopResponse?.error?.message}`);
1568
+ }
1569
+ } catch (err) {
1570
+ logger.error(`Error stopping packet capture on REM server ${server.name}: ${err.message}`);
1571
+ }
1572
+ } else {
1573
+ logger.warn(`REM server ${server.name} is not connected, cannot stop packet capture`);
1574
+ }
1575
+ } else {
1576
+ logger.warn(`No REM servers found or configured for stop packet capture`);
1577
+ }
1578
+
1579
+ logger.info(`Returning ${remLogs.length} REM logs`);
1580
+ return remLogs;
1581
+ }
1582
+ private async initConnection() {
1583
+ try {
1584
+ // find HTTP server
1585
+ return new Promise<void>(async (resolve, reject) => {
1586
+ let self = this;
1587
+ // First, send the connection info for njsPC and see if a connection exists.
1588
+ let url = '/config/checkconnection/';
1589
+ // can & should extend for https/username-password/ssl
1590
+ let data: any = { type: "njspc", isActive: true, id: null, name: "njsPC - automatic", protocol: "http:", ipAddress: webApp.ip(), port: config.getSection('web').servers.http.port || 4200, userName: "", password: "", sslKeyFile: "", sslCertFile: "", hostnames: [] }
1591
+ if (typeof this.cfg.options !== 'undefined' && this.cfg.options.host !== 'undefined' &&
1592
+ this.cfg.options.host.toLowerCase() === 'localhost' || this.cfg.options.host === '127.0.0.1') data.loopback = true;
1593
+ logger.info(`Checking REM Connection ${data.name} ${data.ipAddress}:${data.port}`);
1594
+ try {
1595
+ data.hostnames = await dns.promises.reverse(data.ipAddress);
1596
+ } catch (err) { logger.error(`Error getting hostnames for njsPC REM connection`); }
1597
+ let result = await this.putApiService(url, data, 5000);
1598
+ // If the result code is > 200 we have an issue. (-1 is for timeout)
1599
+ if (result.status.code > 200 || result.status.code < 0) return reject(new Error(`initConnection: ${result.error.message}`));
1600
+ else {
1601
+ this.remoteConnectionId = result.obj.id;
1602
+ };
1603
+
1604
+ // The passed connection has been setup/verified; now test for emit
1605
+ // if this fails, it could be because the remote connection is disabled. We will not
1606
+ // automatically re-enable it
1607
+ url = '/config/checkemit'
1608
+ data = { eventName: "checkemit", property: "result", value: 'success', connectionId: result.obj.id }
1609
+ // wait for REM server to finish resetting
1610
+ setTimeoutSync(async () => {
1611
+ try {
1612
+ let _tmr = setTimeoutSync(() => { return reject(new Error(`initConnection: No socket response received. Check REM→njsPC communications.`)) }, 5000);
1613
+ let srv: HttpServer = webApp.findServer('http') as HttpServer;
1614
+ srv.addListenerOnce('/checkemit', (data: any) => {
1615
+ // if we receive the emit, data will work both ways.
1616
+ // console.log(data);
1617
+ clearTimeout(_tmr);
1618
+ logger.info(`${this.name} bi-directional communications established.`)
1619
+ resolve();
1620
+ });
1621
+ result = await self.putApiService(url, data);
1622
+ // If the result code is > 200 or -1 we have an issue.
1623
+ if (result.status.code > 200 || result.status.code === -1) return reject(new Error(`initConnection: ${result.error.message}`));
1624
+ else {
1625
+ clearTimeout(_tmr);
1626
+ resolve();
1627
+ }
1628
+ }
1629
+ catch (err) {
1630
+ logger.error(`initConnection setTimeout error: ${err.message}`);
1631
+ reject(new Error(`initConnection setTimeout: ${err.message}`));
1632
+ }
1633
+ }, 3000);
1634
+ });
1635
+ }
1636
+ catch (err) {
1637
+ logger.error(`Error with REM Interface Server initConnection: ${err.message}`);
1638
+ }
1639
+ }
1640
+ public async stopAsync() {
1641
+ try {
1642
+ if (typeof this.agent !== 'undefined') this.agent.destroy();
1643
+ if (typeof this.sockClient !== 'undefined') this.sockClient.destroy();
1644
+ logger.info(`Stopped REM Interface Server ${this.name}`);
1645
+ } catch (err) { logger.error(`Error closing REM Server ${this.name}: ${err.message}`); }
1646
+ }
1647
+ public cfg;
1648
+ public sockClient;
1649
+ protected agent: http.Agent = new http.Agent({ keepAlive: true });
1650
+ public get isConnected() { return this.sockClient !== 'undefined' && this.sockClient.connected; };
1651
+ private _sockets: RemoteSocket<ServerToClientEvents, any>[] = [];
1652
+ private async sendClientRequest(method: string, url: string, data?: any, timeout: number = 10000): Promise<InterfaceServerResponse> {
1653
+ try {
1654
+
1655
+ let ret = new InterfaceServerResponse();
1656
+ let opts = extend(true, { headers: {} }, this.cfg.options);
1657
+ if ((typeof opts.hostname === 'undefined' || !opts.hostname) && (typeof opts.host === 'undefined' || !opts.host || opts.host === '*')) {
1658
+ ret.error = new Error(`Interface: ${this.cfg.name} has not resolved to a valid host.`)
1659
+ logger.warn(ret.error);
1660
+ return ret;
1661
+ }
1662
+ let sbody = typeof data === 'undefined' ? '' : typeof data === 'string' ? data : typeof data === 'object' ? JSON.stringify(data) : data.toString();
1663
+ if (typeof sbody !== 'undefined') {
1664
+ if (sbody.charAt(0) === '"' && sbody.charAt(sbody.length - 1) === '"') sbody = sbody.substr(1, sbody.length - 2);
1665
+ opts.headers["CONTENT-LENGTH"] = Buffer.byteLength(sbody || '');
1666
+ }
1667
+ opts.path = url;
1668
+ opts.method = method || 'GET';
1669
+ ret.data = '';
1670
+ opts.agent = this.agent;
1671
+ logger.verbose(`REM server request initiated. ${opts.method} ${opts.path} ${sbody}`);
1672
+ await new Promise<void>((resolve, reject) => {
1673
+ let req: http.ClientRequest;
1674
+ if (opts.port === 443 || (opts.protocol || '').startsWith('https')) {
1675
+ opts.protocol = 'https:';
1676
+ req = https.request(opts, (response: http.IncomingMessage) => {
1677
+ ret.status.code = response.statusCode;
1678
+ ret.status.message = response.statusMessage;
1679
+ response.on('error', (err) => { ret.error = err; resolve(); });
1680
+ response.on('data', (data) => { ret.data += data; });
1681
+ response.on('end', () => { resolve(); });
1682
+ });
1683
+ }
1684
+ else {
1685
+ opts.protocol = undefined;
1686
+ req = http.request(opts, (response: http.IncomingMessage) => {
1687
+ ret.status.code = response.statusCode;
1688
+ ret.status.message = response.statusMessage;
1689
+ response.on('error', (err) => {
1690
+ logger.error(`An error occurred with request: ${err}`);
1691
+ ret.error = err; resolve();
1692
+ });
1693
+ response.on('data', (data) => { ret.data += data; });
1694
+ response.on('end', () => { resolve(); });
1695
+ });
1696
+ }
1697
+ req.setTimeout(timeout, () => { reject(new Error(`Request timeout after ${timeout}ms: ${method} ${url}`)); });
1698
+ req.on('error', (err, req, res) => {
1699
+ logger.error(`Error sending Request: ${opts.method} ${url} ${err.message}`);
1700
+ ret.error = err;
1701
+ reject(new Error(`Error sending Request: ${opts.method} ${url} ${err.message}`));
1702
+ });
1703
+ req.on('abort', () => { logger.warn('Request Aborted'); reject(new Error('Request Aborted.')); });
1704
+ req.end(sbody);
1705
+ }).catch((err) => {
1706
+ logger.error(`Error Sending REM Request: ${opts.method} ${url} ${err.message}`);
1707
+ ret.error = err;
1708
+ });
1709
+ logger.verbose(`REM server request returned. ${opts.method} ${opts.path} ${sbody}`);
1710
+ if (ret.status.code > 200) {
1711
+ // We have an http error so let's parse it up.
1712
+ try {
1713
+ ret.error = JSON.parse(ret.data);
1714
+ } catch (err) { ret.error = new Error(`Unidentified ${ret.status.code} Error: ${ret.status.message}`) }
1715
+ ret.data = '';
1716
+ }
1717
+ else if (ret.status.code === 200 && this.isJSONString(ret.data)) {
1718
+ try { ret.obj = JSON.parse(ret.data); }
1719
+ catch (err) { }
1720
+ }
1721
+ logger.debug(`REM server request returned. ${opts.method} ${opts.path} ${sbody} ${JSON.stringify(ret)}`);
1722
+ return ret;
1723
+ }
1724
+ catch (err) {
1725
+ logger.error(`Error sending HTTP ${method} command to ${url}: ${err.message}`);
1726
+ let errorResponse = new InterfaceServerResponse();
1727
+ errorResponse.error = new Error(`Http ${method} Error ${url}:${err.message}`);
1728
+ return errorResponse;
1729
+ }
1730
+ }
1731
+ private initSockets() {
1732
+ try {
1733
+ let self = this;
1734
+ let url = `${this.cfg.options.protocol || 'http://'}${this.cfg.options.host}${typeof this.cfg.options.port !== 'undefined' ? ':' + this.cfg.options.port : ''}`;
1735
+ logger.info(`Opening ${this.cfg.name} socket on ${url}`);
1736
+ //console.log(this.cfg);
1737
+ this.sockClient = sockClient(url, extend(true,
1738
+ { reconnectionDelay: 2000, reconnection: true, reconnectionDelayMax: 20000, transports: ['websocket'], upgrade: true, }, this.cfg.socket));
1739
+ if (typeof this.sockClient === 'undefined') {
1740
+ logger.error('Could not Initialize REM Server. Invalid configuration.');
1741
+ return;
1742
+ }
1743
+ //this.sockClient = io.connect(url, { reconnectionDelay: 2000, reconnection: true, reconnectionDelayMax: 20000 });
1744
+ //console.log(this.sockClient);
1745
+ //console.log(typeof this.sockClient.on);
1746
+ this.sockClient.on('connect_error', (err) => { logger.error(`${this.cfg.name} socket connection error: ${err}`); });
1747
+ this.sockClient.on('connect_timeout', () => { logger.error(`${this.cfg.name} socket connection timeout`); });
1748
+ this.sockClient.on('reconnect', (attempts) => { logger.info(`${this.cfg.name} socket reconnected after ${attempts}`); });
1749
+ this.sockClient.on('reconnect_attempt', () => { logger.warn(`${this.cfg.name} socket attempting to reconnect`); });
1750
+ this.sockClient.on('reconnecting', (attempts) => { logger.warn(`${this.cfg.name} socket attempting to reconnect: ${attempts}`); });
1751
+ this.sockClient.on('reconnect_failed', (err) => { logger.warn(`${this.cfg.name} socket failed to reconnect: ${err}`); });
1752
+ this.sockClient.on('close', () => { logger.info(`${this.cfg.name} socket closed`); });
1753
+ this.sockClient.on('connect', () => {
1754
+ logger.info(`${this.cfg.name} socket connected`);
1755
+ this.sockClient.on('i2cDataValues', function (data) {
1756
+ //logger.info(`REM Socket i2cDataValues ${JSON.stringify(data)}`);
1757
+ });
1758
+ });
1759
+ this.isRunning = true;
1760
+ }
1761
+ catch (err) {
1762
+ logger.error(`Error Initializing Sockets: ${err.message}`);
1763
+ }
1764
+ }
1765
+ private isJSONString(s: string): boolean {
1766
+ if (typeof s !== 'string') return false;
1767
+ if (s.startsWith('{') || s.startsWith('[')) return true;
1768
+ return false;
1769
+ }
1770
+ public async getApiService(url: string, data?: any, timeout: number = 3600): Promise<InterfaceServerResponse> {
1771
+ // Calls a rest service on the REM to set the state of a connected device.
1772
+ let ret = await this.sendClientRequest('GET', url, data, timeout);
1773
+ return ret;
1774
+ }
1775
+ public async putApiService(url: string, data?: any, timeout: number = 3600): Promise<InterfaceServerResponse> {
1776
+ // Calls a rest service on the REM to set the state of a connected device.
1777
+ let ret = await this.sendClientRequest('PUT', url, data, timeout);
1778
+ return ret;
1779
+ }
1780
+ public async searchApiService(url: string, data?: any, timeout: number = 3600): Promise<InterfaceServerResponse> {
1781
+ // Calls a rest service on the REM to set the state of a connected device.
1782
+ let ret = await this.sendClientRequest('SEARCH', url, data, timeout);
1783
+ return ret;
1784
+ }
1785
+ public async deleteApiService(url: string, data?: any, timeout: number = 3600): Promise<InterfaceServerResponse> {
1786
+ // Calls a rest service on the REM to set the state of a connected device.
1787
+ let ret = await this.sendClientRequest('DELETE', url, data, timeout);
1788
+ return ret;
1789
+ }
1790
+ public async getDevices() {
1791
+ try {
1792
+ let response = await this.sendClientRequest('GET', '/devices/all', undefined, 3000);
1793
+ if (response.status.code !== 200) {
1794
+ // Let's try again. Sometimes the resolver for calls like this are stupid.
1795
+ response = await this.sendClientRequest('GET', '/devices/all', undefined, 10000);
1796
+ }
1797
+ return (response.status.code === 200) ? JSON.parse(response.data) : [];
1798
+ }
1799
+ catch (err) {
1800
+ logger.error(`getDevices: ${err.message}`);
1801
+ return [];
1802
+ }
1803
+ }
1804
+ }
1805
+ export class BackupFile {
1806
+ public static async fromBuffer(filename: string, buff: Buffer) {
1807
+ try {
1808
+ let bf = new BackupFile();
1809
+ bf.filename = filename;
1810
+ bf.filePath = path.join(process.cwd(), 'backups', bf.filename);
1811
+ await bf.extractBackupOptions(buff);
1812
+ return typeof bf.options !== 'undefined' ? bf : undefined;
1813
+ } catch (err) { logger.error(`Error creating buffered backup file: ${filename}`); }
1814
+ }
1815
+ public static async fromFile(filePath: string) {
1816
+ try {
1817
+ let bf = new BackupFile();
1818
+ bf.filePath = filePath;
1819
+ bf.filename = path.parse(filePath).base;
1820
+ await bf.extractBackupOptions(filePath);
1821
+ return typeof bf.options !== 'undefined' ? bf : undefined;
1822
+ } catch (err) { logger.error(`Error creating backup file from file ${filePath}`); }
1823
+ }
1824
+ public options: any;
1825
+ public filename: string;
1826
+ public filePath: string;
1827
+ public errors = [];
1828
+ protected async extractBackupOptions(file: string | Buffer) {
1829
+ try {
1830
+ let jszip = require("jszip");
1831
+ let buff = Buffer.isBuffer(file) ? file : fs.readFileSync(file);
1832
+ let zip = await jszip.loadAsync(buff);
1833
+ await zip.file('options.json').async('string').then((data) => {
1834
+ this.options = JSON.parse(data);
1835
+ if (typeof this.options.backupDate === 'undefined' && typeof file === 'string') {
1836
+ let name = path.parse(file).name;
1837
+ name = name.indexOf('(') !== -1 ? name.substring(0, name.indexOf('(')) : name;
1838
+ if (name.length === 19) {
1839
+ let date = name.substring(0, 10).replace(/-/g, '/');
1840
+ let time = name.substring(11).replace(/-/g, ':');
1841
+ let dt = Date.parse(`${date} ${time}`);
1842
+ if (!isNaN(dt)) this.options.backupDate = Timestamp.toISOLocal(new Date(dt));
1843
+ }
1844
+ }
1845
+ });
1846
+ } catch (err) { this.errors.push(err); logger.error(`Error extracting backup options from ${file}: ${err.message}`); }
1847
+ }
1848
+ }
1849
+ export class RestoreFile {
1850
+ public static async fromFile(filePath: string) {
1851
+ try {
1852
+ let rf = new RestoreFile();
1853
+ rf.filePath = filePath;
1854
+ rf.filename = path.parse(filePath).base;
1855
+ await rf.extractRestoreOptions(filePath);
1856
+ return rf;
1857
+ } catch (err) { logger.error(`Error created restore file options`); }
1858
+ }
1859
+ public filename: string;
1860
+ public filePath: string;
1861
+ public njsPC: { config:any, poolConfig: any, poolState: any };
1862
+ public servers: { name: string, uuid: string, serverConfig: any, controllerConfig: any }[] = [];
1863
+ public options: any;
1864
+ public errors = [];
1865
+ protected async extractFile(zip, path): Promise<any> {
1866
+ try {
1867
+ let obj;
1868
+ await zip.file(path).async('string').then((data) => { obj = JSON.parse(data); });
1869
+ return obj;
1870
+ } catch (err) { logger.error(`Error extracting restore data from ${this.filename}[${path}]: ${err.message}`); }
1871
+ }
1872
+ protected async extractRestoreOptions(file: string | Buffer) {
1873
+ try {
1874
+ let jszip = require("jszip");
1875
+ let buff = Buffer.isBuffer(file) ? file : fs.readFileSync(file);
1876
+ let zip = await jszip.loadAsync(buff);
1877
+ this.options = await this.extractFile(zip, 'options.json');
1878
+ // Now we need to extract all the servers from the file.
1879
+ if (this.options.njsPC) {
1880
+ this.njsPC = { config: {}, poolConfig: {}, poolState: {} };
1881
+ this.njsPC.config = await this.extractFile(zip, 'njsPC/config.json');
1882
+ this.njsPC.poolConfig = await this.extractFile(zip, 'njsPC/data/poolConfig.json');
1883
+ this.njsPC.poolState = await this.extractFile(zip, 'njsPC/data/poolState.json');
1884
+ }
1885
+ if (typeof this.options.servers !== 'undefined') {
1886
+ for (let i = 0; i < this.options.servers.length; i++) {
1887
+ // Extract each server from the file.
1888
+ let srv = this.options.servers[i];
1889
+ if (srv.backup && srv.success) {
1890
+ this.servers.push({
1891
+ name: srv.name,
1892
+ uuid: srv.uuid,
1893
+ serverConfig: await this.extractFile(zip, `${srv.name}/serverConfig.json`),
1894
+ controllerConfig: await this.extractFile(zip, `${srv.name}/data/controllerConfig.json`)
1895
+ });
1896
+ }
1897
+ }
1898
+ }
1899
+ } catch(err) { this.errors.push(err); logger.error(`Error extracting restore options from ${file}: ${err.message}`); }
1900
+ }
1901
+ }
1902
+ export class RestoreResults {
1903
+ public errors = [];
1904
+ public warnings = [];
1905
+ public success = [];
1906
+ public modules: { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number }[] = [];
1907
+ protected getModule(name: string): { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number } {
1908
+ let mod = this.modules.find(elem => name === elem.name);
1909
+ if (typeof mod === 'undefined') {
1910
+ mod = { name: name, errors: [], warnings: [], success: [], restored: 0, ignored: 0 };
1911
+ this.modules.push(mod);
1912
+ }
1913
+ return mod;
1914
+ }
1915
+ public addModuleError(name: string, err: any): { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number } {
1916
+ let mod = this.getModule(name);
1917
+ mod.errors.push(err);
1918
+ mod.ignored++;
1919
+ logger.error(`Restore ${name} -> ${err}`);
1920
+ return mod;
1921
+ }
1922
+ public addModuleWarning(name: string, warn: any): { name: string, errors: any[], warnings: any[], success:any[], restored: number, ignored: number } {
1923
+ let mod = this.getModule(name);
1924
+ mod.warnings.push(warn);
1925
+ mod.restored++;
1926
+ logger.warn(`Restore ${name} -> ${warn}`);
1927
+ return mod;
1928
+ }
1929
+ public addModuleSuccess(name: string, success: any): { name: string, errors: any[], warnings: any[], success: any[], restored: number, ignored: number } {
1930
+ let mod = this.getModule(name);
1931
+ mod.success.push(success);
1932
+ mod.restored++;
1933
+ logger.info(`Restore ${name} -> ${success}`);
1934
+ return mod;
1935
+ }
1936
+ }
1937
+ export const webApp = new WebServer();