UNPKG

50.3 kBJavaScriptView Raw
1/**
2 * @license
3 * MOST Web Framework 2.0 Codename Blueshift
4 * Copyright (c) 2017, THEMOST LP All rights reserved
5 *
6 * Use of this source code is governed by an BSD-3-Clause license that can be
7 * found in the LICENSE file at https://themost.io/license
8 */
9///
10var HttpError = require('@themost/common/errors').HttpError;
11var HttpServerError = require('@themost/common/errors').HttpServerError;
12var HttpNotFoundError = require('@themost/common/errors').HttpNotFoundError;
13var Args = require('@themost/common/utils').Args;
14var TraceUtils = require('@themost/common/utils').TraceUtils;
15var sprintf = require('sprintf').sprintf;
16var _ = require('lodash');
17var mvc = require('./mvc');
18var LangUtils = require('@themost/common/utils').LangUtils;
19var path = require("path");
20var fs = require("fs");
21var ejs = require('ejs');
22var url = require('url');
23var http = require('http');
24var SequentialEventEmitter = require('@themost/common/emitter').SequentialEventEmitter;
25var DataConfigurationStrategy = require('@themost/data/data-configuration').DataConfigurationStrategy;
26var querystring = require('querystring');
27var crypto = require('crypto');
28var Symbol = require('symbol');
29var HttpHandler = require('./types').HttpHandler;
30var AuthStrategy = require('./handlers/auth').AuthStrategy;
31var DefaultAuthStrategy = require('./handlers/auth').DefaultAuthStrategy;
32var EncryptionStrategy = require('./handlers/auth').EncryptionStrategy;
33var DefaultEncryptionStrategy = require('./handlers/auth').DefaultEncryptionStrategy;
34var CacheStrategy = require('./cache').CacheStrategy;
35var DefaultCacheStrategy = require('./cache').DefaultCacheStrategy;
36var LocalizationStrategy = require('./localization').LocalizationStrategy;
37var DefaulLocalizationStrategy = require('./localization').DefaultLocalizationStrategy;
38var HttpConfiguration = require('./config').HttpConfiguration;
39var HttpApplicationService = require('./types').HttpApplicationService;
40var HttpContext = require('./context').HttpContext;
41var StaticHandler = require('./handlers/static').StaticHandler;
42
43var executionPathProperty = Symbol('executionPath');
44var configPathProperty = Symbol('configPath');
45var configProperty = Symbol('config');
46var currentProperty = Symbol('current');
47var servicesProperty = Symbol('services');
48
49var DEFAULT_HTML_ERROR = fs.readFileSync(path.resolve(__dirname, 'http-error.html.ejs'), 'utf8');
50
51/**
52 * @classdesc ApplicationOptions class describes the startup options of a MOST Web Framework application.
53 * @class
54 * @constructor
55 * @property {number} port - The HTTP binding port number.
56 * The default value is either PORT environment variable or 3000.
57 * @property {string} bind - The HTTP binding ip address or hostname.
58 * The default value is either IP environment variable or 127.0.0.1.
59 * @property {number|string} cluster - A number which represents the number of clustered applications.
60 * The default value is zero (no clustering). If cluster is 'auto' then the number of clustered applications
61 * depends on hardware capabilities (number of CPUs).
62 @example
63 //load module
64 var web = require("most-web");
65 //start server
66 web.current.start({ port:80, bind:"0.0.0.0",cluster:'auto' });
67 @example
68 //Environment variables already set: IP=198.51.100.0 PORT=80
69 var web = require("most-web");
70 web.current.start();
71 */
72// eslint-disable-next-line no-unused-vars
73function ApplicationOptions() {
74
75}
76
77/**
78 * Abstract class that represents a data context
79 * @constructor
80 */
81function HttpDataContext() {
82 //
83}
84/**
85 * @returns {DataAdapter}
86 */
87HttpDataContext.prototype.db = function () {
88 return null;
89};
90
91/**
92 * @param {string} name
93 * @returns {DataModel}
94 */
95// eslint-disable-next-line no-unused-vars
96HttpDataContext.prototype.model = function (name) {
97 return null;
98};
99
100/**
101 * @param {string} type
102 * @returns {*}
103 */
104// eslint-disable-next-line no-unused-vars
105HttpDataContext.prototype.dataTypes = function (type) {
106 return null;
107};
108
109/**
110 *
111 * @param {HttpApplication} app
112 * @constructor
113 */
114function HttpContextProvider(app) {
115 HttpContextProvider.super_.bind(this)(app);
116}
117LangUtils.inherits(HttpContextProvider,HttpApplicationService);
118/**
119 * @returns {HttpContext}
120 */
121HttpContextProvider.prototype.create = function(req,res) {
122 var context = new HttpContext(req,res);
123 //set context application
124 context.application = this.getApplication();
125 return context;
126};
127/**
128 * @class
129 * @constructor
130 * @param {string=} executionPath
131 * @augments SequentialEventEmitter
132 * @augments IApplication
133 */
134function HttpApplication(executionPath) {
135
136 //Sets the current execution path
137 this[executionPathProperty] = _.isNil(executionPath) ? path.join(process.cwd()) : path.resolve(executionPath);
138 //Gets the current application configuration path
139 this[configPathProperty] = _.isNil(executionPath) ? path.join(process.cwd(), 'config') : path.resolve(executionPath, 'config');
140 //initialize services
141 this[servicesProperty] = { };
142 //set configuration
143 this[configProperty] = new HttpConfiguration(this[configPathProperty]);
144 /**
145 * Gets or sets a collection of application handlers
146 * @type {Array}
147 */
148 this.handlers = [];
149 var self = this;
150 //initialize handlers collection
151 var configurationHandlers = this.getConfiguration().handlers;
152 var defaultHandlers = require('./resources/app.json').handlers;
153 for (var i = 0; i < defaultHandlers.length; i++) {
154 (function(item) {
155 if (typeof configurationHandlers.filter(function(x) { return x.name === item.name; })[0] === 'undefined') {
156 configurationHandlers.push(item);
157 }
158 })(defaultHandlers[i]);
159 }
160 var reModule = /^@themost\/web\//i;
161 _.forEach(configurationHandlers, function (handlerConfiguration) {
162 try {
163 var handlerPath = handlerConfiguration.type;
164 if (reModule.test(handlerPath)) {
165 handlerPath = handlerPath.replace(reModule,'./');
166 }
167 else if (/^\//.test(handlerPath)) {
168 handlerPath = self.mapPath(handlerPath);
169 }
170 var handlerModule = require(handlerPath), handler = null;
171 if (handlerModule) {
172 //if module exports a constructor
173 if (typeof handlerModule === 'function') {
174 self.handlers.push(new handlerModule());
175 }
176 //else if module exports a method called createInstance()
177 else if (typeof handlerModule.createInstance === 'function') {
178 //call createInstance
179 handler = handlerModule.createInstance();
180 if (handler) {
181 self.handlers.push(handler);
182 }
183 }
184 else {
185 TraceUtils.log('The specified handler (%s) cannot be instantiated. The module does not export a class constructor or createInstance() function.', handlerConfiguration.name);
186 }
187 }
188 }
189 catch (err) {
190 throw new Error(sprintf('The specified handler (%s) cannot be loaded. %s', handlerConfiguration.name, err.message));
191 }
192 });
193 //set default context provider
194 self.useService(HttpContextProvider);
195 //set authentication strategy
196 self.useStrategy(AuthStrategy, DefaultAuthStrategy);
197 //set cache strategy
198 self.useStrategy(CacheStrategy, DefaultCacheStrategy);
199 //set encryption strategy
200 self.useStrategy(EncryptionStrategy, DefaultEncryptionStrategy);
201 //set localization strategy
202 self.useStrategy(LocalizationStrategy, DefaulLocalizationStrategy);
203 //set authentication strategy
204 self.getConfiguration().useStrategy(DataConfigurationStrategy, DataConfigurationStrategy);
205 /**
206 * Gets or sets a boolean that indicates whether the application is in development mode
207 * @type {string}
208 */
209 this.development = (process.env.NODE_ENV === 'development');
210 /**
211 *
212 * @type {{html, text, json, unauthorized}|*}
213 */
214 this.errors = httpApplicationErrors(this);
215
216}
217LangUtils.inherits(HttpApplication, SequentialEventEmitter);
218
219/**
220 * @returns {HttpApplication}
221 */
222HttpApplication.getCurrent = function() {
223 if (typeof HttpApplication[currentProperty] === 'object') {
224 return HttpApplication[currentProperty];
225 }
226 HttpApplication[currentProperty] = new HttpApplication();
227 return HttpApplication[currentProperty];
228};
229/**
230 * @returns {HttpConfiguration}
231 */
232HttpApplication.prototype.getConfiguration = function() {
233 return this[configProperty];
234};
235
236/**
237 * @returns {EncryptionStrategy}
238 */
239HttpApplication.prototype.getEncryptionStrategy = function() {
240 return this.getStrategy(EncryptionStrategy);
241};
242
243/**
244 * @returns {AuthStrategy}
245 */
246HttpApplication.prototype.getAuthStrategy = function() {
247 return this.getStrategy(AuthStrategy);
248};
249
250/**
251 * @returns {LocalizationStrategy}
252 */
253HttpApplication.prototype.getLocalizationStrategy = function() {
254 return this.getStrategy(LocalizationStrategy);
255};
256
257
258HttpApplication.prototype.getExecutionPath = function() {
259 return this[executionPathProperty];
260};
261
262/**
263 * Resolves the given path
264 * @param {string} arg
265 */
266HttpApplication.prototype.mapExecutionPath = function(arg) {
267 Args.check(_.isString(arg),'Path must be a string');
268 return path.resolve(this.getExecutionPath(), arg);
269};
270
271/**
272 * Sets static content root directory
273 * @param {string} rootDir
274 */
275HttpApplication.prototype.useStaticContent = function(rootDir) {
276 /**
277 * @type {StaticHandler}
278 */
279 var staticHandler = _.find(this.handlers, function(x) {
280 return x.constructor === StaticHandler;
281 });
282 if (typeof staticHandler === 'undefined') {
283 throw new Error('An instance of StaticHandler class cannot be found in application handlers');
284 }
285 staticHandler.rootDir = rootDir;
286 return this;
287};
288
289
290HttpApplication.prototype.getConfigurationPath = function() {
291 return this[configPathProperty];
292};
293
294/**
295 * Initializes application configuration.
296 * @return {HttpApplication}
297 */
298HttpApplication.prototype.init = function () {
299
300 //initialize basic directives collection
301 var directives = require("./angular/directives");
302 directives.apply(this);
303 return this;
304};
305
306/**
307 * Returns the path of a physical file based on a given URL.
308 * @param {string} s
309 */
310HttpApplication.prototype.mapPath = function (s) {
311 var uri = url.parse(s).pathname;
312 return path.join(this[executionPathProperty], uri);
313};
314
315/**
316 * Converts an application URL into one that is usable on the requesting client. A valid application relative URL always start with "~/".
317 * If the relativeUrl parameter contains an absolute URL, the URL is returned unchanged.
318 * Note: An HTTP application base path may be set in settings/app/base configuration section. The default value is "/".
319 * @param {string} appRelativeUrl - A string which represents an application relative URL like ~/login
320 */
321HttpApplication.prototype.resolveUrl = function (appRelativeUrl) {
322 if (/^~\//.test(appRelativeUrl)) {
323 var base = this.getConfiguration().getSourceAt("settings/app/base") || "/";
324 base += /\/$/.test(base) ? '' : '/';
325 return appRelativeUrl.replace(/^~\//, base);
326 }
327 return appRelativeUrl;
328};
329
330/**
331 * Resolves ETag header for the given file. If the specified does not exist or is invalid returns null.
332 * @param {string=} file - A string that represents the file we want to query
333 * @param {function(Error,string=)} callback
334 */
335HttpApplication.prototype.resolveETag = function(file, callback) {
336 fs.exists(file, function(exists) {
337 try {
338 if (exists) {
339 fs.stat(file, function(err, stats) {
340 if (err) {
341 callback(err);
342 }
343 else {
344 if (!stats.isFile()) {
345 callback(null);
346 }
347 else {
348 //validate if-none-match
349 var md5 = crypto.createHash('md5');
350 md5.update(stats.mtime.toString());
351 var result = md5.digest('base64');
352 callback(null, result);
353
354 }
355 }
356 });
357 }
358 else {
359 callback(null);
360 }
361 }
362 catch (e) {
363 callback(null);
364 }
365 });
366};
367// noinspection JSUnusedGlobalSymbols
368/**
369 * @param {HttpContext} context
370 * @param {string} executionPath
371 * @param {function(Error, Boolean)} callback
372 */
373HttpApplication.prototype.unmodifiedRequest = function(context, executionPath, callback) {
374 try {
375 var requestETag = context.request.headers['if-none-match'];
376 if (typeof requestETag === 'undefined' || requestETag == null) {
377 callback(null, false);
378 return;
379 }
380 HttpApplication.prototype.resolveETag(executionPath, function(err, result) {
381 callback(null, (requestETag===result));
382 });
383 }
384 catch (err) {
385 TraceUtils.error(err);
386 callback(null, false);
387 }
388};
389
390/**
391 * @param request {string|IncomingMessage}
392 * @returns {*}
393 * */
394HttpApplication.prototype.resolveMime = function (request) {
395 var extensionName;
396 if (typeof request=== 'string') {
397 //get file extension
398 extensionName = path.extname(request);
399 }
400 else if (typeof request=== 'object') {
401 //get file extension
402 extensionName = path.extname(request.url);
403 }
404 else {
405 return;
406 }
407 return _.find(this.getConfiguration().mimes, function(x) {
408 return (x.extension === extensionName);
409 });
410};
411
412
413
414/**
415 *
416 * @param {HttpContext} context
417 * @param {Function} callback
418 */
419HttpApplication.prototype.processRequest = function (context, callback) {
420 var self = this;
421 if (typeof context === 'undefined' || context == null) {
422 callback.call(self);
423 }
424 else {
425 //1. beginRequest
426 context.emit('beginRequest', context, function (err) {
427 if (err) {
428 callback.call(context, err);
429 }
430 else {
431 //2. validateRequest
432 context.emit('validateRequest', context, function (err) {
433 if (err) {
434 callback.call(context, err);
435 }
436 else {
437 //3. authenticateRequest
438 context.emit('authenticateRequest', context, function (err) {
439 if (err) {
440 callback.call(context, err);
441 }
442 else {
443 //4. authorizeRequest
444 context.emit('authorizeRequest', context, function (err) {
445 if (err) {
446 callback.call(context, err);
447 }
448 else {
449 //5. mapRequest
450 context.emit('mapRequest', context, function (err) {
451 if (err) {
452 callback.call(context, err);
453 }
454 else {
455 //5b. postMapRequest
456 context.emit('postMapRequest', context, function(err) {
457 if (err) {
458 callback.call(context, err);
459 }
460 else {
461 //process HEAD request
462 if (context.request.method==='HEAD') {
463 //7. endRequest
464 context.emit('endRequest', context, function (err) {
465 callback.call(context, err);
466 });
467 }
468 else {
469 //6. processRequest
470 if (context.request.currentHandler != null)
471 context.request.currentHandler.processRequest(context, function (err) {
472 if (err) {
473 callback.call(context, err);
474 }
475 else {
476 //7. endRequest
477 context.emit('endRequest', context, function (err) {
478 callback.call(context, err);
479 });
480 }
481 });
482 else {
483 var er = new HttpNotFoundError();
484 if (context.request && context.request.url) {
485 er.resource = context.request.url;
486 }
487 callback.call(context, er);
488 }
489 }
490
491 }
492 });
493 }
494 });
495 }
496 });
497 }
498 });
499 }
500 });
501 }
502 });
503 }
504};
505
506/**
507 * Gets the default data context based on the current configuration
508 * @returns {DataAdapter}
509 */
510HttpApplication.prototype.db = function () {
511 if ((this.config.adapters === null) || (this.config.adapters.length === 0))
512 throw new Error('Data adapters configuration settings are missing or cannot be accessed.');
513 var adapter = null;
514 if (this.config.adapters.length === 1) {
515 //there is only one adapter so try to instantiate it
516 adapter = this.config.adapters[0];
517 }
518 else {
519 adapter = _.find(this.config.adapters,function (x) {
520 return x.default;
521 });
522 }
523 if (adapter === null)
524 throw new Error('There is no default data adapter or the configuration is incorrect.');
525 //try to instantiate adapter
526 if (!adapter.invariantName)
527 throw new Error('The default data adapter has no invariant name.');
528 var adapterType = this.config.adapterTypes[adapter.invariantName];
529 if (adapterType == null)
530 throw new Error('The default data adapter type cannot be found.');
531 if (typeof adapterType.createInstance === 'function') {
532 return adapterType.createInstance(adapter.options);
533 }
534 else if (adapterType.require) {
535 var m = require(adapterType.require);
536 if (typeof m.createInstance === 'function') {
537 return m.createInstance(adapter.options);
538 }
539 throw new Error('The default data adapter cannot be instantiated. The module provided does not export a function called createInstance().')
540 }
541};
542
543/**
544 * @returns {HttpContextProvider}
545 */
546HttpApplication.prototype.getContextProvider = function() {
547 return this.getService(HttpContextProvider);
548};
549
550
551/**
552 * Creates an instance of HttpContext class.
553 * @param {ClientRequest} request
554 * @param {ServerResponse} response
555 * @returns {HttpContext}
556 */
557HttpApplication.prototype.createContext = function (request, response) {
558 var context = this.getContextProvider().create(request, response);
559 //set context application
560 context.application = this;
561 //set handler events
562 for (var i = 0; i < HttpHandler.Events.length; i++) {
563 var eventName = HttpHandler.Events[i];
564 for (var j = 0; j < this.handlers.length; j++) {
565 var handler = this.handlers[j];
566 if (typeof handler[eventName] === 'function') {
567 context.on(eventName, handler[eventName].bind(handler));
568 }
569 }
570 }
571 return context;
572};
573/**
574 * @param {*} options
575 * @param {*} data
576 * @param {Function} callback
577 */
578HttpApplication.prototype.executeExternalRequest = function(options,data, callback) {
579 //make request
580 var https = require('https'),
581 opts = (typeof options==='string') ? url.parse(options) : options,
582 httpModule = (opts.protocol === 'https:') ? https : http;
583 var req = httpModule.request(opts, function(res) {
584 res.setEncoding('utf8');
585 var data = '';
586 res.on('data', function (chunk) {
587 data += chunk;
588 });
589 res.on('end', function(){
590 var result = {
591 statusCode: res.statusCode,
592 headers: res.headers,
593 body:data,
594 encoding:'utf8'
595 };
596 /**
597 * destroy sockets (manually close an unused socket) ?
598 */
599 callback(null, result);
600 });
601 });
602 req.on('error', function(e) {
603 //return error
604 callback(e);
605 });
606 if(data)
607 {
608 if (typeof data ==="object" )
609 req.write(JSON.stringify(data));
610 else
611 req.write(data.toString());
612 }
613 req.end();
614};
615
616/**
617 * Executes an internal process
618 * @param {Function(HttpContext)} fn
619 */
620HttpApplication.prototype.execute = function (fn) {
621 var request = createRequestInternal.call(this);
622 fn.call(this, this.createContext(request, createResponseInternal.call(this,request)));
623};
624
625/**
626 * Executes an unattended internal process
627 * @param {Function} fn
628 */
629HttpApplication.prototype.unattended = function (fn) {
630 //create context
631 var request = createRequestInternal.call(this), context = this.createContext(request, createResponseInternal.call(this,request));
632 //get unattended account
633 var account = this.getAuthStrategy().getUnattendedExecutionAccount();
634 //set unattended execution account
635 if (typeof account !== 'undefined' || account!==null) {
636 context.user = { name: account, authenticationType: 'Basic'};
637 }
638 //execute internal process
639 fn.call(this, context);
640};
641
642/**
643 * Load application extension
644 */
645HttpApplication.prototype.extend = function (extension) {
646 if (typeof extension === 'undefined')
647 {
648 //register all application extensions
649 var extensionFolder = this.mapPath('/extensions');
650 if (fs.existsSync(extensionFolder)) {
651 var arr = fs.readdirSync(extensionFolder);
652 for (var i = 0; i < arr.length; i++) {
653 if (path.extname(arr[i])==='.js')
654 require(path.join(extensionFolder, arr[i]));
655 }
656 }
657 }
658 else {
659 //register the specified extension
660 if (typeof extension === 'string') {
661 var extensionPath = this.mapPath(sprintf('/extensions/%s.js', extension));
662 if (fs.existsSync(extensionPath)) {
663 //load extension
664 require(extensionPath);
665 }
666 }
667 }
668 return this;
669};
670
671/**
672 *
673 * @param {*|string} options
674 * @param {Function} callback
675 */
676HttpApplication.prototype.executeRequest = function (options, callback) {
677 var opts = { };
678 if (typeof options === 'string') {
679 _.assign(opts, { url:options });
680 }
681 else {
682 _.assign(opts, options);
683 }
684 var request = createRequestInternal.call(this,opts),
685 response = createResponseInternal.call(this,request);
686 if (!opts.url) {
687 callback(new Error('Internal request url cannot be empty at this context.'));
688 return;
689 }
690 if (opts.url.indexOf('/') !== 0)
691 {
692 var uri = url.parse(opts.url);
693 opts.host = uri.host;
694 opts.hostname = uri.hostname;
695 opts.path = uri.path;
696 opts.port = uri.port;
697 //execute external request
698 this.executeExternalRequest(opts,null, callback);
699 }
700 else {
701 //todo::set cookie header (for internal requests)
702 /*
703 IMPORTANT: set response Content-Length to -1 in order to force the default HTTP response format.
704 if the content length is unknown (server response does not have this header)
705 in earlier version of node.js <0.11.9 the response contains by default a hexadecimal number that
706 represents the content length. This number appears exactly after response headers and before response body.
707 If the content length is defined the operation omits this hexadecimal value
708 e.g. the wrong or custom formatted response
709 HTTP 1.1 Status OK
710 Content-Type: text/html
711 ...
712 Connection: keep-alive
713
714 6b8
715
716 <html><body>
717 ...
718 </body></html>
719 e.g. the standard format
720 HTTP 1.1 Status OK
721 Content-Type: text/html
722 ...
723 Connection: keep-alive
724
725
726 <html><body>
727 ...
728 </body></html>
729 */
730 response.setHeader('Content-Length',-1);
731 handleRequestInternal.call(this, request, response, function(err) {
732 if (err) {
733 callback(err);
734 }
735 else {
736 try {
737 //get statusCode
738 var statusCode = response.statusCode;
739 //get headers
740 var headers = {};
741 if (response._header) {
742 var arr = response._header.split('\r\n');
743 for (var i = 0; i < arr.length; i++) {
744 var header = arr[i];
745 if (header) {
746 var k = header.indexOf(':');
747 if (k>0) {
748 headers[header.substr(0,k)] = header.substr(k+1);
749 }
750 }
751 }
752 }
753 //get body
754 var body = null;
755 var encoding = null;
756 if (_.isArray(response.output)) {
757 if (response.output.length>0) {
758 body = response.output[0].substr(response._header.length);
759 encoding = response.outputEncodings[0];
760 }
761 }
762 //build result (something like ServerResponse)
763 var result = {
764 statusCode: statusCode,
765 headers: headers,
766 body:body,
767 encoding:encoding
768 };
769 callback(null, result);
770 }
771 catch (e) {
772 callback(e);
773 }
774 }
775 });
776 }
777};
778
779/**
780 * @private
781 * @this HttpApplication
782 * @param {ClientRequest} request
783 * @param {ServerResponse} response
784 * @param callback
785 */
786function handleRequestInternal(request, response, callback)
787{
788 var self = this, context = self.createContext(request, response);
789 //add query string
790 if (request.url.indexOf('?') > 0)
791 _.assign(context.params, querystring.parse(request.url.substring(request.url.indexOf('?') + 1)));
792 //add form
793 if (request.form)
794 _.assign(context.params, request.form);
795 //add files
796 if (request.files)
797 _.assign(context.params, request.files);
798
799 self.processRequest(context, function (err) {
800 if (err) {
801 if (self.listeners('error').length === 0) {
802 onError.bind(self)(context, err, function () {
803 response.end();
804 callback();
805 });
806 }
807 else {
808 //raise application error event
809 self.emit('error', { context:context, error:err } , function () {
810 response.end();
811 callback();
812 });
813 }
814 }
815 else {
816 context.finalize(function() {
817 response.end();
818 callback();
819 });
820 }
821 });
822}
823/**
824 * @private
825 * @param {*} options
826 */
827function createRequestInternal(options) {
828 var opt = options ? options : {};
829 var request = new http.IncomingMessage();
830 request.method = (opt.method) ? opt.method : 'GET';
831 request.url = (opt.url) ? opt.url : '/';
832 request.httpVersion = '1.1';
833 request.headers = (opt.headers) ? opt.headers : {
834 host: 'localhost',
835 'user-agent': 'Mozilla/5.0 (X11; Linux i686; rv:10.0) Gecko/20100101 Firefox/22.0',
836 accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
837 'accept-language': 'en,en-US;q=0.5',
838 'accept-encoding': 'gzip, deflate',
839 connection: 'keep-alive',
840 'cache-control': 'max-age=0' };
841 if (opt.cookie)
842 request.headers.cookie = opt.cookie;
843 request.cookies = (opt.cookies) ? opt.cookies : {};
844 request.session = (opt.session) ? opt.session : {};
845 request.params = (opt.params) ? opt.params : {};
846 request.query = (opt.query) ? opt.query : {};
847 request.form = (opt.form) ? opt.form : {};
848 request.body = (opt.body) ? opt.body : {};
849 request.files = (opt.files) ? opt.files : {};
850 return request;
851}
852
853/**
854 * Creates a mock-up server response
855 * @param {ClientRequest} req
856 * @returns {ServerResponse|*}
857 * @private
858 */
859function createResponseInternal(req) {
860 return new http.ServerResponse(req);
861}
862
863/**
864 *
865 * @param {HttpContext} context
866 * @param {Error|*} err
867 * @param {function(Error=)} callback
868 * @private
869 */
870function onHtmlError(context, err, callback) {
871 try {
872 if (context == null) {
873 return callback(err);
874 }
875 // get request and response
876 var request = context.request;
877 var response = context.response;
878 // validate request
879 if ((request == null) || (response == null)) {
880 return callback(err);
881 }
882 //HTML custom errors
883 var str;
884 if (err instanceof HttpError) {
885 str = ejs.render(DEFAULT_HTML_ERROR, {
886 model:err,
887 html: {
888 resolveUrl: context.resolveUrl.bind(context)
889 }
890 });
891 }
892 else {
893 // convert error to http error
894 var finalErr = new HttpError(500, null, err.message);
895 finalErr.stack = err.stack;
896 str = ejs.render(DEFAULT_HTML_ERROR, {
897 model: finalErr,
898 html: {
899 resolveUrl: context.resolveUrl.bind(context)
900 }
901 });
902 }
903 //write status header
904 response.writeHead(err.statusCode || 500 , { "Content-Type": "text/html" });
905 response.write(str);
906 response.end();
907 return callback();
908 }
909 catch (err) {
910 //log process error
911 TraceUtils.error(err);
912 //and continue execution
913 callback(err);
914 }
915
916}
917
918/**
919 * @private
920 * @this HttpApplication
921 * @param {HttpContext} context
922 * @param {Error|*} err
923 * @param {Function} callback
924 */
925function onError(context, err, callback) {
926 callback = callback || function () { };
927 try {
928
929 if (err == null) {
930 return callback();
931 }
932 // log request
933 if (context.request) {
934 TraceUtils.error(context.request.method + ' ' +
935 ((context.user && context.user.name) || 'unknwon') + ' ' +
936 context.request.url);
937 }
938 //log error
939 TraceUtils.error(err);
940 //get response object
941 var response = context.response;
942 // if response is null exit
943 if (response == null) {
944 return callback();
945 }
946 // if response headers have been sent exit
947 if (response._headerSent) {
948 return callback();
949 }
950 if (context.format) {
951 /**
952 * try to find an error handler based on current request
953 * @type Function
954 */
955 var errorHandler = this.errors[context.format];
956 if (typeof errorHandler === 'function') {
957 return errorHandler(context, err, function(err) {
958 if (err) {
959 TraceUtils.error('An error occurred while handling request error');
960 TraceUtils.error(err);
961 }
962 return callback();
963 });
964 }
965 }
966
967 onHtmlError(context, err, function(err) {
968 if (err) {
969 //send plain text
970 response.writeHead(err.statusCode || 500, {"Content-Type": "text/plain"});
971 //if error is an HTTP Exception
972 if (err instanceof HttpError) {
973 response.write(err.statusCode + ' ' + err.message + "\n");
974 }
975 else {
976 //otherwise send status 500
977 response.write('500 ' + err.message + "\n");
978 }
979 //send extra data (on development)
980 if (process.env.NODE_ENV === 'development') {
981 if (err.innerMessage) {
982 response.write(err.innerMessage + "\n");
983 }
984 if (err.stack) {
985 response.write(err.stack + "\n");
986 }
987 }
988 }
989 return callback();
990 });
991 }
992 catch (err) {
993 TraceUtils.log(err);
994 if (context.response) {
995 context.response.writeHead(500, {"Content-Type": "text/plain"});
996 context.response.write("500 Internal Server Error");
997 return callback.bind(this)();
998 }
999 }
1000}
1001
1002
1003/**
1004 * @private
1005 * @type {string}
1006 */
1007var HTTP_SERVER_DEFAULT_BIND = '127.0.0.1';
1008/**
1009 * @private
1010 * @type {number}
1011 */
1012var HTTP_SERVER_DEFAULT_PORT = 3000;
1013
1014/**
1015 * @private
1016 * @param {Function=} callback
1017 * @param {ApplicationOptions|*} options
1018 */
1019function startInternal(options, callback) {
1020 var self = this;
1021 callback = callback || function() { };
1022 try {
1023 //validate options
1024
1025 if (self.config === null)
1026 self.init();
1027 /**
1028 * @memberof process.env
1029 * @property {number} PORT
1030 * @property {string} IP
1031 * @property {string} NODE_ENV
1032 */
1033 var opts = {
1034 bind:(process.env.IP || HTTP_SERVER_DEFAULT_BIND),
1035 port:(process.env.PORT ? process.env.PORT: HTTP_SERVER_DEFAULT_PORT)
1036 };
1037 //extend options
1038 _.assign(opts, options);
1039
1040 var server_ = http.createServer(function (request, response) {
1041 var context = self.createContext(request, response);
1042 //begin request processing
1043 self.processRequest(context, function (err) {
1044 if (err) {
1045 //handle context error event
1046 if (context.listeners('error').length > 0) {
1047 return context.emit('error', { error:err }, function() {
1048 return context.finalize(function() {
1049 if (context.response) {
1050 context.response.end();
1051 }
1052 });
1053 });
1054 }
1055 if (self.listeners('error').length === 0) {
1056 onError.bind(self)(context, err, function () {
1057 if (context == null) {
1058 return;
1059 }
1060 return context.finalize(function() {
1061 if (context.response) {
1062 context.response.end();
1063 }
1064 });
1065 });
1066 }
1067 else {
1068 //raise application error event
1069 return self.emit('error', { context:context, error:err }, function() {
1070 if (context == null) {
1071 return;
1072 }
1073 context.finalize(function() {
1074 if (context.response) {
1075 context.response.end();
1076 }
1077 });
1078 });
1079 }
1080 }
1081 else {
1082 if (context == null) {
1083 return;
1084 }
1085 return context.finalize(function() {
1086 if (context.response) {
1087 context.response.end();
1088 }
1089 });
1090 }
1091 });
1092 });
1093 /**
1094 * @name HttpApplication#getServer
1095 * @type {Function}
1096 * @returns {Server|*}
1097 */
1098 self.getServer = function() {
1099 return server_;
1100 };
1101
1102 //start listening
1103 server_.listen(opts.port, opts.bind);
1104 TraceUtils.log('Web application is running at http://%s:%s/', opts.bind, opts.port);
1105 //do callback
1106 callback.call(self);
1107 } catch (err) {
1108 TraceUtils.error(err);
1109 }
1110}
1111
1112/**
1113 * @param {ApplicationOptions|*=} options
1114 * @param {Function=} callback
1115 */
1116HttpApplication.prototype.start = function (options, callback) {
1117 callback = callback || function() { };
1118 options = options || { };
1119 if (options.cluster) {
1120 var clusters = 1;
1121 //check if options.cluster="auto"
1122 if (/^auto$/i.test(options.cluster)) {
1123 clusters = require('os').cpus().length;
1124 }
1125 else {
1126 //get cluster number
1127 clusters = LangUtils.parseInt(options.cluster);
1128 }
1129 if (clusters>1) {
1130 var cluster = require('cluster');
1131 if (cluster.isMaster) {
1132 //get debug argument (if any)
1133 var debug = process.execArgv.filter(function(x) { return /^--debug(-brk)?=\d+$/.test(x); })[0], debugPort;
1134 if (debug) {
1135 //get debug port
1136 debugPort = parseInt(/^--debug(-brk)?=(\d+)$/.exec(debug)[2]);
1137 cluster.setupMaster({
1138 execArgv: process.execArgv.filter(function(x) { return !/^--debug(-brk)?=\d+$/.test(x); })
1139 });
1140 }
1141 for (var i = 0; i < clusters; i++) {
1142 if (debug) {
1143 if (/^--debug-brk=/.test(debug))
1144 cluster.settings.execArgv.push('--debug-brk=' + (debugPort + i));
1145 else
1146 cluster.settings.execArgv.push('--debug=' + (debugPort + i));
1147 }
1148 cluster.fork();
1149 if (debug) cluster.settings.execArgv.pop();
1150 }
1151 } else {
1152 startInternal.bind(this)(options, callback);
1153 }
1154 }
1155 else {
1156 startInternal.bind(this)(options, callback);
1157 }
1158 }
1159 else {
1160 startInternal.bind(this)(options, callback);
1161 }
1162};
1163
1164/**
1165 * Registers HttpApplication as express framework middleware
1166 */
1167HttpApplication.prototype.runtime = function() {
1168 var self = this;
1169
1170 function nextError(context, err) {
1171 //handle context error event
1172 if (context.listeners('error').length > 0) {
1173 return context.emit('error', { error:err }, function() {
1174 context.finalize(function() {
1175 if (context.response) {
1176 context.response.end();
1177 }
1178 });
1179 });
1180 }
1181 if (self.listeners('error').length === 0) {
1182 onError.bind(self)(context, err, function () {
1183 if (context == null) {
1184 return;
1185 }
1186 context.finalize(function() {
1187 if (context.response) {
1188 context.response.end();
1189 }
1190 });
1191 });
1192 }
1193 else {
1194 //raise application error event
1195 self.emit('error', { context:context, error:err }, function() {
1196 if (context == null) {
1197 return;
1198 }
1199 context.finalize(function() {
1200 if (context.response) {
1201 context.response.end();
1202 }
1203 });
1204 });
1205 }
1206 }
1207
1208 return function runtimeParser(req, res, next) {
1209 //create context
1210 var context = self.createContext(req,res);
1211 context.request.on('close', function() {
1212 //finalize data context
1213 if (_.isObject(context)) {
1214 context.finalize(function() {
1215 if (context.response) {
1216 //if response is alive
1217 if (context.response.finished === false) {
1218 //end response
1219 context.response.end();
1220 }
1221 }
1222 });
1223 }
1224 });
1225 //process request
1226 self.processRequest(context, function(err) {
1227 if (err) {
1228 if (typeof next === 'function') {
1229 return context.finalize(function() {
1230 return next(err);
1231 });
1232 }
1233 return nextError(context, err);
1234 }
1235 return context.finalize(function() {
1236 context.response.end();
1237 });
1238 });
1239 };
1240};
1241
1242/**
1243 * Registers an application controller
1244 * @param {string} name
1245 * @param {Function|HttpControllerConfiguration} controllerCtor
1246 * @returns HttpApplication
1247 */
1248HttpApplication.prototype.useController = function(name, controllerCtor) {
1249 Args.notString(name,"Controller Name");
1250 Args.notFunction(controllerCtor,"Controller constructor");
1251 //get application controllers or default
1252 var controllers = this.getConfiguration().getSourceAt('controllers') || { };
1253 //set application controller
1254 controllers[name] = controllerCtor;
1255 if (typeof controllerCtor.configure === 'function') {
1256 controllerCtor.configure(this);
1257 }
1258 //apply changes
1259 this.getConfiguration().setSourceAt('controllers', controllers);
1260 return this;
1261};
1262
1263/**
1264 * Registers an application strategy e.g. an singleton service which to be used in application contextr
1265 * @param {Function} serviceCtor
1266 * @param {Function} strategyCtor
1267 * @returns HttpApplication
1268 */
1269HttpApplication.prototype.useStrategy = function(serviceCtor, strategyCtor) {
1270 Args.notFunction(strategyCtor,"Service constructor");
1271 Args.notFunction(strategyCtor,"Strategy constructor");
1272 this[servicesProperty][serviceCtor.name] = new strategyCtor(this);
1273 return this;
1274};
1275/**
1276 * Register a service type in application services
1277 * @param {Function} serviceCtor
1278 * @returns HttpApplication
1279 */
1280HttpApplication.prototype.useService = function(serviceCtor) {
1281 Args.notFunction(serviceCtor,"Service constructor");
1282 this[servicesProperty][serviceCtor.name] = new serviceCtor(this);
1283 return this;
1284};
1285
1286/**
1287 * @param {Function} serviceCtor
1288 * @returns {boolean}
1289 */
1290HttpApplication.prototype.hasStrategy = function(serviceCtor) {
1291 Args.notFunction(serviceCtor,"Service constructor");
1292 return this[servicesProperty].hasOwnProperty(serviceCtor.name);
1293};
1294
1295/**
1296 * @param {Function} serviceCtor
1297 * @returns {boolean}
1298 */
1299HttpApplication.prototype.hasService = function(serviceCtor) {
1300 Args.notFunction(serviceCtor,"Service constructor");
1301 return this[servicesProperty].hasOwnProperty(serviceCtor.name);
1302};
1303
1304/**
1305 * Gets an application strategy based on the given base service type
1306 * @param {Function} serviceCtor
1307 * @return {*}
1308 */
1309HttpApplication.prototype.getStrategy = function(serviceCtor) {
1310 Args.notFunction(serviceCtor,"Service constructor");
1311 return this[servicesProperty][serviceCtor.name];
1312};
1313
1314/**
1315 * Gets an application service based on the given base service type
1316 * @param {Function} serviceCtor
1317 * @return {*}
1318 */
1319HttpApplication.prototype.getService = function(serviceCtor) {
1320 Args.notFunction(serviceCtor,"Service constructor");
1321 return this[servicesProperty][serviceCtor.name];
1322};
1323
1324/**
1325 * @param {HttpApplication} application
1326 * @returns {{html: Function, text: Function, json: Function, unauthorized: Function}}
1327 * @private
1328 */
1329function httpApplicationErrors(application) {
1330 var self = application;
1331 return {
1332 html: function(context, error, callback) {
1333 callback = callback || function () { };
1334 if (_.isNil(error)) { return callback(); }
1335 onHtmlError(context, error, function(err) {
1336 callback.call(self, err);
1337 });
1338 },
1339 text: function(context, error, callback) {
1340 callback = callback || function () { };
1341 if (_.isNil(error)) { return callback(); }
1342 /**
1343 * @type {ServerResponse}
1344 */
1345 var response = context.response;
1346 if (error) {
1347 //send plain text
1348 response.writeHead(error.statusCode || 500, {"Content-Type": "text/plain"});
1349 //if error is an HTTP Exception
1350 if (error instanceof HttpError) {
1351 response.write(error.statusCode + ' ' + error.message + "\n");
1352 }
1353 else {
1354 //otherwise send status 500
1355 response.write('500 ' + error.message + "\n");
1356 }
1357 //send extra data (on development)
1358 if (process.env.NODE_ENV === 'development') {
1359 if (!_.isEmpty(error.innerMessage)) {
1360 response.write(error.innerMessage + "\n");
1361 }
1362 if (!_.isEmpty(error.stack)) {
1363 response.write(error.stack + "\n");
1364 }
1365 }
1366 }
1367 return callback.bind(self)();
1368 },
1369 json: function(context, error, callback) {
1370 callback = callback || function () { };
1371 if (_.isNil(error)) { return callback(); }
1372 context.request.headers = context.request.headers || { };
1373 if (/application\/json/g.test(context.request.headers.accept) || (context.format === 'json')) {
1374 var result;
1375 if (error instanceof HttpError) {
1376 result = new mvc.HttpJsonResult(error);
1377 result.responseStatus = error.statusCode;
1378 }
1379 else if (process.env.NODE_ENV === 'development') {
1380 result = new mvc.HttpJsonResult(error);
1381 result.responseStatus = error.statusCode || 500;
1382 }
1383 else {
1384 result = new mvc.HttpJsonResult(new HttpServerError());
1385 result.responseStatus = 500;
1386 }
1387 //execute redirect result
1388 return result.execute(context, function(err) {
1389 return callback(err);
1390 });
1391 }
1392 //go to next error if any
1393 callback.bind(self)(error);
1394 },
1395 unauthorized: function(context, error, callback) {
1396 callback = callback || function () { };
1397 if (_.isNil(error)) { return callback(); }
1398 if (_.isNil(context)) {
1399 return callback.call(self);
1400 }
1401 if (error.statusCode !== 401) {
1402 //go to next error if any
1403 return callback.call(self, error);
1404 }
1405 context.request.headers = context.request.headers || { };
1406 if (/text\/html/g.test(context.request.headers.accept)) {
1407 if (self.config.settings) {
1408 if (self.config.settings.auth) {
1409 //get login page from configuration
1410 var page = self.config.settings.auth.loginPage || '/login.html';
1411 //prepare redirect result
1412 var result = new mvc.HttpRedirectResult(page.concat('?returnUrl=', encodeURIComponent(context.request.url)));
1413 //execute redirect result
1414 result.execute(context, function(err) {
1415 callback.call(self, err);
1416 });
1417 return;
1418 }
1419 }
1420 }
1421 //go to next error if any
1422 callback.bind(self)(error);
1423 }
1424 }
1425}
1426
1427if (typeof exports !== 'undefined')
1428{
1429 module.exports.HttpApplication = HttpApplication;
1430 module.exports.HttpContextProvider = HttpContextProvider;
1431}