iobroker.rest-api 2.0.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +49 -8
  2. package/admin/i18n/{de/translations.json → de.json} +22 -22
  3. package/admin/i18n/{en/translations.json → en.json} +22 -23
  4. package/admin/i18n/{es/translations.json → es.json} +22 -23
  5. package/admin/i18n/{fr/translations.json → fr.json} +22 -23
  6. package/admin/i18n/{it/translations.json → it.json} +22 -23
  7. package/admin/i18n/{nl/translations.json → nl.json} +22 -23
  8. package/admin/i18n/{pl/translations.json → pl.json} +22 -23
  9. package/admin/i18n/{pt/translations.json → pt.json} +22 -23
  10. package/admin/i18n/{ru/translations.json → ru.json} +22 -23
  11. package/admin/i18n/uk.json +32 -0
  12. package/admin/i18n/{zh-cn/translations.json → zh-cn.json} +22 -23
  13. package/admin/jsonConfig.json +178 -180
  14. package/admin/rest-api.svg +8 -0
  15. package/dist/lib/api/controllers/common.js +129 -0
  16. package/dist/lib/api/controllers/common.js.map +1 -0
  17. package/dist/lib/api/controllers/enum.js +58 -0
  18. package/dist/lib/api/controllers/enum.js.map +1 -0
  19. package/dist/lib/api/controllers/file.js +104 -0
  20. package/dist/lib/api/controllers/file.js.map +1 -0
  21. package/dist/lib/api/controllers/history.js +262 -0
  22. package/dist/lib/api/controllers/history.js.map +1 -0
  23. package/dist/lib/api/controllers/object.js +346 -0
  24. package/dist/lib/api/controllers/object.js.map +1 -0
  25. package/dist/lib/api/controllers/sendTo.js +118 -0
  26. package/dist/lib/api/controllers/sendTo.js.map +1 -0
  27. package/dist/lib/api/controllers/state.js +545 -0
  28. package/dist/lib/api/controllers/state.js.map +1 -0
  29. package/dist/lib/api/swagger/swagger.yaml +2551 -0
  30. package/{lib → dist/lib}/common.js +8 -10
  31. package/dist/lib/common.js.map +1 -0
  32. package/dist/lib/rest-api.js +1216 -0
  33. package/dist/lib/rest-api.js.map +1 -0
  34. package/dist/main.js +159 -0
  35. package/dist/main.js.map +1 -0
  36. package/examples/demoBrowserClient.html +95 -82
  37. package/examples/demoNodeClient.js +8 -7
  38. package/examples/longPolling.js +46 -45
  39. package/io-package.json +43 -34
  40. package/package.json +36 -24
  41. package/lib/api/controllers/common.js +0 -150
  42. package/lib/api/controllers/enum.js +0 -44
  43. package/lib/api/controllers/file.js +0 -74
  44. package/lib/api/controllers/history.js +0 -239
  45. package/lib/api/controllers/object.js +0 -273
  46. package/lib/api/controllers/sendTo.js +0 -123
  47. package/lib/api/controllers/state.js +0 -565
  48. package/lib/api/swagger/swagger.yaml +0 -2624
  49. package/lib/rest-api.js +0 -1123
  50. package/main.js +0 -173
  51. /package/{lib → dist/lib}/config/default.yaml +0 -0
