@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/route.js ADDED
@@ -0,0 +1,519 @@
1
+ 'use strict';
2
+
3
+ const Assert = require('assert');
4
+ const Boom = require('boom');
5
+ const Bounce = require('bounce');
6
+ const Catbox = require('catbox');
7
+ const Hoek = require('hoek');
8
+ const Joi = require('joi');
9
+ const Subtext = require('subtext');
10
+
11
+ const Auth = require('./auth');
12
+ const Config = require('./config');
13
+ const Cors = require('./cors');
14
+ const Ext = require('./ext');
15
+ const Handler = require('./handler');
16
+ const Headers = require('./headers');
17
+ const Security = require('./security');
18
+ const Streams = require('./streams');
19
+ const Validation = require('./validation');
20
+
21
+
22
+ const internals = {};
23
+
24
+
25
+ exports = module.exports = internals.Route = class {
26
+
27
+ constructor(route, server, options = {}) {
28
+
29
+ const core = server._core;
30
+ const realm = server.realm;
31
+
32
+ // Routing information
33
+
34
+ Config.apply('route', route, route.method, route.path);
35
+
36
+ const method = route.method.toLowerCase();
37
+ Hoek.assert(method !== 'head', 'Cannot set HEAD route:', route.path);
38
+
39
+ const path = (realm.modifiers.route.prefix ? realm.modifiers.route.prefix + (route.path !== '/' ? route.path : '') : route.path);
40
+ Hoek.assert(path === '/' || path[path.length - 1] !== '/' || !core.settings.router.stripTrailingSlash, 'Path cannot end with a trailing slash when configured to strip:', route.method, route.path);
41
+
42
+ const vhost = (realm.modifiers.route.vhost || route.vhost);
43
+
44
+ // Set identifying members (assert)
45
+
46
+ this.method = method;
47
+ this.path = path;
48
+
49
+ // Prepare configuration
50
+
51
+ let config = route.options || route.config || {};
52
+ if (typeof config === 'function') {
53
+ config = config.call(realm.settings.bind, server);
54
+ }
55
+
56
+ config = Config.enable(config); // Shallow clone
57
+
58
+ // Verify route level config (as opposed to the merged settings)
59
+
60
+ this._assert(method !== 'get' || !config.payload, 'Cannot set payload settings on HEAD or GET request');
61
+ this._assert(method !== 'get' || !config.validate || !config.validate.payload, 'Cannot validate HEAD or GET request payload');
62
+
63
+ // Rules
64
+
65
+ this._assert(!route.rules || !config.rules, 'Route rules can only appear once'); // XOR
66
+ const rules = (route.rules || config.rules);
67
+ const rulesConfig = internals.rules(rules, { method, path, vhost }, server);
68
+ delete config.rules;
69
+
70
+ // Handler
71
+
72
+ this._assert(route.handler || config.handler, 'Missing or undefined handler');
73
+ this._assert(!!route.handler ^ !!config.handler, 'Handler must only appear once'); // XOR
74
+
75
+ const handler = Config.apply('handler', route.handler || config.handler);
76
+ delete config.handler;
77
+
78
+ const handlerDefaults = Handler.defaults(method, handler, core);
79
+
80
+ // Apply settings in order: server <- handler <- realm <- route
81
+
82
+ const settings = internals.config([core.settings.routes, handlerDefaults, realm.settings, rulesConfig, config]);
83
+ this.settings = Config.apply('routeConfig', settings, method, path);
84
+
85
+ // Validate timeouts
86
+
87
+ const socketTimeout = (this.settings.timeout.socket === undefined ? 2 * 60 * 1000 : this.settings.timeout.socket);
88
+ this._assert(!this.settings.timeout.server || !socketTimeout || this.settings.timeout.server < socketTimeout, 'Server timeout must be shorter than socket timeout');
89
+ this._assert(!this.settings.payload.timeout || !socketTimeout || this.settings.payload.timeout < socketTimeout, 'Payload timeout must be shorter than socket timeout');
90
+
91
+ // Route members
92
+
93
+ this._core = core;
94
+ this.realm = realm;
95
+
96
+ this.settings.vhost = vhost;
97
+ this.settings.plugins = this.settings.plugins || {}; // Route-specific plugins settings, namespaced using plugin name
98
+ this.settings.app = this.settings.app || {}; // Route-specific application settings
99
+
100
+ // Path parsing
101
+
102
+ this._special = !!options.special;
103
+ this._analysis = this._core.router.analyze(this.path);
104
+ this.params = this._analysis.params;
105
+ this.fingerprint = this._analysis.fingerprint;
106
+
107
+ this.public = {
108
+ method: this.method,
109
+ path: this.path,
110
+ vhost,
111
+ realm,
112
+ settings: this.settings,
113
+ fingerprint: this.fingerprint,
114
+ auth: {
115
+ access: (request) => Auth.testAccess(request, this.public)
116
+ }
117
+ };
118
+
119
+ // Validation
120
+
121
+ this._setupValidation();
122
+
123
+ // Payload parsing
124
+
125
+ if (this.method === 'get') {
126
+ this.settings.payload = null;
127
+ }
128
+ else {
129
+ this.settings.payload.decoders = this._core.compression._decoders; // Reference the shared object to keep up to date
130
+ }
131
+
132
+ this._assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled');
133
+ this._assert(!this.settings.validate.state || this.settings.state.parse, 'Route state must be set to \'parse\' when state validation enabled');
134
+ this._assert(!this.settings.jsonp || typeof this.settings.jsonp === 'string', 'Bad route JSONP parameter name');
135
+
136
+ // Authentication configuration
137
+
138
+ this.settings.auth = (this._special ? false : this._core.auth._setupRoute(this.settings.auth, path));
139
+
140
+ // Cache
141
+
142
+ if (this.method === 'get' &&
143
+ typeof this.settings.cache === 'object' &&
144
+ (this.settings.cache.expiresIn || this.settings.cache.expiresAt)) {
145
+
146
+ this.settings.cache._statuses = new Set(this.settings.cache.statuses);
147
+ this._cache = new Catbox.Policy({ expiresIn: this.settings.cache.expiresIn, expiresAt: this.settings.cache.expiresAt });
148
+ }
149
+
150
+ // CORS
151
+
152
+ this.settings.cors = Cors.route(this.settings.cors);
153
+
154
+ // Security
155
+
156
+ this.settings.security = Security.route(this.settings.security);
157
+
158
+ // Handler
159
+
160
+ this.settings.handler = Handler.configure(handler, this);
161
+ this._prerequisites = Handler.prerequisitesConfig(this.settings.pre);
162
+
163
+ // Route lifecycle
164
+
165
+ this._extensions = {
166
+ onPreResponse: Ext.combine(this, 'onPreResponse')
167
+ };
168
+
169
+ if (this._special) {
170
+ this._cycle = [internals.drain, Handler.execute];
171
+ this.rebuild();
172
+ return;
173
+ }
174
+
175
+ this._extensions.onPreAuth = Ext.combine(this, 'onPreAuth');
176
+ this._extensions.onCredentials = Ext.combine(this, 'onCredentials');
177
+ this._extensions.onPostAuth = Ext.combine(this, 'onPostAuth');
178
+ this._extensions.onPreHandler = Ext.combine(this, 'onPreHandler');
179
+ this._extensions.onPostHandler = Ext.combine(this, 'onPostHandler');
180
+
181
+ this.rebuild();
182
+ }
183
+
184
+ _setupValidation() {
185
+
186
+ const validation = this.settings.validate;
187
+ if (this.method === 'get') {
188
+ validation.payload = null;
189
+ }
190
+
191
+ this._assert(!validation.params || this.params.length, 'Cannot set path parameters validations without path parameters');
192
+
193
+ ['headers', 'params', 'query', 'payload', 'state'].forEach((type) => {
194
+
195
+ validation[type] = Validation.compile(validation[type]);
196
+ });
197
+
198
+ if (this.settings.response.schema !== undefined ||
199
+ this.settings.response.status) {
200
+
201
+ this.settings.response._validate = true;
202
+
203
+ const rule = this.settings.response.schema;
204
+ this.settings.response.status = this.settings.response.status || {};
205
+ const statuses = Object.keys(this.settings.response.status);
206
+
207
+ if (rule === true &&
208
+ !statuses.length) {
209
+
210
+ this.settings.response._validate = false;
211
+ }
212
+ else {
213
+ this.settings.response.schema = Validation.compile(rule);
214
+ for (const code of statuses) {
215
+ this.settings.response.status[code] = Validation.compile(this.settings.response.status[code]);
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ rebuild(event) {
222
+
223
+ if (event) {
224
+ this._extensions[event.type].add(event);
225
+ }
226
+
227
+ if (this._special) {
228
+ this._postCycle = (this._extensions.onPreResponse.nodes ? [this._extensions.onPreResponse] : []);
229
+ this._buildMarshalCycle();
230
+ return;
231
+ }
232
+
233
+ // Build lifecycle array
234
+
235
+ this._cycle = [];
236
+
237
+ // 'onRequest'
238
+
239
+ if (this.settings.jsonp) {
240
+ this._cycle.push(internals.parseJSONP);
241
+ }
242
+
243
+ if (this.settings.state.parse) {
244
+ this._cycle.push(internals.state);
245
+ }
246
+
247
+ if (this._extensions.onPreAuth.nodes) {
248
+ this._cycle.push(this._extensions.onPreAuth);
249
+ }
250
+
251
+ if (this._core.auth._enabled(this, 'authenticate')) {
252
+ this._cycle.push(Auth.authenticate);
253
+ }
254
+
255
+ if (this.method !== 'get') {
256
+ this._cycle.push(internals.payload);
257
+
258
+ if (this._core.auth._enabled(this, 'payload')) {
259
+ this._cycle.push(Auth.payload);
260
+ }
261
+ }
262
+
263
+ if (this._core.auth._enabled(this, 'authenticate') &&
264
+ this._extensions.onCredentials.nodes) {
265
+
266
+ this._cycle.push(this._extensions.onCredentials);
267
+ }
268
+
269
+ if (this._core.auth._enabled(this, 'access')) {
270
+ this._cycle.push(Auth.access);
271
+ }
272
+
273
+ if (this._extensions.onPostAuth.nodes) {
274
+ this._cycle.push(this._extensions.onPostAuth);
275
+ }
276
+
277
+ if (this.settings.validate.headers) {
278
+ this._cycle.push(Validation.headers);
279
+ }
280
+
281
+ if (this.settings.validate.params) {
282
+ this._cycle.push(Validation.params);
283
+ }
284
+
285
+ if (this.settings.jsonp) {
286
+ this._cycle.push(internals.cleanupJSONP);
287
+ }
288
+
289
+ if (this.settings.validate.query) {
290
+ this._cycle.push(Validation.query);
291
+ }
292
+
293
+ if (this.settings.validate.payload) {
294
+ this._cycle.push(Validation.payload);
295
+ }
296
+
297
+ if (this.settings.validate.state) {
298
+ this._cycle.push(Validation.state);
299
+ }
300
+
301
+ if (this._extensions.onPreHandler.nodes) {
302
+ this._cycle.push(this._extensions.onPreHandler);
303
+ }
304
+
305
+ this._cycle.push(Handler.execute);
306
+
307
+ if (this._extensions.onPostHandler.nodes) {
308
+ this._cycle.push(this._extensions.onPostHandler);
309
+ }
310
+
311
+ this._postCycle = [];
312
+
313
+ if (this.settings.response._validate &&
314
+ this.settings.response.sample !== 0) {
315
+
316
+ this._postCycle.push(Validation.response);
317
+ }
318
+
319
+ if (this._extensions.onPreResponse.nodes) {
320
+ this._postCycle.push(this._extensions.onPreResponse);
321
+ }
322
+
323
+ this._buildMarshalCycle();
324
+ }
325
+
326
+ _buildMarshalCycle() {
327
+
328
+ this._marshalCycle = [Headers.type];
329
+
330
+ if (this.settings.cors) {
331
+ this._marshalCycle.push(Cors.headers);
332
+ }
333
+
334
+ if (this.settings.security) {
335
+ this._marshalCycle.push(Security.headers);
336
+ }
337
+
338
+ this._marshalCycle.push(Headers.entity);
339
+
340
+ if (this.method === 'get' ||
341
+ this.method === '*') {
342
+
343
+ this._marshalCycle.push(Headers.unmodified);
344
+ }
345
+
346
+ this._marshalCycle.push(Headers.cache);
347
+ this._marshalCycle.push(Headers.state);
348
+ this._marshalCycle.push(Headers.content);
349
+
350
+ if (this._core.auth._enabled(this, 'response')) {
351
+ this._marshalCycle.push(Auth.response); // Must be last in case requires access to headers
352
+ }
353
+ }
354
+
355
+ _assert(condition, message) {
356
+
357
+ if (condition) {
358
+ return;
359
+ }
360
+
361
+ if (this.method[0] !== '_') {
362
+ message = `${message}: ${this.method.toUpperCase()} ${this.path}`;
363
+ }
364
+
365
+ throw new Assert.AssertionError({
366
+ message,
367
+ actual: false,
368
+ expected: true,
369
+ operator: '==',
370
+ stackStartFunction: this._assert
371
+ });
372
+ }
373
+ };
374
+
375
+
376
+ internals.state = async function (request) {
377
+
378
+ request.state = {};
379
+
380
+ const req = request.raw.req;
381
+ const cookies = req.headers.cookie;
382
+ if (!cookies) {
383
+ return;
384
+ }
385
+
386
+ try {
387
+ var result = await request._core.states.parse(cookies);
388
+ }
389
+ catch (err) {
390
+ Bounce.rethrow(err, 'system');
391
+ var parseError = err;
392
+ }
393
+
394
+ const { states, failed = [] } = result || parseError;
395
+ request.state = states || {};
396
+
397
+ // Clear cookies
398
+
399
+ for (const item of failed) {
400
+ if (item.settings.clearInvalid) {
401
+ request._clearState(item.name);
402
+ }
403
+ }
404
+
405
+ if (!parseError) {
406
+ return;
407
+ }
408
+
409
+ parseError.header = cookies;
410
+
411
+ return request._core.toolkit.failAction(request, request.route.settings.state.failAction, parseError, { tags: ['state', 'error'] });
412
+ };
413
+
414
+
415
+ internals.payload = async function (request) {
416
+
417
+ if (request.method === 'get' ||
418
+ request.method === 'head') { // When route.method is '*'
419
+
420
+ return;
421
+ }
422
+
423
+ if (request._expectContinue) {
424
+ request.raw.res.writeContinue();
425
+ }
426
+
427
+ try {
428
+ const { payload, mime } = await Subtext.parse(request.raw.req, request._tap(), request.route.settings.payload);
429
+
430
+ request._isPayloadPending = !!(payload && payload._readableState);
431
+ request.mime = mime;
432
+ request.payload = payload;
433
+ }
434
+ catch (err) {
435
+ Bounce.rethrow(err, 'system');
436
+
437
+ if (request._isPayloadPending) {
438
+ await internals.drain(request);
439
+ request._isPayloadPending = false;
440
+ }
441
+ else {
442
+ request._isPayloadPending = true;
443
+ }
444
+
445
+ request.mime = err.mime;
446
+ request.payload = null;
447
+
448
+ return request._core.toolkit.failAction(request, request.route.settings.payload.failAction, err, { tags: ['payload', 'error'] });
449
+ }
450
+ };
451
+
452
+
453
+ internals.drain = async function (request) {
454
+
455
+ // Flush out any pending request payload not consumed due to errors
456
+
457
+ await Streams.drain(request.raw.req);
458
+ request._isPayloadPending = false;
459
+ };
460
+
461
+
462
+ internals.jsonpRegex = /^[\w\$\[\]\.]+$/;
463
+
464
+
465
+ internals.parseJSONP = function (request) {
466
+
467
+ const jsonp = request.query[request.route.settings.jsonp];
468
+ if (jsonp) {
469
+ if (internals.jsonpRegex.test(jsonp) === false) {
470
+ throw Boom.badRequest('Invalid JSONP parameter value');
471
+ }
472
+
473
+ request.jsonp = jsonp;
474
+ }
475
+ };
476
+
477
+
478
+ internals.cleanupJSONP = function (request) {
479
+
480
+ if (request.jsonp) {
481
+ delete request.query[request.route.settings.jsonp];
482
+ }
483
+ };
484
+
485
+
486
+ internals.config = function (chain) {
487
+
488
+ if (!chain.length) {
489
+ return {};
490
+ }
491
+
492
+ let config = chain[0];
493
+ for (const item of chain) {
494
+ config = Hoek.applyToDefaultsWithShallow(config, item, ['bind', 'validate.headers', 'validate.payload', 'validate.params', 'validate.query', 'validate.state']);
495
+ }
496
+
497
+ return config;
498
+ };
499
+
500
+
501
+ internals.rules = function (rules, info, server) {
502
+
503
+ const configs = [];
504
+
505
+ let realm = server.realm;
506
+ while (realm) {
507
+ if (realm._rules) {
508
+ const source = (!realm._rules.settings.validate ? rules : Joi.attempt(rules, realm._rules.settings.validate.schema, realm._rules.settings.validate.options));
509
+ const config = realm._rules.processor(source, info);
510
+ if (config) {
511
+ configs.unshift(config);
512
+ }
513
+ }
514
+
515
+ realm = realm.parent;
516
+ }
517
+
518
+ return internals.config(configs);
519
+ };
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ const internals = {};
4
+
5
+
6
+ exports.route = function (settings) {
7
+
8
+ if (!settings) {
9
+ return null;
10
+ }
11
+
12
+ const security = settings;
13
+ if (security.hsts) {
14
+ if (security.hsts === true) {
15
+ security._hsts = 'max-age=15768000';
16
+ }
17
+ else if (typeof security.hsts === 'number') {
18
+ security._hsts = 'max-age=' + security.hsts;
19
+ }
20
+ else {
21
+ security._hsts = 'max-age=' + (security.hsts.maxAge || 15768000);
22
+ if (security.hsts.includeSubdomains || security.hsts.includeSubDomains) {
23
+ security._hsts = security._hsts + '; includeSubDomains';
24
+ }
25
+
26
+ if (security.hsts.preload) {
27
+ security._hsts = security._hsts + '; preload';
28
+ }
29
+ }
30
+ }
31
+
32
+ if (security.xframe) {
33
+ if (security.xframe === true) {
34
+ security._xframe = 'DENY';
35
+ }
36
+ else if (typeof security.xframe === 'string') {
37
+ security._xframe = security.xframe.toUpperCase();
38
+ }
39
+ else if (security.xframe.rule === 'allow-from') {
40
+ if (!security.xframe.source) {
41
+ security._xframe = 'SAMEORIGIN';
42
+ }
43
+ else {
44
+ security._xframe = 'ALLOW-FROM ' + security.xframe.source;
45
+ }
46
+ }
47
+ else {
48
+ security._xframe = security.xframe.rule.toUpperCase();
49
+ }
50
+ }
51
+
52
+ return security;
53
+ };
54
+
55
+
56
+ exports.headers = function (request) {
57
+
58
+ const response = request.response;
59
+ const security = response.request.route.settings.security;
60
+
61
+ if (security._hsts) {
62
+ response._header('strict-transport-security', security._hsts, { override: false });
63
+ }
64
+
65
+ if (security._xframe) {
66
+ response._header('x-frame-options', security._xframe, { override: false });
67
+ }
68
+
69
+ if (security.xss) {
70
+ response._header('x-xss-protection', '1; mode=block', { override: false });
71
+ }
72
+
73
+ if (security.noOpen) {
74
+ response._header('x-download-options', 'noopen', { override: false });
75
+ }
76
+
77
+ if (security.noSniff) {
78
+ response._header('x-content-type-options', 'nosniff', { override: false });
79
+ }
80
+
81
+ if (security.referrer !== false) {
82
+ response._header('referrer-policy', security.referrer, { override: false });
83
+ }
84
+ };