Initial commit

This commit is contained in:
zhongjin 2018-07-24 12:21:01 +08:00
commit 7c497407df
17 changed files with 2061 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules
.idea
tmp
admin/i18n/flat.txt
admin/i18n/*/flat.txt
iob_npm.done
package-lock.json

9
.npmignore Normal file
View File

@ -0,0 +1,9 @@
gulpfile.js
tasks
tmp
test
.travis.yml
appveyor.yml
admin/i18n
iob_npm.done
package-lock.json

23
.travis.yml Normal file
View File

@ -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

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2017-2018 bluefox <dogafox@gmail.com>
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.

57
README.md Normal file
View File

@ -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

124
admin/index.html Normal file
View File

@ -0,0 +1,124 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="../../lib/css/themes/jquery-ui/redmond/jquery-ui.min.css"/>
<script type="text/javascript" src="../../lib/js/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
<script type="text/javascript" src="../../lib/js/jquery-ui-1.10.3.full.min.js"></script>
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
<script type="text/javascript" src="../../js/translate.js"></script>
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
<script type="text/javascript" src="words.js"></script>
<style>
.number {
width: 70px
}
.table-values th {
background: #686868;
color: #FFF;
font-weight: bold;
}
.table-values tr:nth-child(even) {
background: #d0d0d0;
}
</style>
<script type="text/javascript">
function load(settings, onChange) {
if (!settings) return;
if (!settings.route) settings.route = 'proxy.' + instance + '/';
if (!settings.errorTimeout) settings.errorTimeout = 10000;
$('.value').each(function () {
var key = $(this).attr('id');
var $key = $('#' + key + '.value');
if ($key.attr('type') === 'checkbox') {
$key.prop('checked', settings[key]).change(function() {
onChange();
});
} else {
$key.val(settings[key]).change(function() {
onChange();
}).keyup(function() {
onChange();
});
}
});
getExtendableInstances(function (result) {
if (result) {
var text = '';
for (var r = 0; r < result.length; r++) {
var name = result[r]._id.substring('system.adapter.'.length);
text += '<option value="' + name + '" ' + (settings.webInstance === name ? 'selected' : '') + '>' + name + '</option>';
}
$('#webInstance').append(text);
}
});
onChange(false);
values2table('values', settings.rules, onChange);
}
function save(callback) {
var obj = {};
$('.value').each(function () {
var $this = $(this);
if ($this.attr('type') === 'checkbox') {
obj[$this.attr('id')] = $this.prop('checked');
} else {
obj[$this.attr('id')] = $this.val();
}
});
if (obj.route === 'proxy.' + instance + '/') obj.route = '';
obj.rules = table2values();
callback(obj);
}
</script>
</head>
<body>
<div id="adapter-container">
<table><tr><td><img src="proxy.png"></td><td><h3 class="translate">Proxy adapter settings</h3></td></tr></table>
<table style="width: 100%;">
<tr>
<td style="width: 200px;"><label for="webInstance" class="translate">Extend WEB adapter:</label></td>
<td><select class="value" id="webInstance">
<option value="*" class="translate">all</option>
</select>
</td>
</tr>
<tr>
<td style="width: 200px;"><label for="route" class="translate">Route path:</label></td>
<td><input class="value" id="route"/></td>
</tr>
<tr>
<td style="width: 200px;"><label for="errorTimeout" class="translate">Error timeout(ms):</label></td>
<td><input class="value" id="errorTimeout" type="number" min="1000"/></td>
</tr>
<tr>
<td colspan="2">&nbsp;
</td>
</tr>
</table>
<div id="values" style="width: 100%; height: calc(100% - 245px)">
<button class="table-button-add" style="margin-left: 10px; width: 1.5em; height: 1.5em"></button>
<div style="width: 100%; height: calc(100% - 30px); overflow: auto;">
<table class="table-values" style="width: 100%;">
<thead>
<tr>
<th data-name="regex" style="width: 30%" class="translate">Context</th>
<th data-name="url" class="translate">URL</th>
<th data-name="timeout" style="width: 70px" data-style="width: 70px" data-type="number" class="translate">Timeout</th>
<th data-buttons="delete up down" style="width: 70px"></th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</body>
</html>

162
admin/index_m.html Normal file
View File

@ -0,0 +1,162 @@
<html>
<head>
<!-- Materialze style -->
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
<link rel="stylesheet" type="text/css" href="../../lib/css/materialize.css">
<script type="text/javascript" src="../../lib/js/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
<script type="text/javascript" src="../../js/translate.js"></script>
<script type="text/javascript" src="../../lib/js/materialize.js"></script>
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
<script type="text/javascript" src="words.js"></script>
<script type="text/javascript">
function load(settings, onChange) {
if (!settings) return;
if (!settings.route) settings.route = 'proxy.' + instance + '/';
if (!settings.errorTimeout) settings.errorTimeout = 10000;
$('.value').each(function () {
var key = $(this).attr('id');
var $key = $('#' + key + '.value');
if ($key.attr('type') === 'checkbox') {
$key.prop('checked', settings[key]).change(function() {
onChange();
});
} else {
$key.val(settings[key]).on('change', function() {
onChange();
}).on('keyup', function() {
onChange();
});
}
});
getExtendableInstances(function (result) {
if (result) {
var text = '';
for (var r = 0; r < result.length; r++) {
var name = result[r]._id.substring('system.adapter.'.length);
text += '<option value="' + name + '" ' + (settings.webInstance === name ? 'selected' : '') + '>' + name + '</option>';
}
$('#webInstance').append(text).select();
}
});
onChange(false);
values2table('values', settings.rules, onChange);
}
function save(callback) {
var obj = {};
$('.value').each(function () {
var $this = $(this);
if ($this.attr('type') === 'checkbox') {
obj[$this.attr('id')] = $this.prop('checked');
} else {
obj[$this.attr('id')] = $this.val();
}
});
if (obj.route === 'proxy.' + instance + '/') obj.route = '';
obj.rules = table2values();
callback(obj);
}
</script>
<style>
#values {
width: 100%;
height: 100%;
}
.table-values-div {
width: 100%;
height: calc(100% - 30px);
overflow: auto;
}
.table-values {
width: 100%;
}
.adapter-container>.row {
margin-bottom: 0;
}
#tab-paths {
height: calc(100% - 50px);
overflow: hidden;
}
#tab-paths>.row {
height: 100%;
}
.m .select-wrapper+label {
top: 100%;
}
.m td, .m th {
padding: 2px 4px;
}
.m td input {
height: 2rem !important;
}
.table-values-div {
height: calc(100% - 40px);
}
</style>
</head>
<body>
<div class="adapter-container m">
<div class="row">
<div class="col s12">
<ul class="tabs">
<li class="tab col s4"><a href="#tab-main" class="translate active">Main settings</a></li>
<li class="tab col s4"><a href="#tab-paths" class="translate">Paths</a></li>
</ul>
</div>
<div id="tab-main" class="col s12 page">
<div class="row">
<div class="col s12 m4 l2">
<img src="proxy.png" class="logo">
</div>
</div>
<div class="row">
<div class="col s12 m4">
<select class="value" id="webInstance">
<option value="*" class="translate">all</option>
</select>
<label for="webInstance" class="translate">Extend WEB adapter:</label>
</div>
</div>
<div class="row">
<div class="col s12 m4">
<input class="value" id="route"/>
<label for="route" class="translate">Route path:</label>
</div>
</div>
<div class="row">
<div class="col s12 m4">
<input class="value" id="errorTimeout" type="number" min="1000"/>
<label for="errorTimeout" class="translate">Error timeout(ms):</label>
</div>
</div>
</div>
<div id="tab-paths" class="col s12 page">
<div class="row">
<div class="col s12" id="values">
<a class="btn-floating waves-effect waves-light blue table-button-add"><i class="material-icons">add</i></a>
<div class="table-values-div">
<table class="table-values">
<thead>
<tr>
<th data-name="regex" style="width: 30%" class="translate">Context</th>
<th data-name="url" class="translate">URL</th>
<th data-name="timeout" style="width: 70px" data-style="width: 70px" data-type="number" class="translate">Timeout</th>
<th data-buttons="delete up down" style="width: 70px"></th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

BIN
admin/proxy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

15
admin/words.js Normal file
View File

@ -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"}
};

32
appveyor.yml Normal file
View File

@ -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'

401
gulpfile.js Normal file
View File

@ -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, '</i>').replace(/<\/ b>/g, '</b>').replace(/<\/ span>/g, '</span>').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']);

111
io-package.json Normal file
View File

@ -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 <dogafox@gmail.com>"
],
"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"
}
]
}
}

212
lib/proxy.js Normal file
View File

@ -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 <pre><code>{secure: settings.secure, port: settings.port}</code></pre>
* @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 ? '<br>' : '') + '<a href="./' + dirs[d] + '">' + dirs[d] + '</a>';
}
res.set('Content-Type', 'text/html');
res.status(200).send('<html><head><title>' + fileName + '</title></head><body>' + text + '</body>');
} 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;

37
package.json Normal file
View File

@ -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"
}

728
test/lib/setup.js Normal file
View File

@ -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;
}

91
test/testPackageFiles.js Normal file
View File

@ -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 <my@email.com>');
}
}
else {
expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
}
}
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();
});
});

30
test/testWebExtension.js Normal file
View File

@ -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();
});
});