hapii-js 0.0.0-stage → 2.0.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,456 @@
1
+ 'use strict';
2
+
3
+ const Assert = require('assert');
4
+
5
+ const Bounce = require('@hapi/bounce');
6
+ const Catbox = require('@hapi/catbox');
7
+ const Hoek = require('@hapi/hoek');
8
+ const Subtext = require('@hapi/subtext');
9
+ const Validate = require('@hapi/validate');
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?.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
+
86
+ // Route members
87
+
88
+ this._core = core;
89
+ this.realm = realm;
90
+
91
+ this.settings.vhost = vhost;
92
+ this.settings.plugins = this.settings.plugins ?? {}; // Route-specific plugins settings, namespaced using plugin name
93
+ this.settings.app = this.settings.app ?? {}; // Route-specific application settings
94
+
95
+ // Path parsing
96
+
97
+ this._special = !!options.special;
98
+ this._analysis = this._core.router.analyze(this.path);
99
+ this.params = this._analysis.params;
100
+ this.fingerprint = this._analysis.fingerprint;
101
+
102
+ this.public = {
103
+ method: this.method,
104
+ path: this.path,
105
+ vhost,
106
+ realm,
107
+ settings: this.settings,
108
+ fingerprint: this.fingerprint,
109
+ auth: {
110
+ access: (request) => Auth.testAccess(request, this.public)
111
+ }
112
+ };
113
+
114
+ // Validation
115
+
116
+ this._setupValidation();
117
+
118
+ // Payload parsing
119
+
120
+ if (this.method === 'get') {
121
+ this.settings.payload = null;
122
+ }
123
+ else {
124
+ this.settings.payload.decoders = this._core.compression.decoders; // Reference the shared object to keep up to date
125
+ }
126
+
127
+ this._assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled');
128
+ this._assert(!this.settings.validate.state || this.settings.state.parse, 'Route state must be set to \'parse\' when state validation enabled');
129
+
130
+ // Authentication configuration
131
+
132
+ this.settings.auth = this._special ? false : this._core.auth._setupRoute(this.settings.auth, path);
133
+
134
+ // Cache
135
+
136
+ if (this.method === 'get' &&
137
+ typeof this.settings.cache === 'object' &&
138
+ (this.settings.cache.expiresIn || this.settings.cache.expiresAt)) {
139
+
140
+ this.settings.cache._statuses = new Set(this.settings.cache.statuses);
141
+ this._cache = new Catbox.Policy({ expiresIn: this.settings.cache.expiresIn, expiresAt: this.settings.cache.expiresAt });
142
+ }
143
+
144
+ // CORS
145
+
146
+ this.settings.cors = Cors.route(this.settings.cors);
147
+
148
+ // Security
149
+
150
+ this.settings.security = Security.route(this.settings.security);
151
+
152
+ // Handler
153
+
154
+ this.settings.handler = Handler.configure(handler, this);
155
+ this._prerequisites = Handler.prerequisitesConfig(this.settings.pre);
156
+
157
+ // Route lifecycle
158
+
159
+ this._extensions = {
160
+ onPreResponse: Ext.combine(this, 'onPreResponse'),
161
+ onPostResponse: Ext.combine(this, 'onPostResponse')
162
+ };
163
+
164
+ if (this._special) {
165
+ this._cycle = [internals.drain, Handler.execute];
166
+ this.rebuild();
167
+ return;
168
+ }
169
+
170
+ this._extensions.onPreAuth = Ext.combine(this, 'onPreAuth');
171
+ this._extensions.onCredentials = Ext.combine(this, 'onCredentials');
172
+ this._extensions.onPostAuth = Ext.combine(this, 'onPostAuth');
173
+ this._extensions.onPreHandler = Ext.combine(this, 'onPreHandler');
174
+ this._extensions.onPostHandler = Ext.combine(this, 'onPostHandler');
175
+
176
+ this.rebuild();
177
+ }
178
+
179
+ _setupValidation() {
180
+
181
+ const validation = this.settings.validate;
182
+ if (this.method === 'get') {
183
+ validation.payload = null;
184
+ }
185
+
186
+ this._assert(!validation.params || this.params.length, 'Cannot set path parameters validations without path parameters');
187
+
188
+ for (const type of ['headers', 'params', 'query', 'payload', 'state']) {
189
+ validation[type] = Validation.compile(validation[type], this.settings.validate.validator, this.realm, this._core);
190
+ }
191
+
192
+ if (this.settings.response.schema !== undefined ||
193
+ this.settings.response.status) {
194
+
195
+ this.settings.response._validate = true;
196
+
197
+ const rule = this.settings.response.schema;
198
+ this.settings.response.status = this.settings.response.status ?? {};
199
+ const statuses = Object.keys(this.settings.response.status);
200
+
201
+ if (rule === true &&
202
+ !statuses.length) {
203
+
204
+ this.settings.response._validate = false;
205
+ }
206
+ else {
207
+ this.settings.response.schema = Validation.compile(rule, this.settings.validate.validator, this.realm, this._core);
208
+ for (const code of statuses) {
209
+ this.settings.response.status[code] = Validation.compile(this.settings.response.status[code], this.settings.validate.validator, this.realm, this._core);
210
+ }
211
+ }
212
+ }
213
+ }
214
+
215
+ rebuild(event) {
216
+
217
+ if (event) {
218
+ this._extensions[event.type].add(event);
219
+ }
220
+
221
+ if (this._special) {
222
+ this._postCycle = this._extensions.onPreResponse.nodes ? [this._extensions.onPreResponse] : [];
223
+ this._buildMarshalCycle();
224
+ return;
225
+ }
226
+
227
+ // Build lifecycle array
228
+
229
+ this._cycle = [];
230
+
231
+ // 'onRequest'
232
+
233
+ if (this.settings.state.parse) {
234
+ this._cycle.push(internals.state);
235
+ }
236
+
237
+ if (this._extensions.onPreAuth.nodes) {
238
+ this._cycle.push(this._extensions.onPreAuth);
239
+ }
240
+
241
+ if (this._core.auth._enabled(this, 'authenticate')) {
242
+ this._cycle.push(Auth.authenticate);
243
+ }
244
+
245
+ if (this.method !== 'get') {
246
+ this._cycle.push(internals.payload);
247
+
248
+ if (this._core.auth._enabled(this, 'payload')) {
249
+ this._cycle.push(Auth.payload);
250
+ }
251
+ }
252
+
253
+ if (this._core.auth._enabled(this, 'authenticate') &&
254
+ this._extensions.onCredentials.nodes) {
255
+
256
+ this._cycle.push(this._extensions.onCredentials);
257
+ }
258
+
259
+ if (this._core.auth._enabled(this, 'access')) {
260
+ this._cycle.push(Auth.access);
261
+ }
262
+
263
+ if (this._extensions.onPostAuth.nodes) {
264
+ this._cycle.push(this._extensions.onPostAuth);
265
+ }
266
+
267
+ if (this.settings.validate.headers) {
268
+ this._cycle.push(Validation.headers);
269
+ }
270
+
271
+ if (this.settings.validate.params) {
272
+ this._cycle.push(Validation.params);
273
+ }
274
+
275
+ if (this.settings.validate.query) {
276
+ this._cycle.push(Validation.query);
277
+ }
278
+
279
+ if (this.settings.validate.payload) {
280
+ this._cycle.push(Validation.payload);
281
+ }
282
+
283
+ if (this.settings.validate.state) {
284
+ this._cycle.push(Validation.state);
285
+ }
286
+
287
+ if (this._extensions.onPreHandler.nodes) {
288
+ this._cycle.push(this._extensions.onPreHandler);
289
+ }
290
+
291
+ this._cycle.push(Handler.execute);
292
+
293
+ if (this._extensions.onPostHandler.nodes) {
294
+ this._cycle.push(this._extensions.onPostHandler);
295
+ }
296
+
297
+ this._postCycle = [];
298
+
299
+ if (this.settings.response._validate &&
300
+ this.settings.response.sample !== 0) {
301
+
302
+ this._postCycle.push(Validation.response);
303
+ }
304
+
305
+ if (this._extensions.onPreResponse.nodes) {
306
+ this._postCycle.push(this._extensions.onPreResponse);
307
+ }
308
+
309
+ this._buildMarshalCycle();
310
+
311
+ // onPostResponse
312
+ }
313
+
314
+ _buildMarshalCycle() {
315
+
316
+ this._marshalCycle = [Headers.type];
317
+
318
+ if (this.settings.cors) {
319
+ this._marshalCycle.push(Cors.headers);
320
+ }
321
+
322
+ if (this.settings.security) {
323
+ this._marshalCycle.push(Security.headers);
324
+ }
325
+
326
+ this._marshalCycle.push(Headers.entity);
327
+
328
+ if (this.method === 'get' ||
329
+ this.method === '*') {
330
+
331
+ this._marshalCycle.push(Headers.unmodified);
332
+ }
333
+
334
+ this._marshalCycle.push(Headers.cache);
335
+ this._marshalCycle.push(Headers.state);
336
+ this._marshalCycle.push(Headers.content);
337
+
338
+ if (this._core.auth._enabled(this, 'response')) {
339
+ this._marshalCycle.push(Auth.response); // Must be last in case requires access to headers
340
+ }
341
+ }
342
+
343
+ _assert(condition, message) {
344
+
345
+ if (condition) {
346
+ return;
347
+ }
348
+
349
+ if (this.method[0] !== '_') {
350
+ message = `${message}: ${this.method.toUpperCase()} ${this.path}`;
351
+ }
352
+
353
+ throw new Assert.AssertionError({
354
+ message,
355
+ actual: false,
356
+ expected: true,
357
+ operator: '==',
358
+ stackStartFunction: this._assert
359
+ });
360
+ }
361
+ };
362
+
363
+
364
+ internals.state = function (request) {
365
+
366
+ return request._parseCookies();
367
+ };
368
+
369
+
370
+ internals.payload = async function (request) {
371
+
372
+ if (request.method === 'get' ||
373
+ request.method === 'head') { // When route.method is '*'
374
+
375
+ return;
376
+ }
377
+
378
+ if (request.payload !== undefined) {
379
+ return internals.drain(request);
380
+ }
381
+
382
+ if (request._expectContinue) {
383
+ request._expectContinue = false;
384
+ request.raw.res.writeContinue();
385
+ }
386
+
387
+ try {
388
+ const { payload, mime } = await Subtext.parse(request.raw.req, request._tap(), request.route.settings.payload);
389
+
390
+ request._isPayloadPending = !!payload?._readableState;
391
+ request.mime = mime;
392
+ request.payload = payload;
393
+ }
394
+ catch (err) {
395
+ Bounce.rethrow(err, 'system');
396
+
397
+ await internals.drain(request);
398
+
399
+ request.mime = err.mime;
400
+ request.payload = null;
401
+
402
+ return request._core.toolkit.failAction(request, request.route.settings.payload.failAction, err, { tags: ['payload', 'error'] });
403
+ }
404
+ };
405
+
406
+
407
+ internals.drain = async function (request) {
408
+
409
+ // Flush out any pending request payload not consumed due to errors
410
+
411
+ if (request._expectContinue) {
412
+ request._isPayloadPending = false; // If we don't continue, client should not send a payload
413
+ request._expectContinue = false;
414
+ }
415
+
416
+ if (request._isPayloadPending) {
417
+ await Streams.drain(request.raw.req);
418
+ request._isPayloadPending = false;
419
+ }
420
+ };
421
+
422
+
423
+ internals.config = function (chain) {
424
+
425
+ if (!chain.length) {
426
+ return {};
427
+ }
428
+
429
+ let config = chain[0];
430
+ for (const item of chain) {
431
+ config = Hoek.applyToDefaults(config, item, { shallow: ['bind', 'validate.headers', 'validate.payload', 'validate.params', 'validate.query', 'validate.state'] });
432
+ }
433
+
434
+ return config;
435
+ };
436
+
437
+
438
+ internals.rules = function (rules, info, server) {
439
+
440
+ const configs = [];
441
+
442
+ let realm = server.realm;
443
+ while (realm) {
444
+ if (realm._rules) {
445
+ const source = !realm._rules.settings.validate ? rules : Validate.attempt(rules, realm._rules.settings.validate.schema, realm._rules.settings.validate.options);
446
+ const config = realm._rules.processor(source, info);
447
+ if (config) {
448
+ configs.unshift(config);
449
+ }
450
+ }
451
+
452
+ realm = realm.parent;
453
+ }
454
+
455
+ return internals.config(configs);
456
+ };
@@ -0,0 +1,86 @@
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 (response) {
57
+
58
+ const security = response.request.route.settings.security;
59
+
60
+ if (security._hsts) {
61
+ response._header('strict-transport-security', security._hsts, { override: false });
62
+ }
63
+
64
+ if (security._xframe) {
65
+ response._header('x-frame-options', security._xframe, { override: false });
66
+ }
67
+
68
+ if (security.xss === 'enabled') {
69
+ response._header('x-xss-protection', '1; mode=block', { override: false });
70
+ }
71
+ else if (security.xss === 'disabled') {
72
+ response._header('x-xss-protection', '0', { override: false });
73
+ }
74
+
75
+ if (security.noOpen) {
76
+ response._header('x-download-options', 'noopen', { override: false });
77
+ }
78
+
79
+ if (security.noSniff) {
80
+ response._header('x-content-type-options', 'nosniff', { override: false });
81
+ }
82
+
83
+ if (security.referrer !== false) {
84
+ response._header('referrer-policy', security.referrer, { override: false });
85
+ }
86
+ };