123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- 'use strict';
- const path = require('path');
- const fs = require('fs');
- const ms = require('ms');
- const is = require('is-type-of');
- const graceful = require('graceful');
- const http = require('http');
- const cluster = require('cluster-client');
- const onFinished = require('on-finished');
- const { assign } = require('utility');
- const eggUtils = require('egg-core').utils;
- const EggApplication = require('./egg');
- const AppWorkerLoader = require('./loader').AppWorkerLoader;
- const KEYS = Symbol('Application#keys');
- const HELPER = Symbol('Application#Helper');
- const LOCALS = Symbol('Application#locals');
- const BIND_EVENTS = Symbol('Application#bindEvents');
- const WARN_CONFUSED_CONFIG = Symbol('Application#warnConfusedConfig');
- const EGG_LOADER = Symbol.for('egg#loader');
- const EGG_PATH = Symbol.for('egg#eggPath');
- const CLUSTER_CLIENTS = Symbol.for('egg#clusterClients');
- const RESPONSE_RAW = Symbol('Application#responseRaw');
- // client error => 400 Bad Request
- // Refs: https://nodejs.org/dist/latest-v8.x/docs/api/http.html#http_event_clienterror
- const DEFAULT_BAD_REQUEST_HTML = `<html>
- <head><title>400 Bad Request</title></head>
- <body bgcolor="white">
- <center><h1>400 Bad Request</h1></center>
- <hr><center>❤</center>
- </body>
- </html>`;
- const DEFAULT_BAD_REQUEST_HTML_LENGTH = Buffer.byteLength(DEFAULT_BAD_REQUEST_HTML);
- const DEFAULT_BAD_REQUEST_RESPONSE =
- `HTTP/1.1 400 Bad Request\r\nContent-Length: ${DEFAULT_BAD_REQUEST_HTML_LENGTH}` +
- `\r\n\r\n${DEFAULT_BAD_REQUEST_HTML}`;
- // Refs: https://github.com/nodejs/node/blob/b38c81/lib/_http_outgoing.js#L706-L710
- function escapeHeaderValue(value) {
- // Protect against response splitting. The regex test is there to
- // minimize the performance impact in the common case.
- return /[\r\n]/.test(value) ? value.replace(/[\r\n]+[ \t]*/g, '') : value;
- }
- // Refs: https://github.com/nodejs/node/blob/b38c81/lib/_http_outgoing.js#L706-L710
- /**
- * Singleton instance in App Worker, extend {@link EggApplication}
- * @extends EggApplication
- */
- class Application extends EggApplication {
- /**
- * @class
- * @param {Object} options - see {@link EggApplication}
- */
- constructor(options = {}) {
- options.type = 'application';
- super(options);
- // will auto set after 'server' event emit
- this.server = null;
- try {
- this.loader.load();
- } catch (e) {
- // close gracefully
- this[CLUSTER_CLIENTS].forEach(cluster.close);
- throw e;
- }
- // dump config after loaded, ensure all the dynamic modifications will be recorded
- const dumpStartTime = Date.now();
- this.dumpConfig();
- this.coreLogger.info('[egg:core] dump config after load, %s', ms(Date.now() - dumpStartTime));
- this[WARN_CONFUSED_CONFIG]();
- this[BIND_EVENTS]();
- }
- get [EGG_LOADER]() {
- return AppWorkerLoader;
- }
- get [EGG_PATH]() {
- return path.join(__dirname, '..');
- }
- [RESPONSE_RAW](socket, raw) {
- /* istanbul ignore next */
- if (!socket.writable) return;
- if (!raw) return socket.end(DEFAULT_BAD_REQUEST_RESPONSE);
- const body = (raw.body == null) ? DEFAULT_BAD_REQUEST_HTML : raw.body;
- const headers = raw.headers || {};
- const status = raw.status || 400;
- let responseHeaderLines = '';
- const firstLine = `HTTP/1.1 ${status} ${http.STATUS_CODES[status] || 'Unknown'}`;
- // Not that safe because no validation for header keys.
- // Refs: https://github.com/nodejs/node/blob/b38c81/lib/_http_outgoing.js#L451
- for (const key of Object.keys(headers)) {
- if (key.toLowerCase() === 'content-length') {
- delete headers[key];
- continue;
- }
- responseHeaderLines += `${key}: ${escapeHeaderValue(headers[key])}\r\n`;
- }
- responseHeaderLines += `Content-Length: ${Buffer.byteLength(body)}\r\n`;
- socket.end(`${firstLine}\r\n${responseHeaderLines}\r\n${body.toString()}`);
- }
- onClientError(err, socket) {
- // ignore when there is no http body, it almost like an ECONNRESET
- if (err.rawPacket) {
- this.logger.warn('A client (%s:%d) error [%s] occurred: %s',
- socket.remoteAddress,
- socket.remotePort,
- err.code,
- err.message);
- }
- if (typeof this.config.onClientError === 'function') {
- const p = eggUtils.callFn(this.config.onClientError, [ err, socket, this ]);
- // the returned object should be something like:
- //
- // {
- // body: '...',
- // headers: {
- // ...
- // },
- // status: 400
- // }
- //
- // default values:
- //
- // + body: ''
- // + headers: {}
- // + status: 400
- p.then(ret => {
- this[RESPONSE_RAW](socket, ret || {});
- }).catch(err => {
- this.logger.error(err);
- this[RESPONSE_RAW](socket);
- });
- } else {
- // because it's a raw socket object, we should return the raw HTTP response
- // packet.
- this[RESPONSE_RAW](socket);
- }
- }
- onServer(server) {
- // expose app.server
- this.server = server;
- /* istanbul ignore next */
- graceful({
- server: [ server ],
- error: (err, throwErrorCount) => {
- const originMessage = err.message;
- if (originMessage) {
- // shouldjs will override error property but only getter
- // https://github.com/shouldjs/should.js/blob/889e22ebf19a06bc2747d24cf34b25cc00b37464/lib/assertion-error.js#L26
- Object.defineProperty(err, 'message', {
- get() {
- return originMessage + ' (uncaughtException throw ' + throwErrorCount + ' times on pid:' + process.pid + ')';
- },
- configurable: true,
- enumerable: false,
- });
- }
- this.coreLogger.error(err);
- },
- });
- server.on('clientError', (err, socket) => this.onClientError(err, socket));
- // server timeout
- if (is.number(this.config.serverTimeout)) server.setTimeout(this.config.serverTimeout);
- }
- /**
- * global locals for view
- * @member {Object} Application#locals
- * @see Context#locals
- */
- get locals() {
- if (!this[LOCALS]) {
- this[LOCALS] = {};
- }
- return this[LOCALS];
- }
- set locals(val) {
- if (!this[LOCALS]) {
- this[LOCALS] = {};
- }
- assign(this[LOCALS], val);
- }
- handleRequest(ctx, fnMiddleware) {
- this.emit('request', ctx);
- onFinished(ctx.res, () => this.emit('response', ctx));
- return super.handleRequest(ctx, fnMiddleware);
- }
- /**
- * save routers to `run/router.json`
- * @private
- */
- dumpConfig() {
- super.dumpConfig();
- // dump routers to router.json
- const rundir = this.config.rundir;
- const FULLPATH = this.loader.FileLoader.FULLPATH;
- try {
- const dumpRouterFile = path.join(rundir, 'router.json');
- const routers = [];
- for (const layer of this.router.stack) {
- routers.push({
- name: layer.name,
- methods: layer.methods,
- paramNames: layer.paramNames,
- path: layer.path,
- regexp: layer.regexp.toString(),
- stack: layer.stack.map(stack => stack[FULLPATH] || stack._name || stack.name || 'anonymous'),
- });
- }
- fs.writeFileSync(dumpRouterFile, JSON.stringify(routers, null, 2));
- } catch (err) {
- this.coreLogger.warn(`dumpConfig router.json error: ${err.message}`);
- }
- }
- /**
- * Run async function in the background
- * @see Context#runInBackground
- * @param {Function} scope - the first args is an anonymous ctx
- */
- runInBackground(scope) {
- const ctx = this.createAnonymousContext();
- if (!scope.name) scope._name = eggUtils.getCalleeFromStack(true);
- ctx.runInBackground(scope);
- }
- /**
- * secret key for Application
- * @member {String} Application#keys
- */
- get keys() {
- if (!this[KEYS]) {
- if (!this.config.keys) {
- if (this.config.env === 'local' || this.config.env === 'unittest') {
- const configPath = path.join(this.config.baseDir, 'config/config.default.js');
- console.error('Cookie need secret key to sign and encrypt.');
- console.error('Please add `config.keys` in %s', configPath);
- }
- throw new Error('Please set config.keys first');
- }
- this[KEYS] = this.config.keys.split(',').map(s => s.trim());
- }
- return this[KEYS];
- }
- set keys(_) {
- // ignore
- }
- /**
- * reference to {@link Helper}
- * @member {Helper} Application#Helper
- */
- get Helper() {
- if (!this[HELPER]) {
- /**
- * The Helper class which can be used as utility function.
- * We support developers to extend Helper through ${baseDir}/app/extend/helper.js ,
- * then you can use all method on `ctx.helper` that is a instance of Helper.
- */
- class Helper extends this.BaseContextClass {}
- this[HELPER] = Helper;
- }
- return this[HELPER];
- }
- /**
- * bind app's events
- *
- * @private
- */
- [BIND_EVENTS]() {
- // Browser Cookie Limits: http://browsercookielimits.squawky.net/
- this.on('cookieLimitExceed', ({ name, value, ctx }) => {
- const err = new Error(`cookie ${name}'s length(${value.length}) exceed the limit(4093)`);
- err.name = 'CookieLimitExceedError';
- err.key = name;
- err.cookie = value;
- ctx.coreLogger.error(err);
- });
- // expose server to support websocket
- this.once('server', server => this.onServer(server));
- }
- /**
- * warn when confused configurations are present
- *
- * @private
- */
- [WARN_CONFUSED_CONFIG]() {
- const confusedConfigurations = this.config.confusedConfigurations;
- Object.keys(confusedConfigurations).forEach(key => {
- if (this.config[key] !== undefined) {
- this.logger.warn('Unexpected config key `%s` exists, Please use `%s` instead.',
- key, confusedConfigurations[key]);
- }
- });
- }
- }
- module.exports = Application;
|