jsharmony
Version:
Rapid Application Development (RAD) Platform for Node.js Database Application Development
511 lines (476 loc) • 20.6 kB
JavaScript
/*
Copyright 2017 apHarmony
This file is part of jsHarmony.
jsHarmony is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
jsHarmony is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this package. If not, see <http://www.gnu.org/licenses/>.
*/
var express = require('express');
var bodyParser = require('body-parser');
var compression = require('compression');
var cookieParser = require('cookie-parser');
var path = require('path');
var fs = require('fs');
var http = require('http');
var https = require('https');
var os = require('os');
var WebSocket = require('ws');
var url = require('url');
var _ = require('lodash');
var jsHarmonyRouter = require('./jsHarmonyRouter.js');
var Helper = require('./lib/Helper.js');
var HelperFS = require('./lib/HelperFS.js');
function jsHarmonyServer(serverConfig, jsh){
this.jsh = jsh; //jsHarmony Object
this.app = null; //Express Server
this.running = false;
this.serverConfig = serverConfig||{};
this.servers = [];
if(!('add_default_routes' in serverConfig)) serverConfig.add_default_routes = true;
this.redirectRouter = null; //Express router for redirect HTTP server
/*
serverConfig: {
add_default_routes: true,
http_port:0,
http_port: 0,
http_ip: '0.0.0.0',
https_port: 0,
https_ip: '0.0.0.0',
https_key: 'path/to/file',
https_cert: 'path/to/file',
https_ca: 'path/to/file',
request_timeout: 2*60*1000,
webSockets = [
{
path: '/path', //Path of the WebSocket for "upgrade" HTTP request
server: wsServer, //WebSocket.Server object handling the request - new WebSocket.Server({ noServer: true })
roles: {}, //jsHarmony roles { "DADMIN": "B" } "B" action is required for websocket access
dev: 1 //Enable for users with the "DEV" role, not for users with the "SYSADMIN" role
}
]
}
*/
}
//Initialize Express
jsHarmonyServer.prototype.Init = function(cb){
var _this = this;
if(!_this.serverConfig.webSockets) _this.serverConfig.webSockets = [];
this.app = express();
var app = this.app;
app.jsh = this.jsh;
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use(compression({
filter: function(req, res){
if(res && res.locals && res.locals.noCompression) return false;
return compression.filter(req, res);
}
}));
if(_this.jsh.Config.debug_params && _this.jsh.Config.debug_params.dev_client_js){
app.get('/js/jsHarmony.js', function(req, res){
var fcontent = 'alert("jsHarmony.dev.js not compiled. Please compile using clientcompiler.cmd")';
var fpath = path.join(__dirname, 'public','js','jsHarmony.dev.js');
if(fs.existsSync(fpath)) fcontent = fs.readFileSync(fpath);
HelperFS.outputContent(req, res, fcontent, 'text/css');
});
}
app.use(jsHarmonyRouter.PublicRoot(path.join(_this.jsh.Config.appbasepath, 'public')));
app.use(jsHarmonyRouter.PublicRoot(path.join(__dirname, 'public')));
app.use('/cmsfiles', express.static(path.join(_this.jsh.Config.datadir, 'cmsfiles')));
app.use(function (req, res, next) { req.env = app.get('env'); next(); });
app.use(_this.jsh.Log.express);
app.set('view engine', 'ejs');
if(_this.serverConfig.add_default_routes){
var siteConfig = _this.jsh.Sites['main'];
if(!siteConfig) throw new Error('serverConfig.add_default_routes: Missing jsh.Site "main"');
var router = jsHarmonyRouter(_this.jsh, 'main');
//Set up cookies
if(siteConfig.cookiesalt) app.use('/', cookieParser(siteConfig.cookiesalt, { path: siteConfig.baseurl }));
else app.use('/', cookieParser({ path: siteConfig.baseurl }));
app.use('/', router);
_this.addDefaultRoutes();
}
if(cb) return cb();
};
jsHarmonyServer.prototype.addDefaultRoutes = function () {
var _this = this;
// catch 404 and forward to error handler
_this.app.use(function (req, res, next) {
_this.jsh.Gen404(req, res);
return;
});
/// error handlers
_this.app.use(function (err, req, res, next) {
var errorpage = 'error';
if (req.jshsite && req.jshsite.show_system_errors) errorpage = 'error_debug';
_this.jsh.Log.error(err);
_this.jsh.Log.info(err.stack);
res.status(err.status || 500);
res.render(_this.jsh.getView(req, errorpage, { disable_override: true }), {
message: err.message,
error: err,
});
});
};
jsHarmonyServer.prototype.initDebugLogSocket = function(){
var _this = this;
//Initialize socket for debug log
if(_this.jsh.Config.debug_params.log_socket){
var logServer = new WebSocket.Server({ noServer: true });
_this.serverConfig.webSockets.push({
path: '/_log',
server: logServer,
roles: {},
dev: 1
});
//On New connection
logServer.on('connection', function(ws, req, socket, head){
ws.sources = {};
if(!req._roles || (!('SYSADMIN' in req._roles) && !('DEV' in req._roles))){
ws.terminate();
_this.jsh.Log.error('Potential Hacking Attempt - Unsecure Debug Log Client Connected from '+Helper.GetIP(req));
return;
}
_this.jsh.Log('Debug Log Client Connected from '+Helper.GetIP(req));
ws.isAlive = true;
ws.on('pong', function(){ ws.isAlive = true; });
ws.on('message', function(data){
//Try to parse message
var jmsg = {};
try{
jmsg = JSON.parse(data);
}
catch(ex){} // eslint-disable-line no-empty
//If message has "setSettings", read the "sources" object
if(jmsg && jmsg.setSettings){
var sources = jmsg.setSettings.sources;
if(sources){
//Enable Database option
if(sources.database){
if(!('_orig_db_requests' in _this.jsh.Config.debug_params)) _this.jsh.Config.debug_params._orig_db_requests = _this.jsh.Config.debug_params.db_requests;
_this.jsh.Config.debug_params.db_requests = true;
}
else {
if('_orig_db_requests' in _this.jsh.Config.debug_params) _this.jsh.Config.debug_params.db_requests = _this.jsh.Config.debug_params._orig_db_requests;
delete _this.jsh.Config._orig_db_requests;
}
//Enable Database Raw SQL option
if(sources.database_raw_sql){
if(!('_orig_db_raw_sql' in _this.jsh.Config.debug_params)) _this.jsh.Config.debug_params._orig_db_raw_sql = _this.jsh.Config.debug_params.db_raw_sql;
_this.jsh.Config.debug_params.db_raw_sql = true;
}
else {
if('_orig_db_raw_sql' in _this.jsh.Config.debug_params) _this.jsh.Config.debug_params.db_raw_sql = _this.jsh.Config.debug_params._orig_db_raw_sql;
delete _this.jsh.Config._orig_db_raw_sql;
}
//Enable Authentication option
if(sources.authentication){
if(!('_orig_auth_debug' in _this.jsh.Config.debug_params)) _this.jsh.Config.debug_params._orig_auth_debug = _this.jsh.Config.debug_params.auth_debug;
_this.jsh.Config.debug_params.auth_debug = true;
}
else {
if('_orig_auth_debug' in _this.jsh.Config.debug_params) _this.jsh.Config.debug_params.auth_debug = _this.jsh.Config.debug_params._orig_auth_debug;
delete _this.jsh.Config._orig_auth_debug;
}
ws.sources.database = !!sources.database;
ws.sources.database_raw_sql = !!sources.database_raw_sql;
ws.sources.authentication = !!sources.authentication;
ws.sources.system = !!sources.system;
ws.sources.webserver = !!sources.webserver;
}
}
//If message has "getHistory", return the history
if(jmsg && jmsg.getHistory){
var history = _this.jsh.Log.getHistory(ws.sources);
_.each(history, function(logObject){
ws.send(JSON.stringify(logObject));
});
}
});
});
//Keepalive
setInterval(function(){
logServer.clients.forEach(function(ws){
if(ws.isAlive===false) return ws.terminate();
ws.isAlive = false;
ws.ping(function(){});
});
}, 30000);
//Send logs to client
_this.jsh.Log.on('log', function(msg){
logServer.clients.forEach(function each(ws) {
if (ws.readyState === WebSocket.OPEN) {
if(ws.sources){
if(msg && msg.source && ws.sources[msg.source]){
ws.send(JSON.stringify(msg));
}
}
}
});
});
}
};
//Add handler for WebSocket endpoints
jsHarmonyServer.prototype.addWebSocketHandler = function(server){
var _this = this;
if(!_this.serverConfig.webSockets) return;
server.on('upgrade', function(req, socket, head){
var pathname = url.parse(req.url).pathname;
for(var i=0;i<_this.serverConfig.webSockets.length;i++){
var webSocket = _this.serverConfig.webSockets[i];
if(webSocket.path==pathname){
var initSocket = function(){
_this.jsh.Log(pathname + ' : Websocket connection initialized '+pathname);
webSocket.server.handleUpgrade(req, socket, head, function(ws){ webSocket.server.emit('connection', ws, req, socket, head); });
};
if(webSocket.roles || webSocket.dev){
//Run route to validate authentication
var siteConfig = _this.jsh.Sites['main'];
if(!siteConfig){
_this.jsh.Log.error('WebSocket Authentication requires jsh.Site "main"');
socket.destroy();
return;
}
var router = _this.app;
router.handle(req, {
writeHead: function(txt){ },
setHeader: function(txt){ },
getHeader: function(){ return null; },
send: function(txt){ },
end: function(txt){
//Route returned WEBSOCKET
if(txt=='WEBSOCKET'){
//Validate Roles
if(!Helper.hasModelAction(req, { actions: 'B', roles: webSocket.roles, dev: webSocket.dev }, 'B')) {
_this.jsh.Log.error('Unauthorized access to WebSocket '+webSocket.path+' by '+Helper.GetIP(req));
socket.destroy();
return;
}
initSocket();
return;
}
else{
_this.jsh.Log.error('Unauthorized access to WebSocket '+webSocket.path+' by '+Helper.GetIP(req));
socket.destroy();
return;
}
}
}, function(){
_this.jsh.Log.error('WebSocket Authentication Failed for: '+webSocket.path+' by '+Helper.GetIP(req));
socket.destroy();
return;
});
}
else initSocket();
return;
}
else {
_this.jsh.Log.error('Unhandled WebSocket request: '+pathname);
socket.destroy();
}
}
});
};
//ListenPortParams Object
jsHarmonyServer.ListenPortParams = function(firstPort){
this.firstRun = true;
this.tryNextPort = false;
this.currentPort = firstPort;
};
//Bind port
jsHarmonyServer.prototype.ListenPort = function(server, firstPort, ip, onSuccess, onError, params){
var _this = this;
if(!params){ params = new jsHarmonyServer.ListenPortParams(firstPort); }
if(params.firstRun){
params.firstRun = false;
server.on('listening',onSuccess);
server.on('error',function (err) {
if (err && (err.code == 'EADDRINUSE')){
params.currentPort = params.currentPort + 1;
if(params.tryNextPort) return _this.ListenPort(server, firstPort,ip,onSuccess,onError,params);
}
onError(err);
});
}
if(!params.currentPort){ params.currentPort = 8080; params.tryNextPort = true; }
server.listen(params.currentPort, ip);
};
jsHarmonyServer.prototype.Run = function(cb){
var _this = this;
if(!_this.jsh) throw new Error('jsHarmony is required to run jsHarmonyServer');
if(!_this.app) throw new Error('Router is required to run jsHarmonyServer');
var http_redirect = false;
var http_server = false;
var https_server = false;
var server = null;
_this.initDebugLogSocket();
//this.serverConfig
if(!_this.jsh.Config.frontsalt){
var xlib = (require('./WebConnect.js')).xlib;
_this.jsh.Config.frontsalt = xlib.getSalt(60);
}
if(typeof _this.serverConfig.request_timeout === 'undefined') _this.serverConfig.request_timeout = 2*60*1000;
if(('http_ip' in _this.serverConfig) && !('http_port' in _this.serverConfig)) _this.serverConfig.http_port = 0;
if(('http_port' in _this.serverConfig) && !('http_ip' in _this.serverConfig)) _this.serverConfig.http_ip = '0.0.0.0';
if(('https_ip' in _this.serverConfig) && !('https_port' in _this.serverConfig)) _this.serverConfig.https_port = 0;
if(('https_port' in _this.serverConfig) && !('https_ip' in _this.serverConfig)) _this.serverConfig.https_ip = '0.0.0.0';
var f_cert = null,f_key = null,f_ca = null;
if('https_port' in _this.serverConfig){
if(!('https_cert' in _this.serverConfig)) throw new Error('HTTPS requires a certificate - https_cert (containing all bundled CAs)');
if(!('https_key' in _this.serverConfig)) throw new Error('HTTPS requires a key file - https_key');
f_cert = fs.readFileSync(_this.serverConfig.https_cert);
f_key = fs.readFileSync(_this.serverConfig.https_key);
if(_this.serverConfig.https_ca){
if(_.isArray(_this.serverConfig.https_ca)){
f_ca = [];
_.each(_this.serverConfig.https_ca,function(ca){ f_ca.push(fs.readFileSync(ca)); });
}
else f_ca = fs.readFileSync(_this.serverConfig.https_ca);
}
}
if('https_port' in _this.serverConfig) https_server = true;
if('http_port' in _this.serverConfig){
if(https_server) http_redirect = true;
else http_server = true;
}
if(http_server){
server = http.createServer(_this.app);
_this.servers.push(server);
_this.addWebSocketHandler(server);
server.timeout = _this.serverConfig.request_timeout;
if(server.timeout > 2*60*1000) server.keepAlive = true;
_this.ListenPort(server, _this.serverConfig.http_port, _this.serverConfig.http_ip, function(){
_this.jsh.Log.info(_this.jsh.Config.app_name + ' listening on HTTP port ' + server.address().port);
_this.jsh.Log.info('Log in at '+_this.getURL());
Helper.triggerAsync(_this.jsh.Config.onServerReady, null, [server]);
if (cb) cb([server]);
}, function(err){
_this.jsh.Log.error('\r\n\r\n\r\nCANNOT START SERVER!!!!!!\r\n\r\n');
if (err && (err.code == 'EADDRINUSE')) {
_this.jsh.Log.error('SERVER ALREADY RUNNING ON PORT '+_this.serverConfig.http_port+'\r\n\r\n');
Helper.triggerAsync(_this.jsh.Config.onServerReady);
if(cb) cb();
}
else throw err;
});
}
if(https_server){
var https_options = {
key: f_key,
cert: f_cert
};
if(f_ca) https_options.ca = f_ca;
server = https.createServer(https_options, _this.app);
_this.servers.push(server);
_this.addWebSocketHandler(server);
server.timeout = _this.serverConfig.request_timeout;
if(server.timeout > 2*60*1000) server.keepAlive = true;
var new_http_port = 0;
var new_https_port = 0;
var start_https_server = function(cb_https,servers){
if(!servers) servers = [];
_this.ListenPort(server, _this.serverConfig.https_port, _this.serverConfig.https_ip, function(){
new_https_port = server.address().port;
if(!http_redirect){
_this.jsh.Log.info(_this.jsh.Config.app_name + ' listening on HTTPS port ' + new_https_port);
}
else {
_this.jsh.Log.info(_this.jsh.Config.app_name + ' listening on HTTP/HTTPS ports ' + new_http_port + '/' + new_https_port);
}
_this.jsh.Log.info('Log in at '+_this.getURL());
if(servers.push(server));
Helper.triggerAsync(_this.jsh.Config.onServerReady, null, servers);
if(cb_https) cb_https(servers);
}, function(err){
_this.jsh.Log.error('\r\n\r\n\r\nCANNOT START SERVER!!!!!!\r\n\r\n');
if (err && (err.code == 'EADDRINUSE')) {
_this.jsh.Log.error('SERVER ALREADY RUNNING ON PORT '+_this.serverConfig.https_port+'\r\n\r\n');
Helper.triggerAsync(_this.jsh.Config.onServerReady);
if (cb_https) cb_https();
}
else throw err;
});
};
if(!http_redirect) start_https_server(cb);
else {
var redirect_app = express();
_this.redirectRouter = express.Router();
redirect_app.use(_this.redirectRouter);
redirect_app.get('*', function (req, res) {
var hostname = Helper.GetIP(req);
if(req.headers && req.headers.host){
hostname = (req.headers.host.match(/:/g)) ? req.headers.host.slice(0, req.headers.host.indexOf(':')) : req.headers.host;
}
res.redirect('https://' + hostname + ':' + new_https_port + req.url);
});
var redirect_server = http.createServer(redirect_app);
_this.servers.push(redirect_server);
redirect_server.timeout = _this.serverConfig.request_timeout;
if(redirect_server.timeout > 2*60*1000) redirect_server.keepAlive = true;
_this.ListenPort(redirect_server, _this.serverConfig.http_port, _this.serverConfig.http_ip, function(){
new_http_port = redirect_server.address().port;
start_https_server(cb,[redirect_server]);
}, function(err){
_this.jsh.Log.error('\r\n\r\n\r\nCANNOT START SERVER!!!!!!\r\n\r\n');
if (err && (err.code == 'EADDRINUSE')) {
_this.jsh.Log.error('SERVER ALREADY RUNNING ON PORT '+_this.serverConfig.http_port+'\r\n\r\n');
Helper.triggerAsync(_this.jsh.Config.onServerReady);
if (cb) cb();
}
else throw err;
});
}
}
};
jsHarmonyServer.prototype.getURLFromReq = function(req){
var hostname = undefined;
if(req && req.headers && req.headers.host){
hostname = req.headers.host.toString();
if(hostname.indexOf(':')>=0) hostname = hostname.split(':')[0];
}
return this.getURL(hostname);
};
jsHarmonyServer.prototype.getURL = function(hostname){
var _this = this;
var https_server = false;
var http_server = false;
if('https_port' in _this.serverConfig) https_server = true;
if('http_port' in _this.serverConfig){
if(!https_server) http_server = true; //If https_server is enabled, the http_server is only used for redirects
}
var server_txt = '';
var server_port = '';
var server_scheme = 'https://';
if(http_server){
server_txt = _this.serverConfig.http_ip;
for(let i=0;i<_this.servers.length;i++){
if(_this.servers[i] instanceof http.Server){ server_port = _this.servers[i].address().port; break; }
}
if(!https_server) server_scheme = 'http://';
}
if(https_server){
server_txt = _this.serverConfig.https_ip;
for(let i=0;i<_this.servers.length;i++){
if(_this.servers[i] instanceof https.Server){ server_port = _this.servers[i].address().port; break; }
}
}
if(hostname && server_port) return server_scheme+hostname+':'+server_port;
if(server_txt == '0.0.0.0') server_txt = os.hostname().toLowerCase();
if(server_txt && server_port) return server_scheme+server_txt+':'+server_port;
return '';
};
jsHarmonyServer.prototype.Close = function(cb){
var _this = this;
for(var i=0;i<_this.servers.length;i++) _this.servers[i].close();
_this.servers = [];
_this.running = false;
if(cb) return cb();
};
exports = module.exports = jsHarmonyServer;