UNPKG

24.7 kBJavaScriptView Raw
1#!/usr/bin/env node
2//
3// -- Zetta Toolkit - Application Framework
4//
5// Copyright (c) 2011-2014 ASPECTRON Inc.
6// All Rights Reserved.
7//
8// Permission is hereby granted, free of charge, to any person obtaining a copy
9// of this software and associated documentation files (the "Software"), to deal
10// in the Software without restriction, including without limitation the rights
11// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12// copies of the Software, and to permit persons to whom the Software is
13// furnished to do so, subject to the following conditions:
14//
15// The above copyright notice and this permission notice shall be included in
16// all copies or substantial portions of the Software.
17//
18// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24// THE SOFTWARE.
25//
26
27var _ = require('underscore');
28var fs = require('fs');
29var os = require('os');
30var util = require('util');
31var events = require('events');
32
33var http = require('http');
34var https = require('https');
35var connect = require('connect');
36var express = require('express');
37var socketio = require("socket.io");
38var path = require('path');
39var UUID = require('node-uuid');
40
41var zutils = require('zetta-utils');
42var zstats = require('zetta-stats');
43var zrpc = require('zetta-rpc');
44var zlogin = require('zetta-login');
45var exec = require('child_process').exec;
46var getmac = require('getmac');
47var nodemailer = require('nodemailer');
48var mongo = require('mongodb');
49var os = require('os');
50var child_process = require('child_process');
51var Translator = require('zetta-translator');
52
53var _cl = console.log;
54console.log = function() {
55 var args = Array.prototype.slice.call(arguments, 0);
56 args.unshift(zutils.tsString()+' ');
57 return _cl.apply(console, args);
58}
59
60function getConfig(name) {
61
62 var filename = name+'.conf';
63 var host_filename = name+'.'+os.hostname()+'.conf';
64 var local_filename = name+'.local.conf';
65
66 var data = [ ]; // undefined;
67
68 fs.existsSync(filename) && data.push(fs.readFileSync(filename) || null);
69 fs.existsSync(host_filename) && data.push(fs.readFileSync(host_filename) || null);
70 fs.existsSync(local_filename) && data.push(fs.readFileSync(local_filename) || null);
71
72 if(!data[0] && !data[1])
73 throw new Error("Unable to read config file:"+(filename+'').magenta.bold)
74 function merge(dst, src) {
75 _.each(src, function(v, k) {
76 if(_.isArray(v)) { dst[k] = [ ]; merge(dst[k], v); }
77 // if(_.isArray(v)) { if(!_.isArray(dst[k])) dst[k] = [ ]; merge(dst[k], v); }
78 else if(_.isObject(v)) { if(!dst[k]) dst[k] = { }; merge(dst[k], v); }
79 else { if(_.isArray(src)) dst.push(v); else dst[k] = v; }
80 })
81 }
82
83 var o = { }
84 _.each(data, function(conf) {
85 if(!conf || !conf.toString('utf-8').length)
86 return;
87 var layer = eval('('+conf.toString('utf-8')+')');
88 merge(o, layer);
89 })
90
91 return o;
92}
93
94function readJSON(filename) {
95 if(!fs.existsSync(filename))
96 return undefined;
97 var text = fs.readFileSync(filename, { encoding : 'utf-8' });
98 if(!text)
99 return undefined;
100 try {
101 return JSON.parse(text);
102 } catch(ex) {
103 console.log("Error parsing file:",filename);
104 console.log(ex);
105 console.log('Offending content follows:',text);
106 }
107 return undefined;
108}
109
110function writeJSON(filename, data) {
111 fs.writeFileSync(filename, JSON.stringify(data, null, '\t'));
112}
113
114function Application(appFolder, appConfig) {
115 var self = this;
116 events.EventEmitter.call(this);
117
118 self.appFolder = appFolder;
119
120 self.pkg = readJSON(path.join(appFolder,'package.json'));
121 if(!self.pkg)
122 throw new Error("Application Unable to read package.json");
123
124 if(!self.pkg.name)
125 throw new Error("package.json must contain module 'name' field");
126
127 self.getConfig = function(name) { return getConfig(path.join(appFolder,'config', name)) }
128 self.readJSON = readJSON;
129 self.writeJSON = writeJSON;
130
131 self.config = self.getConfig(self.pkg.name);
132
133 self.settings = { }
134
135 http.globalAgent.maxSockets = self.config.maxHttpSockets || 1024; // 1024;
136 https.globalAgent.maxSockets = self.config.maxHttpSockets || 1024;
137 if(process.platform != 'win32' && self.config.maxSockets) {
138 if(fs.existsSync('node_modules/posix')) {
139 try { require('posix').setrlimit('nofile', self.config.maxSockets); } catch(ex) {
140 console.error(ex.stack);
141 }
142 }
143 else
144 console.log("WARNING - Please install POSIX module (npm install posix)".red.bold);
145 }
146
147 self.pingDataObject = { }
148
149 // ---
150
151 self.restoreDefaultSettings = function(name, force) {
152 var filename = path.join(self.appFolder,'config', name+'.settings');
153 if(!fs.existsSync(filename)) {
154 self.settings = { }
155 return;
156 }
157 var data = fs.readFileSync(filename);
158 self.settings = eval('('+data.toString('utf-8')+')');
159 }
160
161
162 self.restoreSettings = function(name) {
163 self.restoreDefaultSettings(name);
164
165 var host_filename = path.join(self.appFolder,'config', name+'.'+os.hostname().toLowerCase()+'.settings');
166 if(!fs.existsSync(host_filename))
167 return;
168 var data = fs.readFileSync(host_filename);
169 var settings = eval('('+data.toString('utf-8')+')');
170 _.each(settings, function(o, key) {
171 if(self.settings[key])
172 self.settings[key].value = o.value;
173 })
174 }
175
176 self.storeSettings = function(name) {
177 var host_filename = path.join(self.appFolder,'config', name+'.'+os.hostname().toLowerCase()+'.settings');
178 fs.writeFileSync(host_filename, JSON.stringify(self.settings, null, '\t'));
179 }
180
181
182 // ---
183
184 self.initTranslator = function(callback) {
185 if(self.config.translator) {
186 var options = {
187 storagePath: path.join(appFolder,'config'),
188 rootFolderPath: appFolder
189 };
190 options = _.extend(self.config.translator, options);
191
192 self.translator = new Translator(options, function() {
193 self.translator.separateEditor();
194 });
195 }
196
197 callback();
198 }
199
200 self.initCertificates = function(callback) {
201 if(self.verbose)
202 console.log('zetta-app: loading certificates from ',appFolder+'/'+self.config.certificates);
203 if(self.certificates)
204 callback && callback();
205
206 self.certificates = {
207 key: fs.readFileSync(path.join(appFolder,self.config.certificates)+'.key').toString(),
208 cert: fs.readFileSync(path.join(appFolder,self.config.certificates)+'.crt').toString(),
209 ca: [ ]
210 }
211
212 /* var cert = [ ]
213 var chain = fs.readFileSync(__dirname + '/certificates/gd_bundle-g2.crt').toString().split('\n');
214 _.each(chain, function(line) {
215 cert.push(line);
216 if(line.match('/-END CERTIFICATE-/')) {
217 certificates.ca.push(cert.join('\n'));
218 cert = [ ]
219 }
220 })
221 */
222
223 callback && callback();
224 }
225
226
227 self.initMonitoringInterfaces = function(callback) {
228 self.stats = new zstats.StatsD(self.config.statsd, self.uuid, self.pkg.name);
229// self.stats = new zstats.StatsD(self.config.statsd, self.uuid, self.config.application);
230 self.profiler = new zstats.Profiler(self.stats);
231 self.monitor = new zstats.Monitor(self.stats, self.config.monitor);
232
233 callback();
234 }
235
236/* var databaseConfig = [
237 {
238 config: 'main',
239 collections: [
240 {collection: 'resources', indexes: 'owner'},
241 {collection: 'users', indexes: 'email->unique|google.id|facebook.id'}
242 ]
243 }
244 ];
245*/
246
247 self.initMailer = function(callback) {
248
249 var pickupFolder = path.join(self.appFolder,"mailer");
250
251 if(self.config.mailer.pickup) {
252 self.mailer = nodemailer.createTransport("PICKUP", {
253 directory: pickupFolder
254 })
255 }
256 else
257 {
258 self.mailer = nodemailer.createTransport("SMTP", {
259 service: self.config.mailer.service,
260 // SMTP username and password
261 auth: {
262 user: core.config.mailer.auth.user,
263 pass: core.config.mailer.auth.password
264 },
265 debug: core.config.mailer.debug || false
266 })
267 }
268
269 callback();
270 }
271
272 self.initDatabaseConfig = function(callback) {
273
274 var dbconf = self.databaseConfig;
275 console.log("Connecting to database...".bold);
276
277 self.db = { }
278 self.databases = { }
279
280 initDatabaseConnection();
281
282 function initDatabaseConnection() {
283 var config = dbconf.shift();
284 if (!config)
285 return callback();
286
287 var name = config.config;
288 var db = self.config.mongodb[name];
289
290 if(!db)
291 throw new Error("Config missing database configuration for '"+name+"'");
292
293 mongo.Db.connect(db, function (err, database) {
294 if (err)
295 return callback(err);
296
297 self.databases[name] = database;
298
299 console.log("DB '" + (name) + "' connected", self.config.mongodb[name].bold);
300 zutils.bind_database_config(database, config.collections, function (err, db) {
301 if (err)
302 return callback(err);
303 _.extend(self.db, db);
304 initDatabaseConnection();
305 })
306 })
307 }
308 }
309
310 self.initDatabaseCollections = function(callback) {
311 console.log("Connecting to database...".bold);
312
313 self.db = { }
314 self.databases = { }
315
316 mongo.Db.connect(self.config.mongodb, function (err, database) {
317 if (err)
318 return callback(err);
319
320 self.database = database;
321
322 console.log("Database connected", self.config.mongodb);
323 zutils.bind_database_config(database, self.databaseCollections, function (err, db) {
324 if (err)
325 return callback(err);
326 _.extend(self.db, db);
327 callback();
328 })
329 })
330 }
331
332
333 self.initExpressConfig = function(callback) {
334 var ExpressSession = require('express-session');
335
336 self.app = express();
337
338 self.app.set('views', path.join(appFolder,'views'));
339 self.app.set('view engine', 'ejs');
340 self.app.engine('html', require('ejs').renderFile);
341 self.app.use(require('body-parser')());//express.json());
342 self.app.use(require('method-override')());
343 self.app.use(require('cookie-parser')(self.config.http.session.secret));
344
345 if(self.config.mongodb) {
346 var MongoStore = require('connect-mongo')(ExpressSession);
347 self.app.sessionStore = new MongoStore({url: self.config.mongodb.main || self.config.mongodb}, function() {
348 self.app.use(ExpressSession({
349 secret: self.config.http.session.secret,
350 key: self.config.http.session.key,
351 cookie: self.config.http.session.cookie,
352 store: self.app.sessionStore
353 }));
354
355 return callback();
356 });
357 }
358 else
359 if(self.config.http && self.config.http.session) {
360 self.app.sessionSecret = self.config.http.session.secret;
361 var CookieSession = require('cookie-session');
362 self.app.use(CookieSession({
363 secret: self.config.http.session.secret,
364 key: self.config.http.session.key,
365 }));
366
367 return callback();
368 }
369 }
370
371
372 self.initExpressHandlers = function(callback) {
373 var ErrorHandler = require('errorhandler')();
374
375 var isErrorView = fs.existsSync(path.join(self.appFolder,'views','error.ejs'));
376 self.handleHttpError = function(response, req, res, next) {
377 if(req.xhr) {
378 res.json({errors: _.isArray(response.errors) ? response.errors : [response.errors]});
379 return;
380 }
381 else
382 if(isErrorView) {
383 res.render('error', { error : error });
384 return;
385 }
386 else {
387 res.setHeader('Content-Type', 'text/html; charset=utf-8');
388 res.end("Server Error");
389 return;
390 }
391 }
392
393
394 if(self.config.translator)
395 self.app.use(self.translator.useSession);
396
397
398 /**
399 * response = {
400 * status: {Number}
401 * errors: {String | Array}
402 * }
403 */
404
405 function HttpError(response) {
406 res.status(response.status);
407 self.handleHttpError(respo)
408 }
409
410 self.app.use(function(req, res, next) {
411 res.sendHttpError = function (response) {
412 self.handleHttpError(response, req, res, next);
413 }
414
415 next();
416 })
417
418 var loginConfig = self.config.http.login;
419 if(loginConfig && loginConfig.authenticator) {
420 switch(loginConfig.authenticator.type) {
421 case 'basic' : {
422 console.log("Enabling basic authenticator".bold);
423 self.authenticator = new zlogin.BasicAuthenticator(self, loginConfig.authenticator);
424 self.login = new zlogin.Login(self, self.authenticator, loginConfig);
425 self.login.init(self.app);
426 } break;
427 }
428 }
429
430 if(self.router)
431 self.router.init(self.app);
432
433 self.emit('init::express', self.app);
434
435 if(self.config.http.static) {
436 var ServeStatic = require('serve-static');
437 _.each(self.config.http.static, function(dst, src) {
438 console.log('HTTP serving '+src.cyan.bold+' -> '+dst.cyan.bold);
439 self.app.use(src, ServeStatic(path.join(appFolder, dst)));
440 })
441 }
442
443 /**
444 * Handles errors were sent via next() method
445 *
446 * following formats are supported:
447 * next(new Error('Something blew up'));
448 * next(400);
449 * next({status: 400, errors: 'Activation code is wrong'});
450 * next({status: 400, errors: ['Activation code is wrong']});
451 *
452 */
453 self.app.use(function (err, req, res, next) {
454 if (typeof err == 'number') {
455 err = {
456 status: err,
457 errors: http.STATUS_CODES[err] || "Error"
458 };
459 }
460 else
461 if (typeof err == 'string') {
462 console.error(err);
463 err = {
464 status: 500,
465 errors: 'Internal Server Error'
466 };
467 }
468 else
469 if (err instanceof Error) {
470 if (self.config.development) {
471 err.status = 500;
472 return ErrorHandler(err, req, res, next);
473 }
474 else
475 {
476 console.error(err.stack);
477 err = {
478 status: 500,
479 errors: 'Internal Server Error'
480 };
481 }
482 }
483
484 res.sendHttpError(err);
485 });
486
487 finish();
488
489 function finish() {
490 callback();
491 }
492
493
494 };
495
496 self.initHttpServer = function(callback) {
497
498 var CERTIFICATES = (self.config.http.ssl && self.config.certificates) ? self.certificates : null;
499
500 var https_server = CERTIFICATES ? https.createServer(CERTIFICATES, self.app) : http.createServer(self.app);
501 self.io = socketio.listen(https_server, { 'log level': 0, 'secure': CERTIFICATES ? true : false });
502 if(self.router && self.router.initWebSocket)
503 self.router.initWebSocket(self.io);
504 self.config.websocket && self.initWebsocket(function(){});
505 self.emit('init::websockets');
506 self.emit('init::http::done');
507 https_server.listen(self.config.http.port, function (err) {
508 if (err) {
509 console.error("Unable to start HTTP(S) server on port" + self.config.http.port);
510 return callback(err);
511 }
512
513 console.log('HTTP server listening on port ' + (self.config.http.port+'').bold);
514
515 if (!CERTIFICATES)
516 console.log(("WARNING - SSL is currently disabled").magenta.bold);
517
518 if (self.config.secure_under_username) {
519 console.log("Securing run-time to user '" + self.config.secure_under_username + "'");
520 zutils.secure_under_username(self.config.secure_under_username);
521 }
522
523 self.emit('init::http-server')
524 callback();
525 });
526 };
527
528 self.initSupervisors = function(callback) {
529 // console.log("initSupervisors");
530 if(!self.certificates)
531 throw new Error("Application supervisor requires configured certificates");
532 console.log("Connecting to supervisor(s)...".bold, self.config.supervisor.address);
533 self.supervisor = new zrpc.Client({
534 address: self.config.supervisor.address,
535 auth: self.config.supervisor.auth,
536 certificates: self.certificates,
537 node: self.mac,
538 mac: self.mac,
539 uuid : self.uuid,
540 designation: self.pkg.name, // self.config.application,
541 pingDataObject : self.pingDataObject
542 });
543 self.supervisor.registerListener(self);
544 callback();
545
546 self.on('package::info::get', function(msg) {
547 console.log(msg.op.yellow.bold);
548 self.supervisor.dispatch({ op : 'package::info::data', uuid : self.uuid, pkg : self.pkg })
549 })
550 }
551
552 self.initWebsocket = function(callback) {
553 self.webSocketMap = [ ]
554 self.webSockets = self.io.of(self.config.websocket.path).on('connection', function(socket) {
555 console.log("websocket "+socket.id+" connected");
556 self.emit('websocket::connect', socket);
557 self.webSocketMap[socket.id] = socket;
558 socket.on('disconnect', function() {
559 self.emit('websocket::disconnect', socket);
560 delete self.webSocketMap[socket.id];
561 console.log("websocket "+socket.id+" disconnected");
562 })
563 socket.on('rpc::request', function(msg) {
564 try {
565 var listeners = self.listeners(msg.req.op);
566 if(listeners.length == 1) {
567 listeners[0].call(socket, msg.req, function(err, resp) {
568 socket.emit('rpc::response', {
569 _resp : msg._req,
570 err : err,
571 resp : resp,
572 });
573 })
574 }
575 else
576 if(listeners.length)
577 {
578 socket.emit('rpc::response', {
579 _resp : msg._req,
580 err : { error : "Too many handlers for '"+msg.req.op+"'" }
581 });
582 }
583 else
584 {
585 socket.emit('rpc::response', {
586 _resp : msg._req,
587 err : { error : "No such handler '"+msg.req.op+"'" }
588 });
589 }
590 }
591 catch(ex) { console.error(ex.stack); }
592 });
593
594 socket.on('message', function(msg) {
595 try {
596 self.emit(msg.op, msg, socket);
597 }
598 catch(ex) { console.error(ex.stack); }
599 });
600 });
601
602 callback();
603 }
604
605 // --
606
607 function updateServerStats() {
608
609 self.pingDataObject.loadAvg = self.monitor.stats.loadAvg;
610 self.pingDataObject.memory = self.monitor.stats.memory;
611
612 dpc(5 * 1000, updateServerStats)
613 }
614
615 // --
616
617 var initStepsBeforeHttp_ = [ ]
618 var initSteps_ = [ ]
619
620 self.initBeforeHttp = function(fn) {
621 initStepsBeforeHttp_.push(fn);
622 }
623
624 self.init = function(fn) {
625 initSteps_.push(fn);
626 }
627
628 self.run = function(callback) {
629
630 var steps = new zutils.Steps();
631
632
633 self.config.translator && steps.push(self.initTranslator);
634 self.config.certificates && steps.push(self.initCertificates);
635 self.config.statsd && steps.push(self.initMonitoringInterfaces);
636 self.config.mongodb && self.databaseConfig && steps.push(self.initDatabaseConfig);
637 self.config.mongodb && self.databaseCollections && steps.push(self.initDatabaseCollections);
638 self.emit('init::database', steps);
639 _.each(initStepsBeforeHttp_, function(fn) {
640 steps.push(fn);
641 })
642 if(self.config.http) {
643 steps.push(self.initExpressConfig);
644 steps.push(self.initExpressHandlers);
645 steps.push(self.initHttpServer);
646 }
647 self.config.mailer && steps.push(initMailer);
648 self.config.supervisor && self.config.supervisor.address && steps.push(self.initSupervisors);
649
650 getmac.getMac(function (err, mac) {
651 if (err) return callback(err);
652 self.mac = mac.split(process.platform == 'win32' ? '-' : ':').join('').toLowerCase();
653 self.macBytes = _.map(self.mac.match(/.{1,2}/g), function(v) { return parseInt(v, 16); })
654
655 var uuid = self.appFolder.replace(/\\/g,'/').split('/').pop();
656 if(!uuid || uuid.length != 36) {
657 var local = readJSON('uuid');
658 if(local && local.uuid)
659 uuid = local.uuid;
660 else {
661 uuid = UUID.v1({ node : self.macBytes });
662 Application.writeJSON("uuid", { uuid : uuid });
663 }
664 }
665 self.uuid = uuid;
666
667 console.log("App UUID:".bold,self.uuid.bold);
668
669 _.each(initSteps_, function(fn) {
670 steps.push(fn);
671 })
672
673 self.emit('init::build', steps);
674
675 steps.run(function (err) {
676 if (err)
677 throw err;
678
679 self.config.statsd && updateServerStats();
680 console.log("init OK".bold);
681 self.emit('init::done');
682 callback && callback();
683 })
684
685 })
686
687 return self;
688 }
689
690 self.caption = self.pkg.name;
691 dpc(function() {
692 if(self.caption) {
693 zutils.render(self.caption.replace('-',' '), null, function(err, caption) {
694 console.log('\n'+caption);
695 dpc(function() {
696 self.run();
697 })
698 })
699 }
700 else {
701 self.run();
702 }
703 })
704
705}
706
707util.inherits(Application, events.EventEmitter);
708
709Application.getConfig = getConfig;
710Application.readJSON = readJSON;
711Application.writeJSON = writeJSON;
712
713module.exports = {
714 Application : Application,
715 getConfig : getConfig
716}