commit 7c497407df0e21e2f5ed38d4ede92ff599720b29 Author: zhongjin Date: Tue Jul 24 12:21:01 2018 +0800 Initial commit 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..36db19d --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2017-2018 bluefox + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4a47db --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +![Logo](admin/proxy.png) +# yunkong2.proxy +================= + +[![NPM version](http://img.shields.io/npm/v/yunkong2.proxy.svg)](https://www.npmjs.com/package/yunkong2.proxy) +[![Downloads](https://img.shields.io/npm/dm/yunkong2.proxy.svg)](https://www.npmjs.com/package/yunkong2.proxy) +[![Tests](https://travis-ci.org/yunkong2/yunkong2.proxy.svg?branch=master)](https://travis-ci.org/yunkong2/yunkong2.proxy) + +[![NPM](https://nodei.co/npm/yunkong2.proxy.png?downloads=true)](https://nodei.co/npm/yunkong2.proxy/) + +## Usage +Allows to access defined URLs or local files via one web server. + +Specified routes will be available under ```http://ip:8082/proxy.0/context/...```. Of course port, protocol, "proxy.0", can variate depends on settings. + +## Configuration +- Extend WEB adapter: For which web instance will active this proxy. +- Route path: Path for proxy. If "/proxy.0", so the routes will be available under ```http://webIP:8082/proxy.0/...``` +- Error timeout(ms): Minimal interval between retries if the requested resource was unavailable or returned error. + +## Sample settings +| Context | URL | Description | +|----------------|:---------------------------------------------------|:---------------------------------------------------| +| admin/ | http://localhost:8081 | access to admin page | +| router/ | http://192.168.1.1 | access to local router | +| cam/ | http://user:pass@192.168.1.123 | access to webcam (e.g. call http://ip:8082/proxy.0/cam/web/snapshot.jpg) | +| dir/ | /tmp/ | access to local directory "/tmp/" | +| dir/ | tmp/ | access to local directory "/opt/yunkong2/tmp" | +| file.jpg | /tmp/picture.jpg | access to local file "/tmp/picture.jpg" | + +**Not all devices can be accessed via proxy. + +Some devices wants to be located in the root ```http://ip/``` and cannot run under ```http://ip/proxy.0/context/```. + +You can read more about context [here](https://www.npmjs.com/package/http-proxy-middleware#context-matching) + +Additionally the user can define the route path for proxy requests. + +## Changelog +### 1.0.3 (2018-07-14) +* (bluefox) Newer mime version used + +### 1.0.2 (2018-06-30) +* (bluefox) URI was decoded for usage of special chars in password and login + +### 1.0.1 (2018-03-01) +* (bluefox) Fixed error: after 10 timeouts the web cam was never reachable +* (bluefox) Ready for Admin3 + +### 1.0.0 (2017-10-09) +* (bluefox) do not allow the error generation to fast + +### 0.2.0 (2017-03-13) +* (bluefox) fix run-mode + +### 0.0.1 (2017-01-09) +* (bluefox) initial commit \ No newline at end of file diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..06321df --- /dev/null +++ b/admin/index.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + +
+ +

Proxy adapter settings

+ + + + + + + + + + + + + + + + + +
+
  +