package/lib/rest-api.js DELETED
@@ -1,1123 +0,0 @@
1
- /* jshint -W097 */
2
- /* jshint strict: false */
3
- /* jslint node: true */
4
- /* jshint -W061 */
5
- 'use strict';
6
-
7
- const SwaggerRunner = require('swagger-node-runner-fork');
8
- const swaggerUi = require('swagger-ui-express');
9
- const YAML = require('yamljs');
10
- const bodyParser = require('body-parser');
11
- const crypto = require('crypto');
12
- const axios = require('axios');
13
- const cors = require('cors');
14
- const fs = require('fs');
15
- const path = require('path');
16
- const multer = require('multer');
17
- const utils = require('@iobroker/adapter-core'); // Get common adapter utils
18
- const pattern2RegEx = utils.commonTools.pattern2RegEx;
19
- const CommandsAdmin = require('@iobroker/socket-classes').SocketCommandsAdmin;
20
- const CommandsCommon = require('@iobroker/socket-classes').SocketCommands;
21
- const common = require('./common');
22
-
23
- process.env.SUPPRESS_NO_CONFIG_WARNING = 'true';
24
-
25
- const WEB_EXTENSION_PREFIX = 'rest-api/';
26
-
27
- /*const memStore = { };
28
-
29
- // Writable memory stream
30
- class WMStrm extends Writable {
31
- constructor(key, options) {
32
- super(options); // init super
33
- this.key = key; // save key
34
- this.data = Buffer.from(''); // empty
35
- }
36
-
37
- _write(chunk, enc, cb) {
38
- // our memory store stores things in buffers
39
- const buffer = (Buffer.isBuffer(chunk)) ? chunk : Buffer.from(chunk, enc);
40
-
41
- // concat to the buffer already there
42
- this.data = Buffer.concat([this.data, buffer]);
43
- cb();
44
- }
45
- }
46
- */
47
- function parseQuery(_url) {
48
- let url = decodeURI(_url);
49
- const pos = url.indexOf('?');
50
- const values = {};
51
- if (pos !== -1) {
52
- const arr = url.substring(pos + 1).split('&');
53
- url = url.substring(0, pos);
54
-
55
- for (let i = 0; i < arr.length; i++) {
56
- arr[i] = arr[i].split('=');
57
- values[arr[i][0].trim()] = arr[i][1] === undefined ? null : decodeURIComponent((arr[i][1] + '').replace(/\+/g, '%20'));
58
- }
59
-
60
- // Default value for wait
61
- if (values.timeout === null) {
62
- values.timeout = 2000;
63
- }
64
- }
65
-
66
- const parts = url.split('/');
67
-
68
- // Analyse system.adapter.socketio.0.uptime,system.adapter.history.0.memRss?value=78&timeout=300
69
- if (parts[2]) {
70
- const oId = parts[2].split(',');
71
- for (let j = oId.length - 1; j >= 0; j--) {
72
- oId[j] = oId[j].trim();
73
- if (!oId[j]) {
74
- oId.splice(j, 1);
75
- }
76
- }
77
- }
78
-
79
- return values;
80
- }
81
-
82
- // copied from here: https://github.com/component/escape-html/blob/master/index.js
83
- const matchHtmlRegExp = /["'&<>]/;
84
- function escapeHtml(string) {
85
- const str = '' + string;
86
- const match = matchHtmlRegExp.exec(str);
87
-
88
- if (!match) {
89
- return str;
90
- }
91
-
92
- let escape;
93
- let html = '';
94
- let index = 0;
95
- let lastIndex = 0;
96
-
97
- for (index = match.index; index < str.length; index++) {
98
- switch (str.charCodeAt(index)) {
99
- case 34: // "
100
- escape = '&quot;';
101
- break;
102
- case 38: // &
103
- escape = '&amp;';
104
- break;
105
- case 39: // '
106
- escape = '&#39;';
107
- break;
108
- case 60: // <
109
- escape = '&lt;';
110
- break;
111
- case 62: // >
112
- escape = '&gt;';
113
- break;
114
- default:
115
- continue;
116
- }
117
-
118
- if (lastIndex !== index) {
119
- html += str.substring(lastIndex, index);
120
- }
121
-
122
- lastIndex = index + 1;
123
- html += escape;
124
- }
125
-
126
- return lastIndex !== index
127
- ? html + str.substring(lastIndex, index)
128
- : html;
129
- }
130
-
131
- function decorateLogFile(filename, text) {
132
- const prefix = '<html><head>' +
133
- '<style>\n' +
134
- ' table {' +
135
- ' font-family: monospace;\n' +
136
- ' font-size: 14px;\n' +
137
- ' }\n' +
138
- ' .info {\n' +
139
- ' background: white;' +
140
- ' }\n' +
141
- ' .type {\n' +
142
- ' font-weight: bold;' +
143
- ' }\n' +
144
- ' .silly {\n' +
145
- ' background: #b3b3b3;' +
146
- ' }\n' +
147
- ' .debug {\n' +
148
- ' background: lightgray;' +
149
- ' }\n' +
150
- ' .warn {\n' +
151
- ' background: #ffdb75;' +
152
- ' color: white;' +
153
- ' }\n' +
154
- ' .error {\n' +
155
- ' background: #ff6a5b;' +
156
- ' }\n' +
157
- '</style>\n' +
158
- '<script>\n' +
159
- 'function decorate (line) {\n' +
160
- ' var className = "info";\n' +
161
- ' line = line.replace(/\\x1B\\[39m/g, "</span>");\n' +
162
- ' if (line.indexOf("[32m") !== -1) {\n' +
163
- ' className = "info";\n'+
164
- ' line = line.replace(/\\x1B\\[32m/g, "<span class=\\"type\\">");\n' +
165
- ' } else \n' +
166
- ' if (line.indexOf("[34m") !== -1) {\n' +
167
- ' className = "debug";\n'+
168
- ' line = line.replace(/\\x1B\\[34m/g, "<span class=\\"type\\">");\n' +
169
- ' } else \n' +
170
- ' if (line.indexOf("[33m") !== -1) {\n' +
171
- ' className = "warn";\n'+
172
- ' line = line.replace(/\\x1B\\[33m/g, "<span class=\\"type\\">");\n' +
173
- ' } else \n' +
174
- ' if (line.indexOf("[31m") !== -1) {\n' +
175
- ' className = "error";\n'+
176
- ' line = line.replace(/\\x1B\\[31m/g, "<span class=\\"type\\">");\n' +
177
- ' } else \n' +
178
- ' if (line.indexOf("[35m") !== -1) {\n' +
179
- ' className = "silly";\n'+
180
- ' line = line.replace(/\\x1B\\[35m/g, "<span class=\\"type\\">");\n' +
181
- ' } else {\n' +
182
- ' }\n' +
183
- ' return "<tr class=\\"" + className + "\\"><td>" + line + "</td></tr>";\n'+
184
- '}\n' +
185
- 'document.addEventListener("DOMContentLoaded", function () { \n' +
186
- ' var text = document.body.innerHTML;\n' +
187
- ' var lines = text.split("\\n");\n' +
188
- ' text = "<table>";\n' +
189
- ' for (var i = 0; i < lines.length; i++) {\n' +
190
- ' if (lines[i]) text += decorate(lines[i]);\n' +
191
- ' }\n' +
192
- ' text += "</table>";\n' +
193
- ' document.body.innerHTML = text;\n' +
194
- ' window.scrollTo(0,document.body.scrollHeight);\n' +
195
- '});\n' +
196
- '</script>\n</head>\n<body>\n';
197
- const suffix = '</body></html>';
198
- const log = text || fs.readFileSync(filename).toString();
199
- return prefix + log + suffix;
200
- }
201
-
202
- function removeTextFromFile(fileName, start, end) {
203
- let file = fs.readFileSync(fileName).toString('utf8').split('\n');
204
- // find <!-- START -->
205
- let newFile = [];
206
- let foundStart = false;
207
- let foundEnd = false;
208
- for (let f = 0; f < file.length; f++) {
209
- if (!foundStart && file[f].includes(start)) {
210
- foundStart = true;
211
- continue;
212
- } else
213
- if (file[f].includes(end)) {
214
- foundEnd = true;
215
- continue;
216
- }
217
- if (!foundStart || foundEnd) {
218
- newFile.push(file[f]);
219
- }
220
- }
221
- return newFile.join('\n');
222
- }
223
-
224
- /**
225
- * SwaggerUI class
226
- *
227
- * From settings used only secure, auth and crossDomain
228
- *
229
- * @class
230
- * @param {object} _ignore not used in this web extension
231
- * @param {object} webSettings settings of the web server, like <pre><code>{secure: settings.secure, port: settings.port}</code></pre>
232
- * @param {object} adapter web adapter object
233
- * @param {object} instanceSettings instance object with common and native
234
- * @param {object} app express application
235
- * @param {function} callback called when the engine is initialized
236
- * @return {object} object instance
237
- */
238
- function SwaggerUI(_ignore, webSettings, adapter, instanceSettings, app, callback) {
239
- if (!(this instanceof SwaggerUI)) {
240
- return new SwaggerUI(_ignore, webSettings, adapter, instanceSettings, app, callback);
241
- }
242
-
243
- if (typeof instanceSettings === 'function') {
244
- callback = instanceSettings;
245
- instanceSettings = null;
246
- }
247
-
248
- this.app = app || require('express')();
249
- this.adapter = adapter;
250
- this.settings = webSettings;
251
- this.config = instanceSettings ? instanceSettings.native : adapter.config;
252
- this.namespace = instanceSettings ? instanceSettings._id.substring('system.adapter.'.length) : this.adapter.namespace;
253
- this.subscribes = {};
254
- this.checkInterval = null;
255
- this.extension = !!instanceSettings;
256
- this.routerPrefix = this.extension ? `/${WEB_EXTENSION_PREFIX}` : '/';
257
- this.gcInterval = null;
258
- this.commands = this.adapter.config.noAdminCommands ? new CommandsCommon(adapter) : new CommandsAdmin(adapter);
259
-
260
- this.config.defaultUser = this.extension ? this.config.defaultUser : this.config.defaultUser || 'system.user.admin';
261
-
262
- this.config.checkInterval = this.config.checkInterval === undefined ? 20000 : parseInt(this.config.checkInterval, 10);
263
- if (this.config.checkInterval && this.config.checkInterval < 5000) {
264
- this.config.checkInterval = 5000;
265
- }
266
-
267
- this.config.hookTimeout = parseInt(this.config.hookTimeout, 10) || 3000;
268
- if (this.config.hookTimeout && this.config.hookTimeout < 50) {
269
- this.config.hookTimeout = 50;
270
- }
271
-
272
- if (this.config.onlyAllowWhenUserIsOwner === undefined) {
273
- this.config.onlyAllowWhenUserIsOwner = false;
274
- }
275
-
276
- if (!this.config.defaultUser.match(/^system\.user\./)) {
277
- this.config.defaultUser = 'system.user.' + this.config.defaultUser;
278
- }
279
-
280
- // enable cors only if standalone
281
- !instanceSettings && this.app.use(cors());
282
- const jsonParser = bodyParser.json({
283
- limit: '100mb',
284
- });
285
- const rawParser = bodyParser.raw({
286
- limit: '100mb',
287
- });
288
-
289
- this.app.use((req, res, next) => {
290
- if (req.method !== 'GET' && req.url.startsWith('/v1/binary/')) {
291
- rawParser(req, res, next);
292
- } else {
293
- jsonParser(req, res, next);
294
- }
295
- });
296
- /*this.app.use(bodyParser.json({
297
- limit: '100mb',
298
- }));
299
- this.app.use(bodyParser.urlencoded({
300
- extended: true,
301
- parameterLimit: 100000,
302
- limit: '100mb',
303
- }));*/
304
-
305
- const _options = {
306
- appRoot: __dirname,
307
- };
308
-
309
- // prepare yaml
310
- _options.swaggerFile = `${__dirname}/api/swagger/swagger.yaml`;
311
- if (this.adapter.config.noCommands) {
312
- const newText = removeTextFromFile(_options.swaggerFile, '# commands start', '# commands stop');
313
-
314
- _options.swaggerFile = `${__dirname}/api/swagger/swaggerEdited.yaml`;
315
- if (!fs.existsSync(_options.swaggerFile) || fs.readFileSync(_options.swaggerFile).toString('utf8') !== newText) {
316
- fs.writeFileSync(_options.swaggerFile, newText);
317
- }
318
- } else if (this.adapter.config.noAdminCommands) {
319
- const newText = removeTextFromFile(_options.swaggerFile, '# admin commands start', '# admin commands end');
320
- _options.swaggerFile = `${__dirname}/api/swagger/swaggerEdited.yaml`;
321
- if (!fs.existsSync(_options.swaggerFile) || fs.readFileSync(_options.swaggerFile).toString('utf8') !== newText) {
322
- fs.writeFileSync(_options.swaggerFile, newText);
323
- }
324
- }
325
-
326
- // create swagger.yaml copy with changed basePath
327
- if (this.extension) {
328
- let file = fs.readFileSync(_options.swaggerFile).toString('utf8')
329
- file = file.replace('basePath: "/v1"', `basePath: "/${WEB_EXTENSION_PREFIX}v1"`);
330
-
331
- _options.swaggerFile = `${__dirname}/api/swagger/swagger_extension.yaml`;
332
-
333
- if (!fs.existsSync(_options.swaggerFile) || fs.readFileSync(_options.swaggerFile).toString('utf8') !== file) {
334
- fs.writeFileSync(_options.swaggerFile, file);
335
- }
336
- }
337
-
338
- let swaggerDocument = YAML.load(_options.swaggerFile);
339
- if (this.extension) {
340
- swaggerDocument.basePath = `/${WEB_EXTENSION_PREFIX}v1`;
341
- }
342
- const that = this;
343
-
344
-
345
- if (!this.config.noUI) {
346
- this.app.get(`${this.routerPrefix}api-docs/swagger.json`, (req, res) =>
347
- res.json(swaggerDocument));
348
-
349
- const options = {
350
- customCss: '.swagger-ui .topbar { background-color: #4dabf5; }',
351
- };
352
- // show WEB CSS and so on
353
- this.app.use(`${this.routerPrefix}api-doc/`, swaggerUi.serve, swaggerUi.setup(swaggerDocument, options));
354
- this.app.get(this.routerPrefix, (req, res) =>
355
- res.redirect(`${this.routerPrefix}api-doc/`));
356
- }
357
-
358
- function isAuthenticated(req, res, callback) {
359
- if (that.config.auth) {
360
- let values = parseQuery(req.url);
361
- if (!values.user || !values.pass) {
362
- if (req.headers.authorization && req.headers.authorization.startsWith('Basic ')) {
363
- const auth = Buffer.from(req.headers.authorization.substring(6), 'base64').toString('utf8');
364
- const pos = auth.indexOf(':');
365
- if (pos !== -1) {
366
- values = {
367
- user: auth.substring(0, pos),
368
- pass: auth.substring(pos + 1)
369
- };
370
- }
371
- }
372
- if (!values.user || !values.pass) {
373
- res.status(401).send({error: 'User is required'});
374
- return;
375
- }
376
- }
377
- if (!values.user.match(/^system\.user\./)) {
378
- values.user = `system.user.${values.user}`;
379
- }
380
-
381
- that.adapter.checkPassword(values.user, values.pass, result => {
382
- if (result) {
383
- req._user = values.user;
384
- // that.adapter.log.debug(`Logged in: ${values.user}`);
385
- callback(true);
386
- } else {
387
- callback = null;
388
- that.adapter.log.warn(`Invalid password or user name: ${values.user}`);
389
- res.status(401).send({error: `Invalid password or user name: ${values.user}`});
390
- }
391
- });
392
- } else if (callback) {
393
- req._user = that.config.defaultUser;
394
- callback();
395
- }
396
- }
397
-
398
- async function _validateUrlHook(item) {
399
- try {
400
- await axios.post(item.urlHook, {test: true}, {
401
- timeout: that.config.hookTimeout,
402
- validateStatus: status => status < 400
403
- });
404
- } catch (error) {
405
- if (error.response) {
406
- that.adapter.log.warn(`Cannot report to hook "${item.urlHook}": ${error.response.data || error.response.status}`);
407
- } else {
408
- that.adapter.log.warn(`Cannot report to hook "${item.urlHook}": ${JSON.stringify(error)}`);
409
- }
410
- item.errors = item.errors || 0;
411
- item.errors++;
412
- if (item.errors > 2) {
413
- that.adapter.log.warn(`3 errors by "${item.urlHook}": all subscriptions removed`);
414
- await that.unregisterSubscribe(item.urlHook, null, 'state');
415
- await that.unregisterSubscribe(item.urlHook, null, 'object');
416
- }
417
-
418
- return 'Cannot validate URL';
419
- }
420
- }
421
-
422
- async function reportChange(item, data) {
423
- if (item.polling) {
424
- if (item.promise) {
425
- item.promise.resolve(JSON.stringify(data));
426
- } else {
427
- item.queue = item.queue || [];
428
- const now = Date.now();
429
- item.queue.push({data: JSON.stringify(data), ts: now});
430
-
431
- // delete too old entries
432
- for (let d = item.queue.length - 1; d >= 0; d--) {
433
- if (now - item.queue[d].ts > 3000) {
434
- that.adapter.log.debug(`[${item.urlHook}] Data update skipped, as no handler (${d + 1})`);
435
- item.queue.splice(0, d);
436
- break;
437
- }
438
- }
439
- }
440
- } else {
441
- try {
442
- await axios.post(item.urlHook, data, {
443
- timeout: that.config.hookTimeout,
444
- validateStatus: status => status < 400
445
- });
446
- } catch (error) {
447
- if (error.response) {
448
- that.adapter.log.warn(`Cannot report to hook "${item.urlHook}": ${error.response.data || error.response.status}`);
449
- } else {
450
- that.adapter.log.warn(`Cannot report to hook "${item.urlHook}": ${JSON.stringify(error)}`);
451
- }
452
- item.errors = item.errors || 0;
453
- item.errors++;
454
- if (item.errors > 2) {
455
- that.adapter.log.warn(`3 errors by "${item.urlHook}": all subscriptions removed`);
456
- await that.unregisterSubscribe(item.urlHook, null, 'state');
457
- await that.unregisterSubscribe(item.urlHook, null, 'object');
458
- }
459
- }
460
- }
461
- }
462
-
463
- this._checkHooks = async () => {
464
- const hooks = Object.keys(this.subscribes);
465
- for (let i = 0; i < hooks.length; i++) {
466
- if (!this.subscribes[hooks[i]].polling) {
467
- await _validateUrlHook(this.subscribes[hooks[i]]);
468
- }
469
- }
470
- }
471
-
472
- this._executeGC = () => {
473
- const hashes = Object.keys(this.subscribes)
474
- .filter(urlHash => this.subscribes[urlHash].polling);
475
-
476
- if (!hashes.length) {
477
- clearInterval(this.gcInterval);
478
- this.gcInterval = null;
479
- } else {
480
- const now = Date.now();
481
- hashes.forEach(async urlHash => {
482
- // kill all subscriptions after 2 minutes
483
- if (now - this.subscribes[urlHash].ts > (this.subscribes[urlHash].timeout || 30000) * 1.5) {
484
- if (this.subscribes[urlHash].promise) {
485
- debugger;
486
- // this should never happen
487
- this.subscribes[urlHash].promise.resolve();
488
- }
489
- // unsubscribe
490
- if (this.subscribes[urlHash].state) {
491
- for (let i = 0; i < this.subscribes[urlHash].state.length; i++) {
492
- this.adapter.log.debug(`[${this.subscribes[urlHash].urlHook}] unsubscribe from state: ${this.subscribes[urlHash].state[i].id}`);
493
- await this.adapter.unsubscribeForeignStatesAsync(this.subscribes[urlHash].state[i].id);
494
- }
495
- }
496
- if (this.subscribes[urlHash].object) {
497
- for (let i = 0; i < this.subscribes[urlHash].object.length; i++) {
498
- this.adapter.log.debug(`[${this.subscribes[urlHash].urlHook}] unsubscribe from object: ${this.subscribes[urlHash].object[i].id}`);
499
- await this.adapter.unsubscribeForeignObjectsAsync(this.subscribes[urlHash].object[i].id);
500
- }
501
- }
502
-
503
- this.adapter.log.debug(`[${this.subscribes[urlHash].urlHook}] Destroy connection due inactivity`);
504
-
505
- delete this.subscribes[urlHash];
506
- }
507
- });
508
- }
509
- };
510
-
511
- this.startGC = () => {
512
- this.gcInterval = this.gcInterval || setInterval(() => this._executeGC(), 30000);
513
- }
514
-
515
- this.registerSubscribe = async (urlHook, id, type, user, options) => {
516
- if (typeof options === 'string') {
517
- options = {method: options};
518
- }
519
- if (options.delta) {
520
- options.delta = parseFloat(options.delta);
521
- } else {
522
- delete options.delta;
523
- }
524
- if (options.onchange) {
525
- options.onchange = options.onchange === true || options.onchange === 'true';
526
- } else {
527
- delete options.onchange;
528
- }
529
-
530
- const urlHash = crypto.createHash('md5').update(urlHook).digest('hex');
531
- if (!this.subscribes[urlHash]) {
532
- if (options.method !== 'polling') {
533
- const error = await _validateUrlHook({urlHook});
534
- if (error) {
535
- return `No valid answer from URL hook: ${error}`;
536
- }
537
- } else
538
- if (options.method === 'polling') {
539
- this.adapter.log.debug(`[${urlHook}] Subscribe on connection`);
540
- this.startGC();
541
- }
542
-
543
- this.subscribes[urlHash] = {
544
- state: [],
545
- object: [],
546
- timeout: 30000,
547
- urlHook,
548
- polling: options.method === 'polling',
549
- ts: Date.now()
550
- };
551
- }
552
-
553
- if (!this.subscribes[urlHash][type].find(item => item.id === id && (!item.method || item.method === options.method))) {
554
- const item = {id, delta: options.delta, onchange: options.onchange};
555
- this.subscribes[urlHash][type].push(item);
556
- if (item.id.includes('*')) {
557
- item.regEx = new RegExp(pattern2RegEx(item.id));
558
- }
559
-
560
- if (type === 'state') {
561
- this.adapter.log.debug(`[${urlHook}] Subscribe on state "${id}"`);
562
- await this.adapter.subscribeForeignStatesAsync(id, {user, limitToOwnerRights: adapter.config.onlyAllowWhenUserIsOwner});
563
- if (!item.regEx && (options.onchange || options.delta)) {
564
- item.val = await this.adapter.getForeignStateAsync(id, {user, limitToOwnerRights: adapter.config.onlyAllowWhenUserIsOwner});
565
- if (item.val) {
566
- item.val = item.val.val;
567
- } else {
568
- item.val = null;
569
- }
570
- }
571
- } else {
572
- this.adapter.log.debug(`[${urlHook}] Subscribe on object "${id}"`);
573
- await this.adapter.subscribeForeignObjectsAsync(id, {user, limitToOwnerRights: adapter.config.onlyAllowWhenUserIsOwner});
574
- }
575
- }
576
-
577
- if (this.config.checkInterval) {
578
- if (this.checkInterval) {
579
- this.adapter.log.debug(`start checker`);
580
- this.checkInterval = setInterval(async () => await this._checkHooks(), this.config.checkInterval);
581
- }
582
- } else {
583
- this.adapter.log.warn('No check interval set! The connections are valid forever.');
584
- }
585
-
586
- return null; // success
587
- };
588
-
589
- this.getSubscribes = async (urlHook, id, type) => {
590
- const urlHash = crypto.createHash('md5').update(urlHook).digest('hex');
591
- if (this.subscribes[urlHash]) {
592
- return this.subscribes[urlHash][type].map(item => item.id);
593
- } else {
594
- return null;
595
- }
596
- };
597
-
598
- this.unregisterSubscribe = async (urlHook, id, type, user, method) => {
599
- const urlHash = crypto.createHash('md5').update(urlHook).digest('hex');
600
- if (this.subscribes[urlHash]) {
601
- if (id) {
602
- let pos;
603
- do {
604
- pos = this.subscribes[urlHash][type].findIndex(item => item.id === id);
605
- if (pos !== -1) {
606
- this.subscribes[urlHash][type].splice(pos, 1);
607
- if (type === 'state') {
608
- this.adapter.log.debug(`Unsubscribe from state "${id}"`);
609
- await this.adapter.unsubscribeForeignStatesAsync(id, {user: user || 'system.user.admin', limitToOwnerRights: adapter.config.onlyAllowWhenUserIsOwner}); // allow unsubscribing always
610
- } else {
611
- this.adapter.log.debug(`Unsubscribe from object "${id}"`);
612
- await this.adapter.unsubscribeForeignObjectsAsync(id, {user: user || 'system.user.admin', limitToOwnerRights: adapter.config.onlyAllowWhenUserIsOwner}); // allow unsubscribing always
613
- }
614
- } else {
615
- break;
616
- }
617
- } while (pos !== -1)
618
- } else {
619
- for (let i = 0; i < this.subscribes[urlHash][type].length; i++) {
620
- if (type === 'state') {
621
- await this.adapter.unsubscribeForeignStatesAsync(this.subscribes[urlHash][type][i].id, {user: user || 'system.user.admin', limitToOwnerRights: adapter.config.onlyAllowWhenUserIsOwner}); // allow unsubscribing always
622
- } else {
623
- await this.adapter.unsubscribeForeignObjectsAsync(this.subscribes[urlHash][type][i].id, {user: user || 'system.user.admin', limitToOwnerRights: adapter.config.onlyAllowWhenUserIsOwner}); // allow unsubscribing always
624
- }
625
- }
626
- this.subscribes[urlHash][type] = [];
627
- }
628
-
629
- if (!this.subscribes[urlHash].state.length && !this.subscribes[urlHash].object.length) {
630
- delete this.subscribes[urlHash];
631
-
632
- if (!Object.keys(this.subscribes).length) {
633
- this.adapter.log.debug(`Stop checker`);
634
- this.checkInterval && clearInterval(this.checkInterval);
635
- this.checkInterval = null;
636
- }
637
- }
638
- }
639
-
640
- return null; // success
641
- };
642
-
643
- this.stateChange = (id, state) => {
644
- if (state && state.ack) {
645
- for (let t = this._waitFor.length - 1; t >= 0; t++) {
646
- if (this._waitFor[t].id === id) {
647
- const task = this._waitFor[t];
648
- this._waitFor.splice(t, 1);
649
- if (!Object.keys(this.subscribes).find(item => item.states.includes(id))) {
650
- this.adapter.unsubscribeForeignStates(id);
651
- }
652
-
653
- clearTimeout(task.timer);
654
-
655
- setImmediate((_task, _val) => {
656
- state.id = id;
657
- _task.res.json(state);
658
- }, task, state);
659
- }
660
- }
661
- }
662
-
663
- Object.keys(this.subscribes).forEach(urlHash => {
664
- this.subscribes[urlHash].state.forEach(async item => {
665
- // check if id
666
- if ((!item.regEx && item.id === id) || (item.regEx && item.regEx.test(id))) {
667
- if (state && item.delta && item.val !== null && Math.abs(item.val - state.val) < item.delta) {
668
- // ignore
669
- this.adapter.log.debug(`State change for "${id}" ignored as delta (${item.val} - ${state.val}) is less than ${item.delta}`);
670
- return;
671
- }
672
- if (state && item.onchange && !item.delta && item.val === state.val) {
673
- // ignore
674
- this.adapter.log.debug(`State change for "${id}" ignored as does not changed (${state.val})`);
675
- return;
676
- }
677
- if (state && (item.delta || item.onchange)) {
678
- // remember last value
679
- item.val = state.val;
680
- }
681
-
682
- await reportChange(this.subscribes[urlHash], {id, state});
683
- }
684
- });
685
- });
686
- };
687
-
688
- this.objectChange = (id, obj) => {
689
- Object.keys(this.subscribes).forEach(urlHash => {
690
- this.subscribes[urlHash].object.forEach(async item => {
691
- // check if id
692
- if ((!item.regEx && item.id === id) || (item.regEx && item.regEx.test(id))) {
693
- await reportChange(this.subscribes[urlHash].urlHook, {id, obj});
694
- }
695
- });
696
- });
697
- };
698
-
699
- // wait for ack=true
700
- this._waitFor = [];
701
- this.adapter._addTimeout = async task => {
702
- this._waitFor.push(task);
703
-
704
- // if not already subscribed
705
- if (!Object.keys(this.subscribes).find(item => item.states.includes(task.id))) {
706
- await this.adapter.subscribeForeignStates(task.id);
707
- }
708
-
709
- task.timer = setTimeout(_task => {
710
- // remove this task from the list
711
- const pos = this._waitFor.indexOf(_task);
712
- if (pos !== -1) {
713
- this._waitFor.splice(pos, 1);
714
- }
715
-
716
- if (!Object.keys(this.subscribes).find(item => item.states.includes(task.id))) {
717
- this.adapter.unsubscribeForeignStates(task.id);
718
- }
719
- _task.res.status(501).json({error: 'timeout', id: _task.id, val: _task.val});
720
- _task = null;
721
- }, task.timeout, task);
722
- };
723
-
724
- this.app.get('/favicon.ico', (req, res) => {
725
- res.set('Content-Type', 'image/x-icon');
726
- res.send(fs.readFileSync(`${__dirname}/../img/favicon.ico`));
727
- });
728
-
729
- // authenticate
730
- this.app.use(`${this.routerPrefix}v1/*`, (req, res, next) => {
731
- isAuthenticated(req, res, () => {
732
- req._adapter = this.adapter;
733
- req._swaggerObject = this;
734
- next();
735
- });
736
- });
737
-
738
- this.app.get(`${this.routerPrefix}v1/polling`, (req, res, next) => {
739
- res.writeHead(200, {'Content-Type': 'text/plain'});
740
- const ip = req.query.sid || req.headers['x-forwarded-for'] || req.socket.remoteAddress;
741
- const urlHash = crypto.createHash('md5').update(ip).digest('hex');
742
-
743
- let item = this.subscribes[urlHash];
744
-
745
- this.startGC();
746
-
747
- if (req.query.check === 'true' || req.query.connect === 'true' || req.query.connect === '') {
748
- if (!item) {
749
- this.adapter.log.debug(`[${ip}] Initiate connection`);
750
- this.subscribes[urlHash] = {state: [], object: [], urlHook: ip, polling: true, timeout: parseInt(req.query.timeout, 10) || 30000, ts: Date.now()};
751
- item = this.subscribes[urlHash];
752
- } else if (req.query.timeout) {
753
- item.timeout = parseInt(req.query.timeout, 10);
754
- }
755
- if (item.timeout < 1000) {
756
- item.timeout = 1000;
757
- } else
758
- if (item.timeout > 60000) {
759
- item.timeout = 60000;
760
- }
761
- return res.end('_');
762
- } else if (!item) {
763
- this.subscribes[urlHash] = {state: [], object: [], urlHook: ip, polling: true, timeout: parseInt(req.query.timeout, 10) || 30000, ts: Date.now()};
764
- item = this.subscribes[urlHash];
765
- } else {
766
- item.ts = Date.now();
767
- }
768
-
769
- // If some data wait to be sent
770
- if (item.queue && item.queue.length) {
771
- // delete too old entries
772
- const now = Date.now();
773
- for (let d = item.queue.length - 1; d >= 0; d--) {
774
- if (now - item.queue[d].ts > 3000) {
775
- that.adapter.log.debug(`[${item.urlHook}] Data update was too old and ignored`);
776
- item.queue.splice(0, d);
777
- break;
778
- }
779
- }
780
- if (item.queue.length) {
781
- const chunk = item.queue.shift();
782
- res.end(chunk.data);
783
- return;
784
- }
785
- }
786
-
787
- new Promise(resolve => {
788
- item.promise = {
789
- resolve,
790
- timer: setTimeout(() => {
791
- if (item.promise) {
792
- // could never happen
793
- item.promise.timer = null;
794
- }
795
- resolve();
796
- }, item ? item.timeout : 30000)};
797
- })
798
- .then(data => {
799
- if (item.promise) {
800
- item.promise.timer && clearTimeout(item.promise.timer);
801
- item.promise.timer = null;
802
- item.promise.resolve = null;
803
- item.promise = null;
804
- } else {
805
- debugger;
806
- }
807
- res.end(data);
808
- });
809
-
810
- req.on('error', error => {
811
- if (!error.message.includes('aborted')) {
812
- this.adapter.log.warn(`[${item && item.urlHook}]Error in polling connection: ${error}`);
813
- }
814
- if (item.promise) {
815
- item.promise.timer && clearTimeout(item.promise.timer);
816
- item.promise.timer = null;
817
- item.promise.resolve = null;
818
- item.promise = null;
819
- }
820
- });
821
- });
822
-
823
- this.app.use(`${this.routerPrefix}v1/command/*`, async (req, res) => {
824
- if (this.adapter.config.noCommands) {
825
- res.status(404).json({error: `Commands are disabled`});
826
- return;
827
- }
828
-
829
- let command = req.originalUrl.startsWith(`/${WEB_EXTENSION_PREFIX}`) ? req.originalUrl.split('/')[4] : req.originalUrl.split('/')[3];
830
- command = command.split('?')[0];
831
-
832
- const handler = this.commands.getCommandHandler(command);
833
- if (handler && command !== 'cmdExec' && command !== 'sendToHost') {
834
- const args = common.getParamNames(handler).map(item => item[0] === '_' ? item.substring(1) : item);
835
-
836
- args.shift(); // remove socket
837
- // try to parse a query or body
838
- let params = parseQuery(req.originalUrl);
839
- if (req.body) {
840
- Object.assign(params, req.body);
841
- }
842
- if (common.DEFAULT_VALUES[command]) {
843
- params = Object.assign({}, common.DEFAULT_VALUES[command], params);
844
- }
845
-
846
- // check if all parameters are set
847
- const problem = args.find(name =>
848
- name !== 'callback' &&
849
- name !== 'options' &&
850
- name !== 'adapterName' &&
851
- name !== 'update' &&
852
- !params.hasOwnProperty(name)
853
- );
854
-
855
- if (problem) {
856
- res.status(422).json({error: `Argument '${problem}' not found. Following arguments are expected: '${args.join("', '")}'`});
857
- return;
858
- }
859
-
860
- const _acl = {
861
- user: req._user,
862
- };
863
-
864
- const _arguments = [{_acl}];
865
- let error = '';
866
- args.forEach(name => {
867
- if (name !== 'callback') {
868
- if (name === 'options' || name === 'params' || name === 'obj' || name === 'message') {
869
- // try to convert
870
- if (typeof params[name] === 'string' && params[name].startsWith('{') && params[name].endsWith('}')) {
871
- try {
872
- params[name] = JSON.parse(params[name]);
873
- } catch (_error) {
874
- error = `Cannot parse ${name}: ${_error}`;
875
- return;
876
- }
877
- }
878
- }
879
-
880
- if (name === 'update' && params[name] === undefined) {
881
- params[name] = false;
882
- }
883
-
884
- if (params[name] === 'true') {
885
- params[name] = true;
886
- } else if (params[name] === 'false') {
887
- params[name] = false;
888
- }
889
- _arguments.push(params[name]);
890
- }
891
- });
892
-
893
- // try to convert arguments for setState and setForeignState
894
- if (command === 'setState' || command === 'setForeignState') {
895
- // read object
896
- try {
897
- const obj = await adapter.getForeignObjectAsync(_arguments[1], {user: req._user});
898
- if (obj && obj.common && obj.common.type) {
899
- if (obj.common.type === 'number') {
900
- _arguments[2] = parseFloat(_arguments[2]);
901
- } else if (obj.common.type === 'boolean') {
902
- _arguments[2] = _arguments[2] === 'true' || _arguments[2] === true || _arguments[2] === 1 || _arguments[2] === '1' || _arguments[2] === 'on' || _arguments[2] === 'ON';
903
- } else if (obj.common.type === 'string') {
904
- _arguments[2] = _arguments[2].toString();
905
- }
906
- }
907
- } catch (error) {
908
- res.status(501).json({error});
909
- return;
910
- }
911
- }
912
-
913
- if (!error) {
914
- if (args[args.length - 1] === 'callback') {
915
- _arguments.push((error, ...args) => {
916
- if (command === 'sendTo') {
917
- res.json(error);
918
- } else if (command === 'getHostByIp') {
919
- res.json({ip: error, result: args[0]});
920
- } else
921
- if (command === 'authEnabled') {
922
- res.json({secure: error, user: args[0]});
923
- } else
924
- if (args.length === 1) {
925
- res.status(error ? 500 : 200).json({error, result: args[0]});
926
- } else {
927
- // if file
928
- if ((params.binary === null || params.binary === true || params.binary === 'true') && Buffer.isBuffer(args[0])) {
929
- if (args[1] && typeof args[1] === 'string' && args[1].includes('/')) {
930
- res.set('Content-Type', args[1]);
931
- }
932
- res.send(args[0]);
933
- } else if ((params.binary === null || params.binary === true || params.binary === 'true') && Buffer.isBuffer(args[1])) {
934
- res.send(args[1]);
935
- } else {
936
- res.status(error ? 500 : 200).json({error, results: args});
937
- }
938
- }
939
- });
940
- handler.apply(null, _arguments);
941
- } else {
942
- handler.apply(null, _arguments);
943
- // just execute. No callback
944
- res.json({result: 'OK'});
945
- }
946
- } else {
947
- res.status(422).json({error});
948
- }
949
- } else {
950
- res.status(404).json({error: `Command ${command} not found`});
951
- }
952
- });
953
-
954
- this.app.get(`${this.routerPrefix}log/*`,(req, res) => {
955
- let parts = decodeURIComponent(req.url).split('/');
956
- if (req.originalUrl.startsWith(`/${WEB_EXTENSION_PREFIX}`)) {
957
- parts.shift();
958
- }
959
-
960
- if (parts.length === 5) {
961
- parts.shift();
962
- parts.shift();
963
- const [host, transport] = parts;
964
- parts = parts.splice(2);
965
- let filename = parts.join('/');
966
- this.adapter.sendToHost(`system.host.${host}`, 'getLogFile', {filename, transport}, result => {
967
- if (!result || result.error) {
968
- res.status(404).send(`File ${escapeHtml(filename)} not found`);
969
- } else {
970
- if (result.gz) {
971
- if (result.size > 1024 * 1024) {
972
- res.header('Content-Type', 'application/gzip');
973
- res.send(result.data);
974
- } else {
975
- try {
976
- unzipFile(filename, result.data, res);
977
- } catch (e) {
978
- res.header('Content-Type', 'application/gzip');
979
- res.send(result.data);
980
- adapter.log.error(`Cannot extract file ${filename}: ${e}`);
981
- }
982
- }
983
- } else if (result.data === undefined || result.data === null) {
984
- res.status(404).send(`File ${escapeHtml(filename)} not found`);
985
- } else if (result.size > 2 * 1024 * 1024) {
986
- res.header('Content-Type', 'text/plain');
987
- res.send(result.data);
988
- } else {
989
- res.header('Content-Type', 'text/html');
990
- res.send(decorateLogFile(null, result.data));
991
- }
992
- }
993
- });
994
- } else {
995
- parts = parts.splice(2);
996
- const transport = parts.shift();
997
- let filename = parts.join('/');
998
- const config = adapter.systemConfig;
999
-
1000
- // detect file log
1001
- if (config && config.log && config.log.transport) {
1002
- if (config.log.transport.hasOwnProperty(transport) && config.log.transport[transport].type === 'file') {
1003
- let logFolder;
1004
- if (config.log.transport[transport].filename) {
1005
- parts = config.log.transport[transport].filename.replace(/\\/g, '/').split('/');
1006
- parts.pop();
1007
- logFolder = path.normalize(parts.join('/'));
1008
- } else {
1009
- logFolder = path.join(process.cwd(), 'log');
1010
- }
1011
-
1012
- if (logFolder[0] !== '/' && logFolder[0] !== '\\' && !logFolder.match(/^[a-zA-Z]:/)) {
1013
- const _logFolder = path.normalize(path.join(`${__dirname}/../../../`, logFolder).replace(/\\/g, '/')).replace(/\\/g, '/');
1014
- if (!fs.existsSync(_logFolder)) {
1015
- logFolder = path.normalize(path.join(`${__dirname}/../../`, logFolder).replace(/\\/g, '/')).replace(/\\/g, '/');
1016
- } else {
1017
- logFolder = _logFolder;
1018
- }
1019
- }
1020
-
1021
- filename = path.normalize(path.join(logFolder, filename).replace(/\\/g, '/')).replace(/\\/g, '/');
1022
-
1023
- if (filename.startsWith(logFolder) && fs.existsSync(filename)) {
1024
- const stat = fs.lstatSync(filename);
1025
- // if file is archive
1026
- if (filename.toLowerCase().endsWith('.gz')) {
1027
- // try to not process to big files
1028
- if (stat.size > 1024 * 1024/* || !fs.existsSync('/dev/null')*/) {
1029
- res.header('Content-Type', 'application/gzip');
1030
- res.sendFile(filename);
1031
- } else {
1032
- try {
1033
- unzipFile(filename, fs.readFileSync(filename), res);
1034
- } catch (e) {
1035
- res.header('Content-Type', 'application/gzip');
1036
- res.sendFile(filename);
1037
- adapter.log.error(`Cannot extract file ${filename}: ${e}`);
1038
- }
1039
- }
1040
- } else if (stat.size > 2 * 1024 * 1024) {
1041
- res.header('Content-Type', 'text/plain');
1042
- res.sendFile(filename);
1043
- } else {
1044
- res.header('Content-Type', 'text/html');
1045
- res.send(decorateLogFile(filename));
1046
- }
1047
-
1048
- return;
1049
- }
1050
- }
1051
- }
1052
-
1053
- res.status(404).send(`File ${escapeHtml(filename)} not found`);
1054
- }
1055
- });
1056
-
1057
- // parse binary files
1058
- this.app.post(`${this.routerPrefix}v1/file/*`, multer().fields([{ name: 'file', maxCount: 1 }]), (req, res, next) => next());
1059
-
1060
- this.unload = async () => {
1061
- if (this.config.webInstance) {
1062
- await this.adapter.setForeignStateAsync(`${this.namespace}.info.extension`, false, true);
1063
- }
1064
- this.checkInterval && clearInterval(this.checkInterval);
1065
- this.checkInterval = null;
1066
- this.gcInterval && clearInterval(this.gcInterval);
1067
- this.gcInterval = null;
1068
- // send to all hooks, disconnect event
1069
-
1070
- const hooks = Object.keys(this.subscribes);
1071
- for (let h = 0; h < hooks.length; h++) {
1072
- await reportChange(this.subscribes[hooks[h]], {disconnect: true});
1073
- }
1074
- this.subscribes = {};
1075
- }
1076
-
1077
- // deliver to web the link to Web interface
1078
- this.welcomePage = () => {
1079
- return {
1080
- link: WEB_EXTENSION_PREFIX,
1081
- name: 'REST-API',
1082
- img: 'adapter/rest-api/rest-api.png',
1083
- color: '#157c00',
1084
- order: 10,
1085
- pro: false
1086
- };
1087
- }
1088
-
1089
- // Say to web instance to wait till this instance is initialized
1090
- this.waitForReady = cb => {
1091
- callback = cb;
1092
- }
1093
-
1094
- // read default history
1095
- if (!this.config.dataSource) {
1096
- this.adapter.getForeignObjectAsync('system.config')
1097
- .then(obj => {
1098
- if (obj && obj.common && obj.common.defaultHistory) {
1099
- this.config.dataSource = obj.common.defaultHistory;
1100
- }
1101
- });
1102
- }
1103
-
1104
- this.adapter.WEB_EXTENSION_PREFIX = WEB_EXTENSION_PREFIX.replace('/', '');
1105
-
1106
- SwaggerRunner.create(_options, (err, swaggerRunner) => {
1107
- if (this.config.webInstance) {
1108
- this.adapter.setForeignState(this.namespace + '.info.extension', true, true);
1109
- }
1110
- if (err) {
1111
- throw err;
1112
- }
1113
-
1114
- this.swagger = swaggerRunner;
1115
-
1116
- // install middleware
1117
- swaggerRunner.expressMiddleware().register(this.app);
1118
-
1119
- callback && callback(this.app);
1120
- });
1121
- }
1122
-
1123
- module.exports = SwaggerUI;