@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/request.js ADDED
@@ -0,0 +1,623 @@
1
+ 'use strict';
2
+
3
+ const Url = require('url');
4
+
5
+ const Boom = require('boom');
6
+ const Bounce = require('bounce');
7
+ const Hoek = require('hoek');
8
+ const Podium = require('podium');
9
+
10
+ const Cors = require('./cors');
11
+ const Response = require('./response');
12
+ const Transmit = require('./transmit');
13
+
14
+
15
+ const internals = {
16
+ events: Podium.validate(['finish', { name: 'peek', spread: true }, 'disconnect']),
17
+ reserved: ['server', 'url', 'query', 'path', 'method', 'mime', 'setUrl', 'setMethod', 'headers', 'id', 'app', 'plugins', 'route', 'auth', 'pre', 'preResponses', 'info', 'orig', 'params', 'paramsArray', 'payload', 'state', 'jsonp', 'response', 'raw', 'domain', 'log', 'logs', 'generateResponse']
18
+ };
19
+
20
+
21
+ exports = module.exports = internals.Request = class {
22
+
23
+ constructor(server, req, res, options) {
24
+
25
+ this._allowInternals = !!options.allowInternals;
26
+ this._core = server._core;
27
+ this._entity = null; // Entity information set via h.entity()
28
+ this._eventContext = { request: this };
29
+ this._events = null; // Assigned an emitter when request.events is accessed
30
+ this._expectContinue = !!options.expectContinue;
31
+ this._isPayloadPending = !!(req.headers['content-length'] || req.headers['transfer-encoding']); // Changes to false when incoming payload fully processed
32
+ this._isReplied = false; // true when response processing started
33
+ this._route = this._core.router.specials.notFound.route; // Used prior to routing (only settings are used, not the handler)
34
+ this._serverTimeoutId = null;
35
+ this._states = {};
36
+ this._urlError = null;
37
+
38
+ this.app = (options.app ? Object.assign({}, options.app) : {}); // Place for application-specific state without conflicts with hapi, should not be used by plugins (shallow cloned)
39
+ this.headers = req.headers;
40
+ this.info = internals.info(this._core, req);
41
+ this.jsonp = null;
42
+ this.logs = [];
43
+ this.method = req.method.toLowerCase();
44
+ this.mime = null;
45
+ this.orig = {};
46
+ this.params = null;
47
+ this.paramsArray = null; // Array of path parameters in path order
48
+ this.path = null;
49
+ this.payload = null;
50
+ this.plugins = (options.plugins ? Object.assign({}, options.plugins) : {}); // Place for plugins to store state without conflicts with hapi, should be namespaced using plugin name (shallow cloned)
51
+ this.pre = {}; // Pre raw values
52
+ this.preResponses = {}; // Pre response values
53
+ this.raw = { req, res };
54
+ this.response = null;
55
+ this.route = this._route.public;
56
+ this.query = null;
57
+ this.server = server;
58
+ this.state = null;
59
+ this.url = null;
60
+
61
+ this.auth = {
62
+ isAuthenticated: false,
63
+ isAuthorized: false,
64
+ credentials: options.auth ? options.auth.credentials : null, // Special keys: 'app', 'user', 'scope'
65
+ artifacts: options.auth && options.auth.artifacts || null, // Scheme-specific artifacts
66
+ strategy: options.auth ? options.auth.strategy : null,
67
+ mode: null,
68
+ error: null
69
+ };
70
+
71
+ if (options.auth) {
72
+ this.auth.isInjected = true;
73
+ }
74
+
75
+ // Parse request url
76
+
77
+ this._initializeUrl();
78
+ }
79
+
80
+ static generate(server, req, res, options) {
81
+
82
+ const request = new server._core.Request(server, req, res, options);
83
+
84
+ // Decorate
85
+
86
+ if (server._core._decorations.requestApply) {
87
+ for (const property in server._core._decorations.requestApply) {
88
+ const assignment = server._core._decorations.requestApply[property];
89
+ request[property] = assignment(request);
90
+ }
91
+ }
92
+
93
+ request._listen();
94
+ return request;
95
+ }
96
+
97
+ get events() {
98
+
99
+ if (!this._events) {
100
+ this._events = new Podium(internals.events);
101
+ }
102
+
103
+ return this._events;
104
+ }
105
+
106
+ _initializeUrl() {
107
+
108
+ try {
109
+ this._setUrl(this.raw.req.url, this._core.settings.router.stripTrailingSlash);
110
+ }
111
+ catch (err) {
112
+ this.path = this.raw.req.url;
113
+ this.query = {};
114
+
115
+ this._urlError = Boom.boomify(err, { statusCode: 400, override: false });
116
+ }
117
+ }
118
+
119
+ setUrl(url, stripTrailingSlash) {
120
+
121
+ Hoek.assert(this.params === null, 'Cannot change request URL after routing');
122
+
123
+ if (url instanceof Url.URL) {
124
+ url = url.href;
125
+ }
126
+
127
+ Hoek.assert(typeof url === 'string', 'Url must be a string or URL object');
128
+
129
+ this._setUrl(url, stripTrailingSlash);
130
+ this._urlError = null;
131
+ }
132
+
133
+ _setUrl(url, stripTrailingSlash) {
134
+
135
+ const base = (url[0] === '/' ? `${this._core.info.protocol}://${this.info.host || `${this._core.info.host}:${this._core.info.port}`}` : undefined);
136
+
137
+ url = new Url.URL(url, base);
138
+
139
+ // Apply path modifications
140
+
141
+ let path = this._core.router.normalize(url.pathname); // pathname excludes query
142
+
143
+ if (stripTrailingSlash &&
144
+ path.length > 1 &&
145
+ path[path.length - 1] === '/') {
146
+
147
+ path = path.slice(0, -1);
148
+ }
149
+
150
+ url.pathname = path;
151
+
152
+ // Parse query (must be done before this.url is set in case query parsing throws)
153
+
154
+ this.query = this._parseQuery(url.searchParams);
155
+
156
+ // Store request properties
157
+
158
+ this.url = url;
159
+ this.path = path;
160
+
161
+ this.info.hostname = url.hostname;
162
+ this.info.host = url.host;
163
+ }
164
+
165
+ _parseQuery(searchParams) {
166
+
167
+ // Flatten map
168
+
169
+ let query = Object.create(null);
170
+ for (let [key, value] of searchParams) {
171
+ const entry = query[key];
172
+ if (entry !== undefined) {
173
+ value = [].concat(entry, value);
174
+ }
175
+
176
+ query[key] = value;
177
+ }
178
+
179
+ // Custom parser
180
+
181
+ const parser = this._core.settings.query.parser;
182
+ if (parser) {
183
+ query = parser(query);
184
+ if (!query ||
185
+ typeof query !== 'object') {
186
+
187
+ throw Boom.badImplementation('Parsed query must be an object');
188
+ }
189
+ }
190
+
191
+ return query;
192
+ }
193
+
194
+ setMethod(method) {
195
+
196
+ Hoek.assert(this.params === null, 'Cannot change request method after routing');
197
+ Hoek.assert(method && typeof method === 'string', 'Missing method');
198
+
199
+ this.method = method.toLowerCase();
200
+ }
201
+
202
+ active() {
203
+
204
+ return !!this._eventContext.request;
205
+ }
206
+
207
+ async _execute() {
208
+
209
+ this.info.acceptEncoding = this._core.compression.accept(this);
210
+
211
+ try {
212
+ await this._onRequest();
213
+ }
214
+ catch (err) {
215
+ Bounce.rethrow(err, 'system');
216
+ return this._reply(err);
217
+ }
218
+
219
+ this._lookup();
220
+ this._setTimeouts();
221
+ await this._lifecycle();
222
+ this._reply();
223
+ }
224
+
225
+ async _onRequest() {
226
+
227
+ // onRequest (can change request method and url)
228
+
229
+ if (this._core.extensions.route.onRequest.nodes) {
230
+ const response = await this._invoke(this._core.extensions.route.onRequest);
231
+ if (response) {
232
+ if (!internals.skip(response)) {
233
+ throw Boom.badImplementation('onRequest extension methods must return an error, a takeover response, or a continue signal');
234
+ }
235
+
236
+ throw response;
237
+ }
238
+ }
239
+
240
+ // Validate path
241
+
242
+ if (this._urlError) {
243
+ throw this._urlError;
244
+ }
245
+ }
246
+
247
+ _listen() {
248
+
249
+ if (this._isPayloadPending) {
250
+ this.raw.req.on('end', internals.event.bind(this.raw.req, this._eventContext, 'end'));
251
+ }
252
+
253
+ this.raw.req.on('close', internals.event.bind(this.raw.req, this._eventContext, 'close'));
254
+ this.raw.req.on('error', internals.event.bind(this.raw.req, this._eventContext, 'error'));
255
+ this.raw.req.on('aborted', internals.event.bind(this.raw.req, this._eventContext, 'abort'));
256
+ }
257
+
258
+ _lookup() {
259
+
260
+ const match = this._core.router.route(this.method, this.path, this.info.hostname);
261
+ if (!match.route.settings.isInternal ||
262
+ this._allowInternals) {
263
+
264
+ this._route = match.route;
265
+ this.route = this._route.public;
266
+ }
267
+
268
+ this.params = match.params || {};
269
+ this.paramsArray = match.paramsArray || [];
270
+
271
+ if (this.route.settings.cors) {
272
+ this.info.cors = {
273
+ isOriginMatch: Cors.matchOrigin(this.headers.origin, this.route.settings.cors)
274
+ };
275
+ }
276
+ }
277
+
278
+ _setTimeouts() {
279
+
280
+ if (this.raw.req.socket &&
281
+ this.route.settings.timeout.socket !== undefined) {
282
+
283
+ this.raw.req.socket.setTimeout(this.route.settings.timeout.socket || 0); // Value can be false or positive
284
+ }
285
+
286
+ let serverTimeout = this.route.settings.timeout.server;
287
+ if (!serverTimeout) {
288
+ return;
289
+ }
290
+
291
+ const elapsed = Date.now() - this.info.received;
292
+ serverTimeout = Math.floor(serverTimeout - elapsed); // Calculate the timeout from when the request was constructed
293
+
294
+ if (serverTimeout <= 0) {
295
+ internals.timeoutReply(this, serverTimeout);
296
+ return;
297
+ }
298
+
299
+ this._serverTimeoutId = setTimeout(internals.timeoutReply, serverTimeout, this, serverTimeout);
300
+ }
301
+
302
+ async _lifecycle() {
303
+
304
+ for (const func of this._route._cycle) {
305
+ if (this._isReplied ||
306
+ !this._eventContext.request) {
307
+
308
+ return;
309
+ }
310
+
311
+ try {
312
+ var response = await (typeof func === 'function' ? func(this) : this._invoke(func));
313
+ }
314
+ catch (err) {
315
+ Bounce.rethrow(err, 'system');
316
+ response = Response.wrap(err, this);
317
+ }
318
+
319
+ if (!response ||
320
+ response === this._core.toolkit.continue) { // Continue
321
+
322
+ continue;
323
+ }
324
+
325
+ if (!internals.skip(response)) {
326
+ response = Boom.badImplementation('Lifecycle methods called before the handler can only return an error, a takeover response, or a continue signal');
327
+ }
328
+
329
+ this._setResponse(response);
330
+ return;
331
+ }
332
+ }
333
+
334
+ async _invoke(event) {
335
+
336
+ for (const ext of event.nodes) {
337
+ const bind = (ext.bind || ext.realm.settings.bind);
338
+ const realm = ext.realm;
339
+ const response = await this._core.toolkit.execute(ext.func, this, { bind, realm });
340
+
341
+ if (response === this._core.toolkit.continue) {
342
+ continue;
343
+ }
344
+
345
+ if (internals.skip(response) ||
346
+ this.response === null) {
347
+
348
+ return response;
349
+ }
350
+
351
+ this._setResponse(response);
352
+ }
353
+ }
354
+
355
+ async _reply(exit) {
356
+
357
+ if (this._isReplied) { // Prevent any future responses to this request
358
+ return;
359
+ }
360
+
361
+ this._isReplied = true;
362
+
363
+ if (this._serverTimeoutId) {
364
+ clearTimeout(this._serverTimeoutId);
365
+ }
366
+
367
+ if (!this._eventContext.request) {
368
+ this._finalize();
369
+ return;
370
+ }
371
+
372
+ if (exit) { // Can be a valid response or error (if returned from an ext, already handled because this.response is also set)
373
+ this._setResponse(Response.wrap(exit, this)); // Wrap to ensure any object thrown is always a valid Boom or Response object
374
+ }
375
+
376
+ if (typeof this.response === 'symbol') { // close or abandon
377
+ this._abort();
378
+ return;
379
+ }
380
+
381
+ await this._postCycle();
382
+
383
+ if (!this._eventContext.request ||
384
+ typeof this.response === 'symbol') { // close or abandon
385
+
386
+ this._abort();
387
+ return;
388
+ }
389
+
390
+ await Transmit.send(this);
391
+ this._finalize();
392
+ }
393
+
394
+ async _postCycle() {
395
+
396
+ for (const func of this._route._postCycle) {
397
+ if (!this._eventContext.request) {
398
+ return;
399
+ }
400
+
401
+ try {
402
+ var response = await (typeof func === 'function' ? func(this) : this._invoke(func));
403
+ }
404
+ catch (err) {
405
+ Bounce.rethrow(err, 'system');
406
+ response = Response.wrap(err, this);
407
+ }
408
+
409
+ if (response &&
410
+ response !== this._core.toolkit.continue) { // Continue
411
+
412
+ this._setResponse(response);
413
+ }
414
+ }
415
+ }
416
+
417
+ _abort() {
418
+
419
+ if (this.response === this._core.toolkit.close) {
420
+ this.raw.res.end(); // End the response in case it wasn't already closed
421
+ }
422
+
423
+ this._finalize();
424
+ }
425
+
426
+ _finalize() {
427
+
428
+ if (this.response &&
429
+ this.response.statusCode === 500 &&
430
+ this.response._error) {
431
+
432
+ const tags = this.response._error.isDeveloperError ? ['internal', 'implementation', 'error'] : ['internal', 'error'];
433
+ this._log(tags, this.response._error, 'error');
434
+ }
435
+
436
+ // Cleanup
437
+
438
+ this._eventContext.request = null; // Disable req events
439
+
440
+ if (this.response &&
441
+ this.response._close) {
442
+
443
+ this.response._close(this);
444
+ }
445
+
446
+ this.info.completed = Date.now();
447
+ this._core.events.emit('response', this);
448
+ this._core.queue.release();
449
+ }
450
+
451
+ _setResponse(response) {
452
+
453
+ if (this.response &&
454
+ !this.response.isBoom &&
455
+ this.response !== response &&
456
+ (response.isBoom || this.response.source !== response.source)) {
457
+
458
+ this.response._close(this);
459
+ }
460
+
461
+ if (this.info.completed) {
462
+ if (response._close) {
463
+ response._close(this);
464
+ }
465
+
466
+ return;
467
+ }
468
+
469
+ this.response = response;
470
+ }
471
+
472
+ _setState(name, value, options) {
473
+
474
+ const state = { name, value };
475
+ if (options) {
476
+ Hoek.assert(!options.autoValue, 'Cannot set autoValue directly in a response');
477
+ state.options = Hoek.clone(options);
478
+ }
479
+
480
+ this._states[name] = state;
481
+ }
482
+
483
+ _clearState(name, options = {}) {
484
+
485
+ const state = { name };
486
+
487
+ state.options = Hoek.clone(options);
488
+ state.options.ttl = 0;
489
+
490
+ this._states[name] = state;
491
+ }
492
+
493
+ _tap() {
494
+
495
+ if (!this._events) {
496
+ return null;
497
+ }
498
+
499
+ return (this._events.hasListeners('finish') || this._events.hasListeners('peek') ? new Response.Peek(this._events) : null);
500
+ }
501
+
502
+ log(tags, data) {
503
+
504
+ return this._log(tags, data, 'app');
505
+ }
506
+
507
+ _log(tags, data, channel = 'internal') {
508
+
509
+ if (!this._core.events.hasListeners('request') &&
510
+ !this.route.settings.log.collect) {
511
+
512
+ return;
513
+ }
514
+
515
+ if (!Array.isArray(tags)) {
516
+ tags = [tags];
517
+ }
518
+
519
+ const timestamp = Date.now();
520
+ const field = (data instanceof Error ? 'error' : 'data');
521
+
522
+ let event = [this, { request: this.info.id, timestamp, tags, [field]: data, channel }];
523
+ if (typeof data === 'function') {
524
+ event = () => [this, { request: this.info.id, timestamp, tags, data: data(), channel }];
525
+ }
526
+
527
+ if (this.route.settings.log.collect) {
528
+ if (typeof data === 'function') {
529
+ event = event();
530
+ }
531
+
532
+ this.logs.push(event[1]);
533
+ }
534
+
535
+ this._core.events.emit({ name: 'request', channel, tags }, event);
536
+ }
537
+
538
+ generateResponse(source, options) {
539
+
540
+ return new Response(source, this, options);
541
+ }
542
+ };
543
+
544
+
545
+ internals.Request.reserved = internals.reserved;
546
+
547
+
548
+ internals.info = function (core, req) {
549
+
550
+ const host = req.headers.host ? req.headers.host.trim() : '';
551
+ const received = Date.now();
552
+
553
+ const info = {
554
+ received,
555
+ remoteAddress: req.connection.remoteAddress,
556
+ remotePort: req.connection.remotePort || '',
557
+ referrer: req.headers.referrer || req.headers.referer || '',
558
+ host,
559
+ hostname: host.split(':')[0],
560
+ id: `${received}:${core.info.id}:${core.requestCounter.value++}`,
561
+
562
+ // Assigned later
563
+
564
+ acceptEncoding: null,
565
+ cors: null,
566
+ responded: 0,
567
+ completed: 0
568
+ };
569
+
570
+ if (core.requestCounter.value > core.requestCounter.max) {
571
+ core.requestCounter.value = core.requestCounter.min;
572
+ }
573
+
574
+ return info;
575
+ };
576
+
577
+
578
+ internals.event = function ({ request }, event, err) {
579
+
580
+ if (!request) {
581
+ return;
582
+ }
583
+
584
+ request._isPayloadPending = false;
585
+
586
+ if (event === 'close' &&
587
+ request.raw.res.finished) {
588
+
589
+ return;
590
+ }
591
+
592
+ if (event === 'end') {
593
+ return;
594
+ }
595
+
596
+ request._log(err ? ['request', 'error'] : ['request', 'error', event], err);
597
+
598
+ if (event === 'error') {
599
+ return;
600
+ }
601
+
602
+ request._eventContext.request = null;
603
+
604
+ if (event === 'abort' &&
605
+ request._events) {
606
+
607
+ request._events.emit('disconnect');
608
+ }
609
+ };
610
+
611
+
612
+ internals.timeoutReply = function (request, timeout) {
613
+
614
+ const elapsed = Date.now() - request.info.received;
615
+ request._log(['request', 'server', 'timeout', 'error'], { timeout, elapsed });
616
+ request._reply(Boom.serverUnavailable());
617
+ };
618
+
619
+
620
+ internals.skip = function (response) {
621
+
622
+ return (response.isBoom || response._takeover || typeof response === 'symbol');
623
+ };