+
+ +
+ + + + + + + + + +
ContextURLTimeout
+
+
+
+ + diff --git a/admin/index_m.html b/admin/index_m.html new file mode 100644 index 0000000..12781b3 --- /dev/null +++ b/admin/index_m.html @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+ +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ add +
+ + + + + + + + + +
ContextURLTimeout
+
+
+
+
+
+
+ + diff --git a/admin/proxy.png b/admin/proxy.png new file mode 100644 index 0000000..037ef71 Binary files /dev/null and b/admin/proxy.png differ diff --git a/admin/words.js b/admin/words.js new file mode 100644 index 0000000..aed46fd --- /dev/null +++ b/admin/words.js @@ -0,0 +1,15 @@ +// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n +/*global systemDictionary:true */ +'use strict'; + +systemDictionary = { + "Proxy adapter settings": { "en": "Proxy adapter settings", "de": "Proxy Adapter Einstellungen", "ru": "Настройки Proxy драйвера", "pt": "Configurações do adaptador proxy", "nl": "Proxy-adapterinstellingen", "fr": "Paramètres de l'adaptateur proxy", "it": "Impostazioni dell'adattatore proxy", "es": "Configuración del adaptador proxy", "pl": "Ustawienia adaptera proxy"}, + "Extend WEB adapter:": { "en": "Extend WEB adapter", "de": "Erweitere WEB Adapter", "ru": "Подключить к WEB драйверу", "pt": "Alargar o adaptador WEB", "nl": "Breid WEB-adapter uit", "fr": "Étendre l'adaptateur WEB", "it": "Estendi la scheda WEB", "es": "Extender el adaptador WEB", "pl": "Przedłuż adapter WEB"}, + "all": { "en": "all", "de": "alle", "ru": "все", "pt": "todos", "nl": "alle", "fr": "tout", "it": "tutto", "es": "todo", "pl": "wszystko"}, + "Context": { "en": "Context", "de": "Kontext", "ru": "Контекст", "pt": "Contexto", "nl": "verband", "fr": "Contexte", "it": "contesto", "es": "Contexto", "pl": "Kontekst"}, + "URL": { "en": "URL", "de": "URL", "ru": "URL", "pt": "URL", "nl": "URL", "fr": "URL", "it": "URL", "es": "URL", "pl": "URL"}, + "Route path:": { "en": "Route path", "de": "Route-Weg", "ru": "Путь в URL", "pt": "Caminho da rota", "nl": "Route pad", "fr": "Chemin d'itinéraire", "it": "Percorso percorso", "es": "Ruta de la ruta", "pl": "Ścieżka trasy"}, + "Paths": { "en": "Paths", "de": "Pfade", "ru": "пути", "pt": "Caminhos", "nl": "Paths", "fr": "Chemins", "it": "percorsi", "es": "Caminos", "pl": "Ścieżki"}, + "Error timeout(ms):": { "en": "Error timeout (ms)", "de": "Fehler-Timeout (ms)", "ru": "Тайм-аут ошибки (мс)", "pt": "Tempo limite de erro (ms)", "nl": "Fout-time-out (ms)", "fr": "Délai d'erreur (ms)", "it": "Timeout errore (ms)", "es": "Tiempo de espera de error (ms)", "pl": "Błąd limitu czasu (ms)"}, + "Timeout": { "en": "Timeout", "de": "Auszeit", "ru": "Тайм-аут", "pt": "Tempo limite", "nl": "timeout", "fr": "Timeout", "it": "timeout", "es": "Tiempo de espera", "pl": "Limit czasu"} +}; \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..132f84c --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,32 @@ +version: 'test-{build}' +os: Visual Studio 2013 +environment: + matrix: + - nodejs_version: '4' + - nodejs_version: '6' + - nodejs_version: '8' + - nodejs_version: '10' +platform: + - x86 + - x64 +clone_folder: 'c:\projects\%APPVEYOR_PROJECT_NAME%' +services: + - mssql2014 + - mysql + - postgresql +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 -g npm@3 + - 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 + - ps: Start-Sleep -s 15 + - 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/io-package.json b/io-package.json new file mode 100644 index 0000000..3695a6a --- /dev/null +++ b/io-package.json @@ -0,0 +1,111 @@ +{ + "common": { + "name": "proxy", + "version": "1.0.3", + "news": { + "1.0.3": { + "en": "Newer mime version used", + "de": "Neuere Mime-Version verwendet", + "ru": "Используется новая версия mime", + "pt": "Versão mais recente de mímica usada", + "nl": "Nieuwere mime-versie gebruikt", + "fr": "Nouvelle version mime utilisée", + "it": "È stata utilizzata la versione più recente di mimo", + "es": "Nueva versión mime utilizada", + "pl": "Zastosowano nowszą wersję mime" + }, + "1.0.2": { + "en": "URI was decoded for usage of special chars in password and login", + "de": "URI wurde für die Verwendung spezieller Zeichen in Passwort und Login decodiert", + "ru": "URI был декодирован для использования специальных символов в пароле и логине", + "pt": "URI foi decodificado para uso de caracteres especiais em senha e login", + "nl": "URI is gedecodeerd voor het gebruik van speciale tekens in wachtwoord en aanmelding", + "fr": "URI a été décodé pour l'utilisation de caractères spéciaux dans le mot de passe et la connexion", + "it": "L'URI è stato decodificato per l'utilizzo di caratteri speciali in password e login", + "es": "El URI se decodificó para el uso de caracteres especiales en la contraseña y el inicio de sesión", + "pl": "Identyfikator URI został zdekodowany w celu użycia specjalnych znaków w haśle i logowaniu" + }, + "1.0.1": { + "en": "Fixed error: after 10 timeouts the web cam was never reachable\nReady for Admin3", + "de": "Fehler behoben: Nach 10 Timeouts war die Webcam nie erreichbar\nBereit für Admin3", + "ru": "Исправлена ​​ошибка: после 10 тайм-аутов веб-камера была недоступна\nГотово для Admin3", + "pt": "Erro corrigido: após 10 temporizações, a web cam nunca foi alcançada\nPronto para Admin3", + "nl": "Vaste fout: na 10 time-outs was de webcam nooit bereikbaar\nKlaar voor Admin3", + "fr": "Correction d'une erreur: après 10 timeouts, la webcam n'a jamais été accessible\nPrêt pour Admin3", + "it": "Risolto errore: dopo 10 timeout la web cam non era mai raggiungibile\nPronto per Admin3", + "es": "Error reparado: después de 10 tiempos de espera la cámara web nunca fue alcanzable\nListo para Admin3", + "pl": "Naprawiono błąd: po 10 przekroczeniach czasu kamera internetowa nigdy nie była dostępna\nGotowy na Admin3" + }, + "1.0.0": { + "en": "do not allow the error generation to fast", + "de": "Erlaube nicht die Fehler zu oft zu generieren", + "ru": "Запрет на слишком частые ошибки запросов" + }, + "0.2.0": { + "en": "fix run-mode", + "de": "Korrigiere Run-Modus", + "ru": "Исправлен режим запуска" + }, + "0.1.0": { + "en": "Inital version", + "de": "Erste Version", + "ru": "Первая версия" + } + }, + "title": "proxy", + "desc": { + "en": "This adapter allows to reach other HTTP servers (like WEB CAM) in the same web server", + "de": "Dieser Adapter ermöglicht es, andere HTTP-Server (wie WEB CAM) auf demselben Webserver zu erreichen", + "ru": "Этот адаптер позволяет опрашивать другие HTTP-серверы (например, WEB CAM) на одном и том же веб-сервере", + "pt": "Este adaptador permite alcançar outros servidores HTTP (como WEB CAM) no mesmo servidor web", + "nl": "Met deze adapter kunnen andere HTTP-servers (zoals WEB CAM) op dezelfde webserver worden bereikt", + "fr": "Cet adaptateur permet d'atteindre d'autres serveurs HTTP (comme WEB CAM) sur le même serveur web", + "it": "Questo adattatore consente di raggiungere altri server HTTP (come WEB CAM) nello stesso server web", + "es": "Este adaptador permite llegar a otros servidores HTTP (como WEB CAM) en el mismo servidor web", + "pl": "Ten adapter umożliwia dotarcie do innych serwerów HTTP (takich jak WEB CAM) na tym samym serwerze internetowym" + }, + "authors": [ + "bluefox " + ], + "license": "MIT", + "platform": "Javascript/Node.js", + "mode": "extension", + "loglevel": "info", + "icon": "proxy.png", + "webExtension": "lib/proxy.js", + "readme": "https://git.spacen.net/yunkong2/yunkong2.proxy/blob/master/README.md", + "keywords": [ + "web", + "proxy", + "communication" + ], + "materialize": true, + "enabled": true, + "extIcon": "https://git.spacen.net/yunkong2/yunkong2.proxy/master/admin/proxy.png", + "type": "network", + "dependencies": [ + { + "js-controller": ">=0.15.0" + } + ] + }, + "native": { + "route": "", + "webInstance": "", + "errorTimeout": 10000, + "rules": [ + { + "regex": "sonos/", + "url": "http://localhost:8083" + }, + { + "regex": "api/", + "url": "http://localhost:8084" + }, + { + "regex": "node-red/", + "url": "http://localhost:1880" + } + ] + } +} \ No newline at end of file diff --git a/lib/proxy.js b/lib/proxy.js new file mode 100644 index 0000000..5e3fe65 --- /dev/null +++ b/lib/proxy.js @@ -0,0 +1,212 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +/*jshint -W061 */ +'use strict'; + +/** + * Proxy 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 Proxy(server, webSettings, adapter, instanceSettings, app) { + if (!(this instanceof Proxy)) return new Proxy(server, webSettings, adapter, instanceSettings, app); + + 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.request = {}; + var that = this; + var proxy; + var path; + var fs; + + this.config.errorTimeout = parseInt(this.config.errorTimeout, 10) || 10000; + if (this.config.errorTimeout < 1000) this.config.errorTimeout = 1000; + + this.config.route = this.config.route || (that.namespace + '/'); + var mime = require('mime'); + + this.interval = setInterval(function () { + var now = Date.now(); + for (var e = 0; e < that.config.rules.length; e++) { + var rule = that.config.rules[e]; + if (!rule.request) continue; + + for (var u = rule.request.length - 1; u >= 0; u--) { + if (rule.request[u].res.finished) { + rule.request.splice(u, 1); + } else + if (now - rule.request[u].ts > rule.timeout) { + rule.lastError = new Date().getTime(); + rule.lastErrorText = 'timeout'; + + adapter.log.error('[proxy] Cannot get "' + rule.request[u].req.url + '": timeout'); + rule.request.splice(u, 1); + } + } + } + }, 10000); + + function finishReq(rule, req, res) { + if (rule.request) { + var entry = rule.request.find(function (e) { + return e.res === res; + }); + if (!entry) { + adapter.log.error('Request "' + req.url + '" not found in requests'); + } else { + var ppos = rule.request.indexOf(entry); + rule.request.splice(ppos, 1); + } + } else { + adapter.log.error('URL "' + url + '" not found in requests'); + } + } + + function oneRule(rule) { + adapter.log.info('Install extension on /' + that.config.route + rule.regex); + + rule.timeout = parseInt(rule.timeout, 10) || that.config.errorTimeout; + + if (rule.url.match(/^https?:\/\//)) { + proxy = proxy || require('http-proxy-middleware'); + var options = { + target: rule.url, + ws: true, + secure: false, + changeOrigin: false, + proxyTimeout: rule.timeout, + xfwd: true, + onError: function (err, req, res/* , url*/) { + rule.lastError = new Date().getTime(); + rule.lastErrorText = err.toString(); + + adapter.log.error('[proxy] Cannot get "' + rule.url + '": ' + err); + if (!res.finished) { + try { + if (typeof res.status === 'function') { + res.status(500).send(err); + } else if (typeof res.send === 'function') { + res.send(err); + } else { + adapter.log.error('[proxy] Cannot response'); + } + } catch (e) { + adapter.log.error('[proxy] Cannot response: ' + e); + } + } + + finishReq(rule, req, res); + }, + onProxyReq: function (req /* , origReq, res, options */) { + adapter.log.debug(req.method + ': ' + rule.url + req.path); + }, + onProxyRes: function (req, reqOrig, res) { + finishReq(rule, reqOrig, res); + rule.lastError = 0; + rule.lastErrorText = ''; + adapter.log.debug('[proxy] Response for ' + reqOrig.url + ': ' + req.statusCode + '(' + req.statusMessage + ')'); + }, + /*onProxyReqWs: function () { + console.log('onProxyReqWs'); + }, + + onOpen: function () { + console.log('onOpen'); + }, + onClose: function () { + console.log('onClose'); + },*/ + pathRewrite: {} + }; + var m = rule.url.match(/^https?:\/\/(.+)@/); + if (m && m[1] && m[1].indexOf(':') !== -1) { + rule.url = rule.url.replace(m[1] + '@', ''); + options.auth = decodeURIComponent(m[1]); + } + + options.pathRewrite['^/' + that.config.route + rule.regex] = '/'; + + rule.handler = proxy(options); + that.app.use('/' + that.config.route + rule.regex, function (req, res, next) { + var now = Date.now(); + if (rule.lastError && ((now - rule.lastError) < that.config.errorTimeout)) { + res.status(404).send('[proxy] Cannot read file: ' + rule.lastErrorText); + return + } + + if (rule.request && rule.request.length > 10) { + res.status(500).send('[proxy] too many parallel requests for "' + req.url + '"!'); + adapter.log.warn('[proxy] too many parallel requests for "' + req.url + '"'); + return; + } + rule.request = rule.request || []; + rule.request.push({req: req, res: res, ts: now}); + + rule.handler(req, res, next); + }.bind(this)); + } else { + path = path || require('path'); + fs = fs || require('fs'); + rule.url = rule.url.replace(/\\/g, '/'); + if (rule.url[0] !== '/' && !rule.url.match(/^[A-Za-z]:/)) { + rule.url = path.normalize(__dirname + '/../../' + rule.url); + } + // file handler + that.app.use('/' + that.config.route + rule.regex, function (req, res, next) { + var fileName = rule.url + req.url; + if (fs.existsSync(fileName)) { + var stat = fs.statSync(fileName); + if (stat.isDirectory()) { + var dirs = fs.readdirSync(fileName); + + var text = ''; + dirs.sort(); + for (var d = 0; d < dirs.length; d++) { + text += (text ? '
' : '') + '' + dirs[d] + ''; + } + res.set('Content-Type', 'text/html'); + res.status(200).send('' + fileName + '' + text + ''); + } else { + var data; + try { + data = fs.readFileSync(fileName); + } catch (e) { + res.status(500).send('[proxy] Cannot read file: ' + e); + return; + } + res.contentType(mime.lookup(path.extname(fileName).substring(1))); + res.status(200).send(data); + } + } else { + res.status(404).send('[proxy] File "' + fileName +'" not found.'); + } + }); + } + } + + this.destroy = function () { + if (this.interval) { + clearInterval(this.interval); + this.interval = null; + } + }; + + var __construct = (function () { + for (var e = 0; e < this.config.rules.length; e++) { + oneRule(this.config.rules[e]); + } + }.bind(this))(); +} + +module.exports = Proxy; diff --git a/package.json b/package.json new file mode 100644 index 0000000..0da5f39 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "yunkong2.proxy", + "version": "1.0.3", + "description": "Proxy for WEB server.", + "author": { + "name": "bluefox", + "email": "dogafox@gmail.com" + }, + "homepage": "https://git.spacen.net/yunkong2/yunkong2.proxy", + "keywords": [ + "yunkong2", + "web", + "proxy" + ], + "repository": { + "type": "git", + "url": "https://git.spacen.net/yunkong2/yunkong2.proxy" + }, + "dependencies": { + "http-proxy-middleware": "^0.18.0", + "mime": "^2.3.1" + }, + "devDependencies": { + "gulp": "^3.9.1", + "mocha": "^5.2.0", + "chai": "^4.1.2", + "request": "^2.87.0" + }, + "bugs": { + "url": "https://git.spacen.net/yunkong2/yunkong2.proxy/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/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/testWebExtension.js b/test/testWebExtension.js new file mode 100644 index 0000000..6640c90 --- /dev/null +++ b/test/testWebExtension.js @@ -0,0 +1,30 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: 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) { + var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json'); + var ioPackage = JSON.parse(fileContentIOPackage); + + var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json'); + var npmPackage = JSON.parse(fileContentNPMPackage); + + expect(ioPackage).to.be.an('object'); + expect(npmPackage).to.be.an('object'); + + expect(ioPackage.common.version).to.exist; + expect(npmPackage.version).to.exist; + + if (!expect(ioPackage.common.version).to.be.equal(npmPackage.version)) { + console.log('ERROR: Version numbers in package.json and io-package.json differ!!'); + } + + 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!'); + } + + done(); + }); +});