@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.
@@ -0,0 +1,730 @@
1
+ 'use strict';
2
+
3
+ const Stream = require('stream');
4
+
5
+ const Boom = require('boom');
6
+ const Bounce = require('bounce');
7
+ const Hoek = require('hoek');
8
+ const Podium = require('podium');
9
+
10
+ const Streams = require('./streams');
11
+
12
+
13
+ const internals = {
14
+ events: Podium.validate(['finish', { name: 'peek', spread: true }]),
15
+ hopByHop: {
16
+ connection: true,
17
+ 'keep-alive': true,
18
+ 'proxy-authenticate': true,
19
+ 'proxy-authorization': true,
20
+ 'te': true,
21
+ 'trailers': true,
22
+ 'transfer-encoding': true,
23
+ 'upgrade': true
24
+ }
25
+ };
26
+
27
+
28
+ exports = module.exports = internals.Response = class {
29
+
30
+ constructor(source, request, options = {}) {
31
+
32
+ this.app = {};
33
+ this.headers = {}; // Incomplete as some headers are stored in flags
34
+ this.plugins = {};
35
+ this.request = request;
36
+ this.source = null;
37
+ this.statusCode = null;
38
+ this.variety = null;
39
+
40
+ this.settings = {
41
+ encoding: 'utf8',
42
+ charset: 'utf-8', // '-' required by IANA
43
+ ttl: null,
44
+ stringify: null, // JSON.stringify options
45
+ passThrough: true,
46
+ varyEtag: false,
47
+ message: null
48
+ };
49
+
50
+ this._events = null;
51
+ this._payload = null; // Readable stream
52
+ this._error = null; // The boom object when created from an error (used for logging)
53
+ this._contentEncoding = null; // Set during transmit
54
+ this._contentType = null; // Used if no explicit content-type is set and type is known
55
+ this._takeover = false;
56
+ this._statusCode = false; // true when code() called
57
+
58
+ this._processors = {
59
+ marshal: options.marshal,
60
+ prepare: options.prepare,
61
+ close: options.close
62
+ };
63
+
64
+ this.temporary = null;
65
+ this.permanent = null;
66
+ this.rewritable = null;
67
+
68
+ this._setSource(source, options.variety);
69
+ }
70
+
71
+ static wrap(result, request) {
72
+
73
+ if (result instanceof internals.Response ||
74
+ typeof result === 'symbol') {
75
+
76
+ return result;
77
+ }
78
+
79
+ if (result instanceof Error) {
80
+ return Boom.boomify(result);
81
+ }
82
+
83
+ return new internals.Response(result, request);
84
+ }
85
+
86
+ _setSource(source, variety) {
87
+
88
+ // Method must not set any headers or other properties as source can change later
89
+
90
+ this.variety = variety || 'plain';
91
+
92
+ if (source === null ||
93
+ source === undefined) {
94
+
95
+ source = null;
96
+ }
97
+ else if (Buffer.isBuffer(source)) {
98
+ this.variety = 'buffer';
99
+ this._contentType = 'application/octet-stream';
100
+ }
101
+ else if (source instanceof Stream) {
102
+ this.variety = 'stream';
103
+ this._contentType = 'application/octet-stream';
104
+ }
105
+
106
+ this.source = source;
107
+
108
+ if (this.variety === 'plain' &&
109
+ this.source !== null) {
110
+
111
+ this._contentType = (typeof this.source === 'string' ? 'text/html' : 'application/json');
112
+ }
113
+ }
114
+
115
+ get events() {
116
+
117
+ if (!this._events) {
118
+ this._events = new Podium(internals.events);
119
+ }
120
+
121
+ return this._events;
122
+ }
123
+
124
+ code(statusCode) {
125
+
126
+ Hoek.assert(Number.isSafeInteger(statusCode), 'Status code must be an integer');
127
+
128
+ this.statusCode = statusCode;
129
+ this._statusCode = true;
130
+
131
+ return this;
132
+ }
133
+
134
+ message(httpMessage) {
135
+
136
+ this.settings.message = httpMessage;
137
+ return this;
138
+ }
139
+
140
+ header(key, value, options) {
141
+
142
+ key = key.toLowerCase();
143
+ if (key === 'vary') {
144
+ return this.vary(value);
145
+ }
146
+
147
+ return this._header(key, value, options);
148
+ }
149
+
150
+ _header(key, value, options = {}) {
151
+
152
+ const append = options.append || false;
153
+ const separator = options.separator || ',';
154
+ const override = options.override !== false;
155
+ const duplicate = options.duplicate !== false;
156
+
157
+ if ((!append && override) ||
158
+ !this.headers[key]) {
159
+
160
+ this.headers[key] = value;
161
+ }
162
+ else if (override) {
163
+ if (key === 'set-cookie') {
164
+ this.headers[key] = [].concat(this.headers[key], value);
165
+ }
166
+ else {
167
+ const existing = this.headers[key];
168
+ if (!duplicate) {
169
+ const values = existing.split(separator);
170
+ for (const v of values) {
171
+ if (v === value) {
172
+ return this;
173
+ }
174
+ }
175
+ }
176
+
177
+ this.headers[key] = existing + separator + value;
178
+ }
179
+ }
180
+
181
+ return this;
182
+ }
183
+
184
+ vary(value) {
185
+
186
+ if (value === '*') {
187
+ this.headers.vary = '*';
188
+ }
189
+ else if (!this.headers.vary) {
190
+ this.headers.vary = value;
191
+ }
192
+ else if (this.headers.vary !== '*') {
193
+ this._header('vary', value, { append: true, duplicate: false });
194
+ }
195
+
196
+ return this;
197
+ }
198
+
199
+ etag(tag, options) {
200
+
201
+ const entity = internals.Response.entity(tag, options);
202
+ this._header('etag', entity.etag);
203
+ this.settings.varyEtag = entity.vary;
204
+ return this;
205
+ }
206
+
207
+ static entity(tag, options = {}) {
208
+
209
+ Hoek.assert(tag !== '*', 'ETag cannot be *');
210
+
211
+ return {
212
+ etag: (options.weak ? 'W/' : '') + '"' + tag + '"',
213
+ vary: (options.vary !== false && !options.weak), // vary defaults to true
214
+ modified: options.modified
215
+ };
216
+ }
217
+
218
+ static unmodified(request, options) {
219
+
220
+ if (request.method !== 'get' &&
221
+ request.method !== 'head') {
222
+
223
+ return false;
224
+ }
225
+
226
+ // Strong verifier
227
+
228
+ if (options.etag &&
229
+ request.headers['if-none-match']) {
230
+
231
+ const ifNoneMatch = request.headers['if-none-match'].split(/\s*,\s*/);
232
+ for (const etag of ifNoneMatch) {
233
+ if (etag === options.etag) {
234
+ return true;
235
+ }
236
+
237
+ if (options.vary) {
238
+ const etagBase = options.etag.slice(0, -1);
239
+ const encoders = request._core.compression.encodings;
240
+ for (const encoder of encoders) {
241
+ if (etag === etagBase + `-${encoder}"`) {
242
+ return true;
243
+ }
244
+ }
245
+ }
246
+ }
247
+
248
+ return false;
249
+ }
250
+
251
+ // Weak verifier
252
+
253
+ if (!options.modified) {
254
+ return false;
255
+ }
256
+
257
+ const ifModifiedSinceHeader = request.headers['if-modified-since'];
258
+ if (!ifModifiedSinceHeader) {
259
+ return false;
260
+ }
261
+
262
+ const ifModifiedSince = internals.parseDate(ifModifiedSinceHeader);
263
+ if (!ifModifiedSince) {
264
+ return false;
265
+ }
266
+
267
+ const lastModified = internals.parseDate(options.modified);
268
+ if (!lastModified) {
269
+ return false;
270
+ }
271
+
272
+ return ifModifiedSince >= lastModified;
273
+ }
274
+
275
+ type(type) {
276
+
277
+ this._header('content-type', type);
278
+ return this;
279
+ }
280
+
281
+ bytes(bytes) {
282
+
283
+ this._header('content-length', bytes);
284
+ return this;
285
+ }
286
+
287
+ location(uri) {
288
+
289
+ this._header('location', uri);
290
+ return this;
291
+ }
292
+
293
+ created(location) {
294
+
295
+ Hoek.assert(this.request.method === 'post' ||
296
+ this.request.method === 'put' ||
297
+ this.request.method === 'patch', 'Cannot return 201 status codes for ' + this.request.method.toUpperCase());
298
+
299
+ this.statusCode = 201;
300
+ this.location(location);
301
+ return this;
302
+ }
303
+
304
+ replacer(method) {
305
+
306
+ this.settings.stringify = this.settings.stringify || {};
307
+ this.settings.stringify.replacer = method;
308
+ return this;
309
+ }
310
+
311
+ spaces(count) {
312
+
313
+ this.settings.stringify = this.settings.stringify || {};
314
+ this.settings.stringify.space = count;
315
+ return this;
316
+ }
317
+
318
+ suffix(suffix) {
319
+
320
+ this.settings.stringify = this.settings.stringify || {};
321
+ this.settings.stringify.suffix = suffix;
322
+ return this;
323
+ }
324
+
325
+ escape(escape) {
326
+
327
+ this.settings.stringify = this.settings.stringify || {};
328
+ this.settings.stringify.escape = escape;
329
+ return this;
330
+ }
331
+
332
+ passThrough(enabled) {
333
+
334
+ this.settings.passThrough = (enabled !== false); // Defaults to true
335
+ return this;
336
+ }
337
+
338
+ redirect(location) {
339
+
340
+ this.statusCode = 302;
341
+ this.location(location);
342
+ this.temporary = this._temporary;
343
+ this.permanent = this._permanent;
344
+ this.rewritable = this._rewritable;
345
+ return this;
346
+ }
347
+
348
+ _temporary(isTemporary) {
349
+
350
+ this._setTemporary(isTemporary !== false); // Defaults to true
351
+ return this;
352
+ }
353
+
354
+ _permanent(isPermanent) {
355
+
356
+ this._setTemporary(isPermanent === false); // Defaults to true
357
+ return this;
358
+ }
359
+
360
+ _rewritable(isRewritable) {
361
+
362
+ this._setRewritable(isRewritable !== false); // Defaults to true
363
+ return this;
364
+ }
365
+
366
+ _isTemporary() {
367
+
368
+ return this.statusCode === 302 || this.statusCode === 307;
369
+ }
370
+
371
+ _isRewritable() {
372
+
373
+ return this.statusCode === 301 || this.statusCode === 302;
374
+ }
375
+
376
+ _setTemporary(isTemporary) {
377
+
378
+ if (isTemporary) {
379
+ if (this._isRewritable()) {
380
+ this.statusCode = 302;
381
+ }
382
+ else {
383
+ this.statusCode = 307;
384
+ }
385
+ }
386
+ else {
387
+ if (this._isRewritable()) {
388
+ this.statusCode = 301;
389
+ }
390
+ else {
391
+ this.statusCode = 308;
392
+ }
393
+ }
394
+ }
395
+
396
+ _setRewritable(isRewritable) {
397
+
398
+ if (isRewritable) {
399
+ if (this._isTemporary()) {
400
+ this.statusCode = 302;
401
+ }
402
+ else {
403
+ this.statusCode = 301;
404
+ }
405
+ }
406
+ else {
407
+ if (this._isTemporary()) {
408
+ this.statusCode = 307;
409
+ }
410
+ else {
411
+ this.statusCode = 308;
412
+ }
413
+ }
414
+ }
415
+
416
+ encoding(encoding) {
417
+
418
+ this.settings.encoding = encoding;
419
+ return this;
420
+ }
421
+
422
+ charset(charset) {
423
+
424
+ this.settings.charset = charset || null;
425
+ return this;
426
+ }
427
+
428
+ ttl(ttl) {
429
+
430
+ this.settings.ttl = ttl;
431
+ return this;
432
+ }
433
+
434
+ state(name, value, options) {
435
+
436
+ this.request._setState(name, value, options);
437
+ return this;
438
+ }
439
+
440
+ unstate(name, options) {
441
+
442
+ this.request._clearState(name, options);
443
+ return this;
444
+ }
445
+
446
+ takeover() {
447
+
448
+ this._takeover = true;
449
+ return this;
450
+ }
451
+
452
+ _prepare() {
453
+
454
+ this._passThrough();
455
+
456
+ if (!this._processors.prepare) {
457
+ return this;
458
+ }
459
+
460
+ try {
461
+ return this._processors.prepare(this);
462
+ }
463
+ catch (err) {
464
+ throw Boom.boomify(err);
465
+ }
466
+ }
467
+
468
+ _passThrough() {
469
+
470
+ if (this.variety === 'stream' &&
471
+ this.settings.passThrough) {
472
+
473
+ if (this.source.statusCode &&
474
+ !this.statusCode) {
475
+
476
+ this.statusCode = this.source.statusCode; // Stream is an HTTP response
477
+ }
478
+
479
+ if (this.source.headers) {
480
+ let headerKeys = Object.keys(this.source.headers);
481
+
482
+ if (headerKeys.length) {
483
+ const localHeaders = this.headers;
484
+ this.headers = {};
485
+
486
+ const connection = this.source.headers.connection;
487
+ const byHop = {};
488
+ if (connection) {
489
+ connection.split(/\s*,\s*/).forEach((header) => {
490
+
491
+ byHop[header] = true;
492
+ });
493
+ }
494
+
495
+ for (const key of headerKeys) {
496
+ const lower = key.toLowerCase();
497
+ if (!internals.hopByHop[lower] &&
498
+ !byHop[lower]) {
499
+
500
+ this.header(lower, Hoek.clone(this.source.headers[key])); // Clone arrays
501
+ }
502
+ }
503
+
504
+ headerKeys = Object.keys(localHeaders);
505
+ for (const key of headerKeys) {
506
+ this.header(key, localHeaders[key], { append: key === 'set-cookie' });
507
+ }
508
+ }
509
+ }
510
+ }
511
+
512
+ this.statusCode = this.statusCode || 200;
513
+ }
514
+
515
+ async _marshal() {
516
+
517
+ let source = this.source;
518
+
519
+ // Processor marshal
520
+
521
+ if (this._processors.marshal) {
522
+ try {
523
+ source = await this._processors.marshal(this);
524
+ }
525
+ catch (err) {
526
+ throw Boom.boomify(err);
527
+ }
528
+ }
529
+
530
+ // Stream source
531
+
532
+ if (source instanceof Stream) {
533
+ if (typeof source._read !== 'function') {
534
+ throw Boom.badImplementation('Stream must have a readable interface');
535
+ }
536
+
537
+ if (source._readableState.objectMode) {
538
+ throw Boom.badImplementation('Cannot reply with stream in object mode');
539
+ }
540
+
541
+ this._payload = source;
542
+ return;
543
+ }
544
+
545
+ // Plain source (non string or null)
546
+
547
+ const jsonify = (this.variety === 'plain' && source !== null && typeof source !== 'string');
548
+
549
+ if (!jsonify &&
550
+ this.settings.stringify) {
551
+
552
+ throw Boom.badImplementation('Cannot set formatting options on non object response');
553
+ }
554
+
555
+ let payload = source;
556
+
557
+ if (jsonify) {
558
+ const options = this.settings.stringify || {};
559
+ const space = options.space || this.request.route.settings.json.space;
560
+ const replacer = options.replacer || this.request.route.settings.json.replacer;
561
+ const suffix = options.suffix || this.request.route.settings.json.suffix || '';
562
+ const escape = this.request.route.settings.json.escape || false;
563
+
564
+ try {
565
+ if (replacer || space) {
566
+ payload = JSON.stringify(payload, replacer, space);
567
+ }
568
+ else {
569
+ payload = JSON.stringify(payload);
570
+ }
571
+ }
572
+ catch (err) {
573
+ throw Boom.boomify(err);
574
+ }
575
+
576
+ if (suffix) {
577
+ payload = payload + suffix;
578
+ }
579
+
580
+ if (escape) {
581
+ payload = Hoek.escapeJson(payload);
582
+ }
583
+ }
584
+
585
+ this._payload = new internals.Response.Payload(payload, this.settings);
586
+ }
587
+
588
+ _tap() {
589
+
590
+ if (!this._events) {
591
+ return null;
592
+ }
593
+
594
+ return (this._events.hasListeners('finish') || this._events.hasListeners('peek') ? new internals.Response.Peek(this._events) : null);
595
+ }
596
+
597
+ _close(request) {
598
+
599
+ if (this._processors.close) {
600
+ try {
601
+ this._processors.close(this);
602
+ }
603
+ catch (err) {
604
+ Bounce.rethrow(err, 'system');
605
+ request._log(['response', 'cleanup', 'error'], err);
606
+ }
607
+ }
608
+
609
+ const stream = this._payload || this.source;
610
+ if (stream instanceof Stream) {
611
+ internals.Response.drain(stream);
612
+ }
613
+ }
614
+
615
+ _isPayloadSupported() {
616
+
617
+ return (this.request.method !== 'head' && this.statusCode !== 304 && this.statusCode !== 204);
618
+ }
619
+
620
+ static drain(stream) {
621
+
622
+ if (stream.unpipe) {
623
+ stream.unpipe();
624
+ }
625
+
626
+ if (stream.close) {
627
+ stream.close();
628
+ }
629
+ else if (stream.destroy) {
630
+ stream.destroy();
631
+ }
632
+ else {
633
+ Streams.drain(stream);
634
+ }
635
+ }
636
+ };
637
+
638
+
639
+ internals.parseDate = function (string) {
640
+
641
+ try {
642
+ return Date.parse(string);
643
+ }
644
+ catch (errIgnore) { }
645
+ };
646
+
647
+
648
+ internals.Response.Payload = class extends Stream.Readable {
649
+
650
+ constructor(payload, options) {
651
+
652
+ super();
653
+
654
+ this._data = payload;
655
+ this._prefix = null;
656
+ this._suffix = null;
657
+ this._sizeOffset = 0;
658
+ this._encoding = options.encoding;
659
+ }
660
+
661
+ _read(size) {
662
+
663
+ if (this._prefix) {
664
+ this.push(this._prefix, this._encoding);
665
+ }
666
+
667
+ if (this._data) {
668
+ this.push(this._data, this._encoding);
669
+ }
670
+
671
+ if (this._suffix) {
672
+ this.push(this._suffix, this._encoding);
673
+ }
674
+
675
+ this.push(null);
676
+ }
677
+
678
+ size() {
679
+
680
+ if (!this._data) {
681
+ return this._sizeOffset;
682
+ }
683
+
684
+ return (Buffer.isBuffer(this._data) ? this._data.length : Buffer.byteLength(this._data, this._encoding)) + this._sizeOffset;
685
+ }
686
+
687
+ jsonp(variable) {
688
+
689
+ this._sizeOffset = this._sizeOffset + variable.length + 7;
690
+ this._prefix = '/**/' + variable + '('; // '/**/' prefix prevents CVE-2014-4671 security exploit
691
+ this._data = (this._data === null || Buffer.isBuffer(this._data)) ? this._data : this._data.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
692
+ this._suffix = ');';
693
+ }
694
+
695
+ writeToStream(stream) {
696
+
697
+ if (this._prefix) {
698
+ stream.write(this._prefix, this._encoding);
699
+ }
700
+
701
+ if (this._data) {
702
+ stream.write(this._data, this._encoding);
703
+ }
704
+
705
+ if (this._suffix) {
706
+ stream.write(this._suffix, this._encoding);
707
+ }
708
+
709
+ stream.end();
710
+ }
711
+ };
712
+
713
+
714
+ internals.Response.Peek = class extends Stream.Transform {
715
+
716
+ constructor(podium) {
717
+
718
+ super();
719
+
720
+ this._podium = podium;
721
+ this.on('finish', () => podium.emit('finish'));
722
+ }
723
+
724
+ _transform(chunk, encoding, callback) {
725
+
726
+ this._podium.emit('peek', [chunk, encoding]);
727
+ this.push(chunk, encoding);
728
+ callback();
729
+ }
730
+ };