From 3af4f5f884f8eebcffeee1ce89e190ca96a4daeb Mon Sep 17 00:00:00 2001 From: zhongjin Date: Sat, 21 Jul 2018 14:50:16 +0800 Subject: [PATCH] Initial commit --- .gitignore | 7 + .npmignore | 9 + .travis.yml | 23 + LICENSE | 22 + README.md | 477 +++++++++++ admin/index.html | 159 ++++ admin/index_m.html | 208 +++++ admin/socketio.png | Bin 0 -> 1423 bytes admin/words.js | 22 + appveyor.yml | 25 + example/conn.js | 1262 ++++++++++++++++++++++++++++ example/index.html | 53 ++ gulpfile.js | 401 +++++++++ io-package.json | 204 +++++ lib/socket.js | 1719 ++++++++++++++++++++++++++++++++++++++ lib/utils.js | 83 ++ main.js | 141 ++++ package.json | 44 + tasks/jscs.js | 19 + tasks/jscsRules.js | 36 + tasks/jshint.js | 19 + test/lib/setup.js | 728 ++++++++++++++++ test/testAdapter.js | 142 ++++ test/testPackageFiles.js | 91 ++ 24 files changed, 5894 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 admin/index.html create mode 100644 admin/index_m.html create mode 100644 admin/socketio.png create mode 100644 admin/words.js create mode 100644 appveyor.yml create mode 100644 example/conn.js create mode 100644 example/index.html create mode 100644 gulpfile.js create mode 100644 io-package.json create mode 100644 lib/socket.js create mode 100644 lib/utils.js create mode 100644 main.js create mode 100644 package.json create mode 100644 tasks/jscs.js create mode 100644 tasks/jscsRules.js create mode 100644 tasks/jshint.js create mode 100644 test/lib/setup.js create mode 100644 test/testAdapter.js create mode 100644 test/testPackageFiles.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..319e01a --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +.idea +tmp +admin/i18n/flat.txt +admin/i18n/*/flat.txt +package-lock.json +iob_npm.done \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..b04f18e --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +gruntfile.js +example +tasks +test +.travis.yml +appveyor.yml +admin/i18n +package-lock.json +iob_npm.done diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5a5145d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +os: + - linux + - osx +language: node_js +node_js: + - '4' + - '6' + - '8' + - '10' +before_script: + - export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1) + - 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi' + - npm -v + - npm install winston@2.3.1 + - 'npm install https://git.spacen.net/yunkong2/yunkong2.js-controller/tarball/master --production' +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9362a32 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014-2018 bluefox + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f1a0c9 --- /dev/null +++ b/README.md @@ -0,0 +1,477 @@ +![Logo](admin/socketio.png) +# yunkong2 socket.io +================= + +[![NPM version](http://img.shields.io/npm/v/yunkong2.socketio.svg)](https://www.npmjs.com/package/yunkong2.socketio) +[![Downloads](https://img.shields.io/npm/dm/yunkong2.socketio.svg)](https://www.npmjs.com/package/yunkong2.socketio) + +[![NPM](https://nodei.co/npm/yunkong2.socketio.png?downloads=true)](https://nodei.co/npm/yunkong2.socketio/) + + +This adapter used by some WEB applications and adapters to communicate with yunkong2. + +Users can use this adapter to connect their products to yunkong2 via web sockets. Actually this adapter is used by Flot, Rickshaw, Vis and mobile to extract data from yunkong2. + +You can find in the example [directory](https://git.spacen.net/yunkong2/yunkong2.socketio/tree/master/example) simple application that uses this interface to show some data. + +By using of socket.io interface user should understand the [basics and concept](https://git.spacen.net/yunkong2/yunkong2) of the system. + +It is useful to read about the [structure of the objects](https://git.spacen.net/yunkong2/yunkong2/blob/master/doc/SCHEMA.md) too. + +## Brief description of concept +### Object +Object is description of data point or group. Group could content other datapoints in this case it called channel. If group consists of other channels in this case it called device. + +Object is meta information that describes data point and could content: max/min value, unit, name, default value, type of value, information for adapter for communication (e.g. ip address) and so on. + +### State +State is actual value of the data point and presented by javascript object: +``` +{ + val: VALUE, + ack: ACKNOWLEDGED, + ts: TIMESTAMP, // could be converted into time with "new Date(state.ts)" (In older version of js-controller - "new Date(state.ts * 1000)") + lc: TIMESTAMP of last change, + from: ADAPTER_NAME, + q: QUALITY +} +``` + +States change itself very frequently in compare to objects. (Normally objects should be changed once by creation and that's all) + +### Acknowledgment +Every state has attribute "ack". It shows the direction of command. +- If ack=false, it means some other adapter wants to control (write) this variable, so that command will be executed (e.g. light will be switched on). +- If ack=true, it means that device informs about new value. (e.g. light was switched on manually or motion was detected) + +**Example**: we have some home automation adapter (HAA) that has one lamp connected under address *haa.0.lamp1*. +- Lamp can be switched on manually with physical switch or via wifi with he help of HAA. +- If vis wants to switch the lamp on via wifi it should set the new value with ```{value: true, ack: false}```. +- When the lamp is switched on it is normally inform HAA about new state and the value should be immediately overwritten with ```{value: true, ack: true}```. +- If the lamp is switched off manually via physical switch it informs HAA about new state with ```{value: false, ack: true}```. + +### Quality +Every data point has attribute **q** - *quality*. + + +## Usage +It is suggested to use example/conn.js for communication. + +After inclusion of conn.js file the global object **servConn** could be used to establish the communication with socketio adapter. + +**servConn** object has hollowing methods: + +### init +- function (connOptions, connCallbacks, objectsRequired) + +**connOptions** - is optional parameter: + +``` +connOptions = { + name: 'name of the connection', // optional - default 'vis.0', used to distinguish connections in socket-io adapter. + connLink: 'http://localhost:8084', // optional - URL of the socket.io adapter. By default it is same URL where the WEB server is. + socketSession: '' // optional - default 'nokey', and used by authentication +}; +``` + +You can pass these parameters by defining the global variables before call of "init" too: + +``` +var socketUrl = 'http://localhost:8084'; // is connOptions.connLink +var socketSession = ''; // is connOptions.socketSession +servConn.namespace = 'myapp'; // is connOptions.name +``` + +**connCallbacks** - object with callbacks: + +``` +connCallbacks = { + onConnChange: function (isConnected) {}, // optional - called if connection state changed. + onObjectChange: function (id, obj) {}, // optional - called if content of some object is changed, new object created or object was deleted (obj = null) + onUpdate: function (id, state) {}, // optional - called if state of some object is changed, new state for object is created or state was deleted (state = null) + onError: function (error) {} // optional - called if some error occurs +}; +``` + +### setState +- function (pointId, value, callback) + +set new value of some data point. + +E.g. ```servConn.setState('adapter.0.myvalue', true)``` writes ```{val: true, ack: false}``` into *adapter.0.myvalue*. + +- **pointId** - is ID of the state, like *adapter.0.myvalue*, +- **value** - new value of the state, could be simple value (string, number, boolean) or object like ```{val: newValue, ack: false, q: 0}```. +In case if used simple value, "ack" will be set to "false". +- **callback** - ```function (error) {}``` - called when the write of new value into DB is performed (not when the device was controlled). + + +### getStates +- function (IDs, callback) + +get the states of more than one state. This command normally is called after the connection is established to get the actual states of used data points. + +- **IDs** - pattern or array with IDs. Could be omitted to get all states. Patterns could have wildcards, like: '*.STATE', 'haa.0.*' +- **callback** - ```function (error, states) {}``` - *states* is object like ```{'id1': 'state1', 'id2': 'state2', ...}```. *stateX* are objects with the structure described [above](#state). + +### httpGet +- function (url, callback) + +calls this URL from PC, where socketio adapter runs. +- **url** - is address to call. +- **callback** - ```function (data) {}``` - result of the request (html body). + +### logError +- function (errorText) + +writes error message into controller's log. + +### getConfig +- function (callback) + +reads controller configuration like language, temperature units, point or comma delimiter in floats, date format. + +- **callback** - ```function (err, config) {}``` - config looks like: + +``` +{ + "_id": "system.config", + "type": "config", + "common": { + "name": "System configuration", + "language": "de", + "tempUnit": "°C", + "currency": "€", + "dateFormat": "DD.MM.YYYY", + "isFloatComma": true, + "licenseConfirmed": true, + "activeRepo": "fast-online", + "diag": "extended", + "defaultHistory": "" + } +} +``` + +### getObject +- function (id, callback) + +read specific object from DB. With this function the meta information of some object could be read. + +- **id** - id of the state, like "haa.0.light1", +- **callback** - ```function (error, obj)``` - obj looks like: + +``` +{ + "_id": "haa.0.light1", + "type": "state", + "common": { + "def": false, + "type": "boolean", + "read": false, + "write": true, + "role": "switch", + "name": "light in floor" + }, + "native": { + "CONTROL": "BUTTON.LONG", + "DEFAULT": false, + "FLAGS": 1, + "ID": "PRESS_LONG", + "MAX": true, + "MIN": false, + "OPERATIONS": 6, + "TAB_ORDER": 1, + "TYPE": "ACTION", + "UNIT": "" + }, + "enums": ['enum.rooms.floor'], + "acl": { + "object": 1638, + "state": 1638 + } +} +``` + +### getObjects +- function (callback) + +read all objects from DB. + +- **callback** - ```function (error, objs)``` - objs looks like: ```{'id1': 'object1', 'id2': 'object2', ...}``` + +### readDir +- function (dirName, callback) + +reads files and directories in specified directory. + +Files are stored in DB (or similar) and normally should not be accessed directly. File name consist of path, filename and file extension, like "/mobile.0/data/fileName.txt". + +- dirName - name of the directory like */mobile.0/data* +- callback - ```function (error, list)``` - list looks like: + +``` +[ + { + file: 'file1.txt', + stats: { + mode: 33188, + size: 527, + atime: Mon, 10 Oct 2011 23:24:11 GMT, + mtime: Mon, 10 Oct 2011 23:24:11 GMT, + ctime: Mon, 10 Oct 2011 23:24:11 GMT, + birthtime: Mon, 10 Oct 2011 23:24:11 GMT + }, + isDir: false, + modifiedAt: timeInMs, // new Date().getTime() + createdAt: timeInMs, // new Date().getTime() + }, + { + file: 'main', + stats: { + mode: 33188, + atime: Mon, 10 Oct 2011 23:24:11 GMT, + mtime: Mon, 10 Oct 2011 23:24:11 GMT, + ctime: Mon, 10 Oct 2011 23:24:11 GMT, + birthtime: Mon, 10 Oct 2011 23:24:11 GMT + }, + isDir: true, + modifiedAt: timeInMs, // new Date().getTime() + createdAt: timeInMs, // new Date().getTime() + }, + ... +] +``` + +### mkdir +- function (dirName, callback) + +- **callback** - ```function (error) {}``` + +### unlink +- function (name, callback) + +deletes file or directory. Directory must be empty to be deleted. + +- dirName - name of the directory or file like */mobile.0/data*. +- **callback** - ```function (error) {}``` + +### readFile +- function (filename, callback) + +- **callback** - ```function (error, fileData, mimeType)``` + +### readFile64 +- function (filename, callback) + +- **callback** - ```function (error, data)``` - data is ```{mime: mimeType, data: base64data}``` + +### writeFile +- function (filename, data, mode, callback) + +- **callback** - ```function (error) {}``` + +### writeFile64 +- function (filename, data, mode, callback) + +- **callback** - ```function (error) {}``` + +### renameFile +- function (oldName, newName, callback) + +- **callback** - ```function (error) {}``` + +### getHistory +- function (instance, options, callback) + +- **callback** - ```function (error, data, step, sessionId) {}``` + +### requireLog +- function (isRequire, callback) + +activates/deactivates log receiving for this socket. + +- **callback** - ```function (error) {}``` + +### authEnabled +- function () + +reads if the authentication is enabled and which user is logged in + +- **callback** - ```function (authEnabled, currentUser) {}``` + +If authentication is enabled, so current logged in user will be returned, if auth is disabled, so the default user "running as" will be returned. + +## Tuning Web-Sockets +On some web-sockets clients there is performance problem with communication. Sometimes this problem is due to fallback of socket.io communication on long polling mechanism. +You can set option *Force Web-Sockets* to force using only web-sockets transport. + +## Changelog +### 2.1.1 (2018-06-09) +* (bluefox) Used socket.io Version 1.7.2 +* (bluefox) Fix authentication problem + +### 2.1.0 (2018-05-04) +* (bluefox) Used socket.io Version 1.7.4 + +### 2.0.1 (2018-02-28) +* (bluefox) Dropped support of old browsers. Please do not update if you have iPad 1 and so on. + +### 1.9.0 (2018-01-14) +* (bluefox) Ready for admin3 + +### 1.8.7 (2017-11-29) +* (bluefox) Tune cloud work + +### 1.8.5 (2017-10-22) +* (bluefox) Escape [] in subscriptions + +### 1.8.4 (2017-10-16) +* (bluefox) Check callback validity + +### 1.8.3 (2017-10-09) +* (bluefox) Allow authentication via URL + +### 1.8.2 (2017-09-20) +* (bluefox) Fix cmdExec command + +### 1.8.1 (2017-09-13) +* (bluefox) Fix user access rights for sendToHost + +### 1.8.0 (2017-08-06) +* (bluefox) Support the access to admin via yunkong2.pro + +### 1.7.5 (2017-05-24) +* (bluefox) fix error if subscribe is empty + +### 1.7.4 (2017-01-04) +* (bluefox) fix error with authentication + +### 1.7.3 (2016-11-13) +* (bluefox) support of socket extensions + +### 1.7.2 (2016-11-06) +* (bluefox) Fix unsubscribe of states + +### 1.7.1 (2016-10-11) +* (bluefox) Fix authentication for app + +### 1.7.0 (2016-08-30) +* (bluefox) сompatible only with new admin + +### 1.6.1 (2016-08-29) +* (bluefox) fix error by checking user name + +### 1.6.0 (2016-08-27) +* (bluefox) support of letsencrypt certificates + +### 1.5.4 (2016-08-26) +* (bluefox) fix error in socket.js + +### 1.5.3 (2016-08-14) +* (bluefox) support of force only web sockets transport + +### 1.5.2 (2016-07-06) +* (bluefox) support of chained certificates + +### 1.5.1 (2016-06-28) +* (bluefox) add sendToHost command + +### 1.5.0 (2016-06-17) +* (bluefox) preparations for cloud + +### 1.4.1 (2016-05-13) +* (bluefox) change getHistory function + +### 1.4.0 (2016-04-24) +* (bluefox) encode json files + +### 1.3.0 (2016-03-17) +* (bluefox) rename files + +### 1.2.3 (2015-12-24) +* (bluefox) support of authentication over URL + +### 1.2.2 (2015-12-09) +* (bluefox) remove unused parameter "cache" + +### 1.2.0 (2015-11-15) +* (bluefox) add version compatibility check + +### 1.1.0 (2015-11-14) +* (Smiling_Jack) add getHistory + +### 1.0.0 (2015-09-30) +* (bluefox) stop adapter before update + +### 0.4.5 (2015-08-11) +* (bluefox) update packets + +### 0.4.4 (2015-07-07) +* (bluefox) extend writeFile with mode + +### 0.4.3 (2015-07-06) +* (bluefox) add chmodFile + +### 0.4.1 (2015-06-13) +* (bluefox) add default ttl +* (bluefox) enable run from "web" and add permissions check + +### 0.4.0 (2015-06-13) +* (bluefox) add permissions support + +### 0.3.1 (2015-05-19) +* (bluefox) support of subscribe on objectChanged + +### 0.3.0 (2015-04-23) +* (bluefox) enable security + +### 0.2.3 (2015-03-07) +* (bluefox) extend getStates to support list of objects + +### 0.2.2 (2015-02-14) +* (bluefox) fix error with objectChanged event + +### 0.2.0 (2015-01-16) +* (bluefox) make socket usable as module + +### 0.1.6 (2015-01-08) +* (bluefox) support of subscribe for different sockets. Support of socket names. Diagnostic info in socket.0.connected + +### 0.1.5 (2015-01-07) +* (bluefox) fix error with update of states and objects + +### 0.1.4 (2015-01-06) +* (bluefox) support of file manager in vis + +### 0.1.3 (2015-01-02) +* (bluefox) enable adapter by default + +### 0.1.2 (2015-01-02) +* (bluefox) add "request" module to package.json + +### 0.1.1 (2015-01-02) +* (bluefox) enable npm install + +### 0.1.0 (2014-12-28) +* (bluefox) support of read/write files + +### 0.0.5 (2014-12-19) +* (bluefox) support of setObjects command + +### 0.0.4 (2014-12-10) +* (bluefox) support of https sockets + +### 0.0.3 (2014-12-05) +* (bluefox) support of https sockets + +### 0.0.2 (2014-11-24) +* (bluefox) fix error by start + +### 0.0.1 (2014-10-10) +* (bluefox) authentication works + +## License + +The MIT License (MIT) + +Copyright (c) 2014-2018 bluefox \ No newline at end of file diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..7a9aee0 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + +
+ +

socket.io adapter settings

+ + + + + + + + + + + + + + + + + + + + + + + + + +
 

Let's Encrypt settings

+
+ + diff --git a/admin/index_m.html b/admin/index_m.html new file mode 100644 index 0000000..95a8d4c --- /dev/null +++ b/admin/index_m.html @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + Force Web-Sockets: +
+
+
+
+
+
+ +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + diff --git a/admin/socketio.png b/admin/socketio.png new file mode 100644 index 0000000000000000000000000000000000000000..b1ae4b79b13865b69a7c225696eb03d4becdb881 GIT binary patch literal 1423 zcmV;A1#tR_P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E;Eegmrwuz1prAzK~#8N?Og3OEI|-m2_OPQfCvzQ-3Sl? zB0vO)01+U=eecezrnlx+PtRUj|g#294Fon*-6vl}U7oQsQ#YKGAmicxQGZ|TPxGETKX7y#lE8>^$JrGaG}xKzt$ zB*Z~0HYIa6E5|{{qS8DpBdxu=SqIEbGLC~~B%~{%G7bo2tU*vzHV&4No}Zt`&&i5A zt(|ia1F_QO#~`4~<}^AUS*~?|f8UgGuzQZ3+|$z&om;GtJFo_j9gDaLEEbDyTvHNL z?Gz;kO9}SCLi*TL{e8*Vij!TF13~e0+SYwCgD1Rovd*#w!;DJ9vsH3D_!vEkyha zR^zE1dsrY%eK2ORh?Z-$68Qc7UBy7wAv0HHQ1=6h=pP;)9(oG3f^(?oB`V3m+<3*! z&d$zy3N_pded=*I=Bzm}H-k+?+=hFFs(>Ym?yO%CMfhN5uz2HB)UqPjuNIVwKcR}B zBTA>Do)v7n@5&%7K$j@Mhk~(V$=gwOfQ4>P3JPgYLiE$u2K1djdpt zc`iAIu4Eibxt4>uld&y6r-53<>+5UJ4qF2$Xgh2Tq@eAvHIRa~LuVjX<|uv4h}rVN zoEfzlgKFhLK6L+#qi!80fbC*eS6B57WL<6hEV1RgTN0mLq6la~siVe_YoRb->Y|e6 zXFs3GXaU#>GK2N&xbCgRSgJ0mpi+kn^h9DshxPe>p_mY-j~gL`lIeC&8Lujj-{E=oiPw$33OgZid@X-ACzQ ztiehz2L}iJF)=p-MVtiMwQ2-o4d(9j;th>wm$B?!1yEAhs``d|uk z6_t2=ety!%z*?WhtP3_5aMFk50J}2-Q*8aE!M1d8v}jR<2|bZyl3Fyt1-#{2p=;x-l!oXSG#h0k@E-<+8z+B*u{C9|xoY z+-@j{=O7HzggeCoZ_zr-r6l^un%9&7Ruy7Gij{>nV3f%x@J#?#7u*3x16h#A@0=XE db>hlw_6LCS(a3rnlhObH002ovPDHLkV1i-Nk+J{) literal 0 HcmV?d00001 diff --git a/admin/words.js b/admin/words.js new file mode 100644 index 0000000..9fe5f7b --- /dev/null +++ b/admin/words.js @@ -0,0 +1,22 @@ +// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n +/*global systemDictionary:true */ +'use strict'; + +systemDictionary = { + "socket.io adapter settings": { "en": "socket.io adapter settings", "de": "socket.io adapter settings", "ru": "socket.io adapter settings", "pt": "Configurações do adaptador socket.io", "nl": "socket.io-adapterinstellingen", "fr": "Paramètres de l'adaptateur socket.io", "it": "impostazioni della scheda socket.io", "es": "Configuración del adaptador socket.io", "pl": "ustawienia adaptera socket.io"}, + "Run as:": { "en": "Run as", "de": "Laufen unter Anwender", "ru": "Запустить от пользователя", "pt": "Correr como", "nl": "Rennen als", "fr": "Courir comme", "it": "Correre come", "es": "Correr como", "pl": "Uruchom jako"}, + "IP:": { "en": "IP", "de": "IP", "ru": "IP", "pt": "IP", "nl": "IK P", "fr": "IP", "it": "IP", "es": "IP", "pl": "IP"}, + "Port:": { "en": "Port", "de": "Port", "ru": "Порт", "pt": "Porta", "nl": "Haven", "fr": "Port", "it": "Porta", "es": "Puerto", "pl": "Port"}, + "Secure(HTTPS):": { "en": "Secure(HTTPS)", "de": "Verschlüsselung(HTTPS)", "ru": "Шифрование(HTTPS)", "pt": "Seguro (HTTPS)", "nl": "Secure (HTTPS)", "fr": "Sécurisé (HTTPS)", "it": "Sicuro (HTTPS)", "es": "Seguro (HTTPS)", "pl": "Bezpieczne (HTTPS)"}, + "Authentication:": { "en": "Authentication", "de": "Authentifikation", "ru": "Аутентификация", "pt": "Autenticação", "nl": "authenticatie", "fr": "Authentification", "it": "Autenticazione", "es": "Autenticación", "pl": "Poświadczenie"}, + "Listen on all IPs": { "en": "Listen on all IPs", "de": "An allen IP Adressen hören", "ru": "Открыть сокет на всех IP адресах", "pt": "Ouça todos os IPs", "nl": "Luister op alle IP's", "fr": "Écoutez sur toutes les adresses IP", "it": "Ascolta su tutti gli IP", "es": "Escuchar en todas las direcciones IP", "pl": "Posłuchaj na wszystkich IP"}, + "help_tip": { "en": "On save the adapter restarts with new configuration immediately", "de": "Beim Speichern von Einstellungen der Adapter wird sofort neu gestartet.", "ru": "Сразу после сохранения настроек драйвер перезапуститься с новыми значениями", "pt": "Em salvar, o adaptador reinicia com a nova configuração imediatamente", "nl": "Bij opslaan wordt de adapter onmiddellijk opnieuw opgestart met een nieuwe configuratie", "fr": "Sur enregistrer l'adaptateur redémarre avec la nouvelle configuration immédiatement", "it": "Al salvataggio, l'adattatore si riavvia immediatamente con la nuova configurazione", "es": "Al guardar, el adaptador se reinicia con una nueva configuración de inmediato", "pl": "Po zapisaniu adapter natychmiast uruchamia się ponownie z nową konfiguracją"}, + "Public certificate:": { "en": "Public certificate", "de": "Publikzertifikat", "ru": "'Public' сертификат", "pt": "Certificado público", "nl": "Openbaar certificaat", "fr": "Certificat public", "it": "Certificato pubblico", "es": "Certificado público", "pl": "Certyfikat publiczny"}, + "Private certificate:": { "en": "Private certificate", "de": "Privatzertifikat", "ru": "'Private' сертификат", "pt": "Certificado privado", "nl": "Privé certificaat", "fr": "Certificat privé", "it": "Certificato privato", "es": "Certificado privado", "pl": "Prywatny certyfikat"}, + "Chained certificate:": { "en": "Chained certificate", "de": "Kettenzertifikat", "ru": "'Chained' сертификат", "pt": "Certificado acorrentado", "nl": "Geketend certificaat", "fr": "Certificat chaîné", "it": "Certificato incatenato", "es": "Certificado encadenado", "pl": "Przykuty certyfikat"}, + "Force Web-Sockets:": { "en": "Force Web-Sockets", "de": "Nur Web-Sockets", "ru": "Только Web-Sockets", "pt": "Forçar Web-Sockets", "nl": "Force Web-Sockets", "fr": "Forcer les Web-Sockets", "it": "Force Web-Sockets", "es": "Force Web-Sockets", "pl": "Wymuszaj gniazda internetowe"}, + "Let's Encrypt settings": { "en": "Let's Encrypt settings", "de": "Einstellungen Let's Encrypt", "ru": "Настройки Let's Encrypt", "pt": "Vamos criptografar configurações", "nl": "Laten we de instellingen versleutelen", "fr": "Cryptons les paramètres", "it": "Let's Encrypt settings", "es": "Vamos a cifrar la configuración", "pl": "Zakodujmy ustawienia"}, + "Use Lets Encrypt certificates:": { "en": "Use Let's Encrypt certificates", "de": "Benutzen Let's Encrypt Zertifikate", "ru": "Использовать сертификаты Let's Encrypt", "pt": "Use Vamos criptografar certificados", "nl": "Gebruik Let's Encrypt-certificaten", "fr": "Utiliser les certificats Let's Encrypt", "it": "Utilizza Let's Encrypt certificates", "es": "Utilice los certificados Let's Encrypt", "pl": "Użyj Let's Encrypt certificates"}, + "Use this instance for automatic update:": { "en": "Use this instance for automatic update", "de": "Benutze diese Instanz für automatische Updates", "ru": "Обновлять сертификаты в этом драйвере", "pt": "Use esta instância para atualização automática", "nl": "Gebruik deze instantie voor automatische update", "fr": "Utilisez cette instance pour la mise à jour automatique", "it": "Utilizza questa istanza per l'aggiornamento automatico", "es": "Use esta instancia para la actualización automática", "pl": "Użyj tej instancji do automatycznej aktualizacji"}, + "Port to check the domain:": { "en": "Port to check the domain", "de": "Port um die Domain zu prüfen", "ru": "Порт для проверки доменного имени", "pt": "Porta para verificar o domínio", "nl": "Poort om het domein te controleren", "fr": "Port pour vérifier le domaine", "it": "Porta per controllare il dominio", "es": "Puerto para verificar el dominio", "pl": "Port do sprawdzenia domeny"} +}; \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..d278435 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,25 @@ +version: 'test-{build}' +environment: + matrix: + - nodejs_version: '4' + - nodejs_version: '6' + - nodejs_version: '8' + - nodejs_version: '10' +platform: + - x86 + - x64 +clone_folder: 'c:\projects\%APPVEYOR_PROJECT_NAME%' +install: + - ps: 'Install-Product node $env:nodejs_version $env:platform' + - ps: '$NpmVersion = (npm -v).Substring(0,1)' + - ps: 'if($NpmVersion -eq 5) { npm install -g npm@5 }' + - ps: npm --version + - npm install + - npm install winston@2.3.1 + - 'npm install https://git.spacen.net/yunkong2/yunkong2.js-controller/tarball/master --production' +test_script: + - echo %cd% + - node --version + - npm --version + - npm test +build: 'off' diff --git a/example/conn.js b/example/conn.js new file mode 100644 index 0000000..4ff51a8 --- /dev/null +++ b/example/conn.js @@ -0,0 +1,1262 @@ +////// ----------------------- Connection "class" ---------------------- //////////// +/* jshint browser:true */ +/* global document*/ +/* global console*/ +/* global session*/ +/* global window*/ +/* global location*/ +/* global setTimeout*/ +/* global clearTimeout*/ +/* global io*/ +/* global $*/ +/* global socketNamespace */ +/* global socketUrl */ +/* global socketSession */ +/* global storage */ +/* jshint -W097 */// jshint strict:false + +'use strict'; + +// The idea of servConn is to use this class later in every addon. +// The addon just must say, what must be loaded (values, objects, indexes) and +// the class loads it for addon. Authentication will be done automatically, so addon does not care about it. +// It will be .js file with localData and servConn + +var servConn = { + _socket: null, + _onConnChange: null, + _onUpdate: null, + _isConnected: false, + _disconnectedSince: null, + _connCallbacks: { + onConnChange: null, + onUpdate: null, + onRefresh: null, + onAuth: null, + onCommand: null, + onError: null + }, + _authInfo: null, + _isAuthDone: false, + _isAuthRequired: false, + _authRunning: false, + _cmdQueue: [], + _connTimer: null, + _type: 'socket.io', // [SignalR | socket.io | local] + _timeout: 0, // 0 - use transport default timeout to detect disconnect + _reconnectInterval: 10000, // reconnect interval + _reloadInterval: 30, // if connection was absent longer than 30 seconds + _cmdData: null, + _cmdInstance: null, + _isSecure: false, + _defaultMode: 0x644, + _useStorage: false, + _objects: null, // used if _useStorage === true + _enums: null, // used if _useStorage === true + namespace: 'vis.0', + + getType: function () { + return this._type; + }, + getIsConnected: function () { + return this._isConnected; + }, + getIsLoginRequired: function () { + return this._isSecure; + }, + getUser: function () { + return this._user; + }, + setReloadTimeout: function (timeout){ + this._reloadInterval = parseInt(timeout, 10); + }, + setReconnectInterval: function (interval){ + this._reconnectInterval = parseInt(interval, 10); + }, + _checkConnection: function (func, _arguments) { + if (!this._isConnected) { + console.log('No connection!'); + return false; + } + + if (this._queueCmdIfRequired(func, _arguments)) return false; + + //socket.io + if (this._socket === null) { + console.log('socket.io not initialized'); + return false; + } + return true; + }, + _monitor: function () { + if (this._timer) return; + var ts = (new Date()).getTime(); + if (this._reloadInterval && ts - this._lastTimer > this._reloadInterval * 1000) { + // It seems, that PC was in a sleep => Reload page to request authentication anew + window.location.reload(); + } else { + this._lastTimer = ts; + } + var that = this; + this._timer = setTimeout(function () { + that._timer = null; + that._monitor(); + }, 10000); + }, + _onAuth: function (objectsRequired, isSecure) { + var that = this; + + this._isSecure = isSecure; + + if (this._isSecure) { + that._lastTimer = (new Date()).getTime(); + this._monitor(); + } + + this._socket.emit('subscribe', '*'); + if (objectsRequired) this._socket.emit('subscribeObjects', '*'); + + if (this._isConnected === true) { + // This seems to be a reconnect because we're already connected! + // -> prevent firing onConnChange twice + return; + } + this._isConnected = true; + if (this._connCallbacks.onConnChange) { + setTimeout(function () { + that._socket.emit('authEnabled', function (auth, user) { + that._user = user; + that._connCallbacks.onConnChange(that._isConnected); + if (typeof app !== 'undefined') app.onConnChange(that._isConnected); + }); + }, 0); + } + }, + reconnect: function (connOptions) { + var that = this; + // reconnect + if ((!connOptions.mayReconnect || connOptions.mayReconnect()) && !this._connectInterval) { + this._connectInterval = setInterval(function () { + console.log('Trying connect...'); + that._socket.connect(); + that._countDown = Math.floor(that._reconnectInterval / 1000); + if (typeof $ !== 'undefined') { + $('.splash-screen-text').html(that._countDown + '...').css('color', 'red'); + } + }, this._reconnectInterval); + + this._countDown = Math.floor(this._reconnectInterval / 1000); + if (typeof $ !== 'undefined') { + $('.splash-screen-text').html(this._countDown + '...'); + } + + this._countInterval = setInterval(function () { + that._countDown--; + if (typeof $ !== 'undefined') { + $('.splash-screen-text').html(that._countDown + '...'); + } + }, 1000); + } + }, + init: function (connOptions, connCallbacks, objectsRequired) { + var that = this; // support of old safary + // init namespace + if (typeof socketNamespace !== 'undefined') this.namespace = socketNamespace; + + connOptions = connOptions || {}; + if (!connOptions.name) connOptions.name = this.namespace; + + // To start vis as local use one of: + // - start vis from directory with name local, e.g. c:/blbla/local/yunkong2.vis/www/index.html + // - do not create "_socket/info.js" file in "www" directory + // - create "_socket/info.js" file with + // var socketUrl = "local"; var socketSession = ""; sysLang="en"; + // in this case you can overwrite browser language settings + if (document.URL.split('/local/')[1] || (typeof socketUrl === 'undefined' && !connOptions.connLink) || (typeof socketUrl !== 'undefined' && socketUrl === 'local')) { + this._type = 'local'; + } + + if (typeof session !== 'undefined') { + var user = session.get('user'); + if (user) { + that._authInfo = { + user: user, + hash: session.get('hash'), + salt: session.get('salt') + }; + } + } + + this._connCallbacks = connCallbacks; + + var connLink = connOptions.connLink || window.localStorage.getItem('connLink'); + + // Connection data from "/_socket/info.js" + if (!connLink && typeof socketUrl !== 'undefined') connLink = socketUrl; + if (!connOptions.socketSession && typeof socketSession !== 'undefined') connOptions.socketSession = socketSession; + if (connOptions.socketForceWebSockets === undefined && + typeof socketForceWebSockets !== 'undefined') { + connOptions.socketForceWebSockets = socketForceWebSockets; + } + + // if no remote data + if (this._type === 'local') { + // report connected state + this._isConnected = true; + if (this._connCallbacks.onConnChange) this._connCallbacks.onConnChange(this._isConnected); + if (typeof app !== 'undefined') app.onConnChange(this._isConnected); + } else + if (typeof io !== 'undefined') { + connOptions.socketSession = connOptions.socketSession || 'nokey'; + + var url; + if (connLink) { + url = connLink; + if (typeof connLink !== 'undefined') { + if (connLink[0] === ':') connLink = location.protocol + '://' + location.hostname + connLink; + } + } else { + url = location.protocol + '//' + location.host; + } + + this._socket = io.connect(url, { + query: 'key=' + connOptions.socketSession, + 'reconnection limit': 10000, + 'max reconnection attempts': Infinity, + reconnection: false, + upgrade: !connOptions.socketForceWebSockets, + rememberUpgrade: connOptions.socketForceWebSockets, + transports: connOptions.socketForceWebSockets ? ['websocket'] : undefined + }); + + this._socket.on('connect', function () { + if (that._disconnectedSince) { + var offlineTime = (new Date()).getTime() - that._disconnectedSince; + console.log('was offline for ' + (offlineTime / 1000) + 's'); + + // reload whole page if no connection longer than some period + if (that._reloadInterval && offlineTime > that._reloadInterval * 1000) window.location.reload(); + + that._disconnectedSince = null; + } + + if (that._connectInterval) { + clearInterval(that._connectInterval); + that._connectInterval = null; + } + if (that._countInterval) { + clearInterval(that._countInterval); + that._countInterval = null; + } + var elem = document.getElementById('server-disconnect'); + if (elem) elem.style.display = 'none'; + + that._socket.emit('name', connOptions.name); + console.log((new Date()).toISOString() + ' Connected => authenticate'); + setTimeout(function () { + var wait = setTimeout(function() { + console.error('No answer from server') + window.location.reload(); + }, 3000); + + that._socket.emit('authenticate', function (isOk, isSecure) { + clearTimeout(wait); + console.log((new Date()).toISOString() + ' Authenticated: ' + isOk); + if (isOk) { + that._onAuth(objectsRequired, isSecure); + } else { + console.log('permissionError'); + } + }); + }, 50); + }); + + this._socket.on('reauthenticate', function () { + if (that._connCallbacks.onConnChange) { + that._connCallbacks.onConnChange(false); + if (typeof app !== 'undefined') app.onConnChange(false); + } + console.warn('reauthenticate'); + window.location.reload(); + }); + + this._socket.on('connect_error', function () { + $('.splash-screen-text').css('color', '#002951'); + + that.reconnect(connOptions); + }); + + this._socket.on('disconnect', function () { + that._disconnectedSince = (new Date()).getTime(); + + // called only once when connection lost (and it was here before) + that._isConnected = false; + if (that._connCallbacks.onConnChange) { + setTimeout(function () { + var elem = document.getElementById('server-disconnect'); + if (elem) elem.style.display = ''; + that._connCallbacks.onConnChange(that._isConnected); + if (typeof app !== 'undefined') app.onConnChange(that._isConnected); + }, 5000); + } else { + var elem = document.getElementById('server-disconnect'); + if (elem) elem.style.display = ''; + } + + // reconnect + that.reconnect(connOptions); + }); + + // after reconnect the "connect" event will be called + this._socket.on('reconnect', function () { + var offlineTime = (new Date()).getTime() - that._disconnectedSince; + console.log('was offline for ' + (offlineTime / 1000) + 's'); + + // reload whole page if no connection longer than one minute + if (that._reloadInterval && offlineTime > that._reloadInterval * 1000) { + window.location.reload(); + } + // anyway "on connect" is called + }); + + this._socket.on('objectChange', function (id, obj) { + // If cache used + if (that._useStorage && typeof storage !== 'undefined') { + var objects = that._objects || storage.get('objects'); + if (objects) { + if (obj) { + objects[id] = obj; + } else { + if (objects[id]) delete objects[id]; + } + storage.set('objects', objects); + } + } + + if (that._connCallbacks.onObjectChange) that._connCallbacks.onObjectChange(id, obj); + }); + + this._socket.on('stateChange', function (id, state) { + if (!id || state === null || typeof state !== 'object') return; + + if (that._connCallbacks.onCommand && id === that.namespace + '.control.command') { + if (state.ack) return; + + if (state.val && + typeof state.val === 'string' && + state.val[0] === '{' && + state.val[state.val.length - 1] === '}') { + try { + state.val = JSON.parse(state.val); + } catch (e) { + console.log('Command seems to be an object, but cannot parse it: ' + state.val); + } + } + + // if command is an object {instance: 'iii', command: 'cmd', data: 'ddd'} + if (state.val && state.val.instance) { + if (that._connCallbacks.onCommand(state.val.instance, state.val.command, state.val.data)) { + // clear state + that.setState(id, {val: '', ack: true}); + } + } else { + if (that._connCallbacks.onCommand(that._cmdInstance, state.val, that._cmdData)) { + // clear state + that.setState(id, {val: '', ack: true}); + } + } + } else if (id === that.namespace + '.control.data') { + that._cmdData = state.val; + } else if (id === that.namespace + '.control.instance') { + that._cmdInstance = state.val; + } else if (that._connCallbacks.onUpdate) { + that._connCallbacks.onUpdate(id, state); + } + }); + + this._socket.on('permissionError', function (err) { + if (that._connCallbacks.onError) { + /* { + command: + type: + operation: + arg: + }*/ + that._connCallbacks.onError(err); + } else { + console.log('permissionError'); + } + }); + } + }, + logout: function (callback) { + if (!this._isConnected) { + console.log('No connection!'); + return; + } + + this._socket.emit('logout', callback); + }, + getVersion: function (callback) { + if (!this._checkConnection('getVersion', arguments)) return; + + this._socket.emit('getVersion', function (version) { + if (callback) callback(version); + }); + }, + _checkAuth: function (callback) { + if (!this._isConnected) { + console.log('No connection!'); + return; + } + //socket.io + if (this._socket === null) { + console.log('socket.io not initialized'); + return; + } + this._socket.emit('getVersion', function (version) { + if (callback) + callback(version); + }); + }, + readFile: function (filename, callback, isRemote) { + if (!callback) throw 'No callback set'; + + if (this._type === 'local') { + try { + var data = storage.get(filename); + callback(null, data ? JSON.parse(storage.get(filename)) : null); + } catch (err) { + callback(err, null); + } + } else { + if (!this._checkConnection('readFile', arguments)) return; + + if (!isRemote && typeof app !== 'undefined') { + app.readLocalFile(filename.replace(/^\/vis\.0\//, ''), callback); + } else { + var adapter = this.namespace; + if (filename[0] === '/') { + var p = filename.split('/'); + adapter = p[1]; + p.splice(0, 2); + filename = p.join('/'); + } + + this._socket.emit('readFile', adapter, filename, function (err, data, mimeType) { + setTimeout(function () { + callback(err, data, filename, mimeType); + }, 0); + }); + } + } + }, + getMimeType: function (ext) { + if (ext.indexOf('.') !== -1) ext = ext.toLowerCase().match(/\.[^.]+$/); + var _mimeType; + if (ext === '.css') { + _mimeType = 'text/css'; + } else if (ext === '.bmp') { + _mimeType = 'image/bmp'; + } else if (ext === '.png') { + _mimeType = 'image/png'; + } else if (ext === '.jpg') { + _mimeType = 'image/jpeg'; + } else if (ext === '.jpeg') { + _mimeType = 'image/jpeg'; + } else if (ext === '.gif') { + _mimeType = 'image/gif'; + } else if (ext === '.tif') { + _mimeType = 'image/tiff'; + } else if (ext === '.js') { + _mimeType = 'application/javascript'; + } else if (ext === '.html') { + _mimeType = 'text/html'; + } else if (ext === '.htm') { + _mimeType = 'text/html'; + } else if (ext === '.json') { + _mimeType = 'application/json'; + } else if (ext === '.xml') { + _mimeType = 'text/xml'; + } else if (ext === '.svg') { + _mimeType = 'image/svg+xml'; + } else if (ext === '.eot') { + _mimeType = 'application/vnd.ms-fontobject'; + } else if (ext === '.ttf') { + _mimeType = 'application/font-sfnt'; + } else if (ext === '.woff') { + _mimeType = 'application/font-woff'; + } else if (ext === '.wav') { + _mimeType = 'audio/wav'; + } else if (ext === '.mp3') { + _mimeType = 'audio/mpeg3'; + } else { + _mimeType = 'text/javascript'; + } + return _mimeType; + }, + readFile64: function (filename, callback, isRemote) { + var that = this; + if (!callback) { + throw 'No callback set'; + } + + if (!this._checkConnection('readFile', arguments)) return; + + if (!isRemote && typeof app !== 'undefined') { + app.readLocalFile(filename.replace(/^\/vis\.0\//, ''), function (err, data, mimeType) { + setTimeout(function () { + if (data) { + callback(err, {mime: mimeType || that.getMimeType(filename), data: btoa(data)}, filename); + } else { + callback(err, filename); + } + }, 0); + }); + } else { + var adapter = this.namespace; + if (filename[0] === '/') { + var p = filename.split('/'); + adapter = p[1]; + p.splice(0, 2); + filename = p.join('/'); + } + + this._socket.emit('readFile64', adapter, filename, function (err, data, mimeType) { + setTimeout(function () { + if (data) { + callback(err, {mime: mimeType || that.getMimeType(filename), data: data}, filename); + } else { + callback(err, {mime: mimeType || that.getMimeType(filename)}, filename); + } + }, 0); + }); + } + }, + writeFile: function (filename, data, mode, callback) { + if (typeof mode === 'function') { + callback = mode; + mode = null; + } + if (this._type === 'local') { + storage.set(filename, JSON.stringify(data)); + if (callback) callback(); + } else { + if (!this._checkConnection('writeFile', arguments)) return; + + if (typeof data === 'object') data = JSON.stringify(data, null, 2); + + var parts = filename.split('/'); + var adapter = parts[1]; + parts.splice(0, 2); + if (adapter === 'vis') { + this._socket.emit('writeFile', adapter, parts.join('/'), data, mode ? {mode: this._defaultMode} : {}, callback); + } else { + this._socket.emit('writeFile', this.namespace, filename, data, mode ? {mode: this._defaultMode} : {}, callback); + } + } + }, + // Write file base 64 + writeFile64: function (filename, data, callback) { + if (!this._checkConnection('writeFile', arguments)) return; + + var parts = filename.split('/'); + var adapter = parts[1]; + parts.splice(0, 2); + + this._socket.emit('writeFile', adapter, parts.join('/'), atob(data), {mode: this._defaultMode}, callback); + }, + readDir: function (dirname, callback) { + //socket.io + if (this._socket === null) { + console.log('socket.io not initialized'); + return; + } + if (!dirname) dirname = '/'; + var parts = dirname.split('/'); + var adapter = parts[1]; + parts.splice(0, 2); + + this._socket.emit('readDir', adapter, parts.join('/'), {filter: true}, function (err, data) { + if (callback) callback(err, data); + }); + }, + mkdir: function (dirname, callback) { + var parts = dirname.split('/'); + var adapter = parts[1]; + parts.splice(0, 2); + + this._socket.emit('mkdir', adapter, parts.join('/'), function (err) { + if (callback) callback(err); + }); + }, + unlink: function (name, callback) { + var parts = name.split('/'); + var adapter = parts[1]; + parts.splice(0, 2); + + this._socket.emit('unlink', adapter, parts.join('/'), function (err) { + if (callback) callback(err); + }); + }, + renameFile: function (oldname, newname, callback) { + var parts1 = oldname.split('/'); + var adapter = parts1[1]; + parts1.splice(0, 2); + var parts2 = newname.split('/'); + parts2.splice(0, 2); + this._socket.emit('rename', adapter, parts1.join('/'), parts2.join('/'), function (err) { + if (callback) callback(err); + }); + }, + setState: function (pointId, value, callback) { + //socket.io + if (this._socket === null) { + //console.log('socket.io not initialized'); + return; + } + this._socket.emit('setState', pointId, value, callback); + }, + // callback(err, data) + getStates: function (IDs, callback) { + if (typeof IDs === 'function') { + callback = IDs; + IDs = null; + } + + if (this._type === 'local') { + return callback(null, []); + } else { + if (!this._checkConnection('getStates', arguments)) return; + + this.gettingStates = this.gettingStates || 0; + this.gettingStates++; + if (this.gettingStates > 1) { + // fix for slow devices + console.log('Trying to get empty list, because the whole list could not be loaded'); + IDs = []; + } + var that = this; + this._socket.emit('getStates', IDs, function (err, data) { + that.gettingStates--; + if (err || !data) { + if (callback) { + callback(err || 'Authentication required'); + } + } else if (callback) { + callback(null, data); + } + }); + } + }, + _fillChildren: function (objects) { + var items = []; + + for (var id in objects) { + items.push(id); + } + items.sort(); + + for (var i = 0; i < items.length; i++) { + if (objects[items[i]].common) { + var j = i + 1; + var children = []; + var len = items[i].length + 1; + var name = items[i] + '.'; + while (j < items.length && items[j].substring(0, len) === name) { + children.push(items[j++]); + } + + objects[items[i]].children = children; + } + } + }, + // callback(err, data) + getObjects: function (useCache, callback) { + if (typeof useCache === 'function') { + callback = useCache; + useCache = false; + } + // If cache used + if (this._useStorage && useCache) { + if (typeof storage !== 'undefined') { + var objects = this._objects || storage.get('objects'); + if (objects) return callback(null, objects); + } else if (this._objects) { + return callback(null, this._objects); + } + } + + if (!this._checkConnection('getObjects', arguments)) return; + var that = this; + this._socket.emit('getObjects', function (err, data) { + + // Read all enums + that._socket.emit('getObjectView', 'system', 'enum', {startkey: 'enum.', endkey: 'enum.\u9999'}, function (err, res) { + if (err) { + callback(err); + return; + } + var result = {}; + var enums = {}; + for (var i = 0; i < res.rows.length; i++) { + data[res.rows[i].id] = res.rows[i].value; + enums[res.rows[i].id] = res.rows[i].value; + } + + // Read all adapters for images + that._socket.emit('getObjectView', 'system', 'instance', {startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, function (err, res) { + if (err) { + callback(err); + return; + } + var result = {}; + for (var i = 0; i < res.rows.length; i++) { + data[res.rows[i].id] = res.rows[i].value; + } + // find out default file mode + if (data['system.adapter.' + that.namespace] && + data['system.adapter.' + that.namespace].native && + data['system.adapter.' + that.namespace].native.defaultFileMode) { + that._defaultMode = data['system.adapter.' + that.namespace].native.defaultFileMode; + } + + // Read all channels for images + that._socket.emit('getObjectView', 'system', 'channel', {startkey: '', endkey: '\u9999'}, function (err, res) { + if (err) { + callback(err); + return; + } + var result = {}; + for (var i = 0; i < res.rows.length; i++) { + data[res.rows[i].id] = res.rows[i].value; + } + + // Read all devices for images + that._socket.emit('getObjectView', 'system', 'device', {startkey: '', endkey: '\u9999'}, function (err, res) { + if (err) { + callback(err); + return; + } + var result = {}; + for (var i = 0; i < res.rows.length; i++) { + data[res.rows[i].id] = res.rows[i].value; + } + + if (that._useStorage) { + that._fillChildren(data); + that._objects = data; + that._enums = enums; + + if (typeof storage !== 'undefined') { + storage.set('objects', data); + storage.set('enums', enums); + storage.set('timeSync', (new Date()).getTime()); + } + } + + if (callback) callback(err, data); + }); + }); + }); + }); + }); + }, + getChildren: function (id, useCache, callback) { + if (!this._checkConnection('getChildren', arguments)) return; + + if (typeof id === 'function') { + callback = id; + id = null; + useCache = false; + } + if (typeof id === 'boolean') { + callback = useCache; + useCache = id; + id = null; + } + if (typeof useCache === 'function') { + callback = useCache; + useCache = false; + } + + if (!id) return callback('getChildren: no id given'); + + var that = this; + var data = []; + + if (this._useStorage && useCache) { + if (typeof storage !== 'undefined') { + var objects = storage.get('objects'); + if (objects && objects[id] && objects[id].children) { + return callback(null, objects[id].children); + } + } else if (this._objects && this._objects[id] && this._objects[id].children) { + return callback(null, this._objects[id].children); + } + } + + // Read all devices + that._socket.emit('getObjectView', 'system', 'device', {startkey: id + '.', endkey: id + '.\u9999'}, function (err, res) { + if (err) { + callback(err); + return; + } + var result = {}; + for (var i = 0; i < res.rows.length; i++) { + data[res.rows[i].id] = res.rows[i].value; + } + + that._socket.emit('getObjectView', 'system', 'channel', {startkey: id + '.', endkey: id + '.\u9999'}, function (err, res) { + if (err) { + callback(err); + return; + } + var result = {}; + for (var i = 0; i < res.rows.length; i++) { + data[res.rows[i].id] = res.rows[i].value; + } + + // Read all adapters for images + that._socket.emit('getObjectView', 'system', 'state', {startkey: id + '.', endkey: id + '.\u9999'}, function (err, res) { + if (err) { + callback(err); + return; + } + var result = {}; + for (var i = 0; i < res.rows.length; i++) { + data[res.rows[i].id] = res.rows[i].value; + } + var list = []; + + var count = id.split('.').length; + + // find direct children + for (var _id in data) { + var parts = _id.split('.'); + if (count + 1 === parts.length) { + list.push(_id); + } + } + list.sort(); + + if (this._useStorage && typeof storage !== 'undefined') { + var objects = storage.get('objects') || {}; + + for (var id_ in data) { + objects[id_] = data[id_]; + } + if (objects[id] && objects[id].common) { + objects[id].children = list; + } + // Store for every element theirs children + var items = []; + for (var __id in data) { + items.push(__id); + } + items.sort(); + + for (var k = 0; k < items.length; k++) { + if (objects[items[k]].common) { + var j = k + 1; + var children = []; + var len = items[k].length + 1; + var name = items[k] + '.'; + while (j < items.length && items[j].substring(0, len) === name) { + children.push(items[j++]); + } + + objects[items[k]].children = children; + } + } + + storage.set('objects', objects); + } + + if (callback) callback(err, list); + }.bind(this)); + }.bind(this)); + }.bind(this)); + }, + getObject: function (id, useCache, callback) { + if (typeof id === 'function') { + callback = id; + id = null; + useCache = false; + } + if (typeof id === 'boolean') { + callback = useCache; + useCache = id; + id = null; + } + if (typeof useCache === 'function') { + callback = useCache; + useCache = false; + } + if (!id) return callback('no id given'); + + // If cache used + if (this._useStorage && useCache && typeof storage !== 'undefined') { + if (typeof storage !== 'undefined') { + var objects = this._objects || storage.get('objects'); + if (objects && objects[id]) return callback(null, objects[id]); + } else if (this._enums) { + return callback(null, this._enums); + } + } + + this._socket.emit('getObject', id, function (err, obj) { + if (err) { + callback(err); + return; + } + if (this._useStorage && typeof storage !== 'undefined') { + var objects = storage.get('objects') || {}; + objects[id] = obj; + storage.set('objects', objects); + } + return callback(null, obj); + }.bind(this)); + }, + getEnums: function (enumName, useCache, callback) { + if (typeof enumName === 'function') { + callback = enumName; + enumName = null; + useCache = false; + } + if (typeof enumName === 'boolean') { + callback = useCache; + useCache = enumName; + enumName = null; + } + if (typeof useCache === 'function') { + callback = useCache; + useCache = false; + } + + // If cache used + if (this._useStorage && useCache) { + if (typeof storage !== 'undefined') { + var enums = this._enums || storage.get('enums'); + if (enums) return callback(null, enums); + } else if (this._enums) { + return callback(null, this._enums); + } + } + + if (this._type === 'local') { + return callback(null, []); + } else { + + enumName = enumName ? enumName + '.' : ''; + + // Read all enums + this._socket.emit('getObjectView', 'system', 'enum', {startkey: 'enum.' + enumName, endkey: 'enum.' + enumName + '\u9999'}, function (err, res) { + if (err) { + callback(err); + return; + } + var enums = {}; + for (var i = 0; i < res.rows.length; i++) { + var obj = res.rows[i].value; + enums[obj._id] = obj; + } + if (this._useStorage && typeof storage !== 'undefined') { + storage.set('enums', enums); + } + callback(null, enums); + }.bind(this)); + } + }, + // return time when the objects were synchronized + getSyncTime: function () { + if (this._useStorage && typeof storage !== 'undefined') { + var timeSync = storage.get('timeSync'); + if (timeSync) return new Date(timeSync); + } + return null; + }, + addObject: function (objId, obj, callback) { + if (!this._isConnected) { + console.log('No connection!'); + return; + } + //socket.io + if (this._socket === null) { + console.log('socket.io not initialized'); + return; + } + }, + delObject: function (objId) { + if (!this._checkConnection('delObject', arguments)) return; + + this._socket.emit('delObject', objId); + }, + httpGet: function (url, callback) { + if (!this._isConnected) { + console.log('No connection!'); + return; + } + //socket.io + if (this._socket === null) { + console.log('socket.io not initialized'); + return; + } + this._socket.emit('httpGet', url, function (data) { + if (callback) callback(data); + }); + }, + logError: function (errorText) { + console.log("Error: " + errorText); + if (!this._isConnected) { + //console.log('No connection!'); + return; + } + //socket.io + if (this._socket === null) { + console.log('socket.io not initialized'); + return; + } + this._socket.emit('log', 'error', 'Addon DashUI ' + errorText); + }, + _queueCmdIfRequired: function (func, args) { + var that = this; + if (!this._isAuthDone) { + // Queue command + this._cmdQueue.push({func: func, args: args}); + + if (!this._authRunning) { + this._authRunning = true; + // Try to read version + this._checkAuth(function (version) { + // If we have got version string, so there is no authentication, or we are authenticated + that._authRunning = false; + if (version) { + that._isAuthDone = true; + // Repeat all stored requests + var __cmdQueue = that._cmdQueue; + // Trigger GC + that._cmdQueue = null; + that._cmdQueue = []; + for (var t = 0, len = __cmdQueue.length; t < len; t++) { + that[__cmdQueue[t].func].apply(that, __cmdQueue[t].args); + } + } else { + // Auth required + that._isAuthRequired = true; + // What for AuthRequest from server + } + }); + } + + return true; + } else { + return false; + } + }, + authenticate: function (user, password, salt) { + this._authRunning = true; + + if (user !== undefined) { + this._authInfo = { + user: user, + hash: password + salt, + salt: salt + }; + } + + if (!this._isConnected) { + console.log('No connection!'); + return; + } + + if (!this._authInfo) { + console.log("No credentials!"); + } + }, + getConfig: function (useCache, callback) { + if (!this._checkConnection('getConfig', arguments)) return; + + if (typeof useCache === 'function') { + callback = useCache; + useCache = false; + } + if (this._useStorage && useCache) { + if (typeof storage !== 'undefined') { + var objects = storage.get('objects'); + if (objects && objects['system.config']) { + return callback(null, objects['system.config'].common); + } + } else if (this._objects && this._objects['system.config']) { + return callback(null, this._objects['system.config'].common); + } + } + var that = this; + this._socket.emit('getObject', 'system.config', function (err, obj) { + if (callback && obj && obj.common) { + + if (that._useStorage && typeof storage !== 'undefined') { + var objects = storage.get('objects') || {}; + objects['system.config'] = obj; + storage.set('objects', objects); + } + + callback(null, obj.common); + } else { + callback('Cannot read language'); + } + }); + }, + sendCommand: function (instance, command, data, ack) { + this.setState(this.namespace + '.control.instance', {val: instance || 'notdefined', ack: true}); + this.setState(this.namespace + '.control.data', {val: data, ack: true}); + this.setState(this.namespace + '.control.command', {val: command, ack: ack === undefined ? true : ack}); + }, + _detectViews: function (projectDir, callback) { + this.readDir('/' + this.namespace + '/' + projectDir, function (err, dirs) { + // find vis-views.json + for (var f = 0; f < dirs.length; f++) { + if (dirs[f].file === 'vis-views.json' && (!dirs[f].acl || dirs[f].acl.read)) { + return callback(err, {name: projectDir, readOnly: (dirs[f].acl && !dirs[f].acl.write), mode: dirs[f].acl ? dirs[f].acl.permissions : 0}); + } + } + callback(err); + }); + }, + readProjects: function (callback) { + var that = this; + this.readDir('/' + this.namespace, function (err, dirs) { + var result = []; + var count = 0; + for (var d = 0; d < dirs.length; d++) { + if (dirs[d].isDir) { + count++; + that._detectViews(dirs[d].file, function (subErr, project) { + if (project) result.push(project); + + err = err || subErr; + if (!(--count)) callback(err, result); + }); + } + } + }); + }, + chmodProject: function (projectDir, mode, callback) { + //socket.io + if (this._socket === null) { + console.log('socket.io not initialized'); + return; + } + this._socket.emit('chmodFile', this.namespace, projectDir + '*', {mode: mode}, function (err, data) { + if (callback) callback(err, data); + }); + }, + clearCache: function () { + if (typeof storage !== 'undefined') { + storage.empty(); + } + }, + getHistory: function (id, options, callback) { + if (!this._checkConnection('getHistory', arguments)) return; + + if (!options) options = {}; + if (!options.timeout) options.timeout = 2000; + + var timeout = setTimeout(function () { + timeout = null; + callback('timeout'); + }, options.timeout); + this._socket.emit('getHistory', id, options, function (err, result) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + callback(err, result); + }); + }, + getLiveHost: function (cb) { + var that = this; + this._socket.emit('getObjectView', 'system', 'host', {startkey: 'system.host.', endkey: 'system.host.\u9999'}, function (err, res) { + var _hosts = []; + for (var h = 0; h < res.rows.length; h++) { + _hosts.push(res.rows[h].id + '.alive'); + } + if (!_hosts.length) { + cb(''); + return; + } + that.getStates(_hosts, function (err, states) { + for (var h in states) { + if (states[h].val) { + cb(h.substring(0, h.length - '.alive'.length)); + return; + } + } + cb(''); + }); + }); + }, + readDirAsZip: function (project, useConvert, callback) { + if (!callback) { + callback = useConvert; + useConvert = undefined; + } + if (!this._isConnected) { + console.log('No connection!'); + return; + } + //socket.io + if (this._socket === null) { + console.log('socket.io not initialized'); + return; + } + if (project.match(/\/$/)) project = project.substring(0, project.length - 1); + var that = this; + this.getLiveHost(function (host) { + if (!host) { + window.alert('No active host found'); + return; + } + // to do find active host + that._socket.emit('sendToHost', host, 'readDirAsZip', { + id: that.namespace, + name: project || 'main', + options: { + settings: useConvert + } + }, function (data) { + if (data.error) console.error(data.error); + if (callback) callback(data.error, data.data); + }); + + }); + }, + writeDirAsZip: function (project, base64, callback) { + if (!this._isConnected) { + console.log('No connection!'); + return; + } + //socket.io + if (this._socket === null) { + console.log('socket.io not initialized'); + return; + } + if (project.match(/\/$/)) project = project.substring(0, project.length - 1); + var that = this; + this.getLiveHost(function (host) { + if (!host) { + window.alert('No active host found'); + return; + } + that._socket.emit('sendToHost', host, 'writeDirAsZip', { + id: that.namespace, + name: project || 'main', + data: base64 + }, function (data) { + if (data.error) console.error(data.error); + if (callback) callback(data.error); + }); + + }); + } +}; diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..75b193a --- /dev/null +++ b/example/index.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + + diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..fd48c48 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,401 @@ +'use strict'; + +var gulp = require('gulp'); +var fs = require('fs'); +var pkg = require('./package.json'); +var iopackage = require('./io-package.json'); +var version = (pkg && pkg.version) ? pkg.version : iopackage.common.version; +/*var appName = getAppName(); + +function getAppName() { + var parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 1].split('.')[0].toLowerCase(); +} +*/ +const fileName = 'words.js'; +var languages = { + en: {}, + de: {}, + ru: {}, + pt: {}, + nl: {}, + fr: {}, + it: {}, + es: {}, + pl: {} +}; + +function lang2data(lang, isFlat) { + var str = isFlat ? '' : '{\n'; + var count = 0; + for (var w in lang) { + if (lang.hasOwnProperty(w)) { + count++; + if (isFlat) { + str += (lang[w] === '' ? (isFlat[w] || w) : lang[w]) + '\n'; + } else { + var key = ' "' + w.replace(/"/g, '\\"') + '": '; + str += key + '"' + lang[w].replace(/"/g, '\\"') + '",\n'; + } + } + } + if (!count) return isFlat ? '' : '{\n}'; + if (isFlat) { + return str; + } else { + return str.substring(0, str.length - 2) + '\n}'; + } +} + +function readWordJs(src) { + try { + var words; + if (fs.existsSync(src + 'js/' + fileName)) { + words = fs.readFileSync(src + 'js/' + fileName).toString(); + } else { + words = fs.readFileSync(src + fileName).toString(); + } + + var lines = words.split(/\r\n|\r|\n/g); + var i = 0; + while (!lines[i].match(/^systemDictionary = {/)) { + i++; + } + lines.splice(0, i); + + // remove last empty lines + i = lines.length - 1; + while (!lines[i]) { + i--; + } + if (i < lines.length - 1) { + lines.splice(i + 1); + } + + lines[0] = lines[0].replace('systemDictionary = ', ''); + lines[lines.length - 1] = lines[lines.length - 1].trim().replace(/};$/, '}'); + words = lines.join('\n'); + var resultFunc = new Function('return ' + words + ';'); + + return resultFunc(); + } catch (e) { + return null; + } +} +function padRight(text, totalLength) { + return text + (text.length < totalLength ? new Array(totalLength - text.length).join(' ') : ''); +} +function writeWordJs(data, src) { + var text = '// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n\n'; + text += '/*global systemDictionary:true */\n'; + text += '\'use strict\';\n\n'; + + text += 'systemDictionary = {\n'; + for (var word in data) { + if (data.hasOwnProperty(word)) { + text += ' ' + padRight('"' + word.replace(/"/g, '\\"') + '": {', 50); + var line = ''; + for (var lang in data[word]) { + if (data[word].hasOwnProperty(lang)) { + line += '"' + lang + '": "' + padRight(data[word][lang].replace(/"/g, '\\"') + '",', 50) + ' '; + } + } + if (line) { + line = line.trim(); + line = line.substring(0, line.length - 1); + } + text += line + '},\n'; + } + } + text = text.replace(/},\n$/, '}\n'); + text += '};'; + + if (fs.existsSync(src + 'js/' + fileName)) { + fs.writeFileSync(src + 'js/' + fileName, text); + } else { + fs.writeFileSync(src + '' + fileName, text); + } +} + +const EMPTY = ''; + +function words2languages(src) { + var langs = Object.assign({}, languages); + var data = readWordJs(src); + if (data) { + for (var word in data) { + if (data.hasOwnProperty(word)) { + for (var lang in data[word]) { + if (data[word].hasOwnProperty(lang)) { + langs[lang][word] = data[word][lang]; + // pre-fill all other languages + for (var j in langs) { + if (langs.hasOwnProperty(j)) { + langs[j][word] = langs[j][word] || EMPTY; + } + } + } + } + } + } + if (!fs.existsSync(src + 'i18n/')) { + fs.mkdirSync(src + 'i18n/'); + } + for (var l in langs) { + if (!langs.hasOwnProperty(l)) continue; + var keys = Object.keys(langs[l]); + //keys.sort(); + var obj = {}; + for (var k = 0; k < keys.length; k++) { + obj[keys[k]] = langs[l][keys[k]]; + } + if (!fs.existsSync(src + 'i18n/' + l)) { + fs.mkdirSync(src + 'i18n/' + l); + } + + fs.writeFileSync(src + 'i18n/' + l + '/translations.json', lang2data(obj)); + } + } else { + console.error('Cannot read or parse ' + fileName); + } +} +function words2languagesFlat(src) { + var langs = Object.assign({}, languages); + var data = readWordJs(src); + if (data) { + for (var word in data) { + if (data.hasOwnProperty(word)) { + for (var lang in data[word]) { + if (data[word].hasOwnProperty(lang)) { + langs[lang][word] = data[word][lang]; + // pre-fill all other languages + for (var j in langs) { + if (langs.hasOwnProperty(j)) { + langs[j][word] = langs[j][word] || EMPTY; + } + } + } + } + } + } + var keys = Object.keys(langs.en); + //keys.sort(); + for (var l in langs) { + if (!langs.hasOwnProperty(l)) continue; + var obj = {}; + for (var k = 0; k < keys.length; k++) { + obj[keys[k]] = langs[l][keys[k]]; + } + langs[l] = obj; + } + if (!fs.existsSync(src + 'i18n/')) { + fs.mkdirSync(src + 'i18n/'); + } + for (var ll in langs) { + if (!langs.hasOwnProperty(ll)) continue; + if (!fs.existsSync(src + 'i18n/' + ll)) { + fs.mkdirSync(src + 'i18n/' + ll); + } + + fs.writeFileSync(src + 'i18n/' + ll + '/flat.txt', lang2data(langs[ll], langs.en)); + } + fs.writeFileSync(src + 'i18n/flat.txt', keys.join('\n')); + } else { + console.error('Cannot read or parse ' + fileName); + } +} +function languagesFlat2words(src) { + var dirs = fs.readdirSync(src + 'i18n/'); + var langs = {}; + var bigOne = {}; + var order = Object.keys(languages); + dirs.sort(function (a, b) { + var posA = order.indexOf(a); + var posB = order.indexOf(b); + if (posA === -1 && posB === -1) { + if (a > b) return 1; + if (a < b) return -1; + return 0; + } else if (posA === -1) { + return -1; + } else if (posB === -1) { + return 1; + } else { + if (posA > posB) return 1; + if (posA < posB) return -1; + return 0; + } + }); + var keys = fs.readFileSync(src + 'i18n/flat.txt').toString().split('\n'); + + for (var l = 0; l < dirs.length; l++) { + if (dirs[l] === 'flat.txt') continue; + var lang = dirs[l]; + var values = fs.readFileSync(src + 'i18n/' + lang + '/flat.txt').toString().split('\n'); + langs[lang] = {}; + keys.forEach(function (word, i) { + langs[lang][word] = values[i].replace(/<\/ i>/g, '').replace(/<\/ b>/g, '').replace(/<\/ span>/g, '').replace(/% s/g, ' %s'); + }); + + var words = langs[lang]; + for (var word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + var aWords = readWordJs(); + + var temporaryIgnore = ['pt', 'fr', 'nl', 'flat.txt']; + if (aWords) { + // Merge words together + for (var w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src); +} +function languages2words(src) { + var dirs = fs.readdirSync(src + 'i18n/'); + var langs = {}; + var bigOne = {}; + var order = Object.keys(languages); + dirs.sort(function (a, b) { + var posA = order.indexOf(a); + var posB = order.indexOf(b); + if (posA === -1 && posB === -1) { + if (a > b) return 1; + if (a < b) return -1; + return 0; + } else if (posA === -1) { + return -1; + } else if (posB === -1) { + return 1; + } else { + if (posA > posB) return 1; + if (posA < posB) return -1; + return 0; + } + }); + for (var l = 0; l < dirs.length; l++) { + if (dirs[l] === 'flat.txt') continue; + var lang = dirs[l]; + langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString(); + langs[lang] = JSON.parse(langs[lang]); + var words = langs[lang]; + for (var word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + var aWords = readWordJs(); + + var temporaryIgnore = ['pt', 'fr', 'nl', 'it']; + if (aWords) { + // Merge words together + for (var w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src); +} + +gulp.task('adminWords2languages', function (done) { + words2languages('./admin/'); + done(); +}); + +gulp.task('adminWords2languagesFlat', function (done) { + words2languagesFlat('./admin/'); + done(); +}); + +gulp.task('adminLanguagesFlat2words', function (done) { + languagesFlat2words('./admin/'); + done(); +}); + +gulp.task('adminLanguages2words', function (done) { + languages2words('./admin/'); + done(); +}); + + +gulp.task('updatePackages', function (done) { + iopackage.common.version = pkg.version; + iopackage.common.news = iopackage.common.news || {}; + if (!iopackage.common.news[pkg.version]) { + var news = iopackage.common.news; + var newNews = {}; + + newNews[pkg.version] = { + en: 'news', + de: 'neues', + ru: 'новое' + }; + iopackage.common.news = Object.assign(newNews, news); + } + fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4)); + done(); +}); + +gulp.task('updateReadme', function (done) { + var readme = fs.readFileSync('README.md').toString(); + var pos = readme.indexOf('## Changelog\n'); + if (pos !== -1) { + var readmeStart = readme.substring(0, pos + '## Changelog\n'.length); + var readmeEnd = readme.substring(pos + '## Changelog\n'.length); + + if (readme.indexOf(version) === -1) { + var timestamp = new Date(); + var date = timestamp.getFullYear() + '-' + + ('0' + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' + + ('0' + (timestamp.getDate()).toString(10)).slice(-2); + + var news = ''; + if (iopackage.common.news && iopackage.common.news[pkg.version]) { + news += '* ' + iopackage.common.news[pkg.version].en; + } + + fs.writeFileSync('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd); + } + } + done(); +}); + +gulp.task('default', ['updatePackages', 'updateReadme']); \ No newline at end of file diff --git a/io-package.json b/io-package.json new file mode 100644 index 0000000..288547f --- /dev/null +++ b/io-package.json @@ -0,0 +1,204 @@ +{ + "common": { + "name": "socketio", + "version": "2.1.1", + "title": "socket.io", + "desc": { + "en": "This adapter allows to communicate different web applications with yunkong2", + "de": "Dieser Adapter ermöglicht die Kommunikation verschiedener Web-Anwendungen mit yunkong2", + "ru": "Этот адаптер позволяет соединяться различным веб-приложениям с yunkong2", + "pt": "Este adaptador permite comunicar diferentes aplicativos da web com yunkong2", + "nl": "Met deze adapter kunt u verschillende webtoepassingen communiceren met yunkong2", + "fr": "Cet adaptateur permet de communiquer différentes applications web avec yunkong2", + "it": "Questo adattatore consente di comunicare diverse applicazioni Web con yunkong2", + "es": "Este adaptador permite comunicar diferentes aplicaciones web con yunkong2", + "pl": "Ten adapter umożliwia komunikację z różnymi aplikacjami WWW za pomocą yunkong2" + }, + "news": { + "2.1.1": { + "en": "Used socket.io Version 1.7.5\nAuthentication problem was fixed", + "de": "Gebrauchte socket.io Version 1.7.5\nAuthentifizierungsproblem wurde behoben", + "ru": "Используемая версия socket.io 1.7.5\nИсправлена ​​проблема аутентификации", + "pt": "Usado socket.io versão 1.7.5\nProblema de autenticação foi corrigido", + "nl": "Gebruikte socket.io versie 1.7.5\nVerificatieprobleem is opgelost", + "fr": "Version socket.io utilisée 1.7.5\nLe problème d'authentification a été corrigé", + "it": "Usato socket.io Versione 1.7.5\nIl problema di autenticazione è stato risolto", + "es": "Utiliza socket.io versión 1.7.5\nSe corrigió el problema de autenticación", + "pl": "Użyty socket.io Wersja 1.7.5\nNaprawiono problem z uwierzytelnianiem" + }, + "2.1.0": { + "en": "Used socket.io Version 1.7.4", + "de": "Gebrauchte socket.io Version 1.7.4", + "ru": "Используемая версия socket.io 1.7.4", + "pt": "Usado socket.io Versão 1.7.4", + "nl": "Gebruikte socket.io versie 1.7.4", + "fr": "Version socket.io utilisée 1.7.4", + "it": "Usato socket.io Versione 1.7.4", + "es": "Utiliza socket.io versión 1.7.4", + "pl": "Używane socket.io Wersja 1.7.4" + }, + "2.0.1": { + "en": "The support of the old browsers was dropped, e.g. iPad 1", + "de": "Die Unterstützung der alten Browser abgebrochen, z.B. iPad 1", + "ru": "Поддержка старых браузеров была прекращена, например. iPad 1", + "pt": "O suporte dos navegadores antigos foi descartado, e. iPad 1", + "nl": "De ondersteuning van de oude browsers is weggevallen, b.v. iPad 1", + "fr": "Le support des anciens navigateurs a été supprimé, par ex. iPad 1", + "it": "Il supporto dei vecchi browser è stato eliminato, ad es. iPad 1", + "es": "El soporte de los navegadores antiguos se eliminó, p. iPad 1", + "pl": "Obsługa starych przeglądarek została usunięta, np. iPad 1" + }, + "1.9.0": { + "en": "Ready for Admin3", + "de": "Bereit für Admin3", + "ru": "Готово для администратора3", + "pt": "Pronto para Admin3", + "nl": "Klaar voor Admin3", + "fr": "Prêt pour Admin3", + "it": "Pronto per Admin3", + "es": "Listo para Admin3", + "pl": "Gotowy na administratora3" + }, + "1.8.7": { + "en": "Tune cloud work", + "de": "Verbessern die Cloud-Funktionalität", + "ru": "Улучшения для облака" + }, + "1.8.5": { + "en": "Escape [] in subscriptions", + "de": "Escape [] in Subscriptions", + "ru": "Escape [] in subscriptions" + }, + "1.8.4": { + "en": "Check callback validity", + "de": "Prüfe ob Callback richtig ist", + "ru": "Проверка callbacks" + }, + "1.8.3": { + "en": "Allow authentication via URL", + "de": "Erlaube die Authentifizierung via URL", + "ru": "Разрешена авторизация через URL" + }, + "1.8.2": { + "en": "Fix cmdExec command", + "de": "Korrigiere cmdExec Komando", + "ru": "Исправлена команда cmdExec" + }, + "1.8.1": { + "en": "Fix user access rights for sendToHost", + "de": "Korrigiere die Anwenderrechte für sendToHost", + "ru": "Исправлены права доступа для sendToHost" + }, + "1.8.0": { + "en": "Support the access to admin via yunkong2.pro", + "de": "Unterstützung von Zugriff auf admin über yunkong2.pro", + "ru": "Поддержка доступа к admin через yunkong2.pro" + }, + "1.7.5": { + "en": "fix error if subscribe is empty", + "de": "Fehler gefixt, falls subscribe ist leer", + "ru": "Исправлена ошибка, если subscribe пуст" + }, + "1.7.4": { + "en": "fix error with authentication", + "de": "Korrigiere Authentifizierung", + "ru": "Исправлена аутентификация" + }, + "1.7.3": { + "en": "support of socket extensions", + "de": "Unterstützung von Socketerweiterungen", + "ru": "Поддержка плагинов для socket" + }, + "1.7.2": { + "en": "Fix unsubscribe of states", + "de": "Korrigiere unsubscribe von Zuständen", + "ru": "Исправлено unsubscribe для состояний" + }, + "1.7.1": { + "en": "Fix authentication for app", + "de": "Die Authentifizierung mit APP wurde korrigiert", + "ru": "Аутентификация с app работает снова" + }, + "1.7.0": { + "en": "сompatible only with new admin", + "de": "Nur mit neuem Admin kompatibel", + "ru": "Совместимо только с новым админ-драйвером" + }, + "1.6.1": { + "en": "fix error by checking user name", + "de": "Fehler mit Namensprüfung behoben", + "ru": "Исправлена ошибка при проверке имени" + }, + "1.6.0": { + "en": "support of letsencrypt certificates", + "de": "Unterstützung von letsencrypt Zertifikaten", + "ru": "Поддержка letsencrypt сертификатов" + }, + "1.5.4": { + "en": "fix error in socket.js", + "de": "fix error in socket.js", + "ru": "fix error in socket.js" + }, + "1.5.3": { + "en": "support of force only web sockets transport", + "de": "support of force only web sockets transport", + "ru": "support of force only web sockets transport" + }, + "1.5.2": { + "en": "support of chained certificates", + "de": "support of chained certificates", + "ru": "support of chained certificates" + }, + "1.5.1": { + "en": "add sendToHost command", + "de": "add sendToHost command", + "ru": "add sendToHost command" + } + }, + "authors": [ + "bluefox " + ], + "license": "MIT", + "platform": "Javascript/Node.js", + "mode": "daemon", + "loglevel": "info", + "readme": "https://git.spacen.net/yunkong2/yunkong2.socketio/blob/master/README.md", + "icon": "socketio.png", + "keywords": ["web", "socket.io", "communication"], + "enabled": true, + "extIcon": "https://raw.githubusercontent.com/yunkong2/yunkong2.socketio/master/admin/socketio.png", + "type": "communication", + "stopBeforeUpdate": true, + "materialize": true, + "dependencies": [{"js-controller": ">=0.12.0"}] + }, + "native": { + "port": 8084, + "auth": false, + "secure": false, + "bind": "0.0.0.0", + "ttl": 3600, + "certPublic": "", + "certPrivate": "", + "certChained": "", + "defaultUser": "admin", + "forceWebSockets": false, + + "leEnabled": false, + "leUpdate": false, + "leCheckPort": 80 + }, + "objects": [ + { + "_id": "connected", + "type": "state", + "common": { + "name": "Info about connected socket clients", + "type": "string", + "read": true, + "write": false, + "role": "text" + } + } + ] +} diff --git a/lib/socket.js b/lib/socket.js new file mode 100644 index 0000000..7ba8dcb --- /dev/null +++ b/lib/socket.js @@ -0,0 +1,1719 @@ +/* jshint -W097 */ +/* jshint strict: false */ +/* jslint node: true */ +/* jshint -W061 */ +'use strict'; + +const socketio = require('socket.io'); +const path = require('path'); +const fs = require('fs'); +const cookieParser = require('cookie-parser'); +const EventEmitter = require('events'); +const util = require('util'); +let request = null; + +// From settings used only secure, auth and crossDomain +function IOSocket(server, settings, adapter) { + if (!(this instanceof IOSocket)) return new IOSocket(server, settings, adapter); + + this.settings = settings || {}; + this.adapter = adapter; + this.webServer = server; + this.subscribes = {}; + + let that = this; +// do not send too many state updates + let eventsThreshold = { + count: 0, + timeActivated: 0, + active: false, + accidents: 0, + repeatSeconds: 3, // how many seconds continuously must be number of events > value + value: 200, // how many events allowed in one check interval + checkInterval: 1000 // duration of one check interval + }; + + // Extract user name from socket + function getUserFromSocket(socket, callback) { + let wait = false; + try { + if (socket.handshake.headers.cookie && (!socket.request || !socket.request._query || !socket.request._query.user)) { + let cookie = decodeURIComponent(socket.handshake.headers.cookie); + let m = cookie.match(/connect\.sid=(.+)/); + if (m) { + // If session cookie exists + let c = m[1].split(';')[0]; + let sessionID = cookieParser.signedCookie(c, that.settings.secret); + if (sessionID) { + // Get user for session + wait = true; + that.settings.store.get(sessionID, function (err, obj) { + if (obj && obj.passport && obj.passport.user) { + socket._sessionID = sessionID; + if (typeof callback === 'function') { + callback(null, obj.passport.user); + } else { + that.adapter.log.warn('[getUserFromSocket] Invalid callback') + } + } else { + if (typeof callback === 'function') { + callback('unknown user'); + } else { + that.adapter.log.warn('[getUserFromSocket] Invalid callback') + } + } + }); + } + } + } + if (!wait) { + let user = socket.request._query.user; + let pass = socket.request._query.pass; + if (user && pass) { + wait = true; + that.adapter.checkPassword(user, pass, function (res) { + if (res) { + that.adapter.log.debug('Logged in: ' + user); + if (typeof callback === 'function') { + callback(null, user); + } else { + that.adapter.log.warn('[getUserFromSocket] Invalid callback') + } + } else { + that.adapter.log.warn('Invalid password or user name: ' + user + ', ' + pass[0] + '***(' + pass.length + ')'); + if (typeof callback === 'function') { + callback('unknown user'); + } else { + that.adapter.log.warn('[getUserFromSocket] Invalid callback') + } + } + }); + } + } + } catch (e) { + that.adapter.log.error(e); + wait = false; + } + if (!wait && typeof callback === 'function') { + callback('Cannot detect user'); + } + } + + function disableEventThreshold(readAll) { + if (eventsThreshold.active) { + eventsThreshold.accidents = 0; + eventsThreshold.count = 0; + eventsThreshold.active = false; + eventsThreshold.timeActivated = 0; + that.adapter.log.info('Subscribe on all states again'); + + setTimeout(function () { + if (readAll) { + that.adapter.getForeignStates('*', function (err, res) { + that.adapter.log.info('received all states'); + for (let id in res) { + if (res.hasOwnProperty(id) && JSON.stringify(states[id]) !== JSON.stringify(res[id])) { + that.server.sockets.emit('stateChange', id, res[id]); + states[id] = res[id]; + } + } + }); + } + + that.server.sockets.emit('eventsThreshold', false); + that.adapter.unsubscribeForeignStates('system.adapter.*'); + that.adapter.subscribeForeignStates('*'); + + }, 50); + } + } + + function enableEventThreshold() { + if (!eventsThreshold.active) { + eventsThreshold.active = true; + + setTimeout(function () { + that.adapter.log.info('Unsubscribe from all states, except system\'s, because over ' + eventsThreshold.repeatSeconds + ' seconds the number of events is over ' + eventsThreshold.value + ' (in last second ' + eventsThreshold.count + ')'); + eventsThreshold.timeActivated = new Date().getTime(); + + that.server.sockets.emit('eventsThreshold', true); + that.adapter.unsubscribeForeignStates('*'); + that.adapter.subscribeForeignStates('system.adapter.*'); + }, 100); + } + } + + function getClientAddress(socket) { + let address; + if (socket.handshake) { + address = socket.handshake.address; + } + if (!address && socket.request && socket.request.connection) { + address = socket.request.connection.remoteAddress; + } + return address; + } + + this.initSocket = function (socket) { + if (!socket._acl) { + if (that.settings.auth) { + getUserFromSocket(socket, function (err, user) { + if (err || !user) { + socket.emit('reauthenticate'); + that.adapter.log.error('socket.io ' + (err || 'No user found in cookies')); + socket.disconnect(); + } else { + socket._secure = true; + that.adapter.log.debug('socket.io client ' + user + ' connected'); + that.adapter.calculatePermissions('system.user.' + user, commandsPermissions, function (acl) { + let address = getClientAddress(socket); + // socket._acl = acl; + socket._acl = mergeACLs(address, acl, that.settings.whiteListSettings); + socketEvents(socket, address); + }); + } + }); + } else { + that.adapter.calculatePermissions(that.settings.defaultUser, commandsPermissions, function (acl) { + let address = getClientAddress(socket); + // socket._acl = acl; + socket._acl = mergeACLs(address, acl, that.settings.whiteListSettings); + socketEvents(socket, address); + }); + } + } else { + let address = getClientAddress(socket); + socketEvents(socket, address); + } + }; + + this.getWhiteListIpForAddress = function (address, whiteList){ + return getWhiteListIpForAddress(address, whiteList); + }; + + function getWhiteListIpForAddress(address, whiteList) { + if (!whiteList) return null; + + // check IPv6 or IPv4 direct match + if (whiteList.hasOwnProperty(address)) { + return address; + } + + // check if address is IPv4 + let addressParts = address.split('.'); + if (addressParts.length !== 4) { + return null; + } + + // do we have settings for wild carded ips? + let wildCardIps = Object.keys(whiteList).filter(function (key) { + return key.indexOf('*') !== -1; + }); + + + if (wildCardIps.length === 0) { + // no wild carded ips => no ip configured + return null; + } + + wildCardIps.forEach(function (ip) { + let ipParts = ip.split('.'); + if (ipParts.length === 4) { + for (let i = 0; i < 4; i++) { + if (ipParts[i] === '*' && i === 3) { + // match + return ip; + } + + if (ipParts[i] !== addressParts[i]) break; + } + } + }); + + return null; + } + + function getPermissionsForIp(address, whiteList) { + return whiteList[getWhiteListIpForAddress(address, whiteList) || 'default']; + } + + function mergeACLs(address, acl, whiteList) { + if (whiteList && address) { + let whiteListAcl = getPermissionsForIp(address, whiteList); + if (whiteListAcl) { + ['object', 'state', 'file'].forEach(function (key) { + if (acl.hasOwnProperty(key) && whiteListAcl.hasOwnProperty(key)) { + Object.keys(acl[key]).forEach(function (permission) { + if (whiteListAcl[key].hasOwnProperty(permission)) { + acl[key][permission] = acl[key][permission] && whiteListAcl[key][permission]; + } + }) + } + }); + + if (whiteListAcl.user !== 'auth') { + acl.user = 'system.user.' + whiteListAcl.user; + } + } + } + + return acl; + } + + function pattern2RegEx(pattern) { + if (!pattern) { + return null; + } + if (pattern !== '*') { + if (pattern[0] === '*' && pattern[pattern.length - 1] !== '*') pattern += '$'; + if (pattern[0] !== '*' && pattern[pattern.length - 1] === '*') pattern = '^' + pattern; + } + pattern = pattern.replace(/\./g, '\\.'); + pattern = pattern.replace(/\*/g, '.*'); + pattern = pattern.replace(/\[/g, '\\['); + pattern = pattern.replace(/]/g, '\\]'); + pattern = pattern.replace(/\(/g, '\\('); + pattern = pattern.replace(/\)/g, '\\)'); + return pattern; + } + + this.subscribe = function (socket, type, pattern) { + //console.log((socket._name || socket.id) + ' subscribe ' + pattern); + if (socket) { + socket._subscribe = socket._subscribe || {}; + } + if (!this.subscribes[type]) this.subscribes[type] = {}; + + let s; + if (socket) { + s = socket._subscribe[type] = socket._subscribe[type] || []; + for (let i = 0; i < s.length; i++) { + if (s[i].pattern === pattern) return; + } + } + + let p = pattern2RegEx(pattern); + if (p === null) { + this.adapter.log.warn('Empty pattern!'); + return; + } + if (socket) { + s.push({pattern: pattern, regex: new RegExp(p)}); + } + + if (this.subscribes[type][pattern] === undefined) { + this.subscribes[type][pattern] = 1; + if (type === 'stateChange') { + this.adapter.subscribeForeignStates(pattern); + } else if (type === 'objectChange') { + if (this.adapter.subscribeForeignObjects) { + this.adapter.subscribeForeignObjects(pattern); + } + } else if (type === 'log') { + if (this.adapter.requireLog) this.adapter.requireLog(true); + } + } else { + this.subscribes[type][pattern]++; + } + }; + + function showSubscribes(socket, type) { + if (socket && socket._subscribe) { + let s = socket._subscribe[type] || []; + let ids = []; + for (let i = 0; i < s.length; i++) { + ids.push(s[i].pattern); + } + that.adapter.log.debug('Subscribes: ' + ids.join(', ')); + } else { + that.adapter.log.debug('Subscribes: no subscribes'); + } + } + + this.unsubscribe = function (socket, type, pattern) { + //console.log((socket._name || socket.id) + ' unsubscribe ' + pattern); + if (!this.subscribes[type]) this.subscribes[type] = {}; + + if (socket) { + if (!socket._subscribe || !socket._subscribe[type]) return; + for (let i = socket._subscribe[type].length - 1; i >= 0; i--) { + if (socket._subscribe[type][i].pattern === pattern) { + + // Remove pattern from global list + if (this.subscribes[type][pattern] !== undefined) { + this.subscribes[type][pattern]--; + if (this.subscribes[type][pattern] <= 0) { + if (type === 'stateChange') { + //console.log((socket._name || socket.id) + ' unsubscribeForeignStates ' + pattern); + this.adapter.unsubscribeForeignStates(pattern); + } else if (type === 'objectChange') { + //console.log((socket._name || socket.id) + ' unsubscribeForeignObjects ' + pattern); + if (this.adapter.unsubscribeForeignObjects) this.adapter.unsubscribeForeignObjects(pattern); + } else if (type === 'log') { + //console.log((socket._name || socket.id) + ' requireLog false'); + if (this.adapter.requireLog) this.adapter.requireLog(false); + } + delete this.subscribes[type][pattern]; + } + } + + delete socket._subscribe[type][i]; + socket._subscribe[type].splice(i, 1); + return; + } + } + } else if (pattern) { + // Remove pattern from global list + if (this.subscribes[type][pattern] !== undefined) { + this.subscribes[type][pattern]--; + if (this.subscribes[type][pattern] <= 0) { + if (type === 'stateChange') { + //console.log((socket._name || socket.id) + ' unsubscribeForeignStates ' + pattern); + this.adapter.unsubscribeForeignStates(pattern); + } else if (type === 'objectChange') { + //console.log((socket._name || socket.id) + ' unsubscribeForeignObjects ' + pattern); + if (this.adapter.unsubscribeForeignObjects) this.adapter.unsubscribeForeignObjects(pattern); + } else if (type === 'log') { + //console.log((socket._name || socket.id) + ' requireLog false'); + if (this.adapter.requireLog) this.adapter.requireLog(false); + } + delete this.subscribes[type][pattern]; + } + } + } else { + for (pattern in this.subscribes[type]) { + if (!this.subscribes[type].hasOwnProperty(pattern)) continue; + if (type === 'stateChange') { + //console.log((socket._name || socket.id) + ' unsubscribeForeignStates ' + pattern); + this.adapter.unsubscribeForeignStates(pattern); + } else if (type === 'objectChange') { + //console.log((socket._name || socket.id) + ' unsubscribeForeignObjects ' + pattern); + if (this.adapter.unsubscribeForeignObjects) this.adapter.unsubscribeForeignObjects(pattern); + } else if (type === 'log') { + //console.log((socket._name || socket.id) + ' requireLog false'); + if (this.adapter.requireLog) this.adapter.requireLog(false); + } + delete this.subscribes[type][pattern]; + } + } + }; + + this.unsubscribeAll = function () { + if (this.server && this.server.sockets) { + for (let s in this.server.sockets) { + if (this.server.sockets.hasOwnProperty(s)) { + unsubscribeSocket(s, 'stateChange'); + unsubscribeSocket(s, 'objectChange'); + unsubscribeSocket(s, 'log'); + } + } + } + }; + + function unsubscribeSocket(socket, type) { + if (!socket._subscribe || !socket._subscribe[type]) return; + + for (let i = 0; i < socket._subscribe[type].length; i++) { + let pattern = socket._subscribe[type][i].pattern; + if (that.subscribes[type][pattern] !== undefined) { + that.subscribes[type][pattern]--; + if (that.subscribes[type][pattern] <= 0) { + if (type === 'stateChange') { + that.adapter.unsubscribeForeignStates(pattern); + } else if (type === 'objectChange') { + if (that.adapter.unsubscribeForeignObjects) that.adapter.unsubscribeForeignObjects(pattern); + } else if (type === 'log') { + if (that.adapter.requireLog) that.adapter.requireLog(false); + } + delete that.subscribes[type][pattern]; + } + } + } + } + + function subscribeSocket(socket, type) { + //console.log((socket._name || socket.id) + ' subscribeSocket'); + if (!socket._subscribe || !socket._subscribe[type]) return; + + for (let i = 0; i < socket._subscribe[type].length; i++) { + let pattern = socket._subscribe[type][i].pattern; + if (that.subscribes[type][pattern] === undefined) { + that.subscribes[type][pattern] = 1; + if (type === 'stateChange') { + that.adapter.subscribeForeignStates(pattern); + } else if (type === 'objectChange') { + if (that.adapter.subscribeForeignObjects) that.adapter.subscribeForeignObjects(pattern); + } else if (type === 'log') { + if (that.adapter.requireLog) that.adapter.requireLog(true); + } + } else { + that.subscribes[type][pattern]++; + } + } + } + + function publish(socket, type, id, obj) { + if (!socket._subscribe || !socket._subscribe[type]) return; + let s = socket._subscribe[type]; + for (let i = 0; i < s.length; i++) { + if (s[i].regex.test(id)) { + updateSession(socket); + socket.emit(type, id, obj); + return; + } + } + } + + // update session ID, but not offter than 60 seconds + function updateSession(socket) { + if (socket._sessionID) { + let time = (new Date()).getTime(); + if (socket._lastActivity && time - socket._lastActivity > settings.ttl * 1000) { + socket.emit('reauthenticate'); + socket.disconnect(); + return false; + } + socket._lastActivity = time; + if (!socket._sessionTimer) { + socket._sessionTimer = setTimeout(function () { + socket._sessionTimer = null; + that.settings.store.get(socket._sessionID, function (err, obj) { + if (obj) { + that.adapter.setSession(socket._sessionID, settings.ttl, obj); + } else { + socket.emit('reauthenticate'); + socket.disconnect(); + } + }); + }, 60000); + } + } + return true; + } + + // static information + let commandsPermissions = { + getObject: {type: 'object', operation: 'read'}, + getObjects: {type: 'object', operation: 'list'}, + getObjectView: {type: 'object', operation: 'list'}, + setObject: {type: 'object', operation: 'write'}, + requireLog: {type: 'object', operation: 'write'}, // just mapping to some command + delObject: {type: 'object', operation: 'delete'}, + extendObject: {type: 'object', operation: 'write'}, + getHostByIp: {type: 'object', operation: 'list'}, + subscribeObjects: {type: 'object', operation: 'read'}, + unsubscribeObjects: {type: 'object', operation: 'read'}, + + getStates: {type: 'state', operation: 'list'}, + getState: {type: 'state', operation: 'read'}, + setState: {type: 'state', operation: 'write'}, + delState: {type: 'state', operation: 'delete'}, + createState: {type: 'state', operation: 'create'}, + subscribe: {type: 'state', operation: 'read'}, + unsubscribe: {type: 'state', operation: 'read'}, + getStateHistory: {type: 'state', operation: 'read'}, + getVersion: {type: '', operation: ''}, + + addUser: {type: 'users', operation: 'create'}, + delUser: {type: 'users', operation: 'delete'}, + addGroup: {type: 'users', operation: 'create'}, + delGroup: {type: 'users', operation: 'delete'}, + changePassword: {type: 'users', operation: 'write'}, + + httpGet: {type: 'other', operation: 'http'}, + cmdExec: {type: 'other', operation: 'execute'}, + sendTo: {type: 'other', operation: 'sendto'}, + sendToHost: {type: 'other', operation: 'sendto'}, + readLogs: {type: 'other', operation: 'execute'}, + + readDir: {type: 'file', operation: 'list'}, + createFile: {type: 'file', operation: 'create'}, + writeFile: {type: 'file', operation: 'write'}, + readFile: {type: 'file', operation: 'read'}, + deleteFile: {type: 'file', operation: 'delete'}, + readFile64: {type: 'file', operation: 'read'}, + writeFile64: {type: 'file', operation: 'write'}, + unlink: {type: 'file', operation: 'delete'}, + rename: {type: 'file', operation: 'write'}, + mkdir: {type: 'file', operation: 'write'}, + chmodFile: {type: 'file', operation: 'write'}, + + authEnabled: {type: '', operation: ''}, + disconnect: {type: '', operation: ''}, + listPermissions: {type: '', operation: ''}, + getUserPermissions: {type: 'object', operation: 'read'} + }; + + function addUser(user, pw, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!user.match(/^[-.A-Za-züäößÖÄÜа-яА-Я@+$§0-9=?!&# ]+$/)) { + if (typeof callback === 'function') { + callback('Invalid characters in the name. Only following special characters are allowed: -@+$§=?!&# and letters'); + } + return; + } + + that.adapter.getForeignObject('system.user.' + user, options, (err, obj) => { + if (obj) { + if (typeof callback === 'function') { + callback('User yet exists'); + } + } else { + that.adapter.setForeignObject('system.user.' + user, { + type: 'user', + common: { + name: user, + enabled: true, + groups: [] + } + }, options, () => { + that.adapter.setPassword(user, pw, options, callback); + }); + } + }); + } + + function delUser(user, options, callback) { + that.adapter.getForeignObject('system.user.' + user, options, (err, obj) => { + if (err || !obj) { + if (typeof callback === 'function') { + callback('User does not exist'); + } + } else { + if (obj.common.dontDelete) { + if (typeof callback === 'function') { + callback('Cannot delete user, while is system user'); + } + } else { + that.adapter.delForeignObject('system.user.' + user, options, err => { + // Remove this user from all groups in web client + if (typeof callback === 'function') { + callback(err); + } + }); + } + } + }); + } + + function addGroup(group, desc, acl, options, callback) { + let name = group; + if (typeof acl === 'function') { + callback = acl; + acl = null; + } + if (typeof desc === 'function') { + callback = desc; + desc = null; + } + if (typeof options === 'function') { + callback = options; + options = null; + } + if (name && name.substring(0, 1) !== name.substring(0, 1).toUpperCase()) { + name = name.substring(0, 1).toUpperCase() + name.substring(1); + } + group = group.substring(0, 1).toLowerCase() + group.substring(1); + + if (!group.match(/^[-.A-Za-züäößÖÄÜа-яА-Я@+$§0-9=?!&#_ ]+$/)) { + if (typeof callback === 'function') { + callback('Invalid characters in the group name. Only following special characters are allowed: -@+$§=?!&# and letters'); + } + return; + } + + that.adapter.getForeignObject('system.group.' + group, options, (err, obj) => { + if (obj) { + if (typeof callback === 'function') { + callback('Group yet exists'); + } + } else { + obj = { + _id: 'system.group.' + group, + type: 'group', + common: { + name: name, + desc: desc, + members: [], + acl: acl + } + }; + that.adapter.setForeignObject('system.group.' + group, obj, options, err => { + if (typeof callback === 'function') { + callback(err, obj); + } + }); + } + }); + } + + function delGroup(group, options, callback) { + that.adapter.getForeignObject('system.group.' + group, options, (err, obj) => { + if (err || !obj) { + if (typeof callback === 'function') { + callback('Group does not exist'); + } + } else { + if (obj.common.dontDelete) { + if (typeof callback === 'function') { + callback('Cannot delete group, while is system group'); + } + } else { + that.adapter.delForeignObject('system.group.' + group, options, err => { + // Remove this group from all users in web client + if (typeof callback === 'function') { + callback(err); + } + }); + } + } + }); + } + +function checkPermissions(socket, command, callback, arg) { + if (socket._acl.user !== 'system.user.admin') { + // type: file, object, state, other + // operation: create, read, write, list, delete, sendto, execute, sendToHost, readLogs + if (commandsPermissions[command]) { + // If permission required + if (commandsPermissions[command].type) { + if (socket._acl[commandsPermissions[command].type] && + socket._acl[commandsPermissions[command].type][commandsPermissions[command].operation]) { + return true; + } else { + that.adapter.log.warn('No permission for "' + socket._acl.user + '" to call ' + command + '. Need "' + commandsPermissions[command].type + '"."' + commandsPermissions[command].operation + '"'); + } + } else { + return true; + } + } else { + that.adapter.log.warn('No rule for command: ' + command); + } + + if (typeof callback === 'function') { + callback('permissionError'); + } else { + if (commandsPermissions[command]) { + socket.emit('permissionError', { + command: command, + type: commandsPermissions[command].type, + operation: commandsPermissions[command].operation, + arg: arg + }); + } else { + socket.emit('permissionError', { + command: command, + arg: arg + }); + } + } + return false; + } else { + return true; + } + } + + function checkObject(obj, options, flag) { + // read rights of object + if (!obj || !obj.common || !obj.acl || flag === 'list') { + return true; + } + + if (options.user !== 'system.user.admin' && + options.groups.indexOf('system.group.administrator') === -1) { + if (obj.acl.owner !== options.user) { + // Check if the user is in the group + if (options.groups.indexOf(obj.acl.ownerGroup) !== -1) { + // Check group rights + if (!(obj.acl.object & (flag << 4))) { + return false + } + } else { + // everybody + if (!(obj.acl.object & flag)) { + return false + } + } + } else { + // Check group rights + if (!(obj.acl.object & (flag << 8))) { + return false + } + } + } + return true; + } + + this.send = function (socket, cmd, id, data) { + if (socket._apiKeyOk) { + socket.emit(cmd, id, data); + } + }; + + function stopAdapter(reason, callback) { + reason && that.adapter.log.warn('Adapter stopped. Reason: ' + reason); + that.adapter.getForeignObject('system.adapter.' + that.adapter.namespace, function (err, obj) { + if (err) that.adapter.log.error('[getForeignObject]: ' + err); + if (obj) { + obj.common.enabled = false; + setTimeout(function () { + that.adapter.setForeignObject(obj._id, obj, function (err) { + if (err) that.adapter.log.error('[setForeignObject]: ' + err); + callback && callback(); + }); + }, 5000); + } else { + callback && callback(); + } + }); + } + + function redirectAdapter(url, callback) { + if (!url) { + that.adapter.log.warn('Received redirect command, but no URL'); + } else { + that.adapter.getForeignObject('system.adapter.' + that.adapter.namespace, function (err, obj) { + if (err) that.adapter.log.error('redirectAdapter [getForeignObject]: ' + err); + if (obj) { + obj.native.cloudUrl = url; + setTimeout(function () { + that.adapter.setForeignObject(obj._id, obj, function (err) { + if (err) that.adapter.log.error('redirectAdapter [setForeignObject]: ' + err); + callback && callback(); + }); + }, 3000); + } else { + callback && callback(); + } + }); + } + } + + function waitForConnect(delaySeconds) { + that.emit && that.emit('connectWait', delaySeconds); + } + + function socketEvents(socket, address) { + if (socket.conn) { + that.adapter.log.info('==>Connected ' + socket._acl.user + ' from ' + address); + } else { + that.adapter.log.info('Trying to connect as ' + socket._acl.user + ' from ' + address); + } + + if (!that.infoTimeout) { + that.infoTimeout = setTimeout(updateConnectedInfo, 1000); + } + + socket.on('authenticate', function (user, pass, callback) { + that.adapter.log.debug((new Date()).toISOString() + ' Request authenticate [' + socket._acl.user + ']'); + if (typeof user === 'function') { + callback = user; + user = undefined; + } + if (socket._acl.user !== null) { + if (typeof callback === 'function') { + callback(socket._acl.user !== null, socket._secure); + } + } else { + that.adapter.log.debug((new Date()).toISOString() + ' Request authenticate [' + socket._acl.user + ']'); + socket._authPending = callback; + } + }); + + socket.on('name', function (name, cb) { + that.adapter.log.debug('Connection from ' + name); + updateSession(socket); + if (this._name === undefined) { + this._name = name; + if (!that.infoTimeout) that.infoTimeout = setTimeout(updateConnectedInfo, 1000); + } else if (this._name !== name) { + that.adapter.log.warn('socket ' + this.id + ' changed socket name from ' + this._name + ' to ' + name); + this._name = name; + } + if (typeof cb === 'function') { + cb(); + } + }); + + /* + * objects + */ + socket.on('getObject', function (id, callback) { + if (updateSession(socket) && checkPermissions(socket, 'getObject', callback, id)) { + that.adapter.getForeignObject(id, {user: socket._acl.user}, callback); + } + }); + + socket.on('getObjects', function (callback) { + if (updateSession(socket) && checkPermissions(socket, 'getObjects', callback)) { + that.adapter.getForeignObjects('*', 'state', 'rooms', {user: socket._acl.user}, function (err, objs) { + if (typeof callback === 'function') { + callback(err, objs); + } else { + that.adapter.log.warn('[getObjects] Invalid callback') + } + }); + } + }); + + socket.on('subscribeObjects', function (pattern, callback) { + if (updateSession(socket) && checkPermissions(socket, 'subscribeObjects', callback, pattern)) { + if (pattern && typeof pattern === 'object' && pattern instanceof Array) { + for (let p = 0; p < pattern.length; p++) { + that.subscribe(this, 'objectChange', pattern[p]); + } + } else { + that.subscribe(this, 'objectChange', pattern); + } + if (typeof callback === 'function') { + setImmediate(callback, null); + } + } + }); + + socket.on('unsubscribeObjects', function (pattern, callback) { + if (updateSession(socket) && checkPermissions(socket, 'unsubscribeObjects', callback, pattern)) { + if (pattern && typeof pattern === 'object' && pattern instanceof Array) { + for (let p = 0; p < pattern.length; p++) { + that.unsubscribe(this, 'objectChange', pattern[p]); + } + } else { + that.unsubscribe(this, 'objectChange', pattern); + } + if (typeof callback === 'function') { + setImmediate(callback, null); + } + } + }); + + socket.on('getObjectView', function (design, search, params, callback) { + if (updateSession(socket) && checkPermissions(socket, 'getObjectView', callback, search)) { + that.adapter.objects.getObjectView(design, search, params, {user: socket._acl.user}, callback); + } + }); + + socket.on('setObject', function (id, obj, callback) { + if (updateSession(socket) && checkPermissions(socket, 'setObject', callback, id)) { + that.adapter.setForeignObject(id, obj, {user: socket._acl.user}, callback); + } + }); + + /* + * states + */ + socket.on('getStates', function (pattern, callback) { + if (updateSession(socket) && checkPermissions(socket, 'getStates', callback, pattern)) { + if (typeof pattern === 'function') { + callback = pattern; + pattern = null; + } + that.adapter.getForeignStates(pattern || '*', {user: socket._acl.user}, callback); + } + }); + + socket.on('error', function (err) { + that.adapter.log.error('Socket error: ' + err); + }); + + // allow admin access + if (that.settings.allowAdmin) { + socket.on('getAllObjects', function (callback) { + if (updateSession(socket) && checkPermissions(socket, 'getObjects', callback)) { + that.adapter.objects.getObjectList({include_docs: true}, function (err, res) { + that.adapter.log.info('received all objects'); + res = res.rows; + let objects = {}; + + if (socket._acl && + socket._acl.user !== 'system.user.admin' && + socket._acl.groups.indexOf('system.group.administrator') === -1) { + for (let i = 0; i < res.length; i++) { + if (checkObject(res[i].doc, socket._acl, 4 /* 'read' */)) { + objects[res[i].doc._id] = res[i].doc; + } + } + if (typeof callback === 'function') { + callback(null, objects); + } else { + that.adapter.log.warn('[getAllObjects] Invalid callback') + } + } else { + for (let j = 0; j < res.length; j++) { + objects[res[j].doc._id] = res[j].doc; + } + if (typeof callback === 'function') { + callback(null, objects); + } else { + that.adapter.log.warn('[getAllObjects] Invalid callback') + } + } + }); + } + }); + + socket.on('delObject', function (id, callback) { + if (updateSession(socket) && checkPermissions(socket, 'delObject', callback, id)) { + that.adapter.delForeignObject(id, {user: socket._acl.user}, callback); + } + }); + socket.on('extendObject', function (id, obj, callback) { + if (updateSession(socket) && checkPermissions(socket, 'extendObject', callback, id)) { + that.adapter.extendForeignObject(id, obj, {user: socket._acl.user}, callback); + } + }); + socket.on('getHostByIp', function (ip, callback) { + if (updateSession(socket) && checkPermissions(socket, 'getHostByIp', ip)) { + that.adapter.objects.getObjectView('system', 'host', {}, {user: socket._acl.user}, function (err, data) { + if (data.rows.length) { + for (let i = 0; i < data.rows.length; i++) { + if (data.rows[i].value.common.hostname === ip) { + if (typeof callback === 'function') { + callback(ip, data.rows[i].value); + } else { + that.adapter.log.warn('[getHostByIp] Invalid callback') + } + return; + } + if (data.rows[i].value.native.hardware && data.rows[i].value.native.hardware.networkInterfaces) { + let net = data.rows[i].value.native.hardware.networkInterfaces; + for (let eth in net) { + if (!net.hasOwnProperty(eth)) continue; + for (let j = 0; j < net[eth].length; j++) { + if (net[eth][j].address === ip) { + if (typeof callback === 'function') { + callback(ip, data.rows[i].value); + } else { + that.adapter.log.warn('[getHostByIp] Invalid callback') + } + return; + } + } + } + } + } + } + + if (typeof callback === 'function') { + callback(ip, null); + } else { + that.adapter.log.warn('[getHostByIp] Invalid callback') + } + }); + } + }); + + socket.on('getForeignObjects', function (pattern, type, callback) { + if (updateSession(socket) && checkPermissions(socket, 'getObjects', callback)) { + if (typeof type === 'function') { + callback = type; + type = undefined; + } + + that.adapter.getForeignObjects(pattern, type, {user: socket._acl.user}, function (err, objs) { + if (typeof callback === 'function') { + callback(err, objs); + } else { + that.adapter.log.warn('[getObjects] Invalid callback') + } + }); + } + }); + + socket.on('getForeignStates', function (pattern, callback) { + if (updateSession(socket) && checkPermissions(socket, 'getStates', callback)) { + that.adapter.getForeignStates(pattern, {user: socket._acl.user}, function (err, objs) { + if (typeof callback === 'function') { + callback(err, objs); + } else { + that.adapter.log.warn('[getObjects] Invalid callback') + } + }); + } + }); + + socket.on('requireLog', function (isEnabled, callback) { + if (updateSession(socket) && checkPermissions(socket, 'setObject', callback)) { + if (isEnabled) { + that.subscribe(this, 'log', 'dummy') + } else { + that.unsubscribe(this, 'log', 'dummy') + } + if (that.adapter.log.level === 'debug') showSubscribes(socket, 'log'); + + if (typeof callback === 'function') { + setImmediate(callback, null); + } + } + }); + + socket.on('readLogs', function (callback) { + if (updateSession(socket) && checkPermissions(socket, 'readLogs', callback)) { + let result = {list: []}; + + // deliver file list + try { + let config = adapter.systemConfig; + // detect file log + if (config && config.log && config.log.transport) { + for (let transport in config.log.transport) { + if (config.log.transport.hasOwnProperty(transport) && config.log.transport[transport].type === 'file') { + let filename = config.log.transport[transport].filename || 'log/'; + let parts = filename.replace(/\\/g, '/').split('/'); + parts.pop(); + filename = parts.join('/'); + if (filename[0] === '.') { + filename = path.normalize(__dirname + '/../../../') + filename; + } + if (fs.existsSync(filename)) { + let files = fs.readdirSync(filename); + for (let f = 0; f < files.length; f++) { + try { + if (!fs.lstatSync(filename + '/' + files[f]).isDirectory()) { + result.list.push('log/' + transport + '/' + files[f]); + } + } catch (e) { + // push unchecked + // result.list.push('log/' + transport + '/' + files[f]); + adapter.log.error('Cannot check file: ' + filename + '/' + files[f]); + } + } + } + } + } + } else { + result.error = 'no file loggers'; + result.list = undefined; + } + } catch (e) { + adapter.log.error(e); + result.error = e; + result.list = undefined; + } + if (typeof callback === 'function') { + callback(result.error, result.list); + } + } + }); + } else { + // only flot allowed + socket.on('delObject', function (id, callback) { + if (id.match(/^flot\./)) { + if (updateSession(socket) && checkPermissions(socket, 'delObject', callback, id)) { + that.adapter.delForeignObject(id, {user: socket._acl.user}, callback); + } + } else { + if (typeof callback === 'function') { + callback('permissionError'); + } + } + }); + } + + socket.on('getState', function (id, callback) { + if (updateSession(socket) && checkPermissions(socket, 'getState', callback, id)) { + that.adapter.getForeignState(id, {user: socket._acl.user}, callback); + } + }); + + socket.on('setState', function (id, state, callback) { + if (updateSession(socket) && checkPermissions(socket, 'setState', callback, id)) { + if (typeof state !== 'object') state = {val: state}; + that.adapter.setForeignState(id, state, {user: socket._acl.user}, function (err, res) { + if (typeof callback === 'function') { + callback(err, res); + } + }); + } + }); + + // allow admin access + if (that.settings.allowAdmin) { + socket.on('delState', function (id, callback) { + if (updateSession(socket) && checkPermissions(socket, 'delState', callback, id)) { + that.adapter.delForeignState(id, {user: socket._acl.user}, callback); + } + }); + socket.on('addUser', function (user, pass, callback) { + if (updateSession(socket) && checkPermissions(socket, 'addUser', callback, user)) { + addUser(user, pass, {user: socket._acl.user}, callback); + } + }); + + socket.on('delUser', function (user, callback) { + if (updateSession(socket) && checkPermissions(socket, 'delUser', callback, user)) { + delUser(user, {user: socket._acl.user}, callback); + } + }); + + socket.on('addGroup', function (group, desc, acl, callback) { + if (updateSession(socket) && checkPermissions(socket, 'addGroup', callback, group)) { + addGroup(group, desc, acl, {user: socket._acl.user}, callback); + } + }); + + socket.on('delGroup', function (group, callback) { + if (updateSession(socket) && checkPermissions(socket, 'delGroup', callback, group)) { + delGroup(group, {user: socket._acl.user}, callback); + } + }); + + socket.on('changePassword', function (user, pass, callback) { + if (updateSession(socket)) { + if (user === socket._acl.user || checkPermissions(socket, 'changePassword', callback, user)) { + that.adapter.setPassword(user, pass, {user: socket._acl.user}, callback); + } + } + }); + // commands will be executed on host/controller + // following response commands are expected: cmdStdout, cmdStderr, cmdExit + socket.on('cmdExec', function (host, id, cmd, callback) { + if (updateSession(socket) && checkPermissions(socket, 'cmdExec', callback, cmd)) { + that.adapter.log.debug('cmdExec on ' + host + '(' + id + '): ' + cmd); + that.adapter.sendToHost(host, 'cmdExec', {data: cmd, id: id}); + } + }); + + socket.on('eventsThreshold', function (isActive) { + if (!isActive) { + disableEventThreshold(true); + } else { + enableEventThreshold(); + } + }); + } + + socket.on('getVersion', function (callback) { + if (updateSession(socket) && checkPermissions(socket, 'getVersion', callback)) { + if (typeof callback === 'function') { + callback(that.adapter.version); + } else { + that.adapter.log.warn('[getVersion] Invalid callback') + } + } + }); + + socket.on('subscribe', function (pattern, callback) { + if (updateSession(socket) && checkPermissions(socket, 'subscribe', callback, pattern)) { + if (pattern && typeof pattern === 'object' && pattern instanceof Array) { + for (let p = 0; p < pattern.length; p++) { + that.subscribe(this, 'stateChange', pattern[p]); + } + } else { + that.subscribe(this, 'stateChange', pattern); + } + if (that.adapter.log.level === 'debug') showSubscribes(socket, 'stateChange'); + if (typeof callback === 'function') { + setImmediate(callback, null); + } + } + }); + + socket.on('unsubscribe', function (pattern, callback) { + if (updateSession(socket) && checkPermissions(socket, 'unsubscribe', callback, pattern)) { + if (pattern && typeof pattern === 'object' && pattern instanceof Array) { + for (let p = 0; p < pattern.length; p++) { + that.unsubscribe(this, 'stateChange', pattern[p]); + } + } else { + that.unsubscribe(this, 'stateChange', pattern); + } + if (that.adapter.log.level === 'debug') showSubscribes(socket, 'stateChange'); + if (typeof callback === 'function') { + setImmediate(callback, null); + } + } + }); + + + // new History + socket.on('getHistory', function (id, options, callback) { + if (updateSession(socket) && checkPermissions(socket, 'getStateHistory', callback, id)) { + options = options || {}; + options.user = socket._acl.user; + that.adapter.getHistory(id, options, function (err, data, step, sessionId) { + if (typeof callback === 'function') { + callback(err, data, step, sessionId); + } else { + that.adapter.log.warn('[getHistory] Invalid callback') + } + }); + } + }); + + // HTTP + socket.on('httpGet', function (url, callback) { + if (updateSession(socket) && checkPermissions(socket, 'httpGet', callback, url)) { + if (!request) request = require('request'); + that.adapter.log.debug('httpGet: ' + url); + request(url, callback); + } + }); + + // commands + socket.on('sendTo', function (adapterInstance, command, message, callback) { + if (updateSession(socket) && checkPermissions(socket, 'sendTo', callback, command)) { + that.adapter.sendTo(adapterInstance, command, message, callback); + } + }); + + // following commands are protected and require the extra permissions + const protectedCommands = ['cmdExec', 'getLocationOnDisk', 'getDiagData', 'getDevList', 'delLogs', 'writeDirAsZip', 'writeObjectsAsZip', 'readObjectsAsZip', 'checkLogging', 'updateMultihost']; + + socket.on('sendToHost', function (host, command, message, callback) { + // host can answer following commands: cmdExec, getRepository, getInstalled, getInstalledAdapter, getVersion, getDiagData, getLocationOnDisk, getDevList, getLogs, getHostInfo, + // delLogs, readDirAsZip, writeDirAsZip, readObjectsAsZip, writeObjectsAsZip, checkLogging, updateMultihost + if (updateSession(socket) && checkPermissions(socket, protectedCommands.indexOf(command) !== -1 ? 'cmdExec' : 'sendToHost', callback, command)) { + that.adapter.sendToHost(host, command, message, callback); + } + }); + + socket.on('authEnabled', callback => { + if (updateSession(socket) && checkPermissions(socket, 'authEnabled', callback)) { + if (typeof callback === 'function') { + callback(that.settings.auth, socket._acl.user.replace(/^system\.user\./, '')); + } else { + that.adapter.log.warn('[authEnabled] Invalid callback') + } + } + }); + + // file operations + socket.on('readFile', function (_adapter, fileName, callback) { + if (updateSession(socket) && checkPermissions(socket, 'readFile', callback, fileName)) { + that.adapter.readFile(_adapter, fileName, {user: socket._acl.user}, callback); + } + }); + + socket.on('readFile64', function (_adapter, fileName, callback) { + if (updateSession(socket) && checkPermissions(socket, 'readFile64', callback, fileName)) { + that.adapter.readFile(_adapter, fileName, {user: socket._acl.user}, function (err, buffer, type) { + let data64; + if (buffer) { + if (type === 'application/json') { + data64 = new Buffer(encodeURIComponent(buffer)).toString('base64'); + } else { + if (typeof buffer === 'string') { + data64 = new Buffer(buffer).toString('base64'); + } else { + data64 = buffer.toString('base64'); + } + } + } + + //Convert buffer to base 64 + if (typeof callback === 'function') { + callback(err, data64 || '', type); + } else { + that.adapter.log.warn('[readFile64] Invalid callback') + } + }); + } + }); + + socket.on('writeFile64', function (_adapter, fileName, data64, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {user: socket._acl.user}; + } + if (!options) options = {}; + options.user = socket._acl.user; + + if (updateSession(socket) && checkPermissions(socket, 'writeFile64', callback, fileName)) { + //Convert base 64 to buffer + let buffer = new Buffer(data64, 'base64'); + that.adapter.writeFile(_adapter, fileName, buffer, options, function (err) { + if (typeof callback === 'function') { + callback(err); + } + }); + } + }); + + socket.on('writeFile', function (_adapter, fileName, data, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {user: socket._acl.user}; + } + if (!options) options = {}; + options.user = socket._acl.user; + if (updateSession(socket) && checkPermissions(socket, 'writeFile', callback, fileName)) { + that.adapter.writeFile(_adapter, fileName, data, options, callback); + } + }); + + socket.on('unlink', function (_adapter, name, callback) { + if (updateSession(socket) && checkPermissions(socket, 'unlink', callback, name)) { + that.adapter.unlink(_adapter, name, {user: socket._acl.user}, callback); + } + }); + + socket.on('rename', function (_adapter, oldName, newName, callback) { + if (updateSession(socket) && checkPermissions(socket, 'rename', callback, oldName)) { + that.adapter.rename(_adapter, oldName, newName, {user: socket._acl.user}, callback); + } + }); + + socket.on('mkdir', function (_adapter, dirName, callback) { + if (updateSession(socket) && checkPermissions(socket, 'mkdir', callback, dirName)) { + that.adapter.mkdir(_adapter, dirName, {user: socket._acl.user}, callback); + } + }); + + socket.on('readDir', function (_adapter, dirName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + options = options || {}; + options.user = socket._acl.user; + + if (options.filter === undefined) options.filter = true; + + if (updateSession(socket) && checkPermissions(socket, 'readDir', callback, dirName)) { + that.adapter.readDir(_adapter, dirName, options, callback); + } + }); + + socket.on('chmodFile', function (_adapter, dirName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + options = options || {}; + options.user = socket._acl.user; + + if (options.filter === undefined) options.filter = true; + + if (updateSession(socket) && checkPermissions(socket, 'chmodFile', callback, dirName)) { + that.adapter.chmodFile(_adapter, dirName, options, callback); + } + }); + + // connect/disconnect + socket.on('disconnect', function (error) { + that.adapter.log.info('<==Disconnect ' + socket._acl.user + ' from ' + getClientAddress(socket) + ' ' + (socket._name || '')); + unsubscribeSocket(this, 'stateChange'); + unsubscribeSocket(this, 'objectChange'); + unsubscribeSocket(this, 'log'); + if (!that.infoTimeout) that.infoTimeout = setTimeout(updateConnectedInfo, 1000); + + // if client mode + if (!socket.conn) { + socket._apiKeyOk = false; + that.emit && that.emit('disconnect', error); + } + }); + + socket.on('logout', function (callback) { + that.adapter.destroySession(socket._sessionID, callback); + }); + + socket.on('listPermissions', function (callback) { + if (updateSession(socket)) { + if (typeof callback === 'function') { + callback(commandsPermissions); + } else { + that.adapter.log.warn('[listPermissions] Invalid callback'); + } + } + }); + + socket.on('getUserPermissions', function (callback) { + if (updateSession(socket) && checkPermissions(socket, 'getUserPermissions', callback)) { + if (typeof callback === 'function') { + callback(null, socket._acl); + } else { + that.adapter.log.warn('[getUserPermissions] Invalid callback') + } + } + }); + + if (typeof that.settings.extensions === 'function') { + that.settings.extensions(socket); + } + + // if client mode + if (!socket.conn) { + socket._apiKeyOk = false; + + socket.on('cloudDisconnect', function (err) { + err && that.adapter.log.warn('User disconnected from cloud: ' + err); + unsubscribeSocket(socket, 'stateChange'); + unsubscribeSocket(socket, 'objectChange'); + unsubscribeSocket(socket, 'log'); + that.emit('cloudDisconnect'); + }); + + socket.on('cloudConnect', function () { + // do not autosubscribe. The client must resubscribe all states anew + // subscribeSocket(socket, 'stateChange'); + // subscribeSocket(socket, 'objectChange'); + // subscribeSocket(socket, 'log'); + that.emit('cloudConnect'); + }); + + socket.on('cloudCommand', function (cmd, data) { + if (cmd === 'stop') { + stopAdapter(data); + } else if (cmd === 'redirect') { + redirectAdapter(data); + } else if (cmd === 'wait') { + waitForConnect(data || 30); + } + }); + + // only active in client mode + socket.on('connect', function () { + that.adapter.log.debug('Connected. Check api key...'); + socket._apiKeyOk = false; + + // 2018_01_20 workaround for pro: Remove it after next pro maintenance + if (that.settings.apikey.indexOf('@pro_') !== -1) { + socket._apiKeyOk = true; + that.emit && that.emit('connect'); + } + + // send api key if exists + this.emit('apikey', that.settings.apikey, that.settings.version, that.settings.uuid, function (err, instructions) { + // instructions = { + // validTill: '2018-03-14T01:01:01.567Z', + // command: 'wait' | 'stop' | 'redirect' + // data: some data for command (URL for redirect or seconds for wait' + + if (instructions) { + if (typeof instructions !== 'object') { + that.adapter.setState('info.remoteTill', new Date(instructions).toISOString(), true); + } else { + if (instructions.validTill) { + that.adapter.setState('info.remoteTill', new Date(instructions.validTill).toISOString(), true); + } + if (instructions.command === 'stop') { + stopAdapter(instructions.data); + } else if (instructions.command === 'redirect') { + redirectAdapter(instructions.data); + } else if (instructions.command === 'wait') { + waitForConnect(instructions.data || 30); + } + } + } + + if (!err) { + that.adapter.log.debug('API KEY OK'); + socket._apiKeyOk = true; + + that.emit && that.emit('connect'); + } else { + if (err.indexOf('Please buy remote access to use pro.') !== -1) { + stopAdapter('Please buy remote access to use pro.'); + } + that.adapter.log.error(err); + this.close(); // disconnect + } + }); + + if (socket._sessionID) { + that.adapter.getSession(socket._sessionID, function (obj) { + if (obj && obj.passport) { + socket._acl.user = obj.passport.user; + } else { + socket._acl.user = ''; + socket.emit('reauthenticate'); + socket.disconnect(); + } + if (socket._authPending) { + socket._authPending(!!socket._acl.user, true); + delete socket._authPending; + } + }); + } + + subscribeSocket(this, 'stateChange'); + subscribeSocket(this, 'objectChange'); + subscribeSocket(this, 'log'); + }); + + /*socket.on('reconnect', function (attempt) { + that.adapter.log.debug('Connected after attempt ' + attempt); + }); + socket.on('reconnect_attempt', function (attempt) { + that.adapter.log.debug('reconnect_attempt'); + }); + socket.on('connect_error', function (error) { + that.adapter.log.debug('connect_error: ' + error); + }); + socket.on('connect_timeout', function (error) { + that.adapter.log.debug('connect_timeout'); + }); + socket.on('reconnect_failed', function (error) { + that.adapter.log.debug('reconnect_failed'); + });*/ + } else { + // if server mode + if (socket.conn.request.sessionID) { + socket._secure = true; + socket._sessionID = socket.conn.request.sessionID; + // Get user for session + that.settings.store.get(socket.conn.request.sessionID, function (err, obj) { + if (!obj || !obj.passport) { + socket._acl.user = ''; + socket.emit('reauthenticate'); + socket.disconnect(); + } + if (socket._authPending) { + socket._authPending(!!socket._acl.user, true); + delete socket._authPending; + } + }); + } + + subscribeSocket(socket, 'stateChange'); + subscribeSocket(socket, 'objectChange'); + subscribeSocket(socket, 'log'); + } + } + + function updateConnectedInfo() { + if (that.infoTimeout) { + clearTimeout(that.infoTimeout); + that.infoTimeout = null; + } + if (that.server.sockets) { + let text = ''; + let cnt = 0; + if (that.server) { + let clients = that.server.sockets.connected; + + for (let i in clients) { + if (clients.hasOwnProperty(i)) { + text += (text ? ', ' : '') + (clients[i]._name || 'noname'); + cnt++; + } + } + } + text = '[' + cnt + ']' + text; + that.adapter.setState('connected', text, true); + } + } + + this.publishAll = function (type, id, obj) { + if (id === undefined) { + console.log('Problem'); + } + + let clients = this.server.sockets.connected; + + for (let i in clients) { + if (clients.hasOwnProperty(i)) { + publish(clients[i], type, id, obj); + } + } + }; + + this.sendLog = function (obj) { + // TODO Build in some threshold + if (this.server && this.server.sockets) { + this.server.sockets.emit('log', obj); + } + }; + + (function __constructor() { + + if (that.settings.allowAdmin) { + // detect event bursts + setInterval(function () { + if (!eventsThreshold.active) { + if (eventsThreshold.count > eventsThreshold.value) { + eventsThreshold.accidents++; + + if (eventsThreshold.accidents >= eventsThreshold.repeatSeconds) { + enableEventThreshold(); + } + } else { + eventsThreshold.accidents = 0; + } + eventsThreshold.count = 0; + } else if (new Date().getTime() - eventsThreshold.timeActivated > 60000) { + disableEventThreshold(); + } + }, eventsThreshold.checkInterval); + } + + // it can be used as client too for cloud + if (!that.settings.apikey) { + if (!that.webServer.__inited) { + that.server = socketio.listen(that.webServer); + that.webServer.__inited = true; + } + + // force using only websockets + if (that.settings.forceWebSockets) that.server.set('transports', ['websocket']); + } else { + that.server = server; + } + + // socket = socketio.listen(settings.port, (settings.bind && settings.bind !== "0.0.0.0") ? settings.bind : undefined); + that.settings.defaultUser = that.settings.defaultUser || 'system.user.admin'; + if (!that.settings.defaultUser.match(/^system\.user\./)) that.settings.defaultUser = 'system.user.' + that.settings.defaultUser; + + if (that.settings.auth && that.server) { + that.server.use(function (socket, next) { + if (!socket.request._query.user || !socket.request._query.pass) { + getUserFromSocket(socket, function (err, user) { + if (err || !user) { + socket.emit('reauthenticate'); + that.adapter.log.error('socket.io ' + (err || 'User not found')); + socket.disconnect(); + } else { + socket._secure = true; + that.adapter.calculatePermissions('system.user.' + user, commandsPermissions, function (acl) { + let address = getClientAddress(socket); + // socket._acl = acl; + socket._acl = mergeACLs(address, acl, that.settings.whiteListSettings); + next(); + }); + } + }); + } else { + that.adapter.checkPassword(socket.request._query.user, socket.request._query.pass, function (res) { + if (res) { + that.adapter.log.debug('Logged in: ' + socket.request._query.user + ', ' + socket.request._query.pass); + next(); + } else { + that.adapter.log.warn('Invalid password or user name: ' + socket.request._query.user + ', ' + socket.request._query.pass); + socket.emit('reauthenticate'); + next(new Error('Invalid password or user name')); + } + }); + } + }); + } + + // Enable cross domain access + if (that.settings.crossDomain && that.server.set) { + that.server.set('origins', '*:*'); + } + + that.settings.ttl = that.settings.ttl || 3600; + + that.server.on('connection', that.initSocket); + + if (settings.port) { + that.adapter.log.info((settings.secure ? 'Secure ' : '') + 'socket.io server listening on port ' + settings.port); + } + + if (!that.infoTimeout) that.infoTimeout = setTimeout(updateConnectedInfo, 1000); + + // if client mode => add event handlers + if (that.settings.apikey) { + that.initSocket(that.server); + } + })(); +} + +util.inherits(IOSocket, EventEmitter); + +module.exports = IOSocket; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..6af37ff --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,83 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +let controllerDir; +let appName; + +/** + * returns application name + * + * The name of the application can be different and this function finds it out. + * + * @returns {string} + */ + function getAppName() { + const parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 2].split('.')[0]; +} + +/** + * looks for js-controller home folder + * + * @param {boolean} isInstall + * @returns {string} + */ +function getControllerDir(isInstall) { + // Find the js-controller location + const possibilities = [ + 'yunkong2.js-controller', + 'yunkong2.js-controller', + ]; + /** @type {string} */ + let controllerPath; + for (const pkg of possibilities) { + try { + const possiblePath = require.resolve(pkg); + if (fs.existsSync(possiblePath)) { + controllerPath = possiblePath; + break; + } + } catch (e) { /* not found */ } + } + if (controllerPath == null) { + if (!isInstall) { + console.log('Cannot find js-controller'); + process.exit(10); + } else { + process.exit(); + } + } + // we found the controller + return path.dirname(controllerPath); +} + +/** + * reads controller base settings + * + * @alias getConfig + * @returns {object} + */ + function getConfig() { + let configPath; + if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', appName + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', + appName.toLowerCase() + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else { + throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json'); + } +} +appName = getAppName(); +controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1); +const adapter = require(path.join(controllerDir, 'lib/adapter.js')); + +exports.controllerDir = controllerDir; +exports.getConfig = getConfig; +exports.Adapter = adapter; +exports.appName = appName; diff --git a/main.js b/main.js new file mode 100644 index 0000000..78a969e --- /dev/null +++ b/main.js @@ -0,0 +1,141 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +'use strict'; + +const utils = require(__dirname + '/lib/utils'); // Get common adapter utils +const IOSocket = require(__dirname + '/lib/socket.js'); +const LE = require(utils.controllerDir + '/lib/letsencrypt.js'); + +let webServer = null; +let store = null; +let secret = 'Zgfr56gFe87jJOM'; // Will be generated by first start + +const adapter = new utils.Adapter('socketio'); + +adapter.on('objectChange', (id, obj) => { + if (webServer && webServer.io) { + webServer.io.publishAll('objectChange', id, obj); + } +}); + +adapter.on('stateChange', (id, state) => { + if (webServer && webServer.io) { + webServer.io.publishAll('stateChange', id, state); + } +}); + +adapter.on('unload', callback => { + try { + adapter.log.info('terminating http' + (webServer.settings.secure ? 's' : '') + ' server on port ' + webServer.settings.port); + webServer.io.close(); + + callback(); + } catch (e) { + callback(); + } +}); + +adapter.on('ready', () => { + if (adapter.config.auth) { + // Generate secret for session manager + adapter.getForeignObject('system.config', (err, obj) => { + if (!err && obj) { + if (!obj.native || !obj.native.secret) { + obj.native = obj.native || {}; + require('crypto').randomBytes(24, (ex, buf) => { + secret = buf.toString('hex'); + adapter.extendForeignObject('system.config', {native: {secret: secret}}); + main(); + }); + } else { + secret = obj.native.secret; + main(); + } + } else { + adapter.logger.error('Cannot find object system.config'); + } + }); + } else { + main(); + } + +}); + +adapter.on('log', obj => { + if (webServer && webServer.io) { + webServer.io.sendLog(obj); + } +}); + +function main() { + if (adapter.config.secure) { + // Load certificates + adapter.getCertificates(function (err, certificates, leConfig) { + adapter.config.certificates = certificates; + adapter.config.leConfig = leConfig; + webServer = initWebServer(adapter.config); + }); + } else { + webServer = initWebServer(adapter.config); + } +} + +//settings: { +// "port": 8080, +// "auth": false, +// "secure": false, +// "bind": "0.0.0.0", // "::" +//} +function initWebServer(settings) { + + const server = { + app: null, + server: null, + io: null, + settings: settings + }; + + settings.port = parseInt(settings.port, 10) || 0; + + if (settings.port) { + if (settings.secure) { + if (!settings.certificates) return null; + } + if (settings.auth) { + const session = require('express-session'); + const AdapterStore = require(utils.controllerDir + '/lib/session.js')(session, settings.ttl); + // Authentication checked by server itself + store = new AdapterStore({adapter: adapter}); + settings.secret = secret; + settings.store = store; + settings.ttl = settings.ttl || 3600; + settings.forceWebSockets = settings.forceWebSockets || false; + } + + adapter.getPort(settings.port, port => { + if (parseInt(port, 10) !== settings.port && !adapter.config.findNextPort) { + adapter.log.error('port ' + settings.port + ' already in use'); + process.exit(1); + } + settings.port = port; + + server.server = LE.createServer((req, res) => { + res.writeHead(501); + res.end('Not Implemented'); + }, settings, adapter.config.certificates, adapter.config.leConfig, adapter.log); + + server.server.listen(settings.port, (settings.bind && settings.bind !== '0.0.0.0') ? settings.bind : undefined); + + settings.crossDomain = true; + settings.ttl = settings.ttl || 3600; + settings.forceWebSockets = settings.forceWebSockets || false; + + server.io = new IOSocket(server.server, settings, adapter); + }); + } else { + adapter.log.error('port missing'); + process.exit(1); + } + + return server; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ef0fec2 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "yunkong2.socketio", + "version": "2.1.1", + "description": "This adapter allows to communicate different web applications with yunkong2.", + "author": { + "name": "bluefox", + "email": "dogafox@gmail.com" + }, + "homepage": "https://git.spacen.net/yunkong2/yunkong2.socketio", + "licenses": [ + { + "type": "MIT", + "url": "https://git.spacen.net/yunkong2/yunkong2.socketio/blob/master/LICENSE" + } + ], + "keywords": [ + "yunkong2", + "web" + ], + "repository": { + "type": "git", + "url": "https://git.spacen.net/yunkong2/yunkong2.socketio" + }, + "dependencies": { + "socket.io": "1.7.2", + "request": "^2.87.0", + "cookie-parser": "^1.4.3", + "express-session": "^1.15.6" + }, + "devDependencies": { + "gulp": "^3.9.1", + "mocha": "^5.2.0", + "chai": "^4.1.2" + }, + "bugs": { + "url": "https://git.spacen.net/yunkong2/yunkong2.socketio/issues" + }, + "main": "main.js", + "scripts": { + "test": "node node_modules/mocha/bin/mocha --exit" + }, + "license": "MIT", + "readmeFilename": "README.md" +} diff --git a/tasks/jscs.js b/tasks/jscs.js new file mode 100644 index 0000000..ef0042c --- /dev/null +++ b/tasks/jscs.js @@ -0,0 +1,19 @@ +var srcDir = __dirname + "/../"; + +module.exports = { + all: { + src: [ + srcDir + "*.js", + srcDir + "lib/*.js", + srcDir + "adapter/**/*.js", + srcDir + "tasks/**/*.js", + srcDir + "www/**/*.js", + '!' + srcDir + "www/lib/**/*.js", + '!' + srcDir + 'node_modules/**/*.js', + '!' + srcDir + 'adapter/admin/www/lib/**/*.js', + '!' + srcDir + 'adapter/*/node_modules/**/*.js', + '!' + srcDir + 'adapter/legacy/www/*/**/*.js' + ], + options: require('./jscsRules.js') + } +}; \ No newline at end of file diff --git a/tasks/jscsRules.js b/tasks/jscsRules.js new file mode 100644 index 0000000..ded301d --- /dev/null +++ b/tasks/jscsRules.js @@ -0,0 +1,36 @@ +module.exports = { + force: true, + "requireCurlyBraces": ["else", "for", "while", "do", "try", "catch"], /*"if",*/ + "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], + "requireSpaceBeforeBlockStatements": true, + "requireParenthesesAroundIIFE": true, + "disallowSpacesInFunctionDeclaration": {"beforeOpeningRoundBrace": true}, + "disallowSpacesInNamedFunctionExpression": {"beforeOpeningRoundBrace": true}, + "requireSpacesInFunctionExpression": {"beforeOpeningCurlyBrace": true}, + "requireSpacesInAnonymousFunctionExpression": {"beforeOpeningRoundBrace": true, "beforeOpeningCurlyBrace": true}, + "requireSpacesInNamedFunctionExpression": {"beforeOpeningCurlyBrace": true}, + "requireSpacesInFunctionDeclaration": {"beforeOpeningCurlyBrace": true}, + "disallowMultipleVarDecl": true, + "requireBlocksOnNewline": true, + "disallowEmptyBlocks": true, + "disallowSpacesInsideObjectBrackets": true, + "disallowSpacesInsideArrayBrackets": true, + "disallowSpaceAfterObjectKeys": true, + "disallowSpacesInsideParentheses": true, + "requireCommaBeforeLineBreak": true, + //"requireAlignedObjectValues": "all", + "requireOperatorBeforeLineBreak": ["?", "+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], +// "disallowLeftStickedOperators": ["?", "+", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], +// "requireRightStickedOperators": ["!"], +// "requireSpaceAfterBinaryOperators": ["?", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], + //"disallowSpaceAfterBinaryOperators": [","], + "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], + "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], + "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + "requireSpaceAfterBinaryOperators": ["?", ">", ",", ">=", "<=", "<", "+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + //"validateIndentation": 4, + //"validateQuoteMarks": { "mark": "\"", "escape": true }, + "disallowMixedSpacesAndTabs": true, + "disallowKeywordsOnNewLine": ["else", "catch"] + +}; diff --git a/tasks/jshint.js b/tasks/jshint.js new file mode 100644 index 0000000..5c8d732 --- /dev/null +++ b/tasks/jshint.js @@ -0,0 +1,19 @@ +var srcDir = __dirname + "/../"; + +module.exports = { + options: { + force: true + }, + all: [ + srcDir + "*.js", + srcDir + "lib/*.js", + srcDir + "adapter/**/*.js", + srcDir + "tasks/**/*.js", + srcDir + "www/**/*.js", + '!' + srcDir + "www/lib/**/*.js", + '!' + srcDir + 'node_modules/**/*.js', + '!' + srcDir + 'adapter/admin/www/lib/**/*.js', + '!' + srcDir + 'adapter/*/node_modules/**/*.js', + '!' + srcDir + 'adapter/legacy/www/*/**/*.js' + ] +}; \ No newline at end of file diff --git a/test/lib/setup.js b/test/lib/setup.js new file mode 100644 index 0000000..e2a1680 --- /dev/null +++ b/test/lib/setup.js @@ -0,0 +1,728 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +// check if tmp directory exists +var fs = require('fs'); +var path = require('path'); +var child_process = require('child_process'); +var rootDir = path.normalize(__dirname + '/../../'); +var pkg = require(rootDir + 'package.json'); +var debug = typeof v8debug === 'object'; +pkg.main = pkg.main || 'main.js'; + +var adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/'); +adapterName = adapterName[adapterName.length - 2]; +var adapterStarted = false; + +function getAppName() { + var parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 3].split('.')[0]; +} + +var appName = getAppName().toLowerCase(); + +var objects; +var states; + +var pid = null; + +function copyFileSync(source, target) { + + var targetFile = target; + + //if target is a directory a new file with the same name will be created + if (fs.existsSync(target)) { + if ( fs.lstatSync( target ).isDirectory() ) { + targetFile = path.join(target, path.basename(source)); + } + } + + try { + fs.writeFileSync(targetFile, fs.readFileSync(source)); + } + catch (err) { + console.log("file copy error: " +source +" -> " + targetFile + " (error ignored)"); + } +} + +function copyFolderRecursiveSync(source, target, ignore) { + var files = []; + + var base = path.basename(source); + if (base === adapterName) { + base = pkg.name; + } + //check if folder needs to be created or integrated + var targetFolder = path.join(target, base); + if (!fs.existsSync(targetFolder)) { + fs.mkdirSync(targetFolder); + } + + //copy + if (fs.lstatSync(source).isDirectory()) { + files = fs.readdirSync(source); + files.forEach(function (file) { + if (ignore && ignore.indexOf(file) !== -1) { + return; + } + + var curSource = path.join(source, file); + var curTarget = path.join(targetFolder, file); + if (fs.lstatSync(curSource).isDirectory()) { + // ignore grunt files + if (file.indexOf('grunt') !== -1) return; + if (file === 'chai') return; + if (file === 'mocha') return; + copyFolderRecursiveSync(curSource, targetFolder, ignore); + } else { + copyFileSync(curSource, curTarget); + } + }); + } +} + +if (!fs.existsSync(rootDir + 'tmp')) { + fs.mkdirSync(rootDir + 'tmp'); +} + +function storeOriginalFiles() { + console.log('Store original files...'); + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0'] && objects['system.adapter.admin.0'].common) { + objects['system.adapter.admin.0'].common.enabled = false; + } + if (objects['system.adapter.admin.1'] && objects['system.adapter.admin.1'].common) { + objects['system.adapter.admin.1'].common.enabled = false; + } + + fs.writeFileSync(dataDir + 'objects.json.original', JSON.stringify(objects)); + try { + f = fs.readFileSync(dataDir + 'states.json'); + fs.writeFileSync(dataDir + 'states.json.original', f); + } + catch (err) { + console.log('no states.json found - ignore'); + } +} + +function restoreOriginalFiles() { + console.log('restoreOriginalFiles...'); + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var f = fs.readFileSync(dataDir + 'objects.json.original'); + fs.writeFileSync(dataDir + 'objects.json', f); + try { + f = fs.readFileSync(dataDir + 'states.json.original'); + fs.writeFileSync(dataDir + 'states.json', f); + } + catch (err) { + console.log('no states.json.original found - ignore'); + } + +} + +function checkIsAdapterInstalled(cb, counter, customName) { + customName = customName || pkg.name.split('.').pop(); + counter = counter || 0; + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + console.log('checkIsAdapterInstalled...'); + + try { + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.' + customName + '.0']) { + console.log('checkIsAdapterInstalled: ready!'); + setTimeout(function () { + if (cb) cb(); + }, 100); + return; + } else { + console.warn('checkIsAdapterInstalled: still not ready'); + } + } catch (err) { + + } + + if (counter > 20) { + console.error('checkIsAdapterInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsAdapterInstalled: wait...'); + setTimeout(function() { + checkIsAdapterInstalled(cb, counter + 1); + }, 1000); + } +} + +function checkIsControllerInstalled(cb, counter) { + counter = counter || 0; + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + console.log('checkIsControllerInstalled...'); + try { + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0']) { + console.log('checkIsControllerInstalled: installed!'); + setTimeout(function () { + if (cb) cb(); + }, 100); + return; + } + } catch (err) { + + } + + if (counter > 20) { + console.log('checkIsControllerInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsControllerInstalled: wait...'); + setTimeout(function() { + checkIsControllerInstalled(cb, counter + 1); + }, 1000); + } +} + +function installAdapter(customName, cb) { + if (typeof customName === 'function') { + cb = customName; + customName = null; + } + customName = customName || pkg.name.split('.').pop(); + console.log('Install adapter...'); + var startFile = 'node_modules/' + appName + '.js-controller/' + appName + '.js'; + // make first install + if (debug) { + child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + checkIsAdapterInstalled(function (error) { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + } else { + // add controller + var _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + + waitForEnd(_pid, function () { + checkIsAdapterInstalled(function (error) { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + }); + } +} + +function waitForEnd(_pid, cb) { + if (!_pid) { + cb(-1, -1); + return; + } + _pid.on('exit', function (code, signal) { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); + _pid.on('close', function (code, signal) { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); +} + +function installJsController(cb) { + console.log('installJsController...'); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') || + !fs.existsSync(rootDir + 'tmp/' + appName + '-data')) { + // try to detect appName.js-controller in node_modules/appName.js-controller + // travis CI installs js-controller into node_modules + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"'); + // copy all + // stop controller + console.log('Stop controller if running...'); + var _pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js stop', { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, function () { + // copy all files into + if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp'); + if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules'); + + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){ + console.log('Copy js-controller...'); + copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/'); + } + + console.log('Setup js-controller...'); + var __pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js setup first --console', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + __pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + waitForEnd(__pid, function () { + checkIsControllerInstalled(function () { + // change ports for object and state DBs + var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + console.log('Setup finished.'); + + copyAdapterToController(); + + installAdapter(function () { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }); + } else { + // check if port 9000 is free, else admin adapter will be added to running instance + var client = new require('net').Socket(); + client.connect(9000, '127.0.0.1', function() { + console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.'); + process.exit(0); + }); + + setTimeout(function () { + client.destroy(); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => install from git'); + + child_process.execSync('npm install https://git.spacen.net/' + appName + '/' + appName + '.js-controller/tarball/master --prefix ./ --production', { + cwd: rootDir + 'tmp/', + stdio: [0, 1, 2] + }); + } else { + console.log('Setup js-controller...'); + var __pid; + if (debug) { + // start controller + child_process.exec('node ' + appName + '.js setup first', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + child_process.fork(appName + '.js', ['setup', 'first'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + } + + // let npm install admin and run setup + checkIsControllerInstalled(function () { + var _pid; + + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, function () { + // change ports for object and state DBs + var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + + copyAdapterToController(); + + installAdapter(function () { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }, 1000); + } + } else { + setTimeout(function () { + console.log('installJsController: js-controller installed'); + if (cb) cb(false); + }, 0); + } +} + +function copyAdapterToController() { + console.log('Copy adapter...'); + // Copy adapter to tmp/node_modules/appName.adapter + copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']); + console.log('Adapter copied.'); +} + +function clearControllerLog() { + var dirPath = rootDir + 'tmp/log'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear controller log...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Controller log cleared'); + } catch (err) { + console.error('cannot clear log: ' + err); + } + } +} + +function clearDB() { + var dirPath = rootDir + 'tmp/yunkong2-data/sqlite'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear sqlite DB...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Clear sqlite DB'); + } catch (err) { + console.error('cannot clear DB: ' + err); + } + } +} + +function setupController(cb) { + installJsController(function (isInited) { + clearControllerLog(); + clearDB(); + + if (!isInited) { + restoreOriginalFiles(); + copyAdapterToController(); + } + // read system.config object + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var objs; + try { + objs = fs.readFileSync(dataDir + 'objects.json'); + objs = JSON.parse(objs); + } + catch (e) { + console.log('ERROR reading/parsing system configuration. Ignore'); + objs = {'system.config': {}}; + } + if (!objs || !objs['system.config']) { + objs = {'system.config': {}}; + } + + if (cb) cb(objs['system.config']); + }); +} + +function startAdapter(objects, states, callback) { + if (adapterStarted) { + console.log('Adapter already started ...'); + if (callback) callback(objects, states); + return; + } + adapterStarted = true; + console.log('startAdapter...'); + if (fs.existsSync(rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main)) { + try { + if (debug) { + // start controller + pid = child_process.exec('node node_modules/' + pkg.name + '/' + pkg.main + ' --console silly', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + } else { + // start controller + pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + } + } catch (error) { + console.error(JSON.stringify(error)); + } + } else { + console.error('Cannot find: ' + rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main); + } + if (callback) callback(objects, states); +} + +function startController(isStartAdapter, onObjectChange, onStateChange, callback) { + if (typeof isStartAdapter === 'function') { + callback = onStateChange; + onStateChange = onObjectChange; + onObjectChange = isStartAdapter; + isStartAdapter = true; + } + + if (onStateChange === undefined) { + callback = onObjectChange; + onObjectChange = undefined; + } + + if (pid) { + console.error('Controller is already started!'); + } else { + console.log('startController...'); + adapterStarted = false; + var isObjectConnected; + var isStatesConnected; + + var Objects = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/objects/objectsInMemServer'); + objects = new Objects({ + connection: { + "type" : "file", + "host" : "127.0.0.1", + "port" : 19001, + "user" : "", + "pass" : "", + "noFileCache": false, + "connectTimeout": 2000 + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.warn(msg); + }, + error: function (msg) { + console.error(msg); + } + }, + connected: function () { + isObjectConnected = true; + if (isStatesConnected) { + console.log('startController: started!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) { + callback(objects, states); + callback = null; + } + } + } + }, + change: onObjectChange + }); + + // Just open in memory DB itself + var States = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/states/statesInMemServer'); + states = new States({ + connection: { + type: 'file', + host: '127.0.0.1', + port: 19000, + options: { + auth_pass: null, + retry_max_delay: 15000 + } + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }, + connected: function () { + isStatesConnected = true; + if (isObjectConnected) { + console.log('startController: started!!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) { + callback(objects, states); + callback = null; + } + } + } + }, + change: onStateChange + }); + } +} + +function stopAdapter(cb) { + if (!pid) { + console.error('Controller is not running!'); + if (cb) { + setTimeout(function () { + cb(false); + }, 0); + } + } else { + adapterStarted = false; + pid.on('exit', function (code, signal) { + if (pid) { + console.log('child process terminated due to receipt of signal ' + signal); + if (cb) cb(); + pid = null; + } + }); + + pid.on('close', function (code, signal) { + if (pid) { + if (cb) cb(); + pid = null; + } + }); + + pid.kill('SIGTERM'); + } +} + +function _stopController() { + if (objects) { + objects.destroy(); + objects = null; + } + if (states) { + states.destroy(); + states = null; + } +} + +function stopController(cb) { + var timeout; + if (objects) { + console.log('Set system.adapter.' + pkg.name + '.0'); + objects.setObject('system.adapter.' + pkg.name + '.0', { + common:{ + enabled: false + } + }); + } + + stopAdapter(function () { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + + _stopController(); + + if (cb) { + cb(true); + cb = null; + } + }); + + timeout = setTimeout(function () { + timeout = null; + console.log('child process NOT terminated'); + + _stopController(); + + if (cb) { + cb(false); + cb = null; + } + pid = null; + }, 5000); +} + +// Setup the adapter +function setAdapterConfig(common, native, instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + if (common) objects[id].common = common; + if (native) objects[id].native = native; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/objects.json', JSON.stringify(objects)); +} + +// Read config of the adapter +function getAdapterConfig(instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + return objects[id]; +} + +if (typeof module !== undefined && module.parent) { + module.exports.getAdapterConfig = getAdapterConfig; + module.exports.setAdapterConfig = setAdapterConfig; + module.exports.startController = startController; + module.exports.stopController = stopController; + module.exports.setupController = setupController; + module.exports.stopAdapter = stopAdapter; + module.exports.startAdapter = startAdapter; + module.exports.installAdapter = installAdapter; + module.exports.appName = appName; + module.exports.adapterName = adapterName; + module.exports.adapterStarted = adapterStarted; +} diff --git a/test/testAdapter.js b/test/testAdapter.js new file mode 100644 index 0000000..afe399a --- /dev/null +++ b/test/testAdapter.js @@ -0,0 +1,142 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); + +var objects = null; +var states = null; +var onStateChanged = null; +var onObjectChanged = null; +var sendToID = 1; + +var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.') + 1); +var runningMode = require(__dirname + '/../io-package.json').common.mode; + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + console.log('Try check #' + counter); + if (counter > 30) { + if (cb) cb('Cannot check connection'); + return; + } + + states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) { + if (err) console.error(err); + if (state && state.val) { + if (cb) cb(); + } else { + setTimeout(function () { + checkConnectionOfAdapter(cb, counter + 1); + }, 1000); + } + }); +} + +function checkValueOfState(id, value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + if (cb) cb('Cannot check value Of State ' + id); + return; + } + + states.getState(id, function (err, state) { + if (err) console.error(err); + if (value === null && !state) { + if (cb) cb(); + } else + if (state && (value === undefined || state.val === value)) { + if (cb) cb(); + } else { + setTimeout(function () { + checkValueOfState(id, value, cb, counter + 1); + }, 500); + } + }); +} + +function sendTo(target, command, message, callback) { + onStateChanged = function (id, state) { + if (id === 'messagebox.system.adapter.test.0') { + callback(state.message); + } + }; + + states.pushMessage('system.adapter.' + target, { + command: command, + message: message, + from: 'system.adapter.test.0', + callback: { + message: message, + id: sendToID++, + ack: false, + time: (new Date()).getTime() + } + }); +} + +describe('Test ' + adapterShortName + ' adapter', function() { + before('Test ' + adapterShortName + ' adapter: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + //config.native.dbtype = 'sqlite'; + + setup.setAdapterConfig(config.common, config.native); + + setup.startController(true, function(id, obj) {}, function (id, state) { + if (onStateChanged) onStateChanged(id, state); + }, + function (_objects, _states) { + objects = _objects; + states = _states; + _done(); + }); + }); + }); + + it('Test ' + adapterShortName + ' instance object: it must exists', function (done) { + objects.getObject('system.adapter.' + adapterShortName + '.0', function (err, obj) { + expect(err).to.be.null; + expect(obj).to.be.an('object'); + expect(obj).not.to.be.null; + done(); + }); + }); + + it('Test ' + adapterShortName + ' adapter: Check if adapter started', function (done) { + this.timeout(60000); + checkConnectionOfAdapter(function (res) { + if (res) console.log(res); + if (runningMode === 'daemon') { + expect(res).not.to.be.equal('Cannot check connection'); + } else { + //?? + } + done(); + }); + }); +/**/ + +/* + PUT YOUR OWN TESTS HERE USING + it('Testname', function ( done) { + ... + }); + + You can also use "sendTo" method to send messages to the started adapter +*/ + + after('Test ' + adapterShortName + ' adapter: Stop js-controller', function (done) { + this.timeout(10000); + + setup.stopController(function (normalTerminated) { + console.log('Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); \ No newline at end of file diff --git a/test/testPackageFiles.js b/test/testPackageFiles.js new file mode 100644 index 0000000..d0759c0 --- /dev/null +++ b/test/testPackageFiles.js @@ -0,0 +1,91 @@ +/* jshint -W097 */ +/* jshint strict:false */ +/* jslint node: true */ +/* jshint expr: true */ +var expect = require('chai').expect; +var fs = require('fs'); + +describe('Test package.json and io-package.json', function() { + it('Test package files', function (done) { + console.log(); + + var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8'); + var ioPackage = JSON.parse(fileContentIOPackage); + + var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8'); + var npmPackage = JSON.parse(fileContentNPMPackage); + + expect(ioPackage).to.be.an('object'); + expect(npmPackage).to.be.an('object'); + + expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist; + expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist; + + expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version); + + if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) { + console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!'); + console.log(); + } + + expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist; + expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist; + + if (ioPackage.common.name.indexOf('template') !== 0) { + if (Array.isArray(ioPackage.common.authors)) { + expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0); + if (ioPackage.common.authors.length === 1) { + expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + console.log('WARNING: Testing for set authors field in io-package skipped because template adapter'); + console.log(); + } + expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true; + if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') { + console.log('WARNING: titleLang is not existing in io-package.json. Please add'); + console.log(); + } + if ( + ioPackage.common.title.indexOf('yunkong2') !== -1 || + ioPackage.common.title.indexOf('yunkong2') !== -1 || + ioPackage.common.title.indexOf('adapter') !== -1 || + ioPackage.common.title.indexOf('Adapter') !== -1 + ) { + console.log('WARNING: title contains Adapter or yunkong2. It is clear anyway, that it is adapter for yunkong2.'); + console.log(); + } + + if (ioPackage.common.name.indexOf('vis-') !== 0) { + if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) { + console.log('WARNING: Admin3 support is missing! Please add it'); + console.log(); + } + if (ioPackage.common.materialize) { + expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true; + } + } + + var licenseFileExists = fs.existsSync(__dirname + '/../LICENSE'); + var fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8'); + if (fileContentReadme.indexOf('## Changelog') === -1) { + console.log('Warning: The README.md should have a section ## Changelog'); + console.log(); + } + expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true; + if (!licenseFileExists) { + console.log('Warning: The License should also exist as LICENSE file'); + console.log(); + } + if (fileContentReadme.indexOf('## License') === -1) { + console.log('Warning: The README.md should also have a section ## License to be shown in Admin3'); + console.log(); + } + done(); + }); +});