1061 lines
43 KiB
JavaScript
1061 lines
43 KiB
JavaScript
/* jshint -W097 */
|
|
/* jshint strict: false */
|
|
/* jslint node: true */
|
|
'use strict';
|
|
|
|
const express = require('express');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const utils = require(__dirname + '/lib/utils'); // Get common adapter utils
|
|
const LE = require(utils.controllerDir + '/lib/letsencrypt.js');
|
|
const mime = require('mime-types');
|
|
let session;// = require('express-session');
|
|
let cookieParser;// = require('cookie-parser');
|
|
let bodyParser;// = require('body-parser');
|
|
let AdapterStore;// = require(__dirname + '/../../lib/session.js')(session);
|
|
let passportSocketIo;// = require(__dirname + "/lib/passport.socketio.js");
|
|
let password;// = require(__dirname + '/../../lib/password.js');
|
|
let passport;// = require('passport');
|
|
let LocalStrategy;// = require('passport-local').Strategy;
|
|
let flash;// = require('connect-flash'); // TODO report error to user
|
|
|
|
let webServer = null;
|
|
let store = null;
|
|
let secret = 'Zgfr56gFe87jJOM'; // Will be generated by first start
|
|
let socketUrl = '';
|
|
let cache = {}; // cached web files
|
|
let ownSocket = false;
|
|
let lang = 'en';
|
|
let extensions = {};
|
|
let bruteForce = {};
|
|
let socketIoFile = null;
|
|
let webPreSettings = {};
|
|
let webByVersion = {};
|
|
let loginPage = null;
|
|
|
|
let adapter = new utils.Adapter({
|
|
name: 'web',
|
|
objectChange: (id, obj) => {
|
|
if (obj && obj.common && obj.common.webExtension && obj.native &&
|
|
(extensions[id.substring('system.adapter.'.length)] ||
|
|
obj.native.webInstance === '*' ||
|
|
obj.native.webInstance === 'adapter.namespace'
|
|
)
|
|
) {
|
|
adapter.setForeignState('system.adapter.' + adapter.namespace + '.alive', false, true, () => {
|
|
process.exit(-100);
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (obj && obj.common && obj.common.webPreSettings) {
|
|
updatePreSettings(obj);
|
|
}
|
|
|
|
// 'system.adapter.'length = 15
|
|
const _id = id.substring(15).replace(/\.\d+$/, '');
|
|
if (obj && obj.common && obj.common.webByVersion) {
|
|
webByVersion[_id] = obj.common.version;
|
|
} else if (webByVersion[_id]) {
|
|
delete webByVersion[_id];
|
|
}
|
|
|
|
if (!ownSocket && id === adapter.config.socketio) {
|
|
if (obj && obj.common && obj.common.enabled && obj.native) {
|
|
socketUrl = ':' + obj.native.port;
|
|
} else {
|
|
socketUrl = '';
|
|
}
|
|
}
|
|
|
|
if (webServer.io) {
|
|
webServer.io.publishAll('objectChange', id, obj);
|
|
}
|
|
|
|
if (webServer.api && adapter.config.auth) {
|
|
webServer.api.objectChange(id, obj);
|
|
}
|
|
|
|
if (id === 'system.config') {
|
|
lang = obj && obj.common && obj.common.language ? obj.common.language : 'en';
|
|
}
|
|
|
|
// inform extensions
|
|
for (let e = 0; e < extensions.length; e++) {
|
|
try {
|
|
if (extensions[e].obj && extensions[e].obj.objectChange) {
|
|
extensions[e].obj.objectChange(id, obj);
|
|
}
|
|
} catch (err) {
|
|
adapter.log.error('Cannot call objectChange for "' + e + '": ' + err);
|
|
}
|
|
}
|
|
},
|
|
stateChange: (id, state) => {
|
|
if (webServer.io) webServer.io.publishAll('stateChange', id, state);
|
|
},
|
|
unload: callback => {
|
|
try {
|
|
adapter.log.info('terminating http' + (webServer.settings.secure ? 's' : '') + ' server on port ' + webServer.settings.port);
|
|
webServer.server.close();
|
|
adapter.log.info('terminated http' + (webServer.settings.secure ? 's' : '') + ' server on port ' + webServer.settings.port);
|
|
|
|
callback();
|
|
} catch (e) {
|
|
callback();
|
|
}
|
|
},
|
|
ready: () => {
|
|
// Generate secret for session manager
|
|
adapter.getForeignObject('system.config', (err, obj) => {
|
|
if (!err && obj) {
|
|
if (!obj.native || !obj.native.secret) {
|
|
obj.native = obj.native || {};
|
|
require('crypto').randomBytes(24, (ex, buf) => {
|
|
secret = buf.toString('hex');
|
|
adapter.extendForeignObject('system.config', {native: {secret: secret}});
|
|
main();
|
|
});
|
|
} else {
|
|
secret = obj.native.secret;
|
|
main();
|
|
}
|
|
} else {
|
|
adapter.logger.error('Cannot find object system.config');
|
|
}
|
|
});
|
|
|
|
// information about connected socket.io adapter
|
|
if (adapter.config.socketio && adapter.config.socketio.match(/^system\.adapter\./)) {
|
|
adapter.getForeignObject(adapter.config.socketio, (err, obj) => {
|
|
if ( obj && obj.common && obj.common.enabled && obj.native) {
|
|
socketUrl = ':' + obj.native.port;
|
|
}
|
|
});
|
|
// Listen for changes
|
|
adapter.subscribeForeignObjects(adapter.config.socketio);
|
|
} else {
|
|
socketUrl = adapter.config.socketio;
|
|
ownSocket = (socketUrl !== 'none');
|
|
}
|
|
|
|
// Read language
|
|
adapter.getForeignObject('system.config', (err, data) => {
|
|
if (data && data.common) lang = data.common.language || 'en';
|
|
});
|
|
}
|
|
});
|
|
function extractPreSetting(obj, attr) {
|
|
const parts = attr.split('.');
|
|
if (parts.length === 1) {
|
|
if ((obj && typeof obj === 'object') || (obj !== null && obj !== undefined)) {
|
|
return obj[attr];
|
|
} else {
|
|
return null;
|
|
}
|
|
} else {
|
|
attr = parts.shift();
|
|
if (obj[attr] && typeof obj[attr] === 'object') {
|
|
return extractPreSetting(obj[attr], parts.join('.'));
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
function updatePreSettings(obj) {
|
|
if (!obj || !obj.common) return;
|
|
if (obj.common.webPreSettings) {
|
|
for (let attr in obj.common.webPreSettings) {
|
|
if (!obj.common.webPreSettings.hasOwnProperty(attr)) continue;
|
|
webPreSettings[obj._id] = webPreSettings[obj._id] || {};
|
|
const _attr = attr.replace(/[^\w0-9]/g, '_');
|
|
webPreSettings[obj._id][_attr] = extractPreSetting(obj, obj.common.webPreSettings[attr]);
|
|
if (typeof webPreSettings[obj._id][_attr] === 'object') {
|
|
webPreSettings[obj._id][_attr] = JSON.stringify(webPreSettings[obj._id][_attr]);
|
|
} else {
|
|
webPreSettings[obj._id][_attr] = webPreSettings[obj._id][_attr].replace(/"/g, '\\"');
|
|
}
|
|
}
|
|
} else if (webPreSettings[obj._id]) {
|
|
delete webPreSettings[obj._id];
|
|
}
|
|
}
|
|
|
|
function getExtensionsAndSettings(callback) {
|
|
adapter.objects.getObjectView('system', 'instance', null, (err, doc) => {
|
|
if (err) {
|
|
if (callback) callback (err, []);
|
|
} else {
|
|
if (doc.rows.length === 0) {
|
|
if (callback) callback (null, []);
|
|
} else {
|
|
let res = [];
|
|
for (let i = 0; i < doc.rows.length; i++) {
|
|
const instance = doc.rows[i].value;
|
|
if (instance && instance.common) {
|
|
if (instance.common.enabled &&
|
|
instance.common.webExtension &&
|
|
(instance.native.webInstance === adapter.namespace || instance.native.webInstance === '*')) {
|
|
res.push(doc.rows[i].value);
|
|
}
|
|
if (instance.common.webPreSettings) {
|
|
updatePreSettings(instance);
|
|
}
|
|
if (instance.common.webByVersion) {
|
|
// 'system.adapter.'length = 15
|
|
const _id = doc.rows[i].value._id.substring(15).replace(/\.\d+$/, '');
|
|
webByVersion[_id] = instance.common.version;
|
|
}
|
|
}
|
|
}
|
|
if (callback) callback (null, res);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function main() {
|
|
getExtensionsAndSettings((err, ext) => {
|
|
if (err) adapter.log.error('Cannot read extensions: ' + err);
|
|
if (ext) {
|
|
for (let e = 0; e < ext.length; e++) {
|
|
if (ext[e] && ext[e].common) {
|
|
const instance = ext[e]._id.substring('system.adapter.'.length);
|
|
const name = instance.split('.')[0];
|
|
|
|
extensions[instance] = {
|
|
path: name + '/' + ext[e].common.webExtension,
|
|
config: ext[e]
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (adapter.config.secure) {
|
|
// Load certificates
|
|
adapter.getCertificates((err, certificates, leConfig) => {
|
|
adapter.config.certificates = certificates;
|
|
adapter.config.leConfig = leConfig;
|
|
webServer = initWebServer(adapter.config);
|
|
});
|
|
} else {
|
|
webServer = initWebServer(adapter.config);
|
|
}
|
|
// monitor extensions and pro keys
|
|
adapter.subscribeForeignObjects('system.adapter.*');
|
|
});
|
|
}
|
|
|
|
function readDirs(dirs, cb, result) {
|
|
result = result || [];
|
|
if (!dirs || !dirs.length) {
|
|
return cb && cb(result);
|
|
}
|
|
const dir = dirs.shift();
|
|
adapter.readDir(dir, '', (err, files) => {
|
|
if (!err && files && files.length) {
|
|
for (let f = 0; f < files.length; f++) {
|
|
if (files[f].file.match(/\.html$/)) {
|
|
result.push(dir + '/' + files[f].file);
|
|
}
|
|
}
|
|
}
|
|
setImmediate(readDirs, dirs, cb, result);
|
|
});
|
|
}
|
|
|
|
let indexHtml;
|
|
|
|
function getLinkVar(_var, obj, attr, link) {
|
|
if (attr === 'protocol') attr = 'secure';
|
|
|
|
if (_var === 'ip') {
|
|
link = link.replace('%' + _var + '%', '$host$');
|
|
} else
|
|
if (_var === 'instance') {
|
|
const instance = obj._id.split('.').pop();
|
|
link = link.replace('%' + _var + '%', instance);
|
|
} else {
|
|
if (obj) {
|
|
if (attr.match(/^native_/)) attr = attr.substring(7);
|
|
|
|
let val = obj.native[attr];
|
|
if (_var === 'bind' && (!val || val === '0.0.0.0')) val = '$host$';
|
|
|
|
if (attr === 'secure') {
|
|
link = link.replace('%' + _var + '%', val ? 'https' : 'http');
|
|
} else {
|
|
if (link.indexOf('%' + _var + '%') === -1) {
|
|
link = link.replace('%native_' + _var + '%', val);
|
|
} else {
|
|
link = link.replace('%' + _var + '%', val);
|
|
}
|
|
}
|
|
} else {
|
|
if (attr === 'secure') {
|
|
link = link.replace('%' + _var + '%', 'http');
|
|
} else {
|
|
if (link.indexOf('%' + _var + '%') === -1) {
|
|
link = link.replace('%native_' + _var + '%', '');
|
|
} else {
|
|
link = link.replace('%' + _var + '%', '');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return link;
|
|
}
|
|
|
|
function resolveLink(link, instanceObj, instancesMap) {
|
|
const vars = link.match(/%(\w+)%/g);
|
|
let _var;
|
|
let v;
|
|
let parts;
|
|
let result;
|
|
|
|
if (vars) {
|
|
// first replace simple patterns
|
|
for (v = vars.length - 1; v >= 0; v--) {
|
|
_var = vars[v];
|
|
_var = _var.replace(/%/g, '');
|
|
|
|
parts = _var.split('_');
|
|
// like "port"
|
|
if (_var.match(/^native_/)) {
|
|
link = getLinkVar(_var, instanceObj, _var, link);
|
|
vars.splice(v, 1);
|
|
} else
|
|
if (parts.length === 1) {
|
|
link = getLinkVar(_var, instanceObj, parts[0], link);
|
|
vars.splice(v, 1);
|
|
} else
|
|
// like "web.0_port"
|
|
if (parts[0].match(/\.[0-9]+$/)) {
|
|
link = getLinkVar(_var, instancesMap['system.adapter.' + parts[0]], parts[1], link);
|
|
vars.splice(v, 1);
|
|
}
|
|
}
|
|
let links = {};
|
|
let instances;
|
|
const adptr = parts[0];
|
|
// process web_port
|
|
for (v = 0; v < vars.length; v++) {
|
|
_var = vars[v];
|
|
_var = _var.replace(/%/g, '');
|
|
if (_var.match(/^native_/)) _var = _var.substring(7);
|
|
|
|
parts = _var.split('_');
|
|
if (!instances) {
|
|
instances = [];
|
|
// TODO !
|
|
for (let inst = 0; inst < 10; inst++) {
|
|
if (that.main.objects['system.adapter.' + adptr + '.' + inst]) {
|
|
instances.push(inst);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < instances.length; i++) {
|
|
links[adptr + '.' + i] = {
|
|
instance: adptr + '.' + i,
|
|
link: getLinkVar(_var, instancesMap['system.adapter.' + adptr + '.' + i], parts[1], links[adptr + '.' + i] ? links[adptr + '.' + i].link : link)
|
|
};
|
|
}
|
|
}
|
|
if (instances) {
|
|
result = [];
|
|
let count = 0;
|
|
let firtsLink = '';
|
|
for (let d in links) {
|
|
if (links.hasOwnProperty(d)) {
|
|
result[links[d].instance] = links[d].link;
|
|
if (!firtsLink) firtsLink = links[d].link;
|
|
count++;
|
|
}
|
|
}
|
|
if (count < 2) {
|
|
link = firtsLink;
|
|
result = null;
|
|
}
|
|
}
|
|
}
|
|
return result || link;
|
|
}
|
|
|
|
function replaceInLink(link, instanceObj, instances) {
|
|
if (typeof link === 'object') {
|
|
const links = JSON.parse(JSON.stringify(link));
|
|
let first;
|
|
for (let v in links) {
|
|
if (links.hasOwnProperty(v)) {
|
|
links[v] = resolveLink(links[v], instanceObj, instances);
|
|
if (!first) first = links[v];
|
|
}
|
|
}
|
|
links.__first = first;
|
|
return links;
|
|
} else {
|
|
return resolveLink(link, instanceObj, instances);
|
|
}
|
|
}
|
|
|
|
function getListOfAllAdapters(callback) {
|
|
try {
|
|
// read all instances
|
|
adapter.objects.getObjectView('system', 'instance', {}, (err, instances) => {
|
|
adapter.objects.getObjectView('system', 'adapter', {}, (err, adapters) => {
|
|
let list = [];
|
|
let a;
|
|
let mapInstance = {};
|
|
for (let r = 0; r < instances.rows.length; r++) {
|
|
mapInstance[instances.rows[r].id] = instances.rows[r].value;
|
|
}
|
|
for (a = 0; a < adapters.rows.length; a++) {
|
|
const obj = adapters.rows[a].value;
|
|
let found = '';
|
|
if (instances && instances.rows) {
|
|
found = '';
|
|
// find if any instance of this adapter is exists and started
|
|
for (let i = 0; i < instances.rows.length; i++) {
|
|
let id = instances.rows[i].id;
|
|
let ids = id.split('.');
|
|
ids.pop();
|
|
id = ids.join('.');
|
|
if (id === obj._id && instances.rows[i].value.common && (instances.rows[i].value.common.enabled || instances.rows[i].value.common.onlyWWW)) {
|
|
found = instances.rows[i].id;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (found) {
|
|
if (obj.common.welcomeScreen || obj.common.welcomeScreenPro) {
|
|
if (obj.common.welcomeScreen) {
|
|
if (obj.common.welcomeScreen instanceof Array) {
|
|
for (let w = 0; w < obj.common.welcomeScreen.length; w++) {
|
|
// temporary disabled
|
|
if (obj.common.welcomeScreen[w].name === 'vis editor') {
|
|
continue;
|
|
}
|
|
if (obj.common.welcomeScreen[w].localLink && typeof obj.common.welcomeScreen[w].localLink === 'boolean') {
|
|
obj.common.welcomeScreen[w].localLink = obj.common.localLink;
|
|
}
|
|
if (obj.common.welcomeScreen[w].localLink) {
|
|
obj.common.welcomeScreen[w].id = found;
|
|
}
|
|
list.push(obj.common.welcomeScreen[w]);
|
|
}
|
|
} else {
|
|
if (obj.common.welcomeScreen.localLink && typeof obj.common.welcomeScreen.localLink === 'boolean') {
|
|
obj.common.welcomeScreen.localLink = obj.common.localLink;
|
|
}
|
|
if (obj.common.welcomeScreen.localLink) {
|
|
obj.common.welcomeScreen.id = found;
|
|
}
|
|
list.push(obj.common.welcomeScreen);
|
|
}
|
|
}
|
|
if (obj.common.welcomeScreenPro) {
|
|
if (obj.common.welcomeScreenPro instanceof Array) {
|
|
for (let ww = 0; ww < obj.common.welcomeScreenPro.length; ww++) {
|
|
let tile = Object.assign({}, obj.common.welcomeScreenPro[ww]);
|
|
tile.pro = true;
|
|
if (tile.localLink && typeof tile.localLink === 'boolean') {
|
|
tile.localLink = obj.common.localLink;
|
|
}
|
|
if (tile.localLink) {
|
|
tile.id = found;
|
|
}
|
|
list.push(tile);
|
|
}
|
|
} else {
|
|
let tile_ = Object.assign({}, obj.common.welcomeScreenPro);
|
|
tile_.pro = true;
|
|
if (tile_.localLink && typeof tile_.localLink === 'boolean') {
|
|
tile_.localLink = obj.common.localLink;
|
|
}
|
|
if (tile_.localLink) {
|
|
tile_.id = found;
|
|
}
|
|
list.push(tile_);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
indexHtml = /*indexHtml || */fs.readFileSync(__dirname + '/www/index.html').toString();
|
|
list.sort((a, b) => {
|
|
if (a.order === undefined && b.order === undefined) {
|
|
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
|
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
|
return 0;
|
|
} else if (a.order === undefined) {
|
|
return -1;
|
|
} else if (b.order === undefined) {
|
|
return 1;
|
|
} else {
|
|
if (a.order > b.order) return 1;
|
|
if (a.order < b.order) return -1;
|
|
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
|
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
// calculate localLinks
|
|
for (let t = 0; t < list.length; t++) {
|
|
if (list[t].localLink) {
|
|
list[t].localLink = resolveLink(list[t].localLink, mapInstance[list[t].id], mapInstance);
|
|
}
|
|
}
|
|
|
|
let text = 'systemLang = "' + lang + '";\n';
|
|
text += 'list = ' + JSON.stringify(list, null, 2) + ';\n';
|
|
|
|
// if login
|
|
text += 'let authEnabled = ' + adapter.config.auth + ';\n';
|
|
|
|
callback(null, indexHtml.replace('// -- PLACE THE LIST HERE --', text));
|
|
});
|
|
});
|
|
} catch (e) {
|
|
callback(e);
|
|
}
|
|
}
|
|
|
|
function getInfoJs(settings) {
|
|
let result = [
|
|
'var socketUrl = "' + socketUrl + '";',
|
|
'var socketSession = "' + '' + '";',
|
|
'window.sysLang = "' + lang + '";',
|
|
'window.socketForceWebSockets = ' + (settings.forceWebSockets ? 'true' : 'false') + ';'
|
|
];
|
|
for (const id in webPreSettings) {
|
|
if (webPreSettings.hasOwnProperty(id) && webPreSettings[id]) {
|
|
for (const attr in webPreSettings[id]) {
|
|
if (webPreSettings[id].hasOwnProperty(attr)) {
|
|
result.push(`window.${attr} = "${webPreSettings[id][attr]}";`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result.join(' ');
|
|
}
|
|
|
|
function prepareLoginTemplate() {
|
|
let def =
|
|
' font: 13px/20px \'Lucida Grande\', Tahoma, Verdana, sans-serif;\n' +
|
|
' color: #404040;\n' +
|
|
' background-color: #0ae;\n' +
|
|
' background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(.5, rgba(255, 255, 255, .2)), color-stop(.5, transparent), to(transparent));\n' +
|
|
' background-image: -webkit-linear-gradient(rgba(255, 255, 255, .2) 50%, transparent 50%, transparent);\n' +
|
|
' background-image: -moz-linear-gradient(rgba(255, 255, 255, .2) 50%, transparent 50%, transparent);\n' +
|
|
' background-image: -ms-linear-gradient(rgba(255, 255, 255, .2) 50%, transparent 50%, transparent);\n' +
|
|
' background-image: -o-linear-gradient(rgba(255, 255, 255, .2) 50%, transparent 50%, transparent);\n' +
|
|
' background-image: linear-gradient(rgba(255, 255, 255, .2) 50%, transparent 50%, transparent);\n' +
|
|
' background-size: 50px 50px;\n'
|
|
;
|
|
let template = fs.readFileSync(__dirname + '/www/login/index.html').toString('utf8');
|
|
if (adapter.config.loginBackgroundColor) {
|
|
def = 'background-color: ' + adapter.config.loginBackgroundColor + ';\n'
|
|
}
|
|
if (adapter.config.loginBackgroundImage) {
|
|
def += ' background-image: url(../' + adapter.namespace + '/login-bg.png);\n';
|
|
}
|
|
return template.replace('background: black;', def);
|
|
}
|
|
|
|
|
|
function initAuth(server, settings) {
|
|
session = require('express-session');
|
|
cookieParser = require('cookie-parser');
|
|
bodyParser = require('body-parser');
|
|
AdapterStore = require(utils.controllerDir + '/lib/session.js')(session, settings.ttl);
|
|
passportSocketIo = require('passport.socketio');
|
|
password = require(utils.controllerDir + '/lib/password.js');
|
|
passport = require('passport');
|
|
LocalStrategy = require('passport-local').Strategy;
|
|
flash = require('connect-flash'); // TODO report error to user
|
|
|
|
store = new AdapterStore({adapter: adapter});
|
|
|
|
passport.use(new LocalStrategy(
|
|
function (username, password, done) {
|
|
if (bruteForce[username] && bruteForce[username].errors > 4) {
|
|
let minutes = (new Date().getTime() - bruteForce[username].time);
|
|
if (bruteForce[username].errors < 7) {
|
|
if ((new Date().getTime() - bruteForce[username].time) < 60000) {
|
|
minutes = 1;
|
|
} else {
|
|
minutes = 0;
|
|
}
|
|
} else
|
|
if (bruteForce[username].errors < 10) {
|
|
if ((new Date().getTime() - bruteForce[username].time) < 180000) {
|
|
minutes = Math.ceil((180000 - minutes) / 60000);
|
|
} else {
|
|
minutes = 0;
|
|
}
|
|
} else
|
|
if (bruteForce[username].errors < 15) {
|
|
if ((new Date().getTime() - bruteForce[username].time) < 600000) {
|
|
minutes = Math.ceil((600000 - minutes) / 60000);
|
|
} else {
|
|
minutes = 0;
|
|
}
|
|
} else
|
|
if ((new Date().getTime() - bruteForce[username].time) < 3600000) {
|
|
minutes = Math.ceil((3600000 - minutes) / 60000);
|
|
} else {
|
|
minutes = 0;
|
|
}
|
|
|
|
if (minutes) {
|
|
return done('Too many errors. Try again in ' + minutes + ' ' + (minutes === 1 ? 'minute' : 'minutes') + '.', false);
|
|
}
|
|
}
|
|
adapter.checkPassword(username, password, (res) => {
|
|
if (!res) {
|
|
bruteForce[username] = bruteForce[username] || {errors: 0};
|
|
bruteForce[username].time = new Date().getTime();
|
|
bruteForce[username].errors++;
|
|
} else if (bruteForce[username]) {
|
|
delete bruteForce[username];
|
|
}
|
|
|
|
if (res) {
|
|
return done(null, username);
|
|
} else {
|
|
return done(null, false);
|
|
}
|
|
});
|
|
}
|
|
));
|
|
passport.serializeUser((user, done) => done(null, user));
|
|
|
|
passport.deserializeUser((user, done) => done(null, user));
|
|
|
|
server.app.use(cookieParser());
|
|
server.app.use(bodyParser.urlencoded({
|
|
extended: true
|
|
}));
|
|
server.app.use(bodyParser.json());
|
|
server.app.use(bodyParser.text());
|
|
server.app.use(session({
|
|
secret: secret,
|
|
saveUninitialized: true,
|
|
resave: true,
|
|
cookie: {maxAge: settings.ttl * 1000},
|
|
store: store
|
|
}));
|
|
server.app.use(passport.initialize());
|
|
server.app.use(passport.session());
|
|
server.app.use(flash());
|
|
}
|
|
//settings: {
|
|
// "port": 8080,
|
|
// "auth": false,
|
|
// "secure": false,
|
|
// "bind": "0.0.0.0", // "::"
|
|
// "cache": false
|
|
//}
|
|
function initWebServer(settings) {
|
|
|
|
let server = {
|
|
app: null,
|
|
server: null,
|
|
io: null,
|
|
settings: settings
|
|
};
|
|
adapter.subscribeForeignObjects('system.config');
|
|
|
|
settings.ttl = parseInt(settings.ttl, 10) || 3600;
|
|
if (!settings.whiteListEnabled && settings.whiteListSettings) delete settings.whiteListSettings;
|
|
|
|
settings.defaultUser = settings.defaultUser || 'system.user.admin';
|
|
if (!settings.defaultUser.match(/^system\.user\./)) settings.defaultUser = 'system.user.' + settings.defaultUser;
|
|
|
|
if (settings.port) {
|
|
if (settings.secure) {
|
|
if (!settings.certificates) {
|
|
return null;
|
|
}
|
|
}
|
|
server.app = express();
|
|
|
|
server.app.disable('x-powered-by');
|
|
|
|
if (settings.auth) {
|
|
|
|
initAuth(server, settings);
|
|
|
|
let autoLogonOrRedirectToLogin = (req, res, next, redirect) => {
|
|
if (!settings.whiteListSettings) {
|
|
if (/\.css(\?.*)?$/.test(req.originalUrl)) {
|
|
return res.status(200).send('');
|
|
} else
|
|
if (/\.js(\?.*)?$/.test(req.originalUrl)) {
|
|
// return always valid js file for js, because if cache is active it leads to errors
|
|
const parts = req.originalUrl.split('/');
|
|
parts.shift();
|
|
const ref = parts.join('/');
|
|
// if request for web/lib, ignore it, because no redirect information
|
|
if (parts[0] === 'lib') return res.status(200).send('');
|
|
return res.status(200).send('document.location="/login/index.html?href=" + encodeURI(location.href.replace(location.origin, ""));');
|
|
} else {
|
|
return res.redirect(redirect);
|
|
}
|
|
}
|
|
let remoteIp = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
|
let whiteListIp = server.io.getWhiteListIpForAddress(remoteIp, settings.whiteListSettings);
|
|
adapter.log.silly('whiteListIp ' + whiteListIp);
|
|
if (!whiteListIp || settings.whiteListSettings[whiteListIp].user === 'auth') {
|
|
if (/\.css(\?.*)?$/.test(req.originalUrl)) {
|
|
return res.status(200).send('');
|
|
} else if (/\.js(\?.*)?$/.test(req.originalUrl)) {
|
|
// return always valid js file for js, because if cache is active it leads to errors
|
|
let parts = req.originalUrl.split('/');
|
|
parts.shift();
|
|
const ref = parts.join('/');
|
|
if (parts[0] === 'lib') return res.status(200).send('');
|
|
return res.status(200).send('document.location="/login/index.html?href=" + encodeURI(location.href.replace(location.origin, ""));');
|
|
} else {
|
|
return res.redirect(redirect);
|
|
}
|
|
}
|
|
req.logIn(settings.whiteListSettings[whiteListIp].user, err => {
|
|
return next(err);
|
|
});
|
|
};
|
|
|
|
server.app.post('/login', (req, res, next) => {
|
|
let redirect = '/';
|
|
let parts;
|
|
if (req.body.origin) {
|
|
parts = req.body.origin.split('=');
|
|
if (parts[1]) redirect = decodeURIComponent(parts[1]);
|
|
}
|
|
if (req.body && req.body.username && settings.addUserName && redirect.indexOf('?') === -1) {
|
|
parts = redirect.split('#');
|
|
parts[0] += '?' + req.body.username;
|
|
redirect = parts.join('#');
|
|
}
|
|
let authenticate = passport.authenticate('local', {
|
|
successRedirect: redirect,
|
|
failureRedirect: '/login/index.html' + req.body.origin + (req.body.origin ? '&error' : '?error'),
|
|
failureFlash: 'Invalid username or password.'
|
|
})(req, res, next);
|
|
});
|
|
|
|
server.app.get('/logout', (req, res) => {
|
|
req.logout();
|
|
res.redirect('/login/index.html');
|
|
});
|
|
|
|
// route middleware to make sure a user is logged in
|
|
server.app.use((req, res, next) => {
|
|
// if cache.manifest got back not 200 it makes an error
|
|
if (req.isAuthenticated() ||
|
|
/web\.\d+\/login-bg\.png(\?.*)?$/.test(req.originalUrl) ||
|
|
/cache\.manifest(\?.*)?$/.test(req.originalUrl) ||
|
|
/^\/login\//.test(req.originalUrl) ||
|
|
/\.ico(\?.*)?$/.test(req.originalUrl)
|
|
) {
|
|
return next();
|
|
}
|
|
autoLogonOrRedirectToLogin(req, res, next, '/login/index.html?href=' + encodeURIComponent(req.originalUrl));
|
|
});
|
|
} else {
|
|
server.app.get('/login', (req, res) => {
|
|
res.redirect('/');
|
|
});
|
|
server.app.get('/logout', (req, res) => {
|
|
res.redirect('/');
|
|
});
|
|
|
|
if (settings.whiteListEnabled) {
|
|
initAuth(server, settings);
|
|
server.app.use((req, res, next) => {
|
|
let remoteIp = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
|
let whiteListIp = server.io.getWhiteListIpForAddress(remoteIp, settings.whiteListSettings);
|
|
adapter.log.silly('whiteListIp ' + whiteListIp);
|
|
if (whiteListIp) {
|
|
req.logIn(settings.whiteListSettings[whiteListIp].user, err => {
|
|
return next(err);
|
|
});
|
|
} else {
|
|
req.logIn(settings.defaultUser, err => {
|
|
return next(err);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Init read from states
|
|
server.app.get('/state/*', (req, res) => {
|
|
try {
|
|
const fileName = req.url.split('/', 3)[2].split('?', 2);
|
|
adapter.getForeignObject(fileName[0], (err, obj) => {
|
|
let contentType = 'text/plain';
|
|
if (obj && obj.common.type === 'file') {
|
|
contentType = mime.lookup(fileName[0]);
|
|
}
|
|
adapter.getBinaryState(fileName[0], {user: req.user ? 'system.user.' + req.user : settings.defaultUser}, (err, obj) => {
|
|
if (!err && obj !== null && obj !== undefined) {
|
|
if (obj && typeof obj === 'object' && obj.val !== undefined && obj.ack !== undefined) {
|
|
res.set('Content-Type', 'application/json');
|
|
} else {
|
|
res.set('Content-Type', contentType || 'text/plain');
|
|
}
|
|
res.status(200).send(obj);
|
|
} else {
|
|
res.status(404).send('404 Not found. File ' + fileName[0] + ' not found');
|
|
}
|
|
});
|
|
});
|
|
} catch (e) {
|
|
res.status(500).send('500. Error' + e);
|
|
}
|
|
});
|
|
|
|
server.app.get('*/_socket/info.js', (req, res) => {
|
|
res.set('Content-Type', 'application/javascript');
|
|
res.status(200).send(getInfoJs(settings));
|
|
});
|
|
|
|
// Enable CORS
|
|
if (settings.socketio) {
|
|
server.app.use((req, res, next) => {
|
|
res.header('Access-Control-Allow-Origin', '*');
|
|
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
|
|
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With, *');
|
|
|
|
// intercept OPTIONS method
|
|
if ('OPTIONS' === req.method) {
|
|
res.status(200).send(200);
|
|
} else {
|
|
next();
|
|
}
|
|
});
|
|
}
|
|
|
|
let appOptions = {};
|
|
if (settings.cache) appOptions.maxAge = 30758400000;
|
|
|
|
server.server = LE.createServer(server.app, settings, settings.certificates, settings.leConfig, adapter.log);
|
|
server.server.__server = server;
|
|
} else {
|
|
adapter.log.error('port missing');
|
|
process.exit(1);
|
|
}
|
|
|
|
if (server.server) {
|
|
settings.port = parseInt(settings.port, 10);
|
|
adapter.getPort(settings.port, port => {
|
|
port = parseInt(port, 10);
|
|
if (port !== settings.port && !settings.findNextPort) {
|
|
adapter.log.error('port ' + settings.port + ' already in use');
|
|
process.exit(1);
|
|
}
|
|
server.server.listen(port, (!settings.bind || settings.bind === '0.0.0.0') ? undefined : settings.bind || undefined);
|
|
adapter.log.info('http' + (settings.secure ? 's' : '') + ' server listening on port ' + port);
|
|
});
|
|
}
|
|
|
|
// activate extensions
|
|
for (let e in extensions) {
|
|
if (!extensions.hasOwnProperty(e)) continue;
|
|
try {
|
|
// for debug purposes try to load file in current directory "/lib/file.js" (elsewise node.js cannot debug it)
|
|
let parts = extensions[e].path.split('/');
|
|
parts.shift();
|
|
let extAPI;
|
|
if (fs.existsSync(__dirname + '/' + parts.join('/'))) {
|
|
extAPI = require(__dirname + '/' + parts.join('/'));
|
|
} else {
|
|
extAPI = require(utils.appName + '.' + extensions[e].path);
|
|
}
|
|
|
|
extensions[e].obj = new extAPI(server.server, {secure: settings.secure, port: settings.port}, adapter, extensions[e].config, server.app);
|
|
adapter.log.info('Connect extension "' + extensions[e].path + '"');
|
|
} catch (err) {
|
|
adapter.log.error('Cannot start extension "' + e + '": ' + err);
|
|
}
|
|
}
|
|
|
|
// Activate integrated simple API
|
|
if (settings.simpleapi) {
|
|
let SimpleAPI = require(utils.appName + '.simple-api/lib/simpleapi.js');
|
|
|
|
server.api = new SimpleAPI(server.server, {secure: settings.secure, port: settings.port}, adapter);
|
|
}
|
|
|
|
// Activate integrated socket
|
|
if (ownSocket) {
|
|
let IOSocket = require(utils.appName + '.socketio/lib/socket.js');
|
|
let socketSettings = JSON.parse(JSON.stringify(settings));
|
|
// Authentication checked by server itself
|
|
socketSettings.auth = settings.auth;
|
|
socketSettings.secret = secret;
|
|
socketSettings.store = store;
|
|
socketSettings.ttl = settings.ttl || 3600;
|
|
socketSettings.forceWebSockets = settings.forceWebSockets || false;
|
|
server.io = new IOSocket(server.server, socketSettings, adapter);
|
|
}
|
|
|
|
if (server.app) {
|
|
// deliver web files from objectDB
|
|
server.app.use('/', (req, res) => {
|
|
|
|
let url = decodeURI(req.url);
|
|
// remove all ../
|
|
// important: Linux does not normalize "\" but fs.readFile accepts it as '/'
|
|
url = path.normalize(url.replace(/\\/g, '/')).replace(/\\/g, '/');
|
|
// remove '////' at start and let only one
|
|
if (url[0] === '/' && url[1] === '/') {
|
|
let i = 2;
|
|
while (url[i] === '/') i++;
|
|
url = url.substring(i - 1);
|
|
}
|
|
if ((url[0] === '.' && url[1] === '.') || (url[0] === '/' && url[1] === '.' && url[2] === '.')) {
|
|
res.status(404).send('Not found');
|
|
return;
|
|
}
|
|
|
|
if (server.api && server.api.checkRequest(url)) {
|
|
server.api.restApi(req, res);
|
|
return;
|
|
}
|
|
|
|
if (url === '/' || url === '/index.html') {
|
|
getListOfAllAdapters((err, data) => {
|
|
if (err) {
|
|
res.status(500).send('500. Error' + e);
|
|
} else {
|
|
res
|
|
.set('Content-Type', 'text/html')
|
|
.status(200)
|
|
.send(data);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// add index.html
|
|
url = url.replace(/\/($|\?|#)/, '/index.html$1');
|
|
|
|
if (url.match(/^\/adapter\//)) {
|
|
// add .admin to adapter name
|
|
url = url.replace(/^\/adapter\/([a-zA-Z0-9-_]+)\//, '/$1.admin/');
|
|
}
|
|
|
|
if (url.match(/^\/lib\//)) {
|
|
url = '/' + adapter.name + url;
|
|
}
|
|
if (url.match(/^\/admin\//)) {
|
|
url = '/' + adapter.name + url;
|
|
}
|
|
url = url.split('/');
|
|
// Skip first /
|
|
url.shift();
|
|
// Get ID
|
|
let id = url.shift();
|
|
let versionPrefix = url[0];
|
|
url = url.join('/');
|
|
let pos = url.indexOf('?');
|
|
let noFileCache;
|
|
if (pos !== -1) {
|
|
url = url.substring(0, pos);
|
|
// disable file cache if request like /vis/files/picture.png?noCache
|
|
noFileCache = true;
|
|
}
|
|
|
|
// get adapter name
|
|
if (webByVersion[id]) {
|
|
if (!versionPrefix.match(/^\d+\.\d+.\d+$/)) {
|
|
// redirect to version
|
|
res.set('location', '/' + id + '/' + webByVersion[id] + '/' + url);
|
|
res.status(301).send();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (settings.cache && cache[id + '/' + url] && !noFileCache) {
|
|
res.contentType(cache[id + '/' + url].mimeType);
|
|
res.status(200).send(cache[id + '/' + url].buffer);
|
|
} else {
|
|
if (id === 'login' && url === 'index.html') {
|
|
loginPage = loginPage || prepareLoginTemplate();
|
|
let buffer = loginPage;
|
|
|
|
if (buffer === null || buffer === undefined) {
|
|
res.contentType('text/html');
|
|
res.status(200).send('File ' + url + ' not found', 404);
|
|
} else {
|
|
// Store file in cache
|
|
if (settings.cache) {
|
|
cache[id + '/' + url] = {buffer: buffer.toString(), mimeType: 'text/html'};
|
|
}
|
|
res.contentType('text/html');
|
|
res.status(200).send(buffer.toString());
|
|
}
|
|
} else {
|
|
// special solution for socket.io
|
|
if (socketIoFile !== false && url.match(/\/socket\.io\.js(\?.*)?$/)) {
|
|
if (socketIoFile) {
|
|
res.contentType('text/javascript');
|
|
res.status(200).send(socketIoFile);
|
|
return
|
|
} else {
|
|
try {
|
|
const dir = require.resolve('socket.io-client');
|
|
const fileDir = path.join(path.dirname(dir), '../dist/');
|
|
if (fs.existsSync(fileDir + 'socket.io.min.js')) {
|
|
socketIoFile = fs.readFileSync(fileDir + 'socket.io.min.js');
|
|
} else {
|
|
socketIoFile = fs.readFileSync(fileDir + 'socket.io.js');
|
|
}
|
|
} catch (e) {
|
|
socketIoFile = false;
|
|
}
|
|
if (socketIoFile) {
|
|
res.contentType('text/javascript');
|
|
res.status(200).send(socketIoFile);
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
adapter.readFile(id, webByVersion[id] ? url.substring(versionPrefix.length + 1) : url , {user: req.user ? 'system.user.' + req.user : settings.defaultUser, noFileCache: noFileCache}, (err, buffer, mimeType) => {
|
|
if (buffer === null || buffer === undefined || err) {
|
|
res.contentType('text/html');
|
|
res.status(404).send('File ' + url + ' not found: ' + err);
|
|
} else {
|
|
mimeType = mimeType || mime.lookup(url) || 'text/javascript';
|
|
|
|
// Store file in cache
|
|
if (settings.cache) {
|
|
cache[id + '/' + url] = {
|
|
buffer: buffer,
|
|
mimeType: mimeType
|
|
};
|
|
}
|
|
|
|
res.contentType(mimeType);
|
|
res.status(200).send(buffer);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (server.server) {
|
|
return server;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|