From f645903d194cd88e088ab80b6297e285995d6b6f Mon Sep 17 00:00:00 2001 From: zhongjin Date: Sat, 21 Jul 2018 15:01:25 +0800 Subject: [PATCH] Initial commit --- .gitignore | 7 + .npmignore | 9 + .travis.yml | 23 + LICENSE | 22 + README.md | 498 +++++++++++++++++++ admin/index.html | 176 +++++++ admin/simple-api.png | Bin 0 -> 1423 bytes admin/words.js | 43 ++ appveyor.yml | 25 + gulpfile.js | 401 ++++++++++++++++ img/favicon.ico | Bin 0 -> 32038 bytes io-package.json | 106 ++++ lib/simpleapi.js | 905 +++++++++++++++++++++++++++++++++++ lib/utils.js | 83 ++++ main.js | 133 +++++ package.json | 39 ++ test/lib/setup.js | 728 ++++++++++++++++++++++++++++ test/testApi.js | 460 ++++++++++++++++++ test/testApiAsLimitedUser.js | 277 +++++++++++ test/testApiAsUser.js | 508 ++++++++++++++++++++ test/testPackageFiles.js | 91 ++++ test/testSsl.js | 194 ++++++++ 22 files changed, 4728 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/simple-api.png create mode 100644 admin/words.js create mode 100644 appveyor.yml create mode 100644 gulpfile.js create mode 100644 img/favicon.ico create mode 100644 io-package.json create mode 100644 lib/simpleapi.js create mode 100644 lib/utils.js create mode 100644 main.js create mode 100644 package.json create mode 100644 test/lib/setup.js create mode 100644 test/testApi.js create mode 100644 test/testApiAsLimitedUser.js create mode 100644 test/testApiAsUser.js create mode 100644 test/testPackageFiles.js create mode 100644 test/testSsl.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4a6c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +.idea +tmp +admin/i18n/flat.txt +admin/i18n/*/flat.txt +iob_npm.done +package-lock.json \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..0f2ae68 --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +gulpfile.js +tasks +tmp +test +.travis.yml +appveyor.yml +admin/i18n +iob_npm.done +package-lock.json \ No newline at end of file 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..6c380bb --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015-2017 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..fe55deb --- /dev/null +++ b/README.md @@ -0,0 +1,498 @@ +![Logo](admin/simple-api.png) +yunkong2 simple-api adapter +================= +[![NPM version](http://img.shields.io/npm/v/yunkong2.simple-api.svg)](https://www.npmjs.com/package/yunkong2.simple-api) +[![Downloads](https://img.shields.io/npm/dm/yunkong2.simple-api.svg)](https://www.npmjs.com/package/yunkong2.simple-api) +[![Tests](https://travis-ci.org/yunkong2/yunkong2.simple-api.svg?branch=master)](https://travis-ci.org/yunkong2/yunkong2.simple-api) + +[![NPM](https://nodei.co/npm/yunkong2.simple-api.png?downloads=true)](https://nodei.co/npm/yunkong2.simple-api/) + +This is RESTFul interface to read the objects and states from yunkong2 and to write/control the states over HTTP Get/Post requests. + +## Usage +Call in browser ```http://ipaddress:8087/help``` to get the help about API. The result is: + +``` +{ + "getPlainValue": "http://ipaddress:8087/getPlainValue/stateID", + "get": "http://ipaddress:8087/get/stateID/?prettyPrint", + "getBulk": "http://ipaddress:8087/getBulk/stateID1,stateID2/?prettyPrint", + "set": "http://ipaddress:8087/set/stateID?value=1&prettyPrint", + "toggle": "http://ipaddress:8087/toggle/stateID&prettyPrint", + "setBulk": "http://ipaddress:8087/setBulk?stateID1=0.7&stateID2=0&prettyPrint", + "objects": "http://ipaddress:8087/objects?pattern=system.adapter.admin.0*&prettyPrint", + "objects": "http://ipaddress:8087/objects?pattern=system.adapter.admin.0*&type=adapter&prettyPrint", + "states": "http://ipaddress:8087/states?pattern=system.adapter.admin.0*&prettyPrint" +} +``` + +### getPlainValue +Call e.g.: +``` +http://ipaddress:8087/getPlainValue/system.adapter.admin.0.alive +``` +Result: +``` +true +``` + +### get +Call e.g.: +``` +http://ipaddress:8087/get/system.adapter.admin.0.alive +``` +Result: +``` +{"val":true,"ack":true,"ts":1442432193,"from":"system.adapter.admin.0","lc":1442431190,"expire":23437,"_id":"system.adapter.admin.0.alive","type":"state","common":{"name":"admin.0.alive","type":"boolean","role":"indicator.state"},"native":{}} +``` +or call e.g.: +``` +http://ipaddress:8087/get/system.adapter.admin.0.alive?prettyPrint +``` +Result: +``` +{ + "val": true, + "ack": true, + "ts": 1442432238, + "from": "system.adapter.admin.0", + "lc": 1442431190, + "expire": 28494, + "_id": "system.adapter.admin.0.alive", + "type": "state", + "common": { + "name": "admin.0.alive", + "type": "boolean", + "role": "indicator.state" + }, + "native": {} +} +``` + +### getBulk + get many states with one request, returned as object with ID as key and val/ts as subobject + +### set +Call e.g.: +``` +http://ipaddress:8087/set/javascript.0.test?value=1 +``` +Result: +``` +{"id":"javascript.0.test","value":1} +``` +or call e.g.: +``` +http://ipaddress:8087/set/javascript.0.test?value=1&prettyPrint +``` +Result: +``` +{ + "id": "javascript.0.test", + "value": 1 +} +``` +Of course the data point *javascript.0.test* must exist. + +### toggle + toggles value: +- boolean: true => false, false => true +- number without limits: x => 100-x +- number with limits: x => max - (x - min) + +### setBulk + Set many states with one request. This request supports POST method too, for POST data should be in body and not URL. + +### setValueFromBody + Allows to set the value of a given State be set by the POST body content. + +### objects + +### states + +### help +Gives [this](#usage) output back + + +## Install + +```node yunkong2.js add simple-api``` + +## Usage +Assume, we have no security and the server runs on default port 8087. + +For all queries the name or id of the state can be specified. + +For every requiest that returns JSON you can set parameter *prettyPrint* to get the output in human readable form. + +If authentication is enabled, two other fields are mandatory:
?user=admin&pass=yunkong2
+ +### getPlainValue +Read state value as text. You can specify more ids divided by semicolon + +
http://ip:8087/getPlainValue/admin.0.memHeapTotal
+ +
   
+  31.19
+
+ +
http://ip:8087/getPlainValue/admin.0.memHeapTotal, admin.0.memHeapUsed
+
  
+  31.19
+  17.52
+
+ +### get +Read state and object data of state as json. You can specify more ids divided by semicolon. +If more than one ID requested, the JSON array will be returned. + +
http://localhost:8087/get/admin.0.memHeapTotal/?prettyPrint
+ +
+  {
+    "val": 31.19,
+    "ack": true,
+    "ts": 1423154619,
+    "from": "system.adapter.admin.0",
+    "lc": 1423153989,
+    "_id": "system.adapter.admin.0.memHeapTotal",
+    "type": "state",
+    "common": {
+      "name": "admin.0.memHeapTotal",
+      "type": "number",
+      "role": "indicator.state",
+      "unit": "MB",
+      "history": {
+        "enabled": true,
+        "changesOnly": true,
+        "minLength": 480,
+        "maxLength": 960,
+        "retention": 604800,
+        "debounce": 10000
+      }
+    },
+    "native": {}
+  }
+
+ +
http://ip:8087/get/admin.0.memHeapTotal,admin.0.memHeapUsed/?prettyPrint
+
+  [
+    {
+      "val": 31.19,
+      "ack": true,
+      "ts": 1423154544,
+      "from": "system.adapter.admin.0",
+      "lc": 1423153989,
+      "_id": "system.adapter.admin.0.memHeapTotal",
+      "type": "state",
+      "common": {
+        "name": "admin.0.memHeapTotal",
+        "type": "number",
+        "role": "indicator.state",
+        "unit": "MB",
+        "history": {
+          "enabled": true,
+          "changesOnly": true,
+          "minLength": 480,
+          "maxLength": 960,
+          "retention": 604800,
+          "debounce": 10000
+        }
+      },
+      "native": {}
+    },
+    {
+      "val": 16.25,
+      "ack": true,
+      "ts": 1423154544,
+      "from": "system.adapter.admin.0",
+      "lc": 1423154544,
+      "_id": "system.adapter.admin.0.memHeapUsed",
+      "type": "state",
+      "common": {
+        "name": "admin.0.memHeapUsed",
+        "type": "number",
+        "role": "indicator.state",
+        "unit": "MB",
+        "history": {
+          "enabled": true,
+          "changesOnly": true,
+          "minLength": 480,
+          "maxLength": 960,
+          "retention": 604800,
+          "debounce": 10000
+        }
+      },
+      "native": {}
+    }
+  ]
+
+ +### getBulk +Read the states of more IDs with timestamp. You can specify more ids divided by semicolon. +Always the JSON array will be returned. + +
http://ip:8087/getBulk/admin.0.memHeapTotal,admin.0.memHeapUsed/?prettyPrint
+ +
+  {
+      "admin.0.memHeapTotal": {
+          "val": 31.19,
+          "ts": 1423154754
+      },
+      "admin.0.memHeapUsed": {
+          "val": 15.6,
+          "ts": 1423154754
+      }
+  }
+
+ +### set +Write the states with specified IDs. You can specifiy *wait* option in milliseconds to wait for answer from driver. + +
http://ip:8087/set/hm-rpc.0.IEQ12345.LEVEL?value=1&prettyPrint
+
{
+       "id": "hm-rpc.0.IEQ12345.LEVEL",
+       "value": 1
+     }
+
+ +
http://ip:8087/set/hm-rpc.0.IEQ12345.LEVEL?value=1&wait=5000&prettyPrint
+
{
+       "val": 1,
+       "ack": true,
+       "ts": 1423155399,
+       "from": "hm-rpc.0.IEQ12345.LEVEL",
+       "lc": 1423155399
+     }
+
+ +If no answer will be recieved in specified time, the *null* value will be returned. +In the first case the answer will be returned immediately and *ack* is false. In the second case *ack* is true. That means it was response from driver. + +### setBulk +- write bulk of IDs in one request. + +
http://ip:8087/setBulk?hm-rpc.0.FEQ1234567:1.LEVEL=0.7&Anwesenheit=0&prettyPrint
+
+  [
+    {
+      "id": "hm-rpc.0.FEQ1234567:1.LEVEL",
+      "val": "0.7"
+    },
+    {
+      "error": "error: datapoint \"Anwesenheit\" not found"
+    }
+  ]
+
+You can send this request as POST too. + +### objects +Get the list of all objects for pattern. If no pattern specified all objects as JSON array will be returned. + +
http://ip:8087/objects?prettyPrint
+
+  {
+  "system.adapter.admin.0.uptime": {
+    "_id": "system.adapter.admin.0.uptime",
+    "type": "state",
+    "common": {
+      "name": "admin.0.uptime",
+      "type": "number",
+      "role": "indicator.state",
+      "unit": "seconds"
+    },
+    "native": {}
+  },
+  "system.adapter.admin.0.memRss": {
+    "_id": "system.adapter.admin.0.memRss",
+    "type": "state",
+    "common": {
+      "name": "admin.0.memRss",
+      "desc": "Resident set size",
+      "type": "number",
+      "role": "indicator.state",
+      "unit": "MB",
+      "history": {
+        "enabled": true,
+        "changesOnly": true,
+        "minLength": 480,
+        "maxLength": 960,
+        "retention": 604800,
+        "debounce": 10000
+      }
+    },
+    "native": {}
+  },
+  ...
+
+ + Get all control objects of adapter system.adapter.admin.0: +
http://ip:8087/objects?pattern=system.adapter.admin.0*&prettyPrint
+
+    {
+    "system.adapter.admin.0.uptime": {
+      "_id": "system.adapter.admin.0.uptime",
+      "type": "state",
+      "common": {
+        "name": "admin.0.uptime",
+        "type": "number",
+        "role": "indicator.state",
+        "unit": "seconds"
+      },
+      "native": {}
+    },
+    ...
+
+
+ +### states +Get the list of all states for pattern. If no pattern specified all states as JSON array will be returned. + +
http://ip:8087/states?prettyPrint
+
+  {
+    "system.adapter.admin.0.uptime": {
+      "val": 32176,
+      "ack": true,
+      "ts": 1423156164,
+      "from": "system.adapter.admin.0",
+      "lc": 1423156164
+    },
+    "system.adapter.admin.0.memRss": {
+      "val": 41.14,
+      "ack": true,
+      "ts": 1423156164,
+      "from": "system.adapter.admin.0",
+      "lc": 1423156119
+    },
+    "system.adapter.admin.0.memHeapTotal": {
+      "val": 31.19,
+      "ack": true,
+      "ts": 1423156164,
+      "from": "system.adapter.admin.0",
+      "lc": 1423155084
+    },
+  ...
+
+ + Get all control objects of adapter system.adapter.admin.0: + +
http://ip:8087/states?pattern=system.adapter.admin.0*&prettyPrint
+
+    {
+      "system.adapter.admin.0.uptime": {
+        "val": 32161,
+        "ack": true,
+        "ts": 1423156149,
+        "from": "system.adapter.admin.0",
+        "lc": 1423156149
+      },
+      "system.adapter.admin.0.memRss": {
+        "val": 41.14,
+        "ack": true,
+        "ts": 1423156149,
+        "from": "system.adapter.admin.0",
+        "lc": 1423156119
+      },
+      "system.adapter.admin.0.memHeapTotal": {
+        "val": 31.19,
+        "ack": true,
+        "ts": 1423156149,
+        "from": "system.adapter.admin.0",
+        "lc": 1423155084
+      },
+      "system.adapter.admin.0.memHeapUsed": {
+        "val": 19.07,
+        "ack": true,
+        "ts": 1423156149,
+        "from": "system.adapter.admin.0",
+        "lc": 1423156149
+      },
+      "system.adapter.admin.0.connected": {
+        "val": true,
+        "ack": true,
+        "ts": 1423156149,
+        "from": "system.adapter.admin.0",
+        "lc": 1423128324,
+        "expire": 28100
+      },
+      "system.adapter.admin.0.alive": {
+        "val": true,
+        "ack": true,
+        "ts": 1423156149,
+        "from": "system.adapter.admin.0",
+        "lc": 1423128324,
+        "expire": 28115
+      }
+    }
+
+ + +## Changelog + +### 2.0.0 (2018-06-29) +* (Giermann) BREAKING CHANGE: getBulk is returning data in a different structure + +### 1.6.3 (2018-04-15) +* (Apollon77) Return used character encoding (UTF-8) + +### 1.6.2 (2017-11-27) +* (Apollon77) Fix decoding problems + +### 1.6.1 (2017-09-25) +* (Apollon77) Fix statuscode for setBulk and optimize permission errors + +### 1.6.0 (2017-07-10) +* (Apollon77) Fix handling of URL-encoded values, they are now decoded properly +* (Apollon77) Optimize Permission handling +* (Apollon77) add possibility to only allow access to states where user is also owner, finally works correct with js-controller 1.1.1! + +### 1.5.0 (2017-03-10) +* (greyhound) Add new POST method setValueFromBody + +### 1.4.0 (2017-01-05) +* (bluefox) new web server plugin support + +### 1.3.0 (2016-08-30) +* (bluefox) сompatible only with new admin + +### 1.2.0 (2016-08-27) +* (bluefox) support of letsencrypt certificates + +### 1.1.1 (2016-07-06) +* (bluefox) support of chained certificates + +### 1.1.0 (2016-02-09) +* (bluefox) fix toggle, objects, states, setBulk, POST +* (bluefox) add tests + +### 1.0.0 (2015-09-30) +* (bluefox) stop adapter before update + +### 0.1.2 (2015-06-28) +* (bluefox) add description in readme.md +* (bluefox) change "toggle" for boolean and numbers + +### 0.1.1 (2015-06-28) +* (bluefox) change setForeignState api +* (bluefox) add type to io-package.json +* (bluefox) enable run from "web" +* (bluefox) add default user + +### 0.1.0 (2015-06-10) +* (bluefox) change setForeignState api +* (bluefox) support of user permissions + +### 0.0.4 (2015-03-11) +* (bluefox) remove socket.io from file + +### 0.0.3 (2015-02-13) +* (bluefox) remove socket.io from dependencies + +### 0.0.2 (2015-02-12) +* (bluefox) enable be a part of "web" + +### 0.0.1 (2015-02-06) +* (bluefox) initial commit diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..730c915 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + +
+ +

simpleAPI adapter settings

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

Let's Encrypt settings

+
+ diff --git a/admin/simple-api.png b/admin/simple-api.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..adb3d0d --- /dev/null +++ b/admin/words.js @@ -0,0 +1,43 @@ +systemDictionary = { + "simpleAPI adapter settings": { + "de": "simpleAPI adapter settings", + "ru": "simpleAPI adapter settings" + }, + "Run as:": {"de": "Laufen unter Anwender:", "ru": "Запустить от пользователя:"}, + "Allow only when User is Owner:": {"de": "Nue erlauben wenn Anwender auch Besitzer ist:", "ru": "Allow only when User is Owner:"}, + "IP:": {"de": "IP:", "ru": "IP:"}, + "Port:": {"de": "Port:", "ru": "Порт:"}, + "Secure(HTTPS):": {"de": "Verschlüsselung(HTTPS):", "ru": "Шифрование(HTTPS):"}, + "Authentication:": {"de": "Authentifikation:", "ru": "Аутентификация:"}, + "Public certificate:": {"en": "Public certificate:", "de": "Publikzertifikat:", "ru": "'Public' сертификат:"}, + "Private certificate:": {"en": "Private certificate:", "de": "Privatzertifikat:", "ru": "'Private' сертификат:"}, + "Chained certificate:": {"en": "Chained certificate:", "de": "Kettenzertifikat:", "ru": "'Chained' сертификат:"}, + "Extend WEB adapter:": {"en": "Extend WEB adapter:", "de": "Erweitere WEB Adapter:", "ru": "Подключить к WEB драйверу:"}, + "all": {"en": "all", "de": "alle", "ru": "все"}, + "Listen on all IPs": {"en": "Listen on all IPs", "de": "Alle IPs zulassen", "ru": "Открыть для всех IP адресов"}, + "Let's Encrypt settings": { + "en": "Let's Encrypt settings", + "de": "Einstellungen Let's Encrypt", + "ru": "Настройкт Let's Encrypt" + }, + "Use Lets Encrypt certificates:": { + "en": "Use Let's Encrypt certificates:", + "de": "Benutzen Let's Encrypt Zertifikate:", + "ru": "Использовать сертификаты Let's Encrypt:" + }, + "Use this instance for automatic update:": { + "en": "Use this instance for automatic update:", + "de": "Benutze diese Instanz für automatische Updates:", + "ru": "Обновлять сертификаты в этом драйвере:" + }, + "Port to check the domain:": { + "en": "Port to check the domain:", + "de": "Port um die Domain zu prüfen:", + "ru": "Порт для проверки доменного имени:" + }, + "Set certificates or load it first in the system settings (right top).": { + "en": "Set certificates or load it first in the system settings (right top).", + "de": "Setze Zertificate oder lade die erst unter System/Einstellungen (oben rechts).", + "ru": "Нужно выбрать сертификаты или сначала загрузить их в системных настройках (вверху справа)." + } +}; 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/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/img/favicon.ico b/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c9f4f852b54907759b93d8501894c24775b5be00 GIT binary patch literal 32038 zcmeHQdrVZMQ2s5Md7>0+u91(FuKtMo19C73zQm-L~ON=pwaQFRvYwxvZ?LBYw z)N{Q#-sDa8)WC1)K49D^GSRoqm4}QyISYwwdhAa zU$j0;e1FxInSH5hyIku`FQoUVH=ketEYERI;NRk#wHMECl`oWMX!YB#9!P9y$eL)Z z-TBN_vE^Oq;I`2aoN={~-!ulTuK%*!SeHMeUZvW`#@n68EVnw2()as*y=!~(z9;|5 z-JTQmy}h}9{2VyECmdio-V2CX6xaqcK80Ori*(j z8>+WEF;>os!gWrJL2;?wyasT*IeaI7@qN$fKGgY;+8-V$m<k^aOFZ~!o|w13E-84vNNd( zB{qM4=k5KL_qaG&evymh%saR~!^P#y7ACv!LiqbFafScN)V{q``f9mESN6cuH}LxyBQ2Ib(M<0vrCjijv-tK~h&92b{; zk<_7Ys){$HL2k*VXV^L=qn)`}k4C5+x`5BoNZP`;O!hY_-e|SMNea##=Rx zO;0L|HppdXv6fPwR`UYX_wxQhhY75!i^H{r`W~#MN*l^r`c}9)ScC15+j3ROMww{f zv|HR%2l(&Q`e$t^??bg$S+stD+9-ogKyZ}UQ$zL-9pKaY^V?``mTO8k$*4bxx=W~! zLfels!3)EB7|hE;^^Z0N2oKUJDhk)j6-Ddb4EDX-ojL7F?XDiITQb%S=n;$tdE)or zy=7wmcpfP}qt|c5@9WAlywpxjX*#3l`NDPc#oxs;bceN9UAtxIAhJS(E#o!LO@8o# z51K?6@T}<}VtZ7sKfl#`v2qKeXIK6oHY&@utEsP8sj(g?7omWMEG2;*~xIaSh>xw|C=p`KJL7hJqf) z6T-97))RB-}xPF4MAUII5E1`?a;}s{;PZ9d)sn4p+CwunhwfDgO*D_*YotL{( zi~0A^u^&Mvmb+RGsq{U_=nuU;>Hh80llS^g$pbfYWt9eNdA(@v&}$a~hUXjxlxeM5 zN^RV_ar6=C)I?u`qq`-W$zSOGtA_6s$baw8lT{k@LI0l?O~2x|=r5D^uin|7?M}Be zN_!LNjX|Dnr1CI7%nmirxT9<+bd4v6@=XiTgeC6pno&r8d!I-6VUCU0vo-hUSAO&+c z1^c}f4K+KKLxwG{E6Z43<=D6!>&bGgY0J^?a^U|}sm(MSvT-NMD>?k2?_=z9K;x4# z!QttVbn^GXK>i}|fy=-Ld@jh?E2=V4T^FkO+L+GPG)+GM-srm<_1CEx8mkqmi|+}Cj=`DW`c$=H)8x4HKxLv|%!y|_EMv2Ir~><7tM8=Jl|rdk^+C3HCz2y*mFU(mP<91v_E`{0D8&joYBBw_$&KG_@^w*E^(e2{` z!7RRmPj^}Qxpj|eoPh`P*HOEmQ7+nw3jWL0yXL`e*-yT|Ie%WRf8 z)OU&9v;gqefp^vM=J3PcYd0To*i8}lTUoqe8EEvF=)~>S@EH=%N)CHkRQ%IWy>lb& zQMuif+kPtx*H6Hft;1?VmCt%k!xht>SKNaIlD>e5Zi;pcqJZQTkUH1F0) z@pF}<=V2% z%hAtcviXy5U%=yNW#PJr=-*Vodme1ZR@hl5VLz5B7JzKdn9DNeL3dT@#zZ6h^z5*i z-UAMZXHaOb$PR_4VCP*CsE+X+`dAL1#bah;$DE@vuXb!CJdRcsMFxNKr9I!k)-+D` zZ}I^MWAGF$es0{;rM7}cJJXYslGv?nSBfQ3E*ymS(R%483D>7vChYS7!XL?Sp zV{_Osymo2-T-Znkxt&?DxhrD;TZyaInW3|L8T$v#7h=x3u{P^*Ae)lSa-hT>T84vG zD{zf5KLcVr_}l{?Idx^}A>nUs&YlK4S30**ll@;9g9*^(OC!9-XXn|pX@LDZ!1~z7 zKMD_FY=)p0gq+LpPpkybj$N+Vu8lkKuB&oO&V`DtA>r?6I+z6ec0bt))wmM534SDX z465KqiO8<&Ecj_2=0Tm2j{rZ(@p2>1;xp`{E8%MyqdtjuY5p<(t=e`N;~x}$_+IA0 zMt>G|Hq}0__z9XY254{4UAuFM*iX1J=Idg>`oxF>J(pNb-v+A>zuPL%Y}}2xN3lJe ze}Tv0#+n^L;fJkw8RBuqC?4kqd<+x=q>jNe_*-@YhoE(X!2BVj(MhOC~JOM5;D)K|E7 zn8!)DJbq(fv%oiFSzlsL^OIxn%X|mAjdNcW%|Qi^-MLrmGX2)^d)>Ly`a6%Mli!B> zHptJzd}vqpPr`?_EF7G{ey_0oG!64$Z4f>h!uwB+busSWVg8Mi459Jocr@4V$!o6v zndvKH9rz!YNHbRvl^WO%X^|&;7%FDPrjQiP+ zs|Qy@hCHG1ZwC(xc;vNQ&eFjTU&b8xGL8;(AD*NFI{U64%L&tNS!~hI>!V9_l~c zq4<qq=#1BYcQ_afU*GvSxX0<04v zFA*-d`mkSSGOoYl==b2`r`%Uc^HK4a9?I=%R^b2d2c^?T2CQevZ#s1Qv@DK+r}LNx z{?n*seIz)Qp0O+hth$&Fh6{8f+?P`ux4Vvg4;nrtKP$~g0S^gyX!`T7Ui*jew>|aw z1ess*Ddf?2k-#$5Zy=AzM=J6U%H}rU_CB=txha;S5%f_A%IJ)^k~e`#9$B+ETlT@hfCW3NFYL#n-Ih0bFhz zlWw&g)8MD};dg!?F|*BpzXrbNMfZA7&Vv6r(bIiA3EwBd_m~9!OS;)|I0^DB34Z7# ztSw1cQyPVqG7BZsNUM zyVHw38sdDmda-vxoK(6OeswSQu3o_J#opTs*-6(R@jvMO9r)!f@cg^DCve#54mj3A zraYrLNxpOej~q{r|GXR=%tQP^{?{0T9;!!mn19|Ui%6a_KRnk>Nx!Cb-LOAku}#cY zf&CKgUwyU;C3cVV4Cp?F@o*tFAxVUt%HZb}kd2Rsr-yFmaew^~;unJMJrFl-LcK{v z7AbKY6iY(9C$7ol$5-_tuA4I5>I(E+($)DsnPToR2I}6M*$z~jouYqYEtq8EU^V&e zCm(q2a{&KJTs?#X;=6&v>Fu}&mFI)~d9%PrZu(wX4@efVxN^Q8sAHhS&d_>j+@o*VOMrtF7h+RRhqXULmVSXTcsO)B zU$-6@^`c;7;8yobioJ;CBnV@$L9m-*44B<9^gft)>CT<(c*OT;I)jnd#OGgoZPO8- z@x}c+r!9zoFd-JlG|+Rx1RgR$2Ad#%O<2=ST`k!r@Q?{Qm8rFHp9yhXCdeTZc*u0Q zc9#h}WI|4b2|Q$~bEcbM!!aR-)`Zvy6YOXv@V%+n_R~L9MvHjG%CDCXE;gRA@r?1n z>&XL3JP_qX(O>NTThoE!v_8YN1s9LgibGDT@h>+2UeEa#8`s#l#&{sc12G{FRZ6XBlk2&cf6W5O1noh(%@o1FBzo z+PqNwjmn7s{2-FHr}|A%(Kqq1?|zTCgGY#04d|PnYTIHx;4vSubz>BRNM~V?|A$}5 zjb}Di*arvrJMoT~)}S_Iv5#oWj)JsA_;s4KzQV+aMBBdjJ>p>VDW=UAN9x0$#+|-} zf51QHK(YIJv$Ioz+M&gMA}Lo}GiGDF55pEt{x@wL@J!`T#WsjP#OJA$>q^o;t}59u zN#_A*Hfy3yRP)n?4a zSr{y)lH!>4JfOyt+brFDjycYlzt+Ryo16Uo%NQH@CaZ8USrmv4oFAjXHPE;9m0Kk( zj**=qp~S@^R#}q+h=tb06@mth9M>TC9!{@7eMa)GqG)}U5?@R-P|o%c=>T7OR55#j z`a}%)68MxR>6|X^*CXGgjt2_UoW{C{sr~?X>*TUO9n5Qt^`dz`U$U`=WD%D~B$F9G zU~OjWvwfWw;~h=A;*5PINX({hSTbGPw{60qd1phwT zJ7UdiXdHNqwvP^q&$z5OZE>`1ir*vNZzYc*<-Tcu9$23#))C8yNuLW}`?wKr@!1I) ziFKkWBUZhRcu|a1=d_@C0bOOv_~Q(kIae=aj`BEK9}jTf@ManGe^^!)+7Ar;*NFJ~ zm(7z?ou`NkcTs#e@uNZqWnK~u;PaI$&!r_sLr?Kr#8zZe+$Z8@wK!ru4^S-srj=ry zKxNv0LLct)$J;kAFb|po)eZNK;@OEGDPBT}k>_+!au=$L*G~!m)`p)KBR*zQi7!#& zx5YdFiU+??zNIxBeEfIlsGo+*rPK$K8&XE!Io{RKE zoIlj0#MdZi)fjoeg+7r-5LJ95=0N~m)yD(SnI07tr9~7g!SQdX-d0cX8opcuWsOq! z0sL9MN!ye1I^phZ?gJ}$%q`_m@E9Xe4uIx`;o33&PHP1Ar%yxi0p`IzW4%z%m#cR) z@c4dC2jpw9TpQwti)9&6;os7dJsYt-4=HvYao}oZIq;0pE|&gR$} z8h`Ty&O(hC=EmAxjWjPCYjF*btdM4?%Xs#df<$_NZO)r5WCk%bCdHXovr2L2b{lJkr6Tey)D`A zBj$O6$2$k&0rWG}UXd{~QuqRW2U++RKb#FZQb|E%xwFU&^Bfn@f}KyL@dM3^y3DXJBX=iLI}=Pjn=Aki*r6z9(4bp3b$ zxkShTn(`ja*r)gmIq-oWe$=D)hpl-)t=HOVT_LRq&3IkRju!a0Ze&wP1PLaaZ_!DD%xlgR&ydhclLE1GBEw_U{7SSFtia@=&|i8Z@N zS+|w7TcLw;#wXFC-JR`^|3E<{%UMF+m_H9_av(bz*k8uCR%^;^;HaHzZN#6R@q4G} z$3d=K56LW^W5#tDjSkHRl0?|XAC%5T-rO_fO0b-*KsM}`{fr3 z_zzYH`!D3`_4E8T%AM8a9JVuk-VcAA_c~+buI-^85A>ac96ZtgDsUG2BwHwCp?da_ zU>^eiBVX&!;v?-9nF`46Bc)rh!L897=W%G08JInct74#;;5h(Fau?qeI{f!q0B;vl$K+t(1G&E2eto?c*;Pjg%wNoW z#%4Fg2Z%8|;Dc`<7uRqY#XIuEytIZXxt9KP@bnyw$o@Ti`*b37q9!Ge+NTeYY~eZi zw6@T?qT3s?ynf#q3*2A8>}2Y>KV*+od`3!6BlA}Y@dMSI6X zJ#tt<;oE}5YsA0blIQ|8>ukWIbC)5V?;*dPhhc(ro2gptqfCto^Ug7~U zrV(?9K&JE1Bjo{} zQyiEFe13aBIS_>pfa6bhY2c-*T=@QmLsk&s71V6QebLAwgg%Wgo32UK*GZGd=3U}-~f4LZ=NsJ~j g9*FTkj0a*o5aWRu55#!j_2L2Jc>uBJ|6dgNUk#nw-~a#s literal 0 HcmV?d00001 diff --git a/io-package.json b/io-package.json new file mode 100644 index 0000000..f71a13d --- /dev/null +++ b/io-package.json @@ -0,0 +1,106 @@ +{ + "common": { + "name": "simple-api", + "version": "2.0.0", + "news": { + "2.0.0": { + "en": "BREAKING CHANGE: getBulk is returning data in a different structure", + "de": "BREAKING CHANGE: getBulk gibt Daten in einer anderen Struktur zurück", + "ru": "BREAKING CHANGE: getBulk возвращает данные в другой структуре", + "pt": "BREAKING CHANGE: o getBulk está retornando dados em uma estrutura diferente", + "nl": "BREAKING CHANGE: getBulk retourneert gegevens in een andere structuur", + "fr": "BREAKING CHANGE: getBulk renvoie des données dans une structure différente", + "it": "BREAKING CHANGE: getBulk restituisce i dati in una struttura diversa", + "es": "BREAKING CHANGE: getBulk está devolviendo datos en una estructura diferente", + "pl": "ZMIANA ZMIAN: getBulk zwraca dane w innej strukturze" + }, + "1.6.3": { + "en": "Return used character encoding (UTF-8)", + "de": "Zeichencodierung zurückgegeben (UTF-8)", + "ru": "Возвращаемое кодирование символов (UTF-8)", + "pt": "Retornar a codificação de caracteres usados ​​(UTF-8)", + "nl": "Gebruikte tekencodering retourneren (UTF-8)", + "fr": "Renvoie le codage de caractères utilisé (UTF-8)", + "it": "Restituisci codifica caratteri usati (UTF-8)", + "es": "Devuelve la codificación de caracteres usados ​​(UTF-8)", + "pl": "Zwróć używane kodowanie znaków (UTF-8)" + }, + "1.6.2": { + "en": "Fix decoding problems", + "de": "Dekodierungsprobleme behoben", + "ru": "Fix decoding problems" + }, + "1.6.1": { + "en": "Fix statuscode for setBulk and optimize permission errors", + "de": "Statuscode für setBulk korrigiert und Rechte-Fehler optimiert", + "ru": "Fix statuscode for setBulk and optimize permission errors" + }, + "1.6.0": { + "en": "Optimize Permission handling and add possibility to only allow access to states where user is also owner (needs js-contrioller 1.1.1!), fix handling of url-encoded values", + "de": "Berechtigungsbehandlung optimiert und Möglichkeit eingebaut nur den Zugriff auf Zustände zu erlauben wo der Benutzer auch Besitzer ist (benötigt js-controller 1.1.1), Behandlung von URL-kodierten Werten korrigiert", + "ru": "Optimize Permission handling and add possibility to only allow access to states where user is also owner (needs js-contrioller 1.1.1!), fix handling of url-encoded values" + }, + "1.5.1": { + "en": "Add new POST method setValueFromBody", + "de": "Neue POST-Methode setValueFromBody hinzugefügt", + "ru": "Add new POST method setValueFromBody" + }, + "1.4.0": { + "en": "can be used as web-extension", + "de": "Kann als Web-Extension benutzt werden", + "ru": "Может использоватся, как Web-плагин" + }, + "1.3.0": { + "en": "сompatible only with new admin", + "de": "Nur mit neuem Admin kompatibel", + "ru": "Совместимо только с новым админ-драйвером" + }, + "1.2.0": { + "en": "support of letsencrypt certificates", + "de": "Unterstützung von letsencrypt Zertifikaten", + "ru": "Поддержка letsencrypt сертификатов" + }, + "1.1.1": { + "en": "support of chained certificates", + "de": "support of chained certificates", + "ru": "support of chained certificates" + } + }, + "title": "simpleAPI Adapter", + "desc": "This adapter allows to read and write yunkong2 objects and state with web RESTful API", + "authors": [ + "bluefox ", + "Apollon77 " + ], + "license": "MIT", + "platform": "Javascript/Node.js", + "mode": "daemon", + "loglevel": "info", + "icon": "simple-api.png", + "webExtension": "lib/simpleapi.js", + "readme": "https://git.spacen.net/yunkong2/yunkong2.simple-api/blob/master/README.md", + "keywords": ["web", "simpleAPI", "RESTful", "communication"], + "enabled": true, + "extIcon": "https://git.spacen.net/yunkong2/yunkong2.simple-api/master/admin/simple-api.png", + "type": "communication", + "stopBeforeUpdate": true, + "localLink": "%protocol%://%ip%:%port%/get/system.adapter.simple-api.%instance%.uptime?prettyPrint", + "dependencies": [{"js-controller": ">=0.15.0"}] + }, + "native": { + "port": 8087, + "auth": false, + "secure": false, + "bind": "0.0.0.0", + "certPublic": "", + "certPrivate": "", + "certChained": "", + "defaultUser": "admin", + "onlyAllowWhenUserIsOwner": false, + "webInstance": "", + + "leEnabled": false, + "leUpdate": false, + "leCheckPort": 80 + } +} diff --git a/lib/simpleapi.js b/lib/simpleapi.js new file mode 100644 index 0000000..31a4828 --- /dev/null +++ b/lib/simpleapi.js @@ -0,0 +1,905 @@ +/* jshint -W097 */ +/* jshint strict:false */ +/*jslint node: true */ +/*jshint -W061 */ +'use strict'; + +/** + * SimpleAPI class + * + * From settings used only secure, auth and crossDomain + * + * @class + * @param {object} server http or https node.js object + * @param {object} webSettings settings of the web server, like
{secure: settings.secure, port: settings.port}
+ * @param {object} adapter web adapter object + * @param {object} instanceSettings instance object with common and native + * @param {object} app express application + * @return {object} object instance + */ +function SimpleAPI(server, webSettings, adapter, instanceSettings, app) { + if (!(this instanceof SimpleAPI)) return new SimpleAPI(server, webSettings, adapter, instanceSettings, app); + + this.server = server; + this.app = app; + this.adapter = adapter; + this.settings = webSettings; + this.config = instanceSettings ? instanceSettings.native : {}; + this.namespace = instanceSettings ? instanceSettings._id.substring('system.adapter.'.length) : 'simple-api'; + + this.restApiDelayed = { + timer: null, + responseType: '', + response: null, + waitId: 0 + }; + + var that = this; + // Cache + this.users = {}; + + var __construct = (function () { + that.adapter.log.info((that.settings.secure ? 'Secure ' : '') + 'simpleAPI server listening on port ' + that.settings.port); + that.adapter.config.defaultUser = that.adapter.config.defaultUser || 'system.user.admin'; + if (!that.adapter.config.defaultUser.match(/^system\.user\./)) { + that.adapter.config.defaultUser = 'system.user.' + that.adapter.config.defaultUser; + } + if (that.adapter.config.onlyAllowWhenUserIsOwner === undefined) that.adapter.config.onlyAllowWhenUserIsOwner = false; + adapter.log.info('Allow states only when user is owner: ' + that.adapter.config.onlyAllowWhenUserIsOwner); + + if (that.app) { + adapter.log.info('Install extension on /' + that.namespace + '/'); + that.app.use('/' + that.namespace + '/', function (req, res, next) { + that.restApi.call(that, req, res); + }); + + // let it be accessible under old address too + for (var c in commandsPermissions) { + (function (command) { + adapter.log.info('Install extension on /' + command + '/'); + that.app.use('/' + command + '/', function (req, res, next) { + req.url = '/' + command + req.url; + that.restApi.call(that, req, res); + }); + })(c); + } + } + // Subscribe on user changes to manage the permissions cache + that.adapter.subscribeForeignObjects('system.group.*'); + that.adapter.subscribeForeignObjects('system.user.*'); + }.bind(this))(); + + this.isAuthenticated = function (values, callback) { + if (!values.user || !values.pass) { + that.adapter.log.warn('No password or username!'); + callback(false); + } else { + that.adapter.checkPassword(values.user, values.pass, function (res) { + if (res) { + that.adapter.log.debug('Logged in: ' + values.user); + callback(true); + } else { + that.adapter.log.warn('Invalid password or user name: ' + values.user); + callback(false); + } + }); + } + }; + + this.stateChange = function (id, state) { + if (that.restApiDelayed.id === id && state && state.ack) { + adapter.unsubscribeForeignStates(id); + that.restApiDelayed.response = state; + setTimeout(restApiDelayedAnswer, 0); + } + }; + + this.userReg = new RegExp('^system\.user\.'); + this.groupReg = new RegExp('^system\.group\.'); + + // if user politics changes, clear cache + this.objectChange = function (id, state) { + if (this.userReg.test(id) || this.groupReg.test(id)) { + this.users = {}; + } + }; + + function restApiPost(req, res, command, oId, values) { + var responseType = 'json'; + var status = 500; + var headers = {'Access-Control-Allow-Origin': '*'}; + + var body = ''; + req.on('data', function (data) { + body += data; + }); + req.on('end', function () { + switch (command) { + case 'setBulk': + that.adapter.log.debug('POST-' + command + ': body = ' + body); + var arr = []; + if (body) { + arr = body.split('&'); + } + + for (var i = 0; i < arr.length; i++) { + arr[i] = arr[i].split('='); + try { + values[arr[i][0].trim()] = (arr[i][1] === undefined) ? null : decodeURIComponent((arr[i][1]+'').replace(/\+/g, '%20')); + } + catch (e) { + values[arr[i][0].trim()] = arr[i][1]; + } + } + + if (values.prettyPrint !== undefined) { + if (values.prettyPrint === 'false') values.prettyPrint = false; + if (values.prettyPrint === null) values.prettyPrint = true; + } + + var cnt = 0; + var response = []; + that.adapter.log.debug('POST-' + command + ': values = ' + JSON.stringify(values)); + for (var _id in values) { + if (!values.hasOwnProperty(_id) || _id === 'prettyPrint' || _id === 'user' || _id === 'pass') continue; + cnt++; + that.adapter.log.debug('"' + _id + '"'); + findState(_id, values.user, function (err, id, originId) { + if (err) { + status = 500; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + doResponse(res, 'plain', status, headers, 'error: ' + err, values.prettyPrint); + cnt = 0; + } else if (!id) { + response.push({error: 'error: datapoint "' + originId + '" not found'}); + if (!--cnt) doResponse(res, responseType, status, headers, response, values.prettyPrint); + } else { + var usedId = (values[originId]?originId:id); + that.adapter.log.debug('POST-' + command + ' for id=' + id + ', oid=' + originId + ', used=' + usedId + ', value='+values[usedId]); + if (values[usedId] === 'true') { + values[usedId] = true; + } else if (values[usedId] === 'false') { + values[usedId] = false; + } else if (!isNaN(values[usedId]) && values[usedId] == parseFloat(values[usedId])) { + values[usedId] = parseFloat(values[usedId]); + } + + adapter.setForeignState(id, values[usedId], false, {user: values.user, limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner}, function (err, id) { + if (err) { + status = 500; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + doResponse(res, 'plain', status, headers, 'error: ' + err, values.prettyPrint); + cnt = 0; + } else { + adapter.log.debug('Add to Response: ' + JSON.stringify({id: id, val: values[usedId]})); + response.push({id: id, val: values[usedId]}); + if (!--cnt) { + status = 200; + doResponse(res, responseType, status, headers, response, values.prettyPrint); + } + } + }); + } + }); + } + if (!cnt) doResponse(res, responseType, status, headers, response, values.prettyPrint); + break; + + case 'setValueFromBody': + //that.adapter.log.debug('POST-' + command + ': body = ' + JSON.stringify(body)); // "{0123456xx}" + //that.adapter.log.debug('POST-' + command + ': req.url = ' + JSON.stringify(req.url)); // "/setValueFromBody?javascript.0.Nuki.Devices.NukiSL1.NukiBridgeResponse&prettyPrint" + //that.adapter.log.debug('POST-' + command + ': valuesAA = ' + JSON.stringify(values)); // {"javascript.0.Nuki.Devices.NukiSL1.NukiBridgeResponse":null,"prettyPrint":true,"user":"system.user.admin"} + + for (var _id2 in oId) { + if (oId.hasOwnProperty(_id2)) { + values[oId[_id2]] = body; + } + } + + if (values.prettyPrint !== undefined) { + if (values.prettyPrint === 'false') values.prettyPrint = false; + if (values.prettyPrint === null) values.prettyPrint = true; + } + + if (!oId.length || !oId[0]) { + doResponse(res, responseType, status, headers, {error: 'no object/datapoint given'}, values.prettyPrint); + break; + } + + + var response = []; + that.adapter.log.debug('POST-' + command + ': values = ' + JSON.stringify(values)); + var cnt = oId.length; + for (var k = 0; k < oId.length; k++) { + that.adapter.log.debug('"' + oId[k] + '"'); + findState(oId[k], values.user, function (err, id, originId) { + if (err) { + status = 500; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + doResponse(res, 'plain', status, headers, 'error: ' + err, values.prettyPrint); + cnt = 0; + } else if (!id) { + response.push({error: 'error: datapoint "' + originId + '" not found'}); + if (!--cnt) doResponse(res, responseType, status, headers, response, values.prettyPrint); + } else { + var usedId = (values[originId]?originId:id); + that.adapter.log.debug('POST-' + command + ' for id=' + id + ', oid=' + originId + ', used=' + usedId + ', value='+values[usedId]); + if (values[usedId] === 'true') { + values[usedId] = true; + } else if (values[usedId] === 'false') { + values[usedId] = false; + } else if (!isNaN(values[usedId]) && values[usedId] == parseFloat(values[usedId])) { + values[usedId] = parseFloat(values[usedId]); + } + + adapter.setForeignState(id, values[usedId], false, {user: values.user, limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner}, function (err, id) { + if (err) { + status = 500; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + doResponse(res, 'plain', status, headers, 'error: ' + err, values.prettyPrint); + cnt = 0; + } else { + status = 200; + adapter.log.debug('Add to Response: ' + JSON.stringify({id: id, val: values[usedId]})); + response.push({id: id, val: values[usedId]}); + if (!--cnt) doResponse(res, responseType, status, headers, response, values.prettyPrint); + } + }); + } + }); + } + if (!cnt) doResponse(res, responseType, status, headers, response, values.prettyPrint); + break; + + default: + doResponse(res, responseType, status, headers, {error: 'command ' + command + ' unknown'}, values.prettyPrint); + break; + } + }); + } + + function restApiDelayedAnswer() { + if (that.restApiDelayed.timer) { + clearTimeout(that.restApiDelayed.timer); + that.restApiDelayed.timer = null; + + doResponse(that.restApiDelayed.res, that.restApiDelayed.responseType, 200, {'Access-Control-Allow-Origin': '*'}, that.restApiDelayed.response, that.restApiDelayed.prettyPrint); + that.restApiDelayed.id = null; + that.restApiDelayed.res = null; + that.restApiDelayed.response = null; + that.restApiDelayed.prettyPrint = false; + } + } + + function findState(idOrName, user, type, callback) { + if (typeof type === 'function') { + callback = type; + type = null; + } + adapter.findForeignObject(idOrName, type, {user: user, checked: true}, callback); + } + + function getState(idOrName, user, type, callback) { + if (typeof type === 'function') { + callback = type; + type = null; + } + findState(idOrName, user, type, function (err, id, originId) { + if (err) { + if (callback) callback(err, undefined, null, originId); + } else + if (id) { + that.adapter.getForeignState(id, {user: user, limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner}, function (err, obj) { + if (err || !obj) { + obj = undefined; + } + if (callback) callback (err, obj, id, originId); + }); + } else { + if (callback) callback (null, undefined, null, originId); + } + }); + } + + function doResponse(res, type, status, headers, content, pretty) { + if (!headers) headers = {}; + + status = parseInt(status, 10) || 200; + + if (pretty && typeof content === 'object') { + type = 'plain'; + content = JSON.stringify(content, null, 2); + } + + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); + + switch (type) { + case 'json': + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + res.statusCode = status; + res.end(JSON.stringify(content), 'utf8'); + break; + + case 'plain': + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.statusCode = status; + if (typeof content === 'object') { + content = JSON.stringify(content); + } + + res.end(content, 'utf8'); + break; + } + } + + // static information + var commandsPermissions = { + getPlainValue: {type: 'state', operation: 'read'}, + get: {type: 'state', operation: 'read'}, + getBulk: {type: 'state', operation: 'read'}, + set: {type: 'state', operation: 'write'}, + toggle: {type: 'state', operation: 'write'}, + setBulk: {type: 'state', operation: 'write'}, + setValueFromBody: {type: 'state', operation: 'write'}, + getObjects: {type: 'object', operation: 'list'}, + objects: {type: 'object', operation: 'list'}, + states: {type: 'state', operation: 'list'}, + getStates: {type: 'state', operation: 'list'}, + help: {type: '', operation: ''} + }; + + this.commands = []; + for (var c in commandsPermissions) { + this.commands.push(c); + } + // Register api by express + this.checkRequest = function (url) { + var parts = url.split('/', 2); + return (parts[1] && this.commands.indexOf(parts[1]) !== -1); + }; + + this.checkPermissions = function (user, command, callback) { + adapter.calculatePermissions(user, commandsPermissions, function (acl) { + if (user !== 'system.user.admin') { + // type: file, object, state, other + // operation: create, read, write, list, delete, sendto, execute, sendto + if (commandsPermissions[command]) { + // If permission required + if (commandsPermissions[command].type) { + if (acl[commandsPermissions[command].type] && + acl[commandsPermissions[command].type][commandsPermissions[command].operation]) { + return callback(null); + } + } else { + return callback(null); + } + } + + that.adapter.log.warn('No permission for "' + user + '" to call ' + command); + + if (callback) callback('permissionError'); + } else { + return callback(null); + } + }); + }; + + this.restApi = function (req, res, isAuth, isChecked) { + var values = {}; + var oId = []; + var wait = 0; + var responseType = 'json'; + var status = 500; + var headers = {'Access-Control-Allow-Origin': '*'}; + var response; + + var url; + try { + url = decodeURI(req.url); + } + catch (e) { + url = req.url; + that.adapter.log.warn('Malformed URL encoding: ' + e); + } + var pos = url.indexOf('?'); + if (pos !== -1) { + var arr = url.substring(pos + 1).split('&'); + url = url.substring(0, pos); + + for (var i = 0; i < arr.length; i++) { + arr[i] = arr[i].split('='); + //that.adapter.log.debug('Try Decode ' + i + ': ' + arr[i][1]); + try { + values[arr[i][0].trim()] = (arr[i][1] === undefined) ? null : decodeURIComponent((arr[i][1]+'').replace(/\+/g, '%20')); + } + catch(e) { + values[arr[i][0].trim()] = arr[i][1]; + } + //that.adapter.log.debug(' Decode Result ' + i + ': ' + values[arr[i][0].trim()]); + } + if (values.prettyPrint !== undefined) { + if (values.prettyPrint === 'false') values.prettyPrint = false; + if (values.prettyPrint === null) values.prettyPrint = true; + } + // Default value for wait + if (values.wait === null) values.wait = 2000; + } + + var parts = url.split('/'); + var command = parts[1]; + + // Analyse system.adapter.socketio.0.uptime,system.adapter.history.0.memRss?value=78&wait=300 + if (parts[2]) { + oId = parts[2].split(','); + for (var j = oId.length - 1; j >= 0; j--) { + oId[j] = oId[j].trim(); + if (!oId[j]) oId.splice(j, 1); + } + } + + // If authentication check is required + if (that.settings.auth) { + if (!isAuth) { + this.isAuthenticated(values, function (isAuth) { + if (isAuth) { + that.restApi(req, res, true); + } else { + doResponse(res, 'plain', 401, headers, 'error: authentication failed. Please write "http' + (that.settings.secure ? 's' : '') + '://' + req.headers.host + '?user=UserName&pass=Password"'); + } + }); + return; + } else + if (!isChecked) { + if (!values.user.match(/^system\.user\./)) values.user = 'system.user.' + values.user; + that.checkPermissions(values.user, command, function (err) { + if (!err) { + that.restApi(req, res, true, true); + } else { + doResponse(res, 'plain', 401, headers, 'error: ' + err, values.prettyPrint); + } + }); + return; + } + } else { + req.user = req.user || that.adapter.config.defaultUser; + values.user = req.user; + if (!values.user.match(/^system\.user\./)) values.user = 'system.user.' + values.user; + if (!isChecked && command) { + that.checkPermissions(req.user || that.adapter.config.defaultUser, command, function (err) { + if (!err) { + that.restApi(req, res, true, true); + } else { + doResponse(res, 'plain', 401, headers, 'error: ' + err, values.prettyPrint); + } + }); + return; + } + } + if (!values.user.match(/^system\.user\./)) values.user = 'system.user.' + values.user; + + if (req.method === 'POST') { + restApiPost(req, res, command, oId, values); + return; + } + + switch (command) { + case 'getPlainValue': + responseType = 'plain'; + if (!oId.length || !oId[0]) { + doResponse(res, 'plain', status, headers, 'error: no datapoint given', values.prettyPrint); + break; + } + + var pcnt = oId.length; + response = ''; + for (var g = 0; g < oId.length; g++) { + getState(oId[g], values.user, function (err, obj, id, originId) { + if (err) { + status = 500; + response = 'error: ' + err; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + pcnt = 1; + } else if ((!id && originId) || obj === undefined) { + response += (response ? '\n' : '') + 'error: datapoint "' + originId + '" not found'; + } else { + response += (response ? '\n' : '') + JSON.stringify(obj.val); + status = 200; + } + if (!--pcnt) doResponse(res, ((status === 500)?'plain':responseType), status, headers, response, values.prettyPrint); + }); + } + break; + + case 'get': + if (!oId.length || !oId[0]) { + doResponse(res, responseType, status, headers, {error: 'no object/datapoint given'}, values.prettyPrint); + break; + } + + var gcnt = oId.length; + for (var k = 0; k < oId.length; k++) { + that.adapter.log.debug('work for ID ' + oId[k]); + getState(oId[k], values.user, function (err, state, id, originId) { + that.adapter.log.debug('return err ' + err); + that.adapter.log.debug('return state ' + state); + that.adapter.log.debug('return id ' + JSON.stringify(id)); + that.adapter.log.debug('return originId' + originId); + if (err) { + gcnt = 0; + status = 500; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + doResponse(res, 'plain', status, headers, 'error: ' + err, values.prettyPrint); + } else + if (!id && originId) { + if (!response || obj === undefined) { + response = 'error: datapoint "' + originId + '" not found'; + } else { + if (typeof response !== 'object' || response.constructor !== Array) { + response = [response]; + } + response.push('error: datapoint "' + originId + '" not found'); + } + if (!--gcnt) doResponse(res, responseType, status, headers, response, values.prettyPrint); + } else { + var vObj = state || {}; + status = 200; + that.adapter.getForeignObject(id, function (err, obj) { + if (obj) { + for (var attr in obj) { + if (obj.hasOwnProperty(attr)) { + vObj[attr] = obj[attr]; + } + } + } + + if (!response) { + response = vObj; + } else { + if (typeof response !== 'object' || response.constructor !== Array) response = [response]; + response.push(vObj); + } + + if (!--gcnt) doResponse(res, responseType, status, headers, response, values.prettyPrint); + }); + } + }); + } + break; + + case 'getBulk': + if (!oId.length || !oId[0]) { + doResponse(res, responseType, status, headers, {error: 'no datapoints given'}, values.prettyPrint); + break; + } + var bcnt = oId.length; + response = []; + for (var b = 0; b < oId.length; b++) { + getState(oId[b], values.user, function (err, state, id, originId) { + if (err) { + bcnt = 0; + status = 500; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + doResponse(res, responseType, status, headers, 'error: ' + err, values.prettyPrint); + } else { + if (id) status = 200; + state = state || {}; + response.push({val: state.val, ts: state.ts}); + if (!--bcnt) doResponse(res, responseType, status, headers, response, values.prettyPrint); + } + }); + } + if (!bcnt) doResponse(res, responseType, status, headers, response, values.prettyPrint); + break; + + case 'set': + if (!oId.length || !oId[0]) { + doResponse(res, responseType, status, headers, {error: "object/datapoint not given"}, values.prettyPrint); + break; + } + if (values.value === undefined && values.val === undefined) { + doResponse(res, responseType, status, headers, 'error: no value found for "' + oId[0] + '". Use /set/id?value=1 or /set/id?value=1&wait=1000', values.prettyPrint); + } else { + findState(oId[0], values.user, function (err, id, originId) { + if (err) { + wait = 0; + status = 500; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + doResponse(res, 'plain', status, headers, 'error: ' + err); + } else + if (id) { + wait = values.wait || 0; + if (wait) wait = parseInt(wait, 10); + if (values.val === undefined) values.val = values.value; + + if (values.val === 'true') { + values.val = true; + } else if (values.val === 'false') { + values.val = false; + } else if (!isNaN(values.val)) { + values.val = parseFloat(values.val); + } + + if (wait) adapter.subscribeForeignStates(id); + + adapter.setForeignState(id, values.val, false, {user: values.user, limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner}, function (err) { + if (err) { + status = 500; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + doResponse(res, 'plain', status, headers, 'error: ' + err, values.prettyPrint); + wait = 0; + } else + if (!wait) { + status = 200; + response = {id: id, value: values.val, val: values.val}; + doResponse(res, responseType, status, headers, response, values.prettyPrint); + } + }); + + if (wait) { + that.restApiDelayed.responseType = responseType; + that.restApiDelayed.response = null; + that.restApiDelayed.id = id; + that.restApiDelayed.res = res; + that.restApiDelayed.prettyPrint = values.prettyPrint; + that.restApiDelayed.timer = setTimeout(restApiDelayedAnswer, wait); + } + } else { + doResponse(res, responseType, status, headers, 'error: datapoint "' + originId + '" not found', values.prettyPrint); + } + }); + } + break; + + case 'toggle': + if (!oId.length || !oId[0]) { + doResponse(res, responseType, status, headers, {error: "state not given"}, values.prettyPrint); + break; + } + + findState(oId[0], values.user, function (err, id, originId) { + if (err) { + doResponse(res, 'plain', 500, headers, 'error: ' + err, values.prettyPrint); + wait = 0; + } else if (id) { + wait = values.wait || 0; + if (wait) wait = parseInt(wait, 10); + + // Read type of object + adapter.getForeignObject(id, {user: values.user, checked: true}, function (err, obj) { + if (err) { + status = 500; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + doResponse(res, 'plain', status, headers, 'error: ' + err, values.prettyPrint); + wait = 0; + } else { + // Read actual value + adapter.getForeignState(id, {user: values.user, checked: true}, function (err, state) { + if (err) { + status = 500; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + doResponse(res, 'plain', status, headers, 'error: ' + err, values.prettyPrint); + wait = 0; + } else + if (state) { + if (obj && obj.common && obj.common.type) { + if (obj.common.type === 'bool' || obj.common.type === 'boolean') { + if (state.val === 'true') { + state.val = true; + } else if (state.val === 'false') { + state.val = false; + } + state.val = !state.val; + } else + if (obj.common.type === 'number') { + state.val = parseFloat(state.val); + if (obj.common.max !== undefined) { + if (obj.common.min === undefined) obj.common.min = 0; + if (state.val > obj.common.max) state.val = obj.common.max; + if (state.val < obj.common.min) state.val = obj.common.min; + // Invert + state.val = obj.common.max + obj.common.min - state.val; + } else { + // default number is from 0 to 100 + if (state.val > 100) state.val = 100; + if (state.val < 0) state.val = 0; + state.val = 100 - state.val; + } + } else { + if (state.val === 'true' || state.val === true) { + state.val = false; + } else if (state.val === 'false' || state.val === false) { + state.val = true; + } else if (parseInt(state.val, 10) == state.val) { + state.val = parseInt(state.val, 10) ? 0 : 1; + } else { + doResponse(res, responseType, status, headers, {error: 'state is neither number nor boolean'}, values.prettyPrint); + return; + } + } + } else { + if (state.val === 'true') { + state.val = true; + } else if (state.val === 'false') { + state.val = false; + } else if (!isNaN(state.val)) { + state.val = parseFloat(state.val); + } + + if (state.val === true) state.val = 1; + if (state.val === false) state.val = 0; + state.val = 1 - parseInt(state.val, 10); + } + + if (wait) adapter.subscribeForeignStates(id); + + adapter.setForeignState(id, state.val, false, {user: values.user, limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner}, function (err) { + if (err) { + status = 500; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + doResponse(res, 'plain', status, headers, 'error: ' + err, values.prettyPrint); + wait = 0; + } else + if (!wait) { + status = 200; + doResponse(res, responseType, status, headers, {id: id, value: state.val, val: state.val}, values.prettyPrint); + } + }); + + if (wait) { + that.restApiDelayed.responseType = responseType; + that.restApiDelayed.response = null; + that.restApiDelayed.id = id; + that.restApiDelayed.res = res; + that.restApiDelayed.prettyPrint = values.prettyPrint; + that.restApiDelayed.timer = setTimeout(restApiDelayedAnswer, wait); + } + } else { + doResponse(res, responseType, status, headers, {error: 'object has no state'}, values.prettyPrint); + } + }); + } + }); + } else { + doResponse(res, responseType, status, headers, {error: 'error: datapoint "' + originId + '" not found'}, values.prettyPrint); + } + }); + + break; + + // /setBulk?BidCos-RF.FEQ1234567:1.LEVEL=0.7&Licht-Küche/LEVEL=0.7&Anwesenheit=0&950=1 + case 'setBulk': + var cnt = 0; + response = []; + adapter.log.debug('Values: ' + JSON.stringify(values)); + for (var _id in values) { + if (_id === 'prettyPrint' || _id === 'user' || _id === 'pass') continue; + cnt++; + findState(_id, values.user, function (err, id, originId) { + // id is "name", originId is the yunkong2-ID of the datapoint + if (err) { + adapter.log.debug('Error on ID lookup: ' + err); + status = 500; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + doResponse(res, 'plain', status, headers, 'error: ' + err, values.prettyPrint); + cnt = 0; + } else if (!id) { + response.push({error: 'error: datapoint "' + originId + '" not found'}); + if (!--cnt) doResponse(res, responseType, status, headers, response, values.prettyPrint); + } else { + var usedId = (values[originId]?originId:id); + that.adapter.log.debug('GET-' + command + ' for id=' + id + ', oid=' + originId + 'used=' + usedId + ', value=' + values[usedId]); + if (values[usedId] === 'true') { + values[usedId] = true; + } else if (values[usedId] === 'false') { + values[usedId] = false; + } else if (!isNaN(values[usedId]) && values[usedId] == parseFloat(values[usedId])) { + values[usedId] = parseFloat(values[usedId]); + } + + adapter.setForeignState(id, values[usedId], false, {user: values.user, limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner}, function (err, id) { + if (err) { + status = 500; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + doResponse(res, 'plain', status, headers, 'error: ' + err, values.prettyPrint); + cnt = 0; + } else { + adapter.log.debug('Add to Response-Get: ' + JSON.stringify({id: id, val: values[usedId], value: values[usedId]})); + response.push({id: id, val: values[usedId], value: values[usedId]}); + if (!--cnt) { + status = 200; + doResponse(res, responseType, status, headers, response, values.prettyPrint); + } + } + }); + } + }); + } + if (!cnt) doResponse(res, responseType, status, headers, response, values.prettyPrint); + break; + + case 'getObjects': + case 'objects': + adapter.getForeignObjects(values.pattern || parts[2] || '*', values.type, {user: values.user, limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner}, function (err, list) { + if (err) { + status = 500; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + doResponse(res, responseType, status, headers, {error: JSON.stringify(err)}, values.prettyPrint); + } else { + status = 200; + doResponse(res, responseType, status, headers, list, values.prettyPrint); + } + }); + break; + + case 'getStates': + case 'states': + adapter.getForeignStates(values.pattern || parts[2] || '*', {user: values.user, limitToOwnerRights: that.adapter.config.onlyAllowWhenUserIsOwner}, function (err, list) { + if (err) { + status = 500; + if (err.indexOf('permissionError') !== -1) { + status = 401; + } + doResponse(res, responseType, status, headers, {error: JSON.stringify(err)}, values.prettyPrint); + } else { + status = 200; + doResponse(res, responseType, status, headers, list, values.prettyPrint); + } + }); + break; + + case 'help': + // is default behaviour too + default: + var obj = (command === 'help') ? {} : {error: 'command ' + command + ' unknown'}; + var request = 'http' + (that.settings.secure ? 's' : '') + '://' + req.headers.host; + if (this.app) request += '/' + this.namespace + '/'; + var auth = ''; + if (that.settings.auth) auth = 'user=UserName&pass=Password'; + obj.getPlainValue = request + '/getPlainValue/stateID' + (auth ? '?' + auth : ''); + obj.get = request + '/get/stateID/?prettyPrint' + (auth ? '&' + auth : ''); + obj.getBulk = request + '/getBulk/stateID1,stateID2/?prettyPrint' + (auth ? '&' + auth : ''); + obj.set = request + '/set/stateID?value=1&prettyPrint' + (auth ? '&' + auth : ''); + obj.toggle = request + '/toggle/stateID&prettyPrint' + (auth ? '&' + auth : ''); + obj.setBulk = request + '/setBulk?stateID1=0.7&stateID2=0&prettyPrint' + (auth ? '&' + auth : ''); + obj.setValueFromBody = request + '/setValueFromBody?stateID1' + (auth ? '&' + auth : ''); + obj.objects = request + '/objects?pattern=system.adapter.admin.0*&prettyPrint' + (auth ? '&' + auth : ''); + obj.states = request + '/states?pattern=system.adapter.admin.0*&prettyPrint' + (auth ? '&' + auth : ''); + + doResponse(res, responseType, status, headers, obj, true); + break; + } + }; +} + +module.exports = SimpleAPI; 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..89c69a3 --- /dev/null +++ b/main.js @@ -0,0 +1,133 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +'use strict'; + +var utils = require(__dirname + '/lib/utils'); // Get common adapter utils +var SimpleAPI = require(__dirname + '/lib/simpleapi.js'); +var LE = require(utils.controllerDir + '/lib/letsencrypt.js'); + +var webServer = null; +var fs = null; + +var adapter = new utils.Adapter({ + name: 'simple-api', + stateChange: function (id, state) { + if (webServer && webServer.api) { + webServer.api.stateChange(id, state); + } + }, + objectChange: function (id, obj) { + if (webServer && webServer.api) { + webServer.api.objectChange(id, obj); + } + }, + unload: function (callback) { + try { + adapter.log.info('terminating http' + (webServer.settings.secure ? 's' : '') + ' server on port ' + webServer.settings.port); + //if (webServer.api) webServer.api.close(); + + callback(); + } catch (e) { + callback(); + } + }, + ready: function () { + main(); + } +}); + +function main() { + if (adapter.config.webInstance) { + console.log('Adapter runs as a part of web service'); + adapter.log.warn('Adapter runs as a part of web service'); + adapter.setForeignState('system.adapter.' + adapter.namespace + '.alive', false, true, function () { + setTimeout(function () { + process.exit(); + }, 1000); + }); + return; + } + + if (adapter.config.secure) { + // subscribe on changes of permissions + adapter.subscribeForeignObjects('system.group.*'); + adapter.subscribeForeignObjects('system.user.*'); + + // 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); + } +} + +function requestProcessor(req, res) { + if (req.url.indexOf('favicon.ico') !== -1) { + if (!fs) fs = require('fs'); + var stat = fs.statSync(__dirname + '/img/favicon.ico'); + + res.writeHead(200, { + 'Content-Type': 'image/x-icon', + 'Content-Length': stat.size + }); + + var readStream = fs.createReadStream(__dirname + '/img/favicon.ico'); + // We replaced all the event handlers with a simple call to readStream.pipe() + readStream.pipe(res); + } else { + webServer.api.restApi(req, res); + } +} + +//settings: { +// "port": 8080, +// "auth": false, +// "secure": false, +// "bind": "0.0.0.0", // "::" +// "cache": false +//} +function initWebServer(settings) { + + var server = { + app: null, + server: null, + api: null, + io: null, + settings: settings + }; + + settings.port = parseInt(settings.port, 10); + + if (settings.port) { + + if (settings.secure && !adapter.config.certificates) return null; + + server.server = LE.createServer(requestProcessor, settings, adapter.config.certificates, adapter.config.leConfig, adapter.log); + server.server.__server = server; + } else { + adapter.log.error('port missing'); + process.exit(1); + } + + if (server.server) { + adapter.getPort(settings.port, function (port) { + if (port !== settings.port && !adapter.config.findNextPort) { + adapter.log.error('port ' + settings.port + ' already in use'); + process.exit(1); + } + server.server.listen(port); + adapter.log.info('http' + (settings.secure ? 's' : '') + ' server listening on port ' + port); + }); + } + + server.api = new SimpleAPI(server.server, settings, adapter); + + if (server.server) { + return server; + } else { + return null; + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..40e2db1 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "yunkong2.simple-api", + "version": "2.0.0", + "description": "RESTful interface for yunkong2.", + "author": { + "name": "bluefox", + "email": "dogafox@gmail.com" + }, + "contributors": [ + { + "name": "Apollon77", + "email": "ingo@fischer-ka.de" + } + ], + "homepage": "https://git.spacen.net/yunkong2/yunkong2.simple-api", + "keywords": [ + "yunkong2", + "web" + ], + "repository": { + "type": "git", + "url": "https://git.spacen.net/yunkong2/yunkong2.simple-api" + }, + "dependencies": {}, + "devDependencies": { + "gulp": "^3.9.1", + "mocha": "^5.0.1", + "chai": "^4.1.2", + "request": "^2.83.0" + }, + "bugs": { + "url": "https://git.spacen.net/yunkong2/yunkong2.simple-api/issues" + }, + "main": "main.js", + "scripts": { + "test": "node node_modules/mocha/bin/mocha --exit" + }, + "license": "MIT" +} 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/testApi.js b/test/testApi.js new file mode 100644 index 0000000..c9c12f5 --- /dev/null +++ b/test/testApi.js @@ -0,0 +1,460 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +/*jshint expr: true*/ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); +var request = require('request'); + +var objects = null; +var states = null; + +process.env.NO_PROXY = '127.0.0.1'; + +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.simple-api.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); + } + }); +} + +describe('Test RESTful API', function() { + before('Test RESTful API: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + var brokerStarted = false; + setup.adapterStarted = false; + + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + config.native.port = 18183; + setup.setAdapterConfig(config.common, config.native); + + setup.startController(function (_objects, _states) { + objects = _objects; + states = _states; + // give some time to start server + setTimeout(function () { + _done(); + }, 2000); + }); + }); + }); + + it('Test adapter: Check if adapter started and create upload datapoint', function (done) { + this.timeout(60000); + checkConnectionOfAdapter(function (res) { + if (res) console.log(res); + expect(res).not.to.be.equal('Cannot check connection'); + objects.setObject('javascript.0.test-string', { + common: { + name: 'test', + type: 'string', + role: 'value', + def: '' + }, + native: { + }, + type: 'state' + }, function (err) { + expect(err).to.be.null; + states.setState('javascript.0.test-string','', function(err) { + expect(err).to.be.null; + done(); + }); + }); + }); + }); + + it('Test RESTful API: get - must return value', function (done) { + request('http://127.0.0.1:18183/get/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('get/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + //{ + // "val" : true, + // "ack" : true, + // "ts" : 1455009717, + // "q" : 0, + // "from" : "system.adapter.simple-api.0", + // "lc" : 1455009717, + // "expire" : 30000, + // "_id" : "system.adapter.simple-api.0.alive", + // "type" : "state", + // "common" : { + // "name" : "simple-api.0.alive", + // "type" : "boolean", + // "role" : "indicator.state" + // }, + // "native" : {} + // + //} + + expect(obj).to.be.ok; + expect(obj.val).to.be.true; + expect(obj.ack).to.be.true; + expect(obj.ts).to.be.ok; + //expect(obj.from).to.equal("system.adapter.simple-api.0"); + expect(obj.type).to.equal("state"); + expect(obj._id).to.equal("system.adapter.simple-api.0.alive"); + expect(obj.common).to.be.ok; + expect(obj.native).to.be.ok; + expect(obj.common.name).to.equal("simple-api.0.alive"); + expect(obj.common.role).to.equal("indicator.state"); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + + it('Test RESTful API: getPlainValue - must return plain value', function (done) { + request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('true'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + + it('Test RESTful API: set - must set value', function (done) { + request('http://127.0.0.1:18183/set/system.adapter.simple-api.0.alive?val=false', function (error, response, body) { + console.log('set/system.adapter.simple-api.0.alive?val=false => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).to.be.false; + expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); + expect(response.statusCode).to.equal(200); + request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('false'); + expect(response.statusCode).to.equal(200); + request('http://127.0.0.1:18183/get/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('get/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(JSON.parse(body).val).equal(false); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + }); + + it('Test RESTful API: set - must set easy string value', function (done) { + request('http://127.0.0.1:18183/set/javascript.0.test-string?val=bla', function (error, response, body) { + console.log('set/javascript.0.test-string?val=bla => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).equal('bla'); + expect(obj.id).to.equal('javascript.0.test-string'); + expect(response.statusCode).to.equal(200); + request('http://127.0.0.1:18183/getPlainValue/javascript.0.test-string', function (error, response, body) { + console.log('getPlainValue/javascript.0.test-string => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('"bla"'); + expect(response.statusCode).to.equal(200); + request('http://127.0.0.1:18183/get/javascript.0.test-string', function (error, response, body) { + console.log('get/javascript.0.test-string => ' + body); + expect(error).to.be.not.ok; + expect(JSON.parse(body).val).equal('bla'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + }); + + it('Test RESTful API: set - must set encoded string value', function (done) { + request('http://127.0.0.1:18183/set/javascript.0.test-string?val=bla%26fasel%2efoo%3Dhummer+hey', function (error, response, body) { + console.log('set/javascript.0.test-string?val=bla%20fasel%2efoo => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).equal('bla&fasel.foo=hummer hey'); + expect(obj.id).to.equal('javascript.0.test-string'); + expect(response.statusCode).to.equal(200); + request('http://127.0.0.1:18183/getPlainValue/javascript.0.test-string', function (error, response, body) { + console.log('getPlainValue/javascript.0.test-string => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('"bla&fasel.foo=hummer hey"'); + expect(response.statusCode).to.equal(200); + request('http://127.0.0.1:18183/get/javascript.0.test-string', function (error, response, body) { + console.log('get/javascript.0.test-string => ' + body); + expect(error).to.be.not.ok; + expect(JSON.parse(body).val).equal('bla&fasel.foo=hummer hey'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + }); + + it('Test RESTful API: set - must set val', function (done) { + request('http://127.0.0.1:18183/set/system.adapter.simple-api.0.alive?val=true', function (error, response, body) { + console.log('set/system.adapter.simple-api.0.alive?val=true => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).to.be.true; + expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); + expect(response.statusCode).to.equal(200); + request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('true'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API: toggle - must toggle boolean value to false', function (done) { + request('http://127.0.0.1:18183/toggle/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('toggle/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).to.be.false; + expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('false'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API: toggle - must toggle boolean value to true', function (done) { + request('http://127.0.0.1:18183/toggle/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('toggle/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).to.be.true; + expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('true'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API: toggle - must toggle number value to 100', function (done) { + request('http://127.0.0.1:18183/toggle/system.adapter.simple-api.upload', function (error, response, body) { + console.log('toggle/system.adapter.simple-api.upload => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).to.be.equal(100); + expect(obj.id).to.equal('system.adapter.simple-api.upload'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.upload', function (error, response, body) { + console.log('getPlainValue/system.adapter.simple-api.upload => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('100'); + expect(response.statusCode).to.equal(200); + request('http://127.0.0.1:18183/set/system.adapter.simple-api.upload?val=49', function (error, response, body) { + console.log('set/system.adapter.simple-api.upload?val=49 => ' + body); + request('http://127.0.0.1:18183/toggle/system.adapter.simple-api.upload', function (error, response, body) { + console.log('toggle/system.adapter.simple-api.upload => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).to.be.equal(51); + expect(obj.id).to.equal('system.adapter.simple-api.upload'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.upload', function (error, response, body) { + console.log('getPlainValue/system.adapter.simple-api.upload => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('51'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + }); + }); + }); + + it('Test RESTful API: setBulk - must set values', function (done) { + request('http://127.0.0.1:18183/setBulk?system.adapter.simple-api.upload=50&system.adapter.simple-api.0.alive=false', function (error, response, body) { + console.log('setBulk/?system.adapter.simple-api.upload=50&system.adapter.simple-api.0.alive=false => ' + body); + expect(error).to.be.not.ok; + + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(50); + expect(obj[0].id).to.equal('system.adapter.simple-api.upload'); + expect(obj[1].val).to.be.equal(false); + expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getBulk/system.adapter.simple-api.upload,system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('getBulk/system.adapter.simple-api.upload,system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj[0].val).equal(50); + expect(obj[1].val).equal(false); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API: objects - must return objects', function (done) { + request('http://127.0.0.1:18183/objects?pattern=system.adapter.*', function (error, response, body) { + console.log('objects?pattern=system.adapter.* => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj['system.adapter.simple-api.0.alive']._id).to.be.ok; + expect(response.statusCode).to.equal(200); + done(); + }); + }); + + it('Test RESTful API: objects - must return objects', function (done) { + request('http://127.0.0.1:18183/objects?pattern=system.adapter.*&type=instance', function (error, response, body) { + console.log('objects?pattern=system.adapter.* => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj['system.adapter.simple-api.0']._id).to.be.ok; + expect(response.statusCode).to.equal(200); + done(); + }); + }); + + it('Test RESTful API: states - must return states', function (done) { + request('http://127.0.0.1:18183/states?pattern=system.adapter.*', function (error, response, body) { + console.log('states?pattern=system.adapter.* => ' + body); + expect(error).to.be.not.ok; + var states = JSON.parse(body); + expect(states['system.adapter.simple-api.0.uptime'].val).to.be.least(0); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + + it('Test RESTful API: setBulk(POST) - must set values', function (done) { + + request({ + uri: 'http://127.0.0.1:18183/setBulk', + method: 'POST', + body: 'system.adapter.simple-api.upload=50&system.adapter.simple-api.0.alive=false&javascript.0.test-string=bla%26fasel%2efoo%3Dhummer+hey' + }, function(error, response, body) { + console.log('setBulk/?system.adapter.simple-api.upload=50&system.adapter.simple-api.0.alive=false&javascript.0.test-string=bla%26fasel%2efoo%3Dhummer+hey => ' + JSON.stringify(body)); + expect(error).to.be.not.ok; + + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(50); + expect(obj[0].id).to.equal('system.adapter.simple-api.upload'); + expect(obj[1].val).to.be.equal(false); + expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); + expect(obj[2].val).to.be.equal('bla&fasel.foo=hummer hey'); + expect(obj[2].id).to.equal('javascript.0.test-string'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getBulk/system.adapter.simple-api.upload,system.adapter.simple-api.0.alive,javascript.0.test-string', function (error, response, body) { + console.log('getBulk/system.adapter.simple-api.upload,system.adapter.simple-api.0.alive,javascript.0.test-string => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj[0].val).equal(50); + expect(obj[1].val).equal(false); + expect(obj[2].val).equal('bla&fasel.foo=hummer hey'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API: setBulk(POST-GET-Mix) - must set values', function (done) { + + request({ + uri: 'http://127.0.0.1:18183/setBulk?system.adapter.simple-api.upload=51&system.adapter.simple-api.0.alive=false', + method: 'POST', + body: '' + }, function(error, response, body) { + console.log('setBulk/?system.adapter.simple-api.upload=51&system.adapter.simple-api.0.alive=false => ' + JSON.stringify(body)); + expect(error).to.be.not.ok; + + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(51); + expect(obj[0].id).to.equal('system.adapter.simple-api.upload'); + expect(obj[1].val).to.be.equal(false); + expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getBulk/system.adapter.simple-api.upload,system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('getBulk/system.adapter.simple-api.upload,system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj[0].val).equal(51); + expect(obj[1].val).equal(false); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API: setValueFromBody(POST) - must set one value', function (done) { + request({ + uri: 'http://127.0.0.1:18183/setValueFromBody/system.adapter.simple-api.upload', + method: 'POST', + body: '55' + }, function(error, response, body) { + console.log('setValueFromBody/?system.adapter.simple-api.upload => ' + JSON.stringify(body)); + expect(error).to.be.not.ok; + + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(55); + expect(obj[0].id).to.equal('system.adapter.simple-api.upload'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getBulk/system.adapter.simple-api.upload', function (error, response, body) { + console.log('getBulk/system.adapter.simple-api.upload => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj[0].val).equal(55); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + after('Test RESTful API: Stop js-controller', function (done) { + this.timeout(6000); + setup.stopController(function (normalTerminated) { + console.log('Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); diff --git a/test/testApiAsLimitedUser.js b/test/testApiAsLimitedUser.js new file mode 100644 index 0000000..002736f --- /dev/null +++ b/test/testApiAsLimitedUser.js @@ -0,0 +1,277 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +/*jshint expr: true*/ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); +var request = require('request'); + +var objects = null; +var states = null; + +process.env.NO_PROXY = '127.0.0.1'; + +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.simple-api.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); + } + }); +} + +describe('Test RESTful API as Owner-User', function() { + before('Test RESTful API as Owner-User: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + setup.adapterStarted = false; + + var brokerStarted = false; + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + config.native.port = 18183; + config.native.defaultUser = 'myuser'; + config.native.onlyAllowWhenUserIsOwner = true; + setup.setAdapterConfig(config.common, config.native); + + setup.startController(function (_objects, _states) { + objects = _objects; + states = _states; + // give some time to start server + setTimeout(function () { + _done(); + }, 2000); + }); + }); + }); + + it('Test adapter: Check if adapter started and create upload datapoint', function (done) { + this.timeout(60000); + checkConnectionOfAdapter(function (res) { + if (res) console.log(res); + expect(res).not.to.be.equal('Cannot check connection'); + objects.setObject('system.group.writer', { + "common": { + "name": "Writer", + "desc": "", + "members": [ + "system.user.myuser" + ], + "acl": { + "object": { + "list": false, + "read": false, + "write": false, + "delete": false + }, + "state": { + "list": false, + "read": true, + "write": true, + "create": false, + "delete": false + }, + "users": { + "write": false, + "create": false, + "delete": false + }, + "other": { + "execute": false, + "http": false, + "sendto": false + }, + "file": { + "list": false, + "read": false, + "write": false, + "create": false, + "delete": false + } + } + }, + "native": {}, + "acl": { + "object": 1638, + "owner": "system.user.admin", + "ownerGroup": "system.group.administrator" + }, + "_id": "system.group.writer", + "type": "group" + }, function (err) { + expect(err).to.be.null; + + objects.setObject('system.user.myuser', { + "type": "user", + "common": { + "name": "myuser", + "enabled": true, + "groups": [], + "password": "pbkdf2$10000$ab4104d8bb68390ee7e6c9397588e768de6c025f0c732c18806f3d1270c83f83fa86a7bf62583770e5f8d0b405fbb3ad32214ef3584f5f9332478f2506414443a910bf15863b36ebfcaa7cbb19253ae32cd3ca390dab87b29cd31e11be7fa4ea3a01dad625d9de44e412680e1a694227698788d71f1e089e5831dc1bbacfa794b45e1c995214bf71ee4160d98b4305fa4c3e36ee5f8da19b3708f68e7d2e8197375c0f763d90e31143eb04760cc2148c8f54937b9385c95db1742595634ed004fa567655dfe1d9b9fa698074a9fb70c05a252b2d9cf7ca1c9b009f2cd70d6972ccf0ee281d777d66a0346c6c6525436dd7fe3578b28dca2c7adbfde0ecd45148$31c3248ba4dc9600a024b4e0e7c3e585" + }, + "_id": "system.user.myuser", + "native": {}, + "acl": { + "object": 1638 + } + }, function (err) { + expect(err).to.be.null; + objects.setObject('javascript.0.test', { + common: { + name: 'test', + type: 'number', + role: 'level', + min: -100, + max: 100, + def: 1 + }, + native: { + }, + type: 'state', + acl: { + object: 1638, + owner: "system.user.myuser", + ownerGroup:"system.group.administrator", + state: 1638 + } + }, function (err) { + expect(err).to.be.null; + states.setState('javascript.0.test',1, function(err) { + console.log('END javascript.0.test ' + err); + expect(err).to.be.null; + done(); + }); + }); + }); + }); + }); + }); + + it('Test RESTful API as Owner-User: get - must return value', function (done) { + console.log('START get/system.adapter.simple-api.0.alive'); + request('http://127.0.0.1:18183/get/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('get/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).to.be.equal('error: permissionError'); + expect(response.statusCode).to.equal(401); + done(); + }); + }); + + it('Test RESTful API as Owner-User: getPlainValue - must return plain value', function (done) { + request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(body).to.be.equal('error: permissionError'); + expect(response.statusCode).to.equal(401); + done(); + }); + }); + + it('Test RESTful API as Owner-User: getPlainValue 4 Test-Endpoint - must return plain value', function (done) { + request('http://127.0.0.1:18183/getPlainValue/javascript.0.test', function (error, response, body) { + console.log('getPlainValue/javascript.0.test => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('1'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + + it('Test RESTful API as Owner-User: set 4 Test-Endpoint - must set value', function (done) { + request('http://127.0.0.1:18183/set/javascript.0.test?val=2', function (error, response, body) { + console.log('set/javascript.0.test?val=false => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).to.be.equal(2); + expect(obj.id).to.equal('javascript.0.test'); + expect(response.statusCode).to.equal(200); + request('http://127.0.0.1:18183/getPlainValue/javascript.0.test', function (error, response, body) { + console.log('getPlainValue/javascript.0.test => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('2'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API as Owner-User: set - must set value', function (done) { + request('http://127.0.0.1:18183/set/system.adapter.simple-api.0.alive?val=false', function (error, response, body) { + console.log('set/system.adapter.simple-api.0.alive?val=false => ' + body); + expect(body).to.be.equal('error: permissionError'); + expect(response.statusCode).to.equal(401); + done(); + }); + }); + + it('Test RESTful API as Owner-User: set - must set val', function (done) { + request('http://127.0.0.1:18183/set/system.adapter.simple-api.0.alive?val=true', function (error, response, body) { + console.log('set/system.adapter.simple-api.0.alive?val=true => ' + body); + expect(body).to.be.equal('error: permissionError'); + expect(response.statusCode).to.equal(401); + done(); + }); + }); + + + it('Test RESTful API as Owner-User: objects - must return objects', function (done) { + request('http://127.0.0.1:18183/objects?pattern=system.adapter.*', function (error, response, body) { + console.log('objects?pattern=system.adapter.* => ' + body); + expect(body).to.be.equal('error: permissionError'); + expect(response.statusCode).to.equal(401); + done(); + }); + }); + + it('Test RESTful API as Owner-User: objects - must return objects', function (done) { + request('http://127.0.0.1:18183/objects?pattern=system.adapter.*&type=instance', function (error, response, body) { + console.log('objects?pattern=system.adapter.* => ' + body); + expect(body).to.be.equal('error: permissionError'); + expect(response.statusCode).to.equal(401); + done(); + }); + }); + + it('Test RESTful API as Owner-User: states - must return states', function (done) { + request('http://127.0.0.1:18183/states?pattern=system.adapter.*', function (error, response, body) { + console.log('states?pattern=system.adapter.* => ' + body); + expect(body).to.be.equal('error: permissionError'); + expect(response.statusCode).to.equal(401); + done(); + }); + }); + + it('Test RESTful API as Owner-User: setValueFromBody(POST) - must set one value', function (done) { + request({ + uri: 'http://127.0.0.1:18183/setValueFromBody/system.adapter.simple-api.upload', + method: 'POST', + body: '55' + }, function(error, response, body) { + console.log('setValueFromBody/?system.adapter.simple-api.upload => ' + JSON.stringify(body)); + expect(body).to.be.equal('error: permissionError'); + expect(response.statusCode).to.equal(401); + done(); + }); + }); + + after('Test RESTful API as Owner-User: Stop js-controller', function (done) { + this.timeout(6000); + setup.stopController(function (normalTerminated) { + console.log('Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); diff --git a/test/testApiAsUser.js b/test/testApiAsUser.js new file mode 100644 index 0000000..1755391 --- /dev/null +++ b/test/testApiAsUser.js @@ -0,0 +1,508 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +/*jshint expr: true*/ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); +var request = require('request'); + +var objects = null; +var states = null; + +process.env.NO_PROXY = '127.0.0.1'; + +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.simple-api.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); + } + }); +} + +describe('Test RESTful API as User', function() { + before('Test RESTful API as User: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + setup.adapterStarted = false; + + var brokerStarted = false; + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + config.native.port = 18183; + config.native.defaultUser = 'myuser'; + setup.setAdapterConfig(config.common, config.native); + + setup.startController(function (_objects, _states) { + objects = _objects; + states = _states; + // give some time to start server + setTimeout(function () { + _done(); + }, 2000); + }); + }); + }); + + it('Test adapter: Check if adapter started and create upload datapoint', function (done) { + this.timeout(60000); + checkConnectionOfAdapter(function (res) { + if (res) console.log(res); + expect(res).not.to.be.equal('Cannot check connection'); + objects.setObject('system.group.writer', { + "common": { + "name": "Writer", + "desc": "", + "members": [ + "system.user.myuser" + ], + "acl": { + "object": { + "list": false, + "read": false, + "write": false, + "delete": false + }, + "state": { + "list": false, + "read": true, + "write": true, + "create": false, + "delete": false + }, + "users": { + "write": false, + "create": false, + "delete": false + }, + "other": { + "execute": false, + "http": false, + "sendto": false + }, + "file": { + "list": false, + "read": false, + "write": false, + "create": false, + "delete": false + } + } + }, + "native": {}, + "acl": { + "object": 1638, + "owner": "system.user.admin", + "ownerGroup": "system.group.administrator" + }, + "_id": "system.group.writer", + "type": "group" + }, function (err) { + expect(err).to.be.null; + + objects.setObject('system.user.myuser', { + "type": "user", + "common": { + "name": "myuser", + "enabled": true, + "groups": [], + "password": "pbkdf2$10000$ab4104d8bb68390ee7e6c9397588e768de6c025f0c732c18806f3d1270c83f83fa86a7bf62583770e5f8d0b405fbb3ad32214ef3584f5f9332478f2506414443a910bf15863b36ebfcaa7cbb19253ae32cd3ca390dab87b29cd31e11be7fa4ea3a01dad625d9de44e412680e1a694227698788d71f1e089e5831dc1bbacfa794b45e1c995214bf71ee4160d98b4305fa4c3e36ee5f8da19b3708f68e7d2e8197375c0f763d90e31143eb04760cc2148c8f54937b9385c95db1742595634ed004fa567655dfe1d9b9fa698074a9fb70c05a252b2d9cf7ca1c9b009f2cd70d6972ccf0ee281d777d66a0346c6c6525436dd7fe3578b28dca2c7adbfde0ecd45148$31c3248ba4dc9600a024b4e0e7c3e585" + }, + "_id": "system.user.myuser", + "native": {}, + "acl": { + "object": 1638 + } + }, function (err) { + expect(err).to.be.null; + objects.setObject('javascript.0.test', { + common: { + name: 'test', + type: 'number', + role: 'level', + min: -100, + max: 100, + def: 1 + }, + native: { + }, + type: 'state', + acl: { + object: 1638, + owner: "system.user.myuser", + ownerGroup:"system.group.administrator", + state: 1638 + } + }, function (err) { + expect(err).to.be.null; + states.setState('javascript.0.test',1, function(err) { + expect(err).to.be.null; + done(); + }); + }); + }); + }); + }); + }); + + it('Test RESTful API as User: get - must return value', function (done) { + request('http://127.0.0.1:18183/get/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('get/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + //{ + // "val" : true, + // "ack" : true, + // "ts" : 1455009717, + // "q" : 0, + // "from" : "system.adapter.simple-api.0", + // "lc" : 1455009717, + // "expire" : 30000, + // "_id" : "system.adapter.simple-api.0.alive", + // "type" : "state", + // "common" : { + // "name" : "simple-api.0.alive", + // "type" : "boolean", + // "role" : "indicator.state" + // }, + // "native" : {} + // + //} + + expect(obj).to.be.ok; + expect(obj.val).to.be.true; + expect(obj.ack).to.be.true; + expect(obj.ts).to.be.ok; + //expect(obj.from).to.equal("system.adapter.simple-api.0"); + expect(obj.type).to.equal("state"); + expect(obj._id).to.equal("system.adapter.simple-api.0.alive"); + expect(obj.common).to.be.ok; + expect(obj.native).to.be.ok; + expect(obj.common.name).to.equal("simple-api.0.alive"); + expect(obj.common.role).to.equal("indicator.state"); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + + it('Test RESTful API as User: getPlainValue - must return plain value', function (done) { + request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('true'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + + it('Test RESTful API as User: getPlainValue 4 Test-Endpoint - must return plain value', function (done) { + request('http://127.0.0.1:18183/getPlainValue/javascript.0.test', function (error, response, body) { + console.log('getPlainValue/javascript.0.test => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('1'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + + it('Test RESTful API as User: set 4 Test-Endpoint - must set value', function (done) { + request('http://127.0.0.1:18183/set/javascript.0.test?val=2', function (error, response, body) { + console.log('set/javascript.0.test?val=false => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).to.be.equal(2); + expect(obj.id).to.equal('javascript.0.test'); + expect(response.statusCode).to.equal(200); + request('http://127.0.0.1:18183/getPlainValue/javascript.0.test', function (error, response, body) { + console.log('getPlainValue/javascript.0.test => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('2'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API as User: set - must set value', function (done) { + request('http://127.0.0.1:18183/set/system.adapter.simple-api.0.alive?val=false', function (error, response, body) { + console.log('set/system.adapter.simple-api.0.alive?val=false => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).to.be.false; + expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); + expect(response.statusCode).to.equal(200); + request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('false'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API as User: set - must set val', function (done) { + request('http://127.0.0.1:18183/set/system.adapter.simple-api.0.alive?val=true', function (error, response, body) { + console.log('set/system.adapter.simple-api.0.alive?val=true => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).to.be.true; + expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); + expect(response.statusCode).to.equal(200); + request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('true'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API as User: toggle - must toggle boolean value to false', function (done) { + request('http://127.0.0.1:18183/toggle/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('toggle/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).to.be.false; + expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('false'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API as User: toggle - must toggle boolean value to true', function (done) { + request('http://127.0.0.1:18183/toggle/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('toggle/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).to.be.true; + expect(obj.id).to.equal('system.adapter.simple-api.0.alive'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('getPlainValue/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('true'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API as User: toggle - must toggle number value to 100', function (done) { + request('http://127.0.0.1:18183/toggle/system.adapter.simple-api.upload', function (error, response, body) { + console.log('toggle/system.adapter.simple-api.upload => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).to.be.equal(100); + expect(obj.id).to.equal('system.adapter.simple-api.upload'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.upload', function (error, response, body) { + console.log('getPlainValue/system.adapter.simple-api.upload => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('100'); + expect(response.statusCode).to.equal(200); + request('http://127.0.0.1:18183/set/system.adapter.simple-api.upload?val=49', function (error, response, body) { + console.log('set/system.adapter.simple-api.upload?val=49 => ' + body); + request('http://127.0.0.1:18183/toggle/system.adapter.simple-api.upload', function (error, response, body) { + console.log('toggle/system.adapter.simple-api.upload => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj.val).to.be.equal(51); + expect(obj.id).to.equal('system.adapter.simple-api.upload'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getPlainValue/system.adapter.simple-api.upload', function (error, response, body) { + console.log('getPlainValue/system.adapter.simple-api.upload => ' + body); + expect(error).to.be.not.ok; + expect(body).equal('51'); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + }); + }); + }); + + it('Test RESTful API as User: setBulk - must set values', function (done) { + request('http://127.0.0.1:18183/setBulk/?system.adapter.simple-api.upload=50&system.adapter.simple-api.0.alive=false&javascript.0.test=3', function (error, response, body) { + console.log('setBulk/?system.adapter.simple-api.upload=50&system.adapter.simple-api.0.alive=false&javascript.0.test=3 => ' + body); + expect(error).to.be.not.ok; + + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(50); + expect(obj[0].id).to.equal('system.adapter.simple-api.upload'); + expect(obj[1].val).to.be.equal(false); + expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); + expect(obj[2].val).to.be.equal(3); + expect(obj[2].id).to.equal('javascript.0.test'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getBulk/system.adapter.simple-api.upload,system.adapter.simple-api.0.alive,javascript.0.test', function (error, response, body) { + console.log('getBulk/system.adapter.simple-api.upload,system.adapter.simple-api.0.alive&javascript.0.test => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj[0].val).equal(50); + expect(obj[1].val).equal(false); + expect(obj[2].val).equal(3); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API as User: objects - must return objects', function (done) { + request('http://127.0.0.1:18183/objects?pattern=system.adapter.*', function (error, response, body) { + console.log('objects?pattern=system.adapter.* => ' + body); + expect(body).to.be.equal('error: permissionError'); + expect(response.statusCode).to.equal(401); + done(); + }); + }); + + it('Test RESTful API as User: objects - must return objects', function (done) { + request('http://127.0.0.1:18183/objects?pattern=system.adapter.*&type=instance', function (error, response, body) { + console.log('objects?pattern=system.adapter.* => ' + body); + expect(body).to.be.equal('error: permissionError'); + expect(response.statusCode).to.equal(401); + done(); + }); + }); + + it('Test RESTful API as User: states - must return states', function (done) { + request('http://127.0.0.1:18183/states?pattern=system.adapter.*', function (error, response, body) { + console.log('states?pattern=system.adapter.* => ' + body); + expect(body).to.be.equal('error: permissionError'); + expect(response.statusCode).to.equal(401); + done(); + }); + }); + + it('Test RESTful API as User: setBulk(POST) - must set values', function (done) { + + request({ + uri: 'http://127.0.0.1:18183/setBulk', + method: 'POST', + body: 'system.adapter.simple-api.upload=50&system.adapter.simple-api.0.alive=false&javascript.0.test=4' + }, function(error, response, body) { + console.log('setBulk/?system.adapter.simple-api.upload=50&system.adapter.simple-api.0.alive=false&javascript.0.test=4 => ' + JSON.stringify(body)); + expect(error).to.be.not.ok; + + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(50); + expect(obj[0].id).to.equal('system.adapter.simple-api.upload'); + expect(obj[1].val).to.be.equal(false); + expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); + expect(obj[2].val).to.be.equal(4); + expect(obj[2].id).to.equal('javascript.0.test'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getBulk/system.adapter.simple-api.upload,system.adapter.simple-api.0.alive,javascript.0.test', function (error, response, body) { + console.log('getBulk/system.adapter.simple-api.upload,system.adapter.simple-api.0.alive,javascript.0.test => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj[0].val).equal(50); + expect(obj[1].val).equal(false); + expect(obj[2].val).equal(4); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API as User: setBulk(POST-GET-Mix) - must set values', function (done) { + + request({ + uri: 'http://127.0.0.1:18183/setBulk?system.adapter.simple-api.upload=51&system.adapter.simple-api.0.alive=false', + method: 'POST', + body: '' + }, function(error, response, body) { + console.log('setBulk/?system.adapter.simple-api.upload=51&system.adapter.simple-api.0.alive=false => ' + JSON.stringify(body)); + expect(error).to.be.not.ok; + expect(response.statusCode).to.equal(200); + + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(51); + expect(obj[0].id).to.equal('system.adapter.simple-api.upload'); + expect(obj[1].val).to.be.equal(false); + expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getBulk/system.adapter.simple-api.upload,system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('getBulk/system.adapter.simple-api.upload,system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj[0].val).equal(51); + expect(obj[1].val).equal(false); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API as User: setValueFromBody(POST) - must set one value', function (done) { + request({ + uri: 'http://127.0.0.1:18183/setValueFromBody/system.adapter.simple-api.upload', + method: 'POST', + body: '55' + }, function(error, response, body) { + console.log('setValueFromBody/?system.adapter.simple-api.upload => ' + JSON.stringify(body)); + expect(error).to.be.not.ok; + + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(55); + expect(obj[0].id).to.equal('system.adapter.simple-api.upload'); + expect(response.statusCode).to.equal(200); + + request('http://127.0.0.1:18183/getBulk/system.adapter.simple-api.upload', function (error, response, body) { + console.log('getBulk/system.adapter.simple-api.upload => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj[0].val).equal(55); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + after('Test RESTful API as User: Stop js-controller', function (done) { + this.timeout(6000); + setup.stopController(function (normalTerminated) { + console.log('Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); 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(); + }); +}); diff --git a/test/testSsl.js b/test/testSsl.js new file mode 100644 index 0000000..81fe22a --- /dev/null +++ b/test/testSsl.js @@ -0,0 +1,194 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +/*jshint expr: true*/ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); +var request = require('request'); + +var objects = null; +var states = null; + +process.env.NO_PROXY = '127.0.0.1'; +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +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.simple-api.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); + } + }); +} + +describe('Test RESTful API SSL', function() { + before('Test RESTful API SSL: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + setup.adapterStarted = false; + + var brokerStarted = false; + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + config.native.port = 18183; + config.native.auth = true; + config.native.secure = true; + config.native.certPublic = 'defaultPublic'; + config.native.certPrivate = 'defaultPrivate'; + + setup.setAdapterConfig(config.common, config.native); + + setup.startController(function (_objects, _states) { + objects = _objects; + states = _states; + // give some time to start server + setTimeout(function () { + _done(); + }, 2000); + }); + }); + }); + + it('Test adapter: Check if adapter started and create upload datapoint', function (done) { + this.timeout(60000); + checkConnectionOfAdapter(function (res) { + if (res) console.log(res); + expect(res).not.to.be.equal('Cannot check connection'); + done(); + }); + }); + + it('Test RESTful API SSL: get - must return value', function (done) { + request('https://127.0.0.1:18183/get/system.adapter.simple-api.0.alive?user=admin&pass=yunkong2', function (error, response, body) { + console.log('get/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + //{ + // "val" : true, + // "ack" : true, + // "ts" : 1455009717, + // "q" : 0, + // "from" : "system.adapter.simple-api.0", + // "lc" : 1455009717, + // "expire" : 30000, + // "_id" : "system.adapter.simple-api.0.alive", + // "type" : "state", + // "common" : { + // "name" : "simple-api.0.alive", + // "type" : "boolean", + // "role" : "indicator.state" + // }, + // "native" : {} + // + //} + + expect(obj).to.be.ok; + expect(obj.val).to.be.true; + expect(obj.ack).to.be.true; + expect(obj.ts).to.be.ok; + //expect(obj.from).to.equal("system.adapter.simple-api.0"); + expect(obj.type).to.equal("state"); + expect(obj._id).to.equal("system.adapter.simple-api.0.alive"); + expect(obj.common).to.be.ok; + expect(obj.native).to.be.ok; + expect(obj.common.name).to.equal("simple-api.0.alive"); + expect(obj.common.role).to.equal("indicator.state"); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + + it('Test RESTful API SSL: get with no credentials', function (done) { + request('https://127.0.0.1:18183/get/system.adapter.simple-api.0.alive?user=admin&pass=io', function (error, response, body) { + console.log('get/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(response.statusCode).to.be.equal(401); + done(); + }); + }); + + it('Test RESTful API SSL: get with wrong credentials', function (done) { + request('https://127.0.0.1:18183/get/system.adapter.simple-api.0.alive', function (error, response, body) { + console.log('get/system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + expect(response.statusCode).to.be.equal(401); + done(); + }); + }); + + it('Test RESTful API SSL: setBulk(POST) - must set values', function (done) { + + request({ + uri: 'https://127.0.0.1:18183/setBulk?user=admin&pass=yunkong2', + method: 'POST', + body: 'system.adapter.simple-api.upload=50&system.adapter.simple-api.0.alive=false' + }, function(error, response, body) { + console.log('setBulk/?system.adapter.simple-api.upload=50&system.adapter.simple-api.0.alive=false => ' + JSON.stringify(body)); + expect(error).to.be.not.ok; + + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(50); + expect(obj[0].id).to.equal('system.adapter.simple-api.upload'); + expect(obj[1].val).to.be.equal(false); + expect(obj[1].id).to.equal('system.adapter.simple-api.0.alive'); + expect(response.statusCode).to.equal(200); + + request('https://127.0.0.1:18183/getBulk/system.adapter.simple-api.upload,system.adapter.simple-api.0.alive?user=admin&pass=yunkong2', function (error, response, body) { + console.log('getBulk/system.adapter.simple-api.upload,system.adapter.simple-api.0.alive => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj[0].val).equal(50); + expect(obj[1].val).equal(false); + expect(response.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('Test RESTful API SSL: setValueFromBody(POST) - must set values', function (done) { + request({ + uri: 'https://127.0.0.1:18183/setValueFromBody/system.adapter.simple-api.upload?user=admin&pass=yunkong2&', + method: 'POST', + body: '55' + }, function(error, response, body) { + console.log('setValueFromBody/?system.adapter.simple-api.upload => ' + JSON.stringify(body)); + expect(error).to.be.not.ok; + + var obj = JSON.parse(body); + expect(obj).to.be.ok; + expect(obj[0].val).to.be.equal(55); + expect(obj[0].id).to.equal('system.adapter.simple-api.upload'); + expect(response.statusCode).to.equal(200); + + body = ""; + request('https://127.0.0.1:18183/getBulk/system.adapter.simple-api.upload?user=admin&pass=yunkong2', function (error, response, body) { + console.log('getBulk/system.adapter.simple-api.upload => ' + body); + expect(error).to.be.not.ok; + var obj = JSON.parse(body); + expect(obj[0].val).equal(55); + expect(response.statusCode).to.equal(200); + done(); + }); + }); +}); + after('Test RESTful API SSL: Stop js-controller', function (done) { + this.timeout(6000); + setup.stopController(function (normalTerminated) { + console.log('Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +});