@depup/hapi 18.1.0-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/core.js ADDED
@@ -0,0 +1,680 @@
1
+ 'use strict';
2
+
3
+ const Http = require('http');
4
+ const Https = require('https');
5
+ const Os = require('os');
6
+ const Path = require('path');
7
+
8
+ const Boom = require('boom');
9
+ const Bounce = require('bounce');
10
+ const Call = require('call');
11
+ const Catbox = require('catbox');
12
+ const CatboxMemory = require('catbox-memory');
13
+ const Heavy = require('heavy');
14
+ const Hoek = require('hoek');
15
+ const Mimos = require('mimos');
16
+ const Podium = require('podium');
17
+ const Somever = require('somever');
18
+ const Statehood = require('statehood');
19
+
20
+ const Auth = require('./auth');
21
+ const Compression = require('./compression');
22
+ const Config = require('./config');
23
+ const Cors = require('./cors');
24
+ const Ext = require('./ext');
25
+ const Methods = require('./methods');
26
+ const Request = require('./request');
27
+ const Route = require('./route');
28
+ const Toolkit = require('./toolkit');
29
+
30
+
31
+ const internals = {
32
+ counter: {
33
+ min: 10000,
34
+ max: 99999
35
+ },
36
+ events: [
37
+ { name: 'log', channels: ['app', 'internal'], tags: true },
38
+ { name: 'request', channels: ['app', 'internal', 'error'], tags: true, spread: true },
39
+ 'response',
40
+ 'route',
41
+ 'start',
42
+ 'stop'
43
+ ],
44
+ badRequestResponse: Buffer.from('HTTP/1.1 400 Bad Request\r\n\r\n', 'ascii')
45
+ };
46
+
47
+
48
+ exports = module.exports = internals.Core = class {
49
+
50
+ constructor(options) {
51
+
52
+ this.root = null; // Dispatch reference of the root server
53
+
54
+ const { settings, type } = internals.setup(options);
55
+
56
+ this.settings = settings;
57
+ this.type = type;
58
+
59
+ this.app = {};
60
+ this.auth = new Auth(this);
61
+ this.caches = new Map(); // Cache clients
62
+ this.compression = new Compression();
63
+ this.controlled = null; // Other servers linked to the phases of this server
64
+ this.decorations = { handler: [], request: [], server: [], toolkit: [] }; // Public decoration names
65
+ this.dependencies = []; // Plugin dependencies
66
+ this.events = new Podium(internals.events);
67
+ this.heavy = new Heavy(this.settings.load);
68
+ this.instances = new Set();
69
+ this.methods = new Methods(this); // Server methods
70
+ this.mime = new Mimos(this.settings.mime);
71
+ this.onConnection = null; // Used to remove event listener on stop
72
+ this.plugins = {}; // Exposed plugin properties by name
73
+ this.queue = new internals.Queue(this.settings.load);
74
+ this.registrations = {}; // Tracks plugin for dependency validation { name -> { version } }
75
+ this.registring = 0; // > 0 while register() is waiting for plugin callbacks
76
+ this.requestCounter = { value: internals.counter.min, min: internals.counter.min, max: internals.counter.max };
77
+ this.router = new Call.Router(this.settings.router);
78
+ this.phase = 'stopped'; // 'stopped', 'initializing', 'initialized', 'starting', 'started', 'stopping', 'invalid'
79
+ this.sockets = null; // Track open sockets for graceful shutdown
80
+ this.started = false;
81
+ this.states = new Statehood.Definitions(this.settings.state);
82
+ this.toolkit = new Toolkit();
83
+
84
+ this.extensionsSeq = 0; // Used to keep absolute order of extensions based on the order added across locations
85
+ this.extensions = {
86
+ server: {
87
+ onPreStart: new Ext('onPreStart', this),
88
+ onPostStart: new Ext('onPostStart', this),
89
+ onPreStop: new Ext('onPreStop', this),
90
+ onPostStop: new Ext('onPostStop', this)
91
+ },
92
+ route: {
93
+ onRequest: new Ext('onRequest', this),
94
+ onPreAuth: new Ext('onPreAuth', this),
95
+ onCredentials: new Ext('onCredentials', this),
96
+ onPostAuth: new Ext('onPostAuth', this),
97
+ onPreHandler: new Ext('onPreHandler', this),
98
+ onPostHandler: new Ext('onPostHandler', this),
99
+ onPreResponse: new Ext('onPreResponse', this)
100
+ }
101
+ };
102
+
103
+ this.Request = class extends Request { };
104
+
105
+ this._debug();
106
+ this._decorations = { handler: {}, request: {}, server: {}, toolkit: {}, requestApply: null };
107
+ this._initializeCache();
108
+
109
+ this.listener = this._createListener();
110
+ this._initializeListener();
111
+ this.info = this._info();
112
+ }
113
+
114
+ _debug() {
115
+
116
+ // Subscribe to server log events
117
+
118
+ if (this.settings.debug) {
119
+ const debug = (request, event) => {
120
+
121
+ const data = event.error || event.data;
122
+ console.error('Debug:', event.tags.join(', '), (data ? '\n ' + (data.stack || (typeof data === 'object' ? Hoek.stringify(data) : data)) : ''));
123
+ };
124
+
125
+ if (this.settings.debug.log) {
126
+ const filter = this.settings.debug.log.some((tag) => tag === '*') ? undefined : this.settings.debug.log;
127
+ this.events.on({ name: 'log', filter }, (event) => debug(null, event));
128
+ }
129
+
130
+ if (this.settings.debug.request) {
131
+ const filter = this.settings.debug.request.some((tag) => tag === '*') ? undefined : this.settings.debug.request;
132
+ this.events.on({ name: 'request', filter }, debug);
133
+ }
134
+ }
135
+ }
136
+
137
+ _initializeCache() {
138
+
139
+ if (this.settings.cache) {
140
+ this._createCache(this.settings.cache);
141
+ }
142
+
143
+ if (!this.caches.has('_default')) {
144
+ this._createCache([{ provider: CatboxMemory }]); // Defaults to memory-based
145
+ }
146
+ }
147
+
148
+ _info() {
149
+
150
+ const now = Date.now();
151
+ const protocol = this.type === 'tcp' ? (this.settings.tls ? 'https' : 'http') : this.type;
152
+ const host = this.settings.host || Os.hostname() || 'localhost';
153
+ const port = this.settings.port;
154
+
155
+ const info = {
156
+ created: now,
157
+ started: 0,
158
+ host,
159
+ port,
160
+ protocol,
161
+ id: Os.hostname() + ':' + process.pid + ':' + now.toString(36),
162
+ uri: this.settings.uri || (protocol + ':' + (this.type === 'tcp' ? '//' + host + (port ? ':' + port : '') : port))
163
+ };
164
+
165
+ return info;
166
+ }
167
+
168
+ _createCache(configs) {
169
+
170
+ Hoek.assert(this.phase !== 'initializing', 'Cannot provision server cache while server is initializing');
171
+
172
+ configs = Config.apply('cache', configs);
173
+
174
+ const added = [];
175
+ for (let config of configs) {
176
+
177
+ // <function>
178
+ // { provider: <function> }
179
+ // { provider: { constructor: <function>, options } }
180
+ // { engine }
181
+
182
+ if (typeof config === 'function') {
183
+ config = { provider: { constructor: config } };
184
+ }
185
+
186
+ const name = config.name || '_default';
187
+ Hoek.assert(!this.caches.has(name), 'Cannot configure the same cache more than once: ', name === '_default' ? 'default cache' : name);
188
+
189
+ let client = null;
190
+
191
+ if (config.provider) {
192
+ let provider = config.provider;
193
+ if (typeof provider === 'function') {
194
+ provider = { constructor: provider };
195
+ }
196
+
197
+ client = new Catbox.Client(provider.constructor, provider.options || { partition: 'hapi-cache' });
198
+ }
199
+ else {
200
+ client = new Catbox.Client(config.engine);
201
+ }
202
+
203
+ this.caches.set(name, { client, segments: {}, shared: config.shared || false });
204
+ added.push(client);
205
+ }
206
+
207
+ return added;
208
+ }
209
+
210
+ registerServer(server) {
211
+
212
+ if (!this.root) {
213
+ this.root = server;
214
+ this._defaultRoutes();
215
+ }
216
+
217
+ this.instances.add(server);
218
+ }
219
+
220
+ async _start() {
221
+
222
+ if (this.phase === 'initialized' ||
223
+ this.phase === 'started') {
224
+
225
+ this._validateDeps();
226
+ }
227
+
228
+ if (this.phase === 'started') {
229
+ return;
230
+ }
231
+
232
+ if (this.phase !== 'stopped' &&
233
+ this.phase !== 'initialized') {
234
+
235
+ throw new Error('Cannot start server while it is in ' + this.phase + ' phase');
236
+ }
237
+
238
+ if (this.phase !== 'initialized') {
239
+ await this._initialize();
240
+ }
241
+
242
+ this.phase = 'starting';
243
+ this.started = true;
244
+ this.info.started = Date.now();
245
+
246
+ try {
247
+ await this._listen();
248
+ }
249
+ catch (err) {
250
+ this.started = false;
251
+ this.phase = 'invalid';
252
+ throw err;
253
+ }
254
+
255
+ this.phase = 'started';
256
+ await this.events.emit('start');
257
+
258
+ try {
259
+ if (this.controlled) {
260
+ await Promise.all(this.controlled.map((control) => control.start()));
261
+ }
262
+
263
+ await this._invoke('onPostStart');
264
+ }
265
+ catch (err) {
266
+ this.phase = 'invalid';
267
+ throw err;
268
+ }
269
+ }
270
+
271
+ _listen() {
272
+
273
+ return new Promise((resolve, reject) => {
274
+
275
+ if (!this.settings.autoListen) {
276
+ resolve();
277
+ return;
278
+ }
279
+
280
+ const onError = (err) => {
281
+
282
+ reject(err);
283
+ return;
284
+ };
285
+
286
+ this.listener.once('error', onError);
287
+
288
+ const finalize = () => {
289
+
290
+ this.listener.removeListener('error', onError);
291
+ resolve();
292
+ return;
293
+ };
294
+
295
+ if (this.type !== 'tcp') {
296
+ this.listener.listen(this.settings.port, finalize);
297
+ }
298
+ else {
299
+ const address = this.settings.address || this.settings.host || '0.0.0.0';
300
+ this.listener.listen(this.settings.port, address, finalize);
301
+ }
302
+ });
303
+ }
304
+
305
+ async _initialize() {
306
+
307
+ if (this.registring) {
308
+ throw new Error('Cannot start server before plugins finished registration');
309
+ }
310
+
311
+ if (this.phase === 'initialized') {
312
+ return;
313
+ }
314
+
315
+ if (this.phase !== 'stopped') {
316
+ throw new Error('Cannot initialize server while it is in ' + this.phase + ' phase');
317
+ }
318
+
319
+ this._validateDeps();
320
+ this.phase = 'initializing';
321
+
322
+ // Start cache
323
+
324
+ try {
325
+ const caches = [];
326
+ this.caches.forEach((cache) => caches.push(cache.client.start()));
327
+ await Promise.all(caches);
328
+ await this._invoke('onPreStart');
329
+ this.heavy.start();
330
+ this.phase = 'initialized';
331
+
332
+ if (this.controlled) {
333
+ await Promise.all(this.controlled.map((control) => control.initialize()));
334
+ }
335
+ }
336
+ catch (err) {
337
+ this.phase = 'invalid';
338
+ throw err;
339
+ }
340
+ }
341
+
342
+ _validateDeps() {
343
+
344
+ for (const { deps, plugin } of this.dependencies) {
345
+ for (const dep in deps) {
346
+ const version = deps[dep];
347
+ Hoek.assert(this.registrations[dep], 'Plugin', plugin, 'missing dependency', dep);
348
+ Hoek.assert(version === '*' || Somever.match(this.registrations[dep].version, version), 'Plugin', plugin, 'requires', dep, 'version', version, 'but found', this.registrations[dep].version);
349
+ }
350
+ }
351
+ }
352
+
353
+ async _stop(options = {}) {
354
+
355
+ options.timeout = options.timeout || 5000; // Default timeout to 5 seconds
356
+
357
+ if (['stopped', 'initialized', 'started', 'invalid'].indexOf(this.phase) === -1) {
358
+ throw new Error('Cannot stop server while in ' + this.phase + ' phase');
359
+ }
360
+
361
+ this.phase = 'stopping';
362
+
363
+ try {
364
+ await this._invoke('onPreStop');
365
+
366
+ if (this.started) {
367
+ this.started = false;
368
+ this.info.started = 0;
369
+
370
+ await this._unlisten(options);
371
+ }
372
+
373
+ const caches = [];
374
+ this.caches.forEach((cache) => caches.push(cache.client.stop()));
375
+ await Promise.all(caches);
376
+
377
+ await this.events.emit('stop');
378
+ this.heavy.stop();
379
+
380
+ if (this.controlled) {
381
+ await Promise.all(this.controlled.map((control) => control.stop(options)));
382
+ }
383
+
384
+ await this._invoke('onPostStop');
385
+ this.phase = 'stopped';
386
+ }
387
+ catch (err) {
388
+ this.phase = 'invalid';
389
+ throw err;
390
+ }
391
+ }
392
+
393
+ _unlisten(options) {
394
+
395
+ let timeoutId = null;
396
+ if (this.settings.operations.cleanStop) {
397
+
398
+ // Set connections timeout
399
+
400
+ const timeout = () => {
401
+
402
+ this.sockets.forEach((connection) => connection.destroy());
403
+ this.sockets.clear();
404
+ };
405
+
406
+ timeoutId = setTimeout(timeout, options.timeout);
407
+
408
+ // Tell idle keep-alive connections to close
409
+
410
+ this.sockets.forEach((connection) => {
411
+
412
+ if (!connection._isHapiProcessing) {
413
+ connection.end();
414
+ }
415
+ });
416
+ }
417
+
418
+ // Close connection
419
+
420
+ return new Promise((resolve) => {
421
+
422
+ this.listener.close(() => {
423
+
424
+ if (this.settings.operations.cleanStop) {
425
+ this.listener.removeListener(this.settings.tls ? 'secureConnection' : 'connection', this.onConnection);
426
+ clearTimeout(timeoutId);
427
+ }
428
+
429
+ this._initializeListener();
430
+ resolve();
431
+ });
432
+ });
433
+ }
434
+
435
+ async _invoke(type) {
436
+
437
+ const exts = this.extensions.server[type];
438
+ if (!exts.nodes) {
439
+ return;
440
+ }
441
+
442
+ for (const ext of exts.nodes) {
443
+ const bind = (ext.bind || ext.realm.settings.bind);
444
+ await ext.func.call(bind, ext.server, bind);
445
+ }
446
+ }
447
+
448
+ _defaultRoutes() {
449
+
450
+ this.router.special('notFound', new Route({ method: '_special', path: '/{p*}', handler: internals.notFound }, this.root, { special: true }));
451
+ this.router.special('badRequest', new Route({ method: '_special', path: '/{p*}', handler: internals.badRequest }, this.root, { special: true }));
452
+
453
+ if (this.settings.routes.cors) {
454
+ Cors.handler(this.root);
455
+ }
456
+ }
457
+
458
+ _dispatch(options = {}) {
459
+
460
+ return (req, res) => {
461
+
462
+ // Track socket request processing state
463
+
464
+ if (req.socket) {
465
+ req.socket._isHapiProcessing = true;
466
+ const env = { core: this, req };
467
+ res.on('finish', internals.onFinish.bind(res, env));
468
+ }
469
+
470
+ // Create request
471
+
472
+ const request = Request.generate(this.root, req, res, options);
473
+
474
+ // Check load
475
+
476
+ if (this.settings.load.sampleInterval) {
477
+ try {
478
+ this.heavy.check();
479
+ }
480
+ catch (err) {
481
+ Bounce.rethrow(err, 'system');
482
+ this._log(['load'], this.heavy.load);
483
+ request._reply(err);
484
+ return;
485
+ }
486
+ }
487
+
488
+ this.queue.add(request);
489
+ };
490
+ }
491
+
492
+ _createListener() {
493
+
494
+ const listener = this.settings.listener || (this.settings.tls ? Https.createServer(this.settings.tls) : Http.createServer());
495
+ listener.on('request', this._dispatch());
496
+ listener.on('checkContinue', this._dispatch({ expectContinue: true }));
497
+
498
+ listener.on('clientError', (err, socket) => {
499
+
500
+ this._log(['connection', 'client', 'error'], err);
501
+
502
+ if (socket.writable) {
503
+ socket.end(internals.badRequestResponse);
504
+ }
505
+ else {
506
+ socket.destroy(err);
507
+ }
508
+ });
509
+
510
+ return listener;
511
+ }
512
+
513
+ _initializeListener() {
514
+
515
+ this.listener.once('listening', () => {
516
+
517
+ // Update the address, port, and uri with active values
518
+
519
+ if (this.type === 'tcp') {
520
+ const address = this.listener.address();
521
+ this.info.address = address.address;
522
+ this.info.port = address.port;
523
+ this.info.uri = (this.settings.uri || (this.info.protocol + '://' + this.info.host + ':' + this.info.port));
524
+ }
525
+
526
+ if (this.settings.operations.cleanStop) {
527
+ this.sockets = new Set();
528
+
529
+ const self = this;
530
+ const onClose = function () { // 'this' is bound to the emitter
531
+
532
+ self.sockets.delete(this);
533
+ };
534
+
535
+ this.onConnection = (connection) => {
536
+
537
+ this.sockets.add(connection);
538
+ connection.on('close', onClose);
539
+ };
540
+
541
+ this.listener.on(this.settings.tls ? 'secureConnection' : 'connection', this.onConnection);
542
+ }
543
+ });
544
+ }
545
+
546
+ _cachePolicy(options, _segment, realm) {
547
+
548
+ options = Config.apply('cachePolicy', options);
549
+
550
+ const plugin = realm && realm.plugin;
551
+ const segment = options.segment || _segment || (plugin ? `!${plugin}` : '');
552
+ Hoek.assert(segment, 'Missing cache segment name');
553
+
554
+ const cacheName = options.cache || '_default';
555
+ const cache = this.caches.get(cacheName);
556
+ Hoek.assert(cache, 'Unknown cache', cacheName);
557
+ Hoek.assert(!cache.segments[segment] || cache.shared || options.shared, 'Cannot provision the same cache segment more than once');
558
+ cache.segments[segment] = true;
559
+
560
+ return new Catbox.Policy(options, cache.client, segment);
561
+ }
562
+
563
+ log(tags, data) {
564
+
565
+ return this._log(tags, data, 'app');
566
+ }
567
+
568
+ _log(tags, data, channel = 'internal') {
569
+
570
+ if (!this.events.hasListeners('log')) {
571
+ return;
572
+ }
573
+
574
+ if (!Array.isArray(tags)) {
575
+ tags = [tags];
576
+ }
577
+
578
+ const timestamp = Date.now();
579
+ const field = (data instanceof Error ? 'error' : 'data');
580
+
581
+ let event = { timestamp, tags, [field]: data, channel };
582
+
583
+ if (typeof data === 'function') {
584
+ event = () => ({ timestamp, tags, data: data(), channel });
585
+ }
586
+
587
+ this.events.emit({ name: 'log', tags, channel }, event);
588
+ }
589
+ };
590
+
591
+
592
+ internals.setup = function (options = {}) {
593
+
594
+ let settings = Hoek.cloneWithShallow(options, ['cache', 'listener', 'routes.bind']);
595
+ settings.routes = Config.enable(settings.routes);
596
+ settings = Config.apply('server', settings);
597
+
598
+ if (settings.port === undefined) {
599
+ settings.port = 0;
600
+ }
601
+
602
+ const type = (typeof settings.port === 'string' ? 'socket' : 'tcp');
603
+ if (type === 'socket') {
604
+ settings.port = (settings.port.indexOf('/') !== -1 ? Path.resolve(settings.port) : settings.port.toLowerCase());
605
+ }
606
+
607
+ if (settings.autoListen === undefined) {
608
+ settings.autoListen = true;
609
+ }
610
+
611
+ Hoek.assert(settings.autoListen || !settings.port, 'Cannot specify port when autoListen is false');
612
+ Hoek.assert(settings.autoListen || !settings.address, 'Cannot specify address when autoListen is false');
613
+
614
+ return { settings, type };
615
+ };
616
+
617
+
618
+ internals.notFound = function () {
619
+
620
+ throw Boom.notFound();
621
+ };
622
+
623
+
624
+ internals.badRequest = function () {
625
+
626
+ throw Boom.badRequest();
627
+ };
628
+
629
+
630
+ internals.onFinish = function (env) {
631
+
632
+ const { core, req } = env;
633
+
634
+ req.socket._isHapiProcessing = false;
635
+ if (!core.started) {
636
+ req.socket.end();
637
+ }
638
+ };
639
+
640
+
641
+ internals.Queue = class {
642
+
643
+ constructor(options) {
644
+
645
+ this.settings = options;
646
+
647
+ this.active = 0;
648
+ this.queue = [];
649
+ }
650
+
651
+ add(request) {
652
+
653
+ if (this.settings.concurrent) {
654
+ this.queue.push(request);
655
+ this.next();
656
+ }
657
+ else {
658
+ request._execute();
659
+ }
660
+ }
661
+
662
+ next() {
663
+
664
+ if (this.queue.length &&
665
+ this.active < this.settings.concurrent) {
666
+
667
+ const request = this.queue.shift();
668
+ ++this.active;
669
+ request._execute();
670
+ }
671
+ }
672
+
673
+ release() {
674
+
675
+ if (this.settings.concurrent) {
676
+ --this.active;
677
+ this.next();
678
+ }
679
+ }
680
+ };