@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/toolkit.js ADDED
@@ -0,0 +1,201 @@
1
+ 'use strict';
2
+
3
+ const Boom = require('boom');
4
+ const Bounce = require('bounce');
5
+ const Hoek = require('hoek');
6
+
7
+ const Response = require('./response');
8
+
9
+
10
+ const internals = {
11
+ reserved: ['abandon', 'authenticated', 'close', 'context', 'continue', 'entity', 'redirect', 'realm', 'request', 'response', 'state', 'unauthenticated', 'unstate']
12
+ };
13
+
14
+
15
+ exports = module.exports = internals.Manager = class {
16
+
17
+ constructor() {
18
+
19
+ this.abandon = Symbol('abandon');
20
+ this.close = Symbol('close');
21
+ this.continue = Symbol('continue');
22
+ this.reserved = internals.reserved;
23
+ }
24
+
25
+ async execute(method, request, options) {
26
+
27
+ const h = new internals.Toolkit(request, this, options);
28
+ const bind = options.bind || null;
29
+
30
+ try {
31
+ var response = await (options.args ? method.call(bind, request, h, ...options.args) : method.call(bind, request, h));
32
+ }
33
+ catch (err) {
34
+ if (Bounce.isSystem(err)) {
35
+ response = Boom.badImplementation(err);
36
+ }
37
+ else if (!Bounce.isError(err)) {
38
+ response = Boom.badImplementation('Cannot throw non-error object', err);
39
+ }
40
+ else {
41
+ response = Boom.boomify(err);
42
+ }
43
+ }
44
+
45
+ // Process response
46
+
47
+ if (response === undefined) {
48
+ response = Boom.badImplementation(`${method.name} method did not return a value, a promise, or throw an error`);
49
+ }
50
+
51
+ if (options.continue &&
52
+ response === this.continue) {
53
+
54
+ if (options.continue === 'undefined') {
55
+ return;
56
+ }
57
+
58
+ // 'null'
59
+
60
+ response = null;
61
+ }
62
+
63
+ if (options.auth &&
64
+ response instanceof internals.Auth) {
65
+
66
+ return response;
67
+ }
68
+
69
+ if (typeof response !== 'symbol') {
70
+ response = Response.wrap(response, request);
71
+ if (!response.isBoom) {
72
+ response = await response._prepare();
73
+ }
74
+ }
75
+
76
+ return response;
77
+ }
78
+
79
+ failAction(request, failAction, err, options) {
80
+
81
+ const retain = options.retain ? err : undefined;
82
+ if (failAction === 'ignore') {
83
+ return retain;
84
+ }
85
+
86
+ if (failAction === 'log') {
87
+ request._log(options.tags, err);
88
+ return retain;
89
+ }
90
+
91
+ if (failAction === 'error') {
92
+ throw err;
93
+ }
94
+
95
+ return this.execute(failAction, request, { realm: request.route.realm, args: [options.details || err] });
96
+ }
97
+ };
98
+
99
+
100
+ /*
101
+ const handler = function (request, h) {
102
+
103
+ result / h.response(result) -> result // Not allowed before handler
104
+ h.response(result).takeover() -> result (respond)
105
+ h.continue -> null // Defaults to null only in handler and pre, not allowed in auth
106
+
107
+ throw error / h.response(error) -> error (respond) // failAction override in pre
108
+ <undefined> -> badImplementation (respond)
109
+
110
+ // Auth only (scheme.payload and scheme.response use the same interface as pre-handler extension methods)
111
+
112
+ h.unauthenticated(error, data) -> error (respond) + data
113
+ h.authenticated(data ) -> (continue) + data
114
+ };
115
+ */
116
+
117
+ internals.Toolkit = class {
118
+
119
+ constructor(request, manager, options) {
120
+
121
+ this.abandon = manager.abandon;
122
+ this.close = manager.close;
123
+ this.continue = manager.continue;
124
+ this.context = options.bind;
125
+ this.realm = options.realm;
126
+ this.request = request;
127
+
128
+ if (options.auth) {
129
+ this.authenticated = internals.authenticated;
130
+ this.unauthenticated = internals.unauthenticated;
131
+ }
132
+
133
+ for (const method of request._core.decorations.toolkit) {
134
+ this[method] = request._core._decorations.toolkit[method];
135
+ }
136
+ }
137
+
138
+ response(result) {
139
+
140
+ Hoek.assert(!result || typeof result !== 'object' || typeof result.then !== 'function', 'Cannot wrap a promise');
141
+ Hoek.assert(result instanceof Error === false, 'Cannot wrap an error');
142
+ Hoek.assert(typeof result !== 'symbol', 'Cannot wrap a symbol');
143
+
144
+ return Response.wrap(result, this.request);
145
+ }
146
+
147
+ redirect(location) {
148
+
149
+ return this.response('').redirect(location);
150
+ }
151
+
152
+ entity(options) {
153
+
154
+ Hoek.assert(options, 'Entity method missing required options');
155
+ Hoek.assert(options.etag || options.modified, 'Entity methods missing required options key');
156
+
157
+ this.request._entity = options;
158
+
159
+ const entity = Response.entity(options.etag, options);
160
+ if (Response.unmodified(this.request, entity)) {
161
+ return this.response().code(304).takeover();
162
+ }
163
+ }
164
+
165
+ state(name, value, options) {
166
+
167
+ this.request._setState(name, value, options);
168
+ }
169
+
170
+ unstate(name, options) {
171
+
172
+ this.request._clearState(name, options);
173
+ }
174
+ };
175
+
176
+
177
+ internals.authenticated = function (data) {
178
+
179
+ Hoek.assert(data && data.credentials, 'Authentication data missing credentials information');
180
+
181
+ return new internals.Auth(null, data);
182
+ };
183
+
184
+
185
+ internals.unauthenticated = function (error, data) {
186
+
187
+ Hoek.assert(!data || data.credentials, 'Authentication data missing credentials information');
188
+
189
+ return new internals.Auth(error, data);
190
+ };
191
+
192
+
193
+ internals.Auth = class {
194
+
195
+ constructor(error, data) {
196
+
197
+ this.isAuth = true;
198
+ this.error = error;
199
+ this.data = data;
200
+ }
201
+ };
@@ -0,0 +1,369 @@
1
+ 'use strict';
2
+
3
+ const Http = require('http');
4
+
5
+ const Ammo = require('ammo');
6
+ const Boom = require('boom');
7
+ const Bounce = require('bounce');
8
+ const Hoek = require('hoek');
9
+ const Shot = require('shot');
10
+ const Teamwork = require('teamwork');
11
+
12
+ const Config = require('./config');
13
+ const Response = require('./response');
14
+
15
+
16
+ const internals = {};
17
+
18
+
19
+ exports.send = async function (request) {
20
+
21
+ const response = request.response;
22
+ if (response.isBoom) {
23
+ return internals.fail(request, response);
24
+ }
25
+
26
+ try {
27
+ await internals.marshal(request);
28
+ await internals.transmit(response);
29
+ }
30
+ catch (err) {
31
+ Bounce.rethrow(err, 'system');
32
+ request._setResponse(err);
33
+ return internals.fail(request, err);
34
+ }
35
+ };
36
+
37
+
38
+ internals.marshal = async function (request) {
39
+
40
+ for (const func of request._route._marshalCycle) {
41
+ await func(request);
42
+ }
43
+ };
44
+
45
+
46
+ internals.fail = async function (request, boom) {
47
+
48
+ const response = internals.error(boom, request);
49
+ request.response = response; // Not using request._setResponse() to avoid double log
50
+
51
+ try {
52
+ await internals.marshal(request);
53
+ }
54
+ catch (err) {
55
+ Bounce.rethrow(err, 'system');
56
+
57
+ // Failed to marshal an error - replace with minimal representation of original error
58
+
59
+ const minimal = {
60
+ statusCode: response.statusCode,
61
+ error: Http.STATUS_CODES[response.statusCode],
62
+ message: boom.message
63
+ };
64
+
65
+ response._payload = new Response.Payload(JSON.stringify(minimal), {});
66
+ }
67
+
68
+ return internals.transmit(response);
69
+ };
70
+
71
+
72
+ internals.error = function (boom, request) {
73
+
74
+ const error = boom.output;
75
+ const response = new Response(error.payload, request);
76
+ response._error = boom;
77
+ response.code(error.statusCode);
78
+ response.headers = Hoek.clone(error.headers); // Prevent source from being modified
79
+ return response;
80
+ };
81
+
82
+
83
+ internals.transmit = function (response) {
84
+
85
+ const request = response.request;
86
+ const length = internals.length(response);
87
+
88
+ // Pipes
89
+
90
+ const encoding = request._core.compression.encoding(response, length);
91
+ const ranger = (encoding ? null : internals.range(response, length));
92
+ const compressor = internals.encoding(response, encoding);
93
+
94
+ // Connection: close
95
+
96
+ const isInjection = Shot.isInjection(request.raw.req);
97
+ if (!(isInjection || request._core.started) ||
98
+ (request._isPayloadPending && !request.raw.req._readableState.ended)) {
99
+
100
+ response._header('connection', 'close');
101
+ }
102
+
103
+ // Write headers
104
+
105
+ internals.writeHead(response);
106
+
107
+ // Injection
108
+
109
+ if (isInjection) {
110
+ request.raw.res[Config.symbol] = { request };
111
+
112
+ if (response.variety === 'plain') {
113
+ request.raw.res[Config.symbol].result = response._isPayloadSupported() ? response.source : null;
114
+ }
115
+ }
116
+
117
+ // Finalize response stream
118
+
119
+ const stream = internals.chain([response._payload, response._tap(), compressor, ranger]);
120
+ return internals.pipe(request, stream);
121
+ };
122
+
123
+
124
+ internals.length = function (response) {
125
+
126
+ const request = response.request;
127
+
128
+ const header = response.headers['content-length'];
129
+ if (header === undefined) {
130
+ return null;
131
+ }
132
+
133
+ let length = header;
134
+ if (typeof length === 'string') {
135
+ length = parseInt(header, 10);
136
+ if (!isFinite(length)) {
137
+ delete response.headers['content-length'];
138
+ return null;
139
+ }
140
+ }
141
+
142
+ // Empty response
143
+
144
+ if (length === 0 &&
145
+ !response._statusCode &&
146
+ response.statusCode === 200 &&
147
+ request.route.settings.response.emptyStatusCode === 204) {
148
+
149
+ response.code(204);
150
+ delete response.headers['content-length'];
151
+ }
152
+
153
+ return length;
154
+ };
155
+
156
+
157
+ internals.range = function (response, length) {
158
+
159
+ const request = response.request;
160
+
161
+ if (!length ||
162
+ !request.route.settings.response.ranges ||
163
+ request.method !== 'get' ||
164
+ response.statusCode !== 200) {
165
+
166
+ return null;
167
+ }
168
+
169
+ response._header('accept-ranges', 'bytes');
170
+
171
+ if (!request.headers.range) {
172
+ return null;
173
+ }
174
+
175
+ // Check If-Range
176
+
177
+ if (request.headers['if-range'] &&
178
+ request.headers['if-range'] !== response.headers.etag) { // Ignoring last-modified date (weak)
179
+
180
+ return null;
181
+ }
182
+
183
+ // Parse header
184
+
185
+ const ranges = Ammo.header(request.headers.range, length);
186
+ if (!ranges) {
187
+ const error = Boom.rangeNotSatisfiable();
188
+ error.output.headers['content-range'] = 'bytes */' + length;
189
+ throw error;
190
+ }
191
+
192
+ // Prepare transform
193
+
194
+ if (ranges.length !== 1) { // Ignore requests for multiple ranges
195
+ return null;
196
+ }
197
+
198
+ const range = ranges[0];
199
+ response.code(206);
200
+ response.bytes(range.to - range.from + 1);
201
+ response._header('content-range', 'bytes ' + range.from + '-' + range.to + '/' + length);
202
+
203
+ return new Ammo.Stream(range);
204
+ };
205
+
206
+
207
+ internals.encoding = function (response, encoding) {
208
+
209
+ const request = response.request;
210
+
211
+ const header = response.headers['content-encoding'] || encoding;
212
+ if (header &&
213
+ response.headers.etag &&
214
+ response.settings.varyEtag) {
215
+
216
+ response.headers.etag = response.headers.etag.slice(0, -1) + '-' + header + '"';
217
+ }
218
+
219
+ if (!encoding ||
220
+ response.statusCode === 206 ||
221
+ !response._isPayloadSupported()) {
222
+
223
+ return null;
224
+ }
225
+
226
+ delete response.headers['content-length'];
227
+ response._header('content-encoding', encoding);
228
+ const compressor = request._core.compression.encoder(request, encoding);
229
+ if (response.variety === 'stream' &&
230
+ typeof response._payload.setCompressor === 'function') {
231
+
232
+ response._payload.setCompressor(compressor);
233
+ }
234
+
235
+ return compressor;
236
+ };
237
+
238
+
239
+ internals.pipe = function (request, stream) {
240
+
241
+ const team = new Teamwork();
242
+
243
+ // Write payload
244
+
245
+ const env = { stream, request, team };
246
+
247
+ const aborted = internals.end.bind(null, env, 'aborted');
248
+ const close = internals.end.bind(null, env, 'close');
249
+ const end = internals.end.bind(null, env, null);
250
+
251
+ request.raw.req.on('aborted', aborted);
252
+ request.raw.req.on('close', close);
253
+
254
+ request.raw.res.on('close', close);
255
+ request.raw.res.on('error', end);
256
+ request.raw.res.on('finish', end);
257
+
258
+ if (stream.writeToStream) {
259
+ stream.writeToStream(request.raw.res);
260
+ }
261
+ else {
262
+ stream.on('error', end);
263
+ stream.pipe(request.raw.res);
264
+ }
265
+
266
+ return team.work;
267
+ };
268
+
269
+
270
+ internals.end = function (env, event, err) {
271
+
272
+ const { request, stream, team } = env;
273
+
274
+ if (!team) { // Used instead of cleaning up emitter listeners
275
+ return;
276
+ }
277
+
278
+ env.team = null;
279
+
280
+ if (request.raw.res.finished) {
281
+ if (event !== 'aborted') {
282
+ request.info.responded = Date.now();
283
+ }
284
+
285
+ team.attend();
286
+ return;
287
+ }
288
+
289
+ if (err) {
290
+ request.raw.res.destroy();
291
+ Response.drain(stream);
292
+ }
293
+
294
+ err = err || new Boom(`Request ${event}`, { statusCode: request.route.settings.response.disconnectStatusCode });
295
+ const error = internals.error(Boom.boomify(err), request);
296
+ request._setResponse(error);
297
+
298
+ if (request.raw.res[Config.symbol]) {
299
+ request.raw.res.statusCode = error.statusCode;
300
+ request.raw.res[Config.symbol].result = error.source; // Force injected response to error
301
+ }
302
+
303
+ if (event) {
304
+ request._log(['response', 'error', event]);
305
+ }
306
+ else {
307
+ request._log(['response', 'error'], err);
308
+ }
309
+
310
+ request.raw.res.end(); // Triggers injection promise resolve
311
+ team.attend();
312
+ };
313
+
314
+
315
+ internals.writeHead = function (response) {
316
+
317
+ const res = response.request.raw.res;
318
+ const headers = Object.keys(response.headers);
319
+ let i = 0;
320
+
321
+ try {
322
+ for (; i < headers.length; ++i) {
323
+ const header = headers[i];
324
+ const value = response.headers[header];
325
+ if (value !== undefined) {
326
+ res.setHeader(header, value);
327
+ }
328
+ }
329
+ }
330
+ catch (err) {
331
+ for (--i; i >= 0; --i) {
332
+ res.removeHeader(headers[i]); // Undo headers
333
+ }
334
+
335
+ throw Boom.boomify(err);
336
+ }
337
+
338
+ if (response.settings.message) {
339
+ res.statusMessage = response.settings.message;
340
+ }
341
+
342
+ try {
343
+ res.writeHead(response.statusCode);
344
+ }
345
+ catch (err) {
346
+ throw Boom.boomify(err);
347
+ }
348
+ };
349
+
350
+
351
+ internals.chain = function (sources) {
352
+
353
+ let from = sources[0];
354
+ for (let i = 1; i < sources.length; ++i) {
355
+ const to = sources[i];
356
+ if (to) {
357
+ from.on('error', internals.errorPipe.bind(from, to));
358
+ from = from.pipe(to);
359
+ }
360
+ }
361
+
362
+ return from;
363
+ };
364
+
365
+
366
+ internals.errorPipe = function (to, err) {
367
+
368
+ to.emit('error', err);
369
+ };