nodejs-poolcontroller 8.3.0 → 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.
- package/.eslintrc.json +36 -36
- package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -84
- package/.github/ISSUE_TEMPLATE/2-docs.md +12 -12
- package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -28
- package/.github/ISSUE_TEMPLATE/config.yml +8 -8
- package/.github/copilot-instructions.md +63 -63
- package/.github/workflows/ghcr-publish.yml +67 -67
- package/AGENTS.md +597 -0
- package/CONTRIBUTING.md +74 -74
- package/Changelog +292 -284
- package/Dockerfile +62 -62
- package/Gruntfile.js +40 -40
- package/LICENSE +661 -661
- package/README.md +318 -309
- package/anslq25/MessagesMock.ts +221 -221
- package/anslq25/boards/MockBoardFactory.ts +49 -49
- package/anslq25/boards/MockEasyTouchBoard.ts +696 -696
- package/anslq25/boards/MockSystemBoard.ts +216 -216
- package/anslq25/chemistry/MockChlorinator.ts +98 -98
- package/anslq25/pumps/MockPump.ts +83 -83
- package/app.ts +115 -115
- package/config/Config.ts +0 -0
- package/config/VersionCheck.ts +0 -0
- package/controller/Constants.ts +809 -805
- package/controller/Equipment.ts +2688 -2664
- package/controller/Errors.ts +181 -181
- package/controller/Lockouts.ts +549 -549
- package/controller/State.ts +3738 -3701
- package/controller/boards/AquaLinkBoard.ts +1003 -1003
- package/controller/boards/BoardFactory.ts +53 -53
- package/controller/boards/EasyTouchBoard.ts +3202 -3202
- package/controller/boards/IntelliCenterBoard.ts +4393 -3899
- package/controller/boards/IntelliComBoard.ts +69 -69
- package/controller/boards/IntelliTouchBoard.ts +382 -382
- package/controller/boards/NixieBoard.ts +1944 -1944
- package/controller/boards/SunTouchBoard.ts +400 -400
- package/controller/boards/SystemBoard.ts +5268 -5268
- package/controller/comms/Comms.ts +1272 -1255
- package/controller/comms/ScreenLogic.ts +1665 -1665
- package/controller/comms/messages/Messages.ts +1433 -1406
- package/controller/comms/messages/config/ChlorinatorMessage.ts +5 -0
- package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
- package/controller/comms/messages/config/CircuitMessage.ts +0 -0
- package/controller/comms/messages/config/ConfigMessage.ts +6 -0
- package/controller/comms/messages/config/CoverMessage.ts +0 -0
- package/controller/comms/messages/config/CustomNameMessage.ts +31 -31
- package/controller/comms/messages/config/EquipmentMessage.ts +216 -210
- package/controller/comms/messages/config/ExternalMessage.ts +96 -10
- package/controller/comms/messages/config/FeatureMessage.ts +0 -0
- package/controller/comms/messages/config/GeneralMessage.ts +0 -0
- package/controller/comms/messages/config/HeaterMessage.ts +0 -0
- package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
- package/controller/comms/messages/config/OptionsMessage.ts +194 -174
- package/controller/comms/messages/config/PumpMessage.ts +0 -0
- package/controller/comms/messages/config/RemoteMessage.ts +0 -0
- package/controller/comms/messages/config/ScheduleMessage.ts +401 -390
- package/controller/comms/messages/config/SecurityMessage.ts +0 -0
- package/controller/comms/messages/config/ValveMessage.ts +0 -0
- package/controller/comms/messages/status/ChlorinatorStateMessage.ts +0 -0
- package/controller/comms/messages/status/EquipmentStateMessage.ts +1158 -822
- package/controller/comms/messages/status/HeaterStateMessage.ts +135 -135
- package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -448
- package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -36
- package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
- package/controller/comms/messages/status/RegalModbusStateMessage.ts +410 -410
- package/controller/comms/messages/status/VersionMessage.ts +103 -41
- package/controller/nixie/Nixie.ts +173 -173
- package/controller/nixie/NixieEquipment.ts +104 -104
- package/controller/nixie/bodies/Body.ts +120 -120
- package/controller/nixie/bodies/Filter.ts +135 -135
- package/controller/nixie/chemistry/ChemController.ts +2724 -2724
- package/controller/nixie/chemistry/ChemDoser.ts +806 -806
- package/controller/nixie/chemistry/Chlorinator.ts +367 -367
- package/controller/nixie/circuits/Circuit.ts +478 -478
- package/controller/nixie/heaters/Heater.ts +834 -834
- package/controller/nixie/pumps/Pump.ts +1193 -1193
- package/controller/nixie/schedules/Schedule.ts +401 -401
- package/controller/nixie/valves/Valve.ts +170 -170
- package/defaultConfig.json +352 -352
- package/docker-compose.yml +31 -31
- package/logger/DataLogger.ts +448 -448
- package/logger/Logger.ts +448 -436
- package/package.json +58 -58
- package/sendSocket.js +32 -32
- package/tsconfig.json +25 -25
- package/types/express-multer.d.ts +32 -32
- package/web/Server.ts +1937 -1927
- package/web/bindings/aqualinkD.json +559 -559
- package/web/bindings/influxDB.json +1066 -1066
- package/web/bindings/mqtt.json +721 -721
- package/web/bindings/mqttAlt.json +746 -746
- package/web/bindings/rulesManager.json +54 -54
- package/web/bindings/smartThings-Hubitat.json +31 -31
- package/web/bindings/valveRelays.json +20 -20
- package/web/bindings/vera.json +25 -25
- package/web/interfaces/baseInterface.ts +188 -188
- package/web/interfaces/httpInterface.ts +148 -148
- package/web/interfaces/influxInterface.ts +283 -283
- package/web/interfaces/mqttInterface.ts +695 -695
- package/web/interfaces/ruleInterface.ts +101 -87
- package/web/services/config/Config.ts +1063 -1053
- package/web/services/config/ConfigSocket.ts +0 -0
- package/web/services/state/State.ts +0 -0
- package/web/services/state/StateSocket.ts +0 -0
- package/web/services/utilities/Utilities.ts +233 -233
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
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
logger.
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
})
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
});
|
|
684
|
-
sock.on('
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
this.
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
//
|
|
788
|
-
this.
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
public
|
|
806
|
-
//
|
|
807
|
-
this.
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
//
|
|
879
|
-
this.
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
let
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
<
|
|
976
|
-
<
|
|
977
|
-
<
|
|
978
|
-
<
|
|
979
|
-
<
|
|
980
|
-
<
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
},
|
|
1053
|
-
{
|
|
1054
|
-
name: '
|
|
1055
|
-
type: '
|
|
1056
|
-
data:
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
public
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
this.
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
public
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
this.
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
public
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
public
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
this.
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
this.
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
public
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
errorResponse
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
return
|
|
1447
|
-
} catch (err) {
|
|
1448
|
-
logger.error(`Error requesting PUT /config/restore/
|
|
1449
|
-
let errorResponse = new InterfaceServerResponse();
|
|
1450
|
-
errorResponse.error = new Error(`Error requesting PUT /config/restore/
|
|
1451
|
-
return errorResponse;
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
public async
|
|
1455
|
-
try {
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
errorResponse
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
errorResponse
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
errorResponse
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
logger.
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
public
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
}
|
|
1657
|
-
opts.
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
if (
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
opts.protocol =
|
|
1676
|
-
req =
|
|
1677
|
-
ret.status.code = response.statusCode;
|
|
1678
|
-
ret.status.message = response.statusMessage;
|
|
1679
|
-
response.on('error', (err) => {
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
logger.
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
this.sockClient
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
this.sockClient.
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
});
|
|
1749
|
-
this.
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
return
|
|
1769
|
-
}
|
|
1770
|
-
public async
|
|
1771
|
-
// Calls a rest service on the REM to set the state of a connected device.
|
|
1772
|
-
let ret = await this.sendClientRequest('
|
|
1773
|
-
return ret;
|
|
1774
|
-
}
|
|
1775
|
-
public async
|
|
1776
|
-
// Calls a rest service on the REM to set the state of a connected device.
|
|
1777
|
-
let ret = await this.sendClientRequest('
|
|
1778
|
-
return ret;
|
|
1779
|
-
}
|
|
1780
|
-
public async
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
bf
|
|
1809
|
-
bf.filename =
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
public
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
public filePath: string
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
public
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
mod.
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
}
|
|
1927
|
-
|
|
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();
|