application.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. 'use strict';
  2. const path = require('path');
  3. const fs = require('fs');
  4. const ms = require('ms');
  5. const is = require('is-type-of');
  6. const graceful = require('graceful');
  7. const http = require('http');
  8. const cluster = require('cluster-client');
  9. const onFinished = require('on-finished');
  10. const { assign } = require('utility');
  11. const eggUtils = require('egg-core').utils;
  12. const EggApplication = require('./egg');
  13. const AppWorkerLoader = require('./loader').AppWorkerLoader;
  14. const KEYS = Symbol('Application#keys');
  15. const HELPER = Symbol('Application#Helper');
  16. const LOCALS = Symbol('Application#locals');
  17. const BIND_EVENTS = Symbol('Application#bindEvents');
  18. const WARN_CONFUSED_CONFIG = Symbol('Application#warnConfusedConfig');
  19. const EGG_LOADER = Symbol.for('egg#loader');
  20. const EGG_PATH = Symbol.for('egg#eggPath');
  21. const CLUSTER_CLIENTS = Symbol.for('egg#clusterClients');
  22. const RESPONSE_RAW = Symbol('Application#responseRaw');
  23. // client error => 400 Bad Request
  24. // Refs: https://nodejs.org/dist/latest-v8.x/docs/api/http.html#http_event_clienterror
  25. const DEFAULT_BAD_REQUEST_HTML = `<html>
  26. <head><title>400 Bad Request</title></head>
  27. <body bgcolor="white">
  28. <center><h1>400 Bad Request</h1></center>
  29. <hr><center>❤</center>
  30. </body>
  31. </html>`;
  32. const DEFAULT_BAD_REQUEST_HTML_LENGTH = Buffer.byteLength(DEFAULT_BAD_REQUEST_HTML);
  33. const DEFAULT_BAD_REQUEST_RESPONSE =
  34. `HTTP/1.1 400 Bad Request\r\nContent-Length: ${DEFAULT_BAD_REQUEST_HTML_LENGTH}` +
  35. `\r\n\r\n${DEFAULT_BAD_REQUEST_HTML}`;
  36. // Refs: https://github.com/nodejs/node/blob/b38c81/lib/_http_outgoing.js#L706-L710
  37. function escapeHeaderValue(value) {
  38. // Protect against response splitting. The regex test is there to
  39. // minimize the performance impact in the common case.
  40. return /[\r\n]/.test(value) ? value.replace(/[\r\n]+[ \t]*/g, '') : value;
  41. }
  42. // Refs: https://github.com/nodejs/node/blob/b38c81/lib/_http_outgoing.js#L706-L710
  43. /**
  44. * Singleton instance in App Worker, extend {@link EggApplication}
  45. * @extends EggApplication
  46. */
  47. class Application extends EggApplication {
  48. /**
  49. * @class
  50. * @param {Object} options - see {@link EggApplication}
  51. */
  52. constructor(options = {}) {
  53. options.type = 'application';
  54. super(options);
  55. // will auto set after 'server' event emit
  56. this.server = null;
  57. try {
  58. this.loader.load();
  59. } catch (e) {
  60. // close gracefully
  61. this[CLUSTER_CLIENTS].forEach(cluster.close);
  62. throw e;
  63. }
  64. // dump config after loaded, ensure all the dynamic modifications will be recorded
  65. const dumpStartTime = Date.now();
  66. this.dumpConfig();
  67. this.coreLogger.info('[egg:core] dump config after load, %s', ms(Date.now() - dumpStartTime));
  68. this[WARN_CONFUSED_CONFIG]();
  69. this[BIND_EVENTS]();
  70. }
  71. get [EGG_LOADER]() {
  72. return AppWorkerLoader;
  73. }
  74. get [EGG_PATH]() {
  75. return path.join(__dirname, '..');
  76. }
  77. [RESPONSE_RAW](socket, raw) {
  78. /* istanbul ignore next */
  79. if (!socket.writable) return;
  80. if (!raw) return socket.end(DEFAULT_BAD_REQUEST_RESPONSE);
  81. const body = (raw.body == null) ? DEFAULT_BAD_REQUEST_HTML : raw.body;
  82. const headers = raw.headers || {};
  83. const status = raw.status || 400;
  84. let responseHeaderLines = '';
  85. const firstLine = `HTTP/1.1 ${status} ${http.STATUS_CODES[status] || 'Unknown'}`;
  86. // Not that safe because no validation for header keys.
  87. // Refs: https://github.com/nodejs/node/blob/b38c81/lib/_http_outgoing.js#L451
  88. for (const key of Object.keys(headers)) {
  89. if (key.toLowerCase() === 'content-length') {
  90. delete headers[key];
  91. continue;
  92. }
  93. responseHeaderLines += `${key}: ${escapeHeaderValue(headers[key])}\r\n`;
  94. }
  95. responseHeaderLines += `Content-Length: ${Buffer.byteLength(body)}\r\n`;
  96. socket.end(`${firstLine}\r\n${responseHeaderLines}\r\n${body.toString()}`);
  97. }
  98. onClientError(err, socket) {
  99. // ignore when there is no http body, it almost like an ECONNRESET
  100. if (err.rawPacket) {
  101. this.logger.warn('A client (%s:%d) error [%s] occurred: %s',
  102. socket.remoteAddress,
  103. socket.remotePort,
  104. err.code,
  105. err.message);
  106. }
  107. if (typeof this.config.onClientError === 'function') {
  108. const p = eggUtils.callFn(this.config.onClientError, [ err, socket, this ]);
  109. // the returned object should be something like:
  110. //
  111. // {
  112. // body: '...',
  113. // headers: {
  114. // ...
  115. // },
  116. // status: 400
  117. // }
  118. //
  119. // default values:
  120. //
  121. // + body: ''
  122. // + headers: {}
  123. // + status: 400
  124. p.then(ret => {
  125. this[RESPONSE_RAW](socket, ret || {});
  126. }).catch(err => {
  127. this.logger.error(err);
  128. this[RESPONSE_RAW](socket);
  129. });
  130. } else {
  131. // because it's a raw socket object, we should return the raw HTTP response
  132. // packet.
  133. this[RESPONSE_RAW](socket);
  134. }
  135. }
  136. onServer(server) {
  137. // expose app.server
  138. this.server = server;
  139. /* istanbul ignore next */
  140. graceful({
  141. server: [ server ],
  142. error: (err, throwErrorCount) => {
  143. const originMessage = err.message;
  144. if (originMessage) {
  145. // shouldjs will override error property but only getter
  146. // https://github.com/shouldjs/should.js/blob/889e22ebf19a06bc2747d24cf34b25cc00b37464/lib/assertion-error.js#L26
  147. Object.defineProperty(err, 'message', {
  148. get() {
  149. return originMessage + ' (uncaughtException throw ' + throwErrorCount + ' times on pid:' + process.pid + ')';
  150. },
  151. configurable: true,
  152. enumerable: false,
  153. });
  154. }
  155. this.coreLogger.error(err);
  156. },
  157. });
  158. server.on('clientError', (err, socket) => this.onClientError(err, socket));
  159. // server timeout
  160. if (is.number(this.config.serverTimeout)) server.setTimeout(this.config.serverTimeout);
  161. }
  162. /**
  163. * global locals for view
  164. * @member {Object} Application#locals
  165. * @see Context#locals
  166. */
  167. get locals() {
  168. if (!this[LOCALS]) {
  169. this[LOCALS] = {};
  170. }
  171. return this[LOCALS];
  172. }
  173. set locals(val) {
  174. if (!this[LOCALS]) {
  175. this[LOCALS] = {};
  176. }
  177. assign(this[LOCALS], val);
  178. }
  179. handleRequest(ctx, fnMiddleware) {
  180. this.emit('request', ctx);
  181. onFinished(ctx.res, () => this.emit('response', ctx));
  182. return super.handleRequest(ctx, fnMiddleware);
  183. }
  184. /**
  185. * save routers to `run/router.json`
  186. * @private
  187. */
  188. dumpConfig() {
  189. super.dumpConfig();
  190. // dump routers to router.json
  191. const rundir = this.config.rundir;
  192. const FULLPATH = this.loader.FileLoader.FULLPATH;
  193. try {
  194. const dumpRouterFile = path.join(rundir, 'router.json');
  195. const routers = [];
  196. for (const layer of this.router.stack) {
  197. routers.push({
  198. name: layer.name,
  199. methods: layer.methods,
  200. paramNames: layer.paramNames,
  201. path: layer.path,
  202. regexp: layer.regexp.toString(),
  203. stack: layer.stack.map(stack => stack[FULLPATH] || stack._name || stack.name || 'anonymous'),
  204. });
  205. }
  206. fs.writeFileSync(dumpRouterFile, JSON.stringify(routers, null, 2));
  207. } catch (err) {
  208. this.coreLogger.warn(`dumpConfig router.json error: ${err.message}`);
  209. }
  210. }
  211. /**
  212. * Run async function in the background
  213. * @see Context#runInBackground
  214. * @param {Function} scope - the first args is an anonymous ctx
  215. */
  216. runInBackground(scope) {
  217. const ctx = this.createAnonymousContext();
  218. if (!scope.name) scope._name = eggUtils.getCalleeFromStack(true);
  219. ctx.runInBackground(scope);
  220. }
  221. /**
  222. * secret key for Application
  223. * @member {String} Application#keys
  224. */
  225. get keys() {
  226. if (!this[KEYS]) {
  227. if (!this.config.keys) {
  228. if (this.config.env === 'local' || this.config.env === 'unittest') {
  229. const configPath = path.join(this.config.baseDir, 'config/config.default.js');
  230. console.error('Cookie need secret key to sign and encrypt.');
  231. console.error('Please add `config.keys` in %s', configPath);
  232. }
  233. throw new Error('Please set config.keys first');
  234. }
  235. this[KEYS] = this.config.keys.split(',').map(s => s.trim());
  236. }
  237. return this[KEYS];
  238. }
  239. set keys(_) {
  240. // ignore
  241. }
  242. /**
  243. * reference to {@link Helper}
  244. * @member {Helper} Application#Helper
  245. */
  246. get Helper() {
  247. if (!this[HELPER]) {
  248. /**
  249. * The Helper class which can be used as utility function.
  250. * We support developers to extend Helper through ${baseDir}/app/extend/helper.js ,
  251. * then you can use all method on `ctx.helper` that is a instance of Helper.
  252. */
  253. class Helper extends this.BaseContextClass {}
  254. this[HELPER] = Helper;
  255. }
  256. return this[HELPER];
  257. }
  258. /**
  259. * bind app's events
  260. *
  261. * @private
  262. */
  263. [BIND_EVENTS]() {
  264. // Browser Cookie Limits: http://browsercookielimits.squawky.net/
  265. this.on('cookieLimitExceed', ({ name, value, ctx }) => {
  266. const err = new Error(`cookie ${name}'s length(${value.length}) exceed the limit(4093)`);
  267. err.name = 'CookieLimitExceedError';
  268. err.key = name;
  269. err.cookie = value;
  270. ctx.coreLogger.error(err);
  271. });
  272. // expose server to support websocket
  273. this.once('server', server => this.onServer(server));
  274. }
  275. /**
  276. * warn when confused configurations are present
  277. *
  278. * @private
  279. */
  280. [WARN_CONFUSED_CONFIG]() {
  281. const confusedConfigurations = this.config.confusedConfigurations;
  282. Object.keys(confusedConfigurations).forEach(key => {
  283. if (this.config[key] !== undefined) {
  284. this.logger.warn('Unexpected config key `%s` exists, Please use `%s` instead.',
  285. key, confusedConfigurations[key]);
  286. }
  287. });
  288. }
  289. }
  290. module.exports = Application;