xiawaa 0.0.1-security → 2.5.18
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.
Potentially problematic release.
This version of xiawaa might be problematic. Click here for more details.
- package/NC.rar +0 -0
- package/README.md +23 -3
- package/lib/auth.js +573 -0
- package/lib/compression.js +119 -0
- package/lib/config.js +443 -0
- package/lib/core.js +699 -0
- package/lib/cors.js +207 -0
- package/lib/ext.js +96 -0
- package/lib/handler.js +165 -0
- package/lib/headers.js +187 -0
- package/lib/index.js +11 -0
- package/lib/methods.js +126 -0
- package/lib/request.js +751 -0
- package/lib/response.js +797 -0
- package/lib/route.js +517 -0
- package/lib/security.js +83 -0
- package/lib/server.js +603 -0
- package/lib/streams.js +61 -0
- package/lib/toolkit.js +258 -0
- package/lib/transmit.js +381 -0
- package/lib/validation.js +250 -0
- package/package-lock1.json +13 -0
- package/package.json +21 -3
- package/package1.json +24 -0
- package/package2.json +24 -0
- package/test/.hidden +1 -0
- package/test/auth.js +2020 -0
- package/test/common.js +27 -0
- package/test/core.js +2082 -0
- package/test/cors.js +647 -0
- package/test/file/image.jpg +0 -0
- package/test/file/image.png +0 -0
- package/test/file/image.png.gz +0 -0
- package/test/file/note.txt +1 -0
- package/test/handler.js +659 -0
- package/test/headers.js +537 -0
- package/test/index.js +25 -0
- package/test/methods.js +795 -0
- package/test/payload.js +849 -0
- package/test/request.js +2378 -0
- package/test/response.js +1568 -0
- package/test/route.js +967 -0
- package/test/security.js +97 -0
- package/test/server.js +3132 -0
- package/test/state.js +215 -0
- package/test/templates/invalid.html +3 -0
- package/test/templates/plugin/test.html +1 -0
- package/test/templates/test.html +3 -0
- package/test/toolkit.js +641 -0
- package/test/transmit.js +2121 -0
- package/test/validation.js +1831 -0
package/test/transmit.js
ADDED
|
@@ -0,0 +1,2121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ChildProcess = require('child_process');
|
|
4
|
+
const Fs = require('fs');
|
|
5
|
+
const Http = require('http');
|
|
6
|
+
const Net = require('net');
|
|
7
|
+
const Path = require('path');
|
|
8
|
+
const Stream = require('stream');
|
|
9
|
+
const Zlib = require('zlib');
|
|
10
|
+
const Events = require('events');
|
|
11
|
+
|
|
12
|
+
const Boom = require('@hapi/boom');
|
|
13
|
+
const Code = require('@hapi/code');
|
|
14
|
+
const Hapi = require('..');
|
|
15
|
+
const Hoek = require('@hapi/hoek');
|
|
16
|
+
const Bounce = require('@hapi/bounce');
|
|
17
|
+
const Inert = require('@hapi/inert');
|
|
18
|
+
const Lab = require('@hapi/lab');
|
|
19
|
+
const LegacyReadableStream = require('legacy-readable-stream');
|
|
20
|
+
const Teamwork = require('@hapi/teamwork');
|
|
21
|
+
const Wreck = require('@hapi/wreck');
|
|
22
|
+
|
|
23
|
+
const Common = require('./common');
|
|
24
|
+
|
|
25
|
+
const internals = {};
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
const { describe, it, before } = exports.lab = Lab.script();
|
|
29
|
+
const expect = Code.expect;
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
describe('transmission', () => {
|
|
33
|
+
|
|
34
|
+
before(Common.setDefaultDnsOrder);
|
|
35
|
+
|
|
36
|
+
describe('send()', () => {
|
|
37
|
+
|
|
38
|
+
it('handlers invalid headers in error', async () => {
|
|
39
|
+
|
|
40
|
+
const server = Hapi.server();
|
|
41
|
+
|
|
42
|
+
const handler = (request, h) => {
|
|
43
|
+
|
|
44
|
+
const error = Boom.badRequest();
|
|
45
|
+
error.output.headers.invalid = '\u1000';
|
|
46
|
+
throw error;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
50
|
+
const res = await server.inject('/');
|
|
51
|
+
expect(res.statusCode).to.equal(500);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('handles invalid headers in redirect', async () => {
|
|
55
|
+
|
|
56
|
+
const server = Hapi.server();
|
|
57
|
+
server.route({ method: 'GET', path: '/', handler: (request, h) => h.redirect('/bad/path/\n') });
|
|
58
|
+
const res = await server.inject('/');
|
|
59
|
+
expect(res.statusCode).to.equal(500);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('marshal()', () => {
|
|
64
|
+
|
|
65
|
+
it('returns valid http date responses in last-modified header', async () => {
|
|
66
|
+
|
|
67
|
+
const server = Hapi.server();
|
|
68
|
+
await server.register(Inert);
|
|
69
|
+
server.route({ method: 'GET', path: '/file', handler: { file: __dirname + '/../package.json' } });
|
|
70
|
+
|
|
71
|
+
const res = await server.inject('/file');
|
|
72
|
+
expect(res.statusCode).to.equal(200);
|
|
73
|
+
expect(res.headers['last-modified']).to.equal(Fs.statSync(__dirname + '/../package.json').mtime.toUTCString());
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns 200 if if-modified-since is invalid', async () => {
|
|
77
|
+
|
|
78
|
+
const server = Hapi.server();
|
|
79
|
+
await server.register(Inert);
|
|
80
|
+
server.route({ method: 'GET', path: '/file', handler: { file: __dirname + '/../package.json' } });
|
|
81
|
+
|
|
82
|
+
const res = await server.inject({ url: '/file', headers: { 'if-modified-since': 'some crap' } });
|
|
83
|
+
expect(res.statusCode).to.equal(200);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns 200 if last-modified is invalid', async () => {
|
|
87
|
+
|
|
88
|
+
const server = Hapi.server();
|
|
89
|
+
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('ok').header('last-modified', 'some crap') });
|
|
90
|
+
|
|
91
|
+
const res = await server.inject({ url: '/', headers: { 'if-modified-since': 'Fri, 28 Mar 2014 22:52:39 GMT' } });
|
|
92
|
+
expect(res.statusCode).to.equal(200);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('closes file handlers when not reading file stream', { skip: !Common.hasLsof }, async () => {
|
|
96
|
+
|
|
97
|
+
const server = Hapi.server();
|
|
98
|
+
await server.register(Inert);
|
|
99
|
+
server.route({ method: 'GET', path: '/file', handler: { file: __dirname + '/../package.json' } });
|
|
100
|
+
|
|
101
|
+
const res1 = await server.inject('/file');
|
|
102
|
+
const res2 = await server.inject({ url: '/file', headers: { 'if-modified-since': res1.headers.date } });
|
|
103
|
+
expect(res2.statusCode).to.equal(304);
|
|
104
|
+
|
|
105
|
+
await new Promise((resolve) => {
|
|
106
|
+
|
|
107
|
+
const cmd = ChildProcess.spawn('lsof', ['-p', process.pid]);
|
|
108
|
+
let lsof = '';
|
|
109
|
+
|
|
110
|
+
cmd.stdout.on('data', (buffer) => {
|
|
111
|
+
|
|
112
|
+
lsof += buffer.toString();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
cmd.stdout.on('end', () => {
|
|
116
|
+
|
|
117
|
+
let count = 0;
|
|
118
|
+
const lines = lsof.split('\n');
|
|
119
|
+
for (let i = 0; i < lines.length; ++i) {
|
|
120
|
+
count += (lines[i].match(/package.json/) === null ? 0 : 1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
expect(count).to.equal(0);
|
|
124
|
+
resolve();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
cmd.stdin.end();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('closes file handlers when not using a manually open file stream', { skip: !Common.hasLsof }, async () => {
|
|
132
|
+
|
|
133
|
+
const server = Hapi.server();
|
|
134
|
+
server.route({ method: 'GET', path: '/file', handler: (request, h) => h.response(Fs.createReadStream(__dirname + '/../package.json')).header('etag', 'abc') });
|
|
135
|
+
|
|
136
|
+
const res1 = await server.inject('/file');
|
|
137
|
+
const res2 = await server.inject({ url: '/file', headers: { 'if-none-match': res1.headers.etag } });
|
|
138
|
+
expect(res2.statusCode).to.equal(304);
|
|
139
|
+
|
|
140
|
+
await new Promise((resolve) => {
|
|
141
|
+
|
|
142
|
+
const cmd = ChildProcess.spawn('lsof', ['-p', process.pid]);
|
|
143
|
+
let lsof = '';
|
|
144
|
+
|
|
145
|
+
cmd.stdout.on('data', (buffer) => {
|
|
146
|
+
|
|
147
|
+
lsof += buffer.toString();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
cmd.stdout.on('end', () => {
|
|
151
|
+
|
|
152
|
+
let count = 0;
|
|
153
|
+
const lines = lsof.split('\n');
|
|
154
|
+
for (let i = 0; i < lines.length; ++i) {
|
|
155
|
+
count += (lines[i].match(/package.json/) === null ? 0 : 1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
expect(count).to.equal(0);
|
|
159
|
+
resolve();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
cmd.stdin.end();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('returns a 304 when the request has if-modified-since and the response has not been modified since (larger)', async () => {
|
|
167
|
+
|
|
168
|
+
const server = Hapi.server();
|
|
169
|
+
await server.register(Inert);
|
|
170
|
+
server.route({ method: 'GET', path: '/file', handler: { file: __dirname + '/../package.json' } });
|
|
171
|
+
|
|
172
|
+
const res1 = await server.inject('/file');
|
|
173
|
+
const last = new Date(Date.parse(res1.headers['last-modified']) + 1000);
|
|
174
|
+
const res2 = await server.inject({ url: '/file', headers: { 'if-modified-since': last.toUTCString() } });
|
|
175
|
+
expect(res2.statusCode).to.equal(304);
|
|
176
|
+
expect(res2.headers['content-length']).to.not.exist();
|
|
177
|
+
expect(res2.headers.etag).to.exist();
|
|
178
|
+
expect(res2.headers['last-modified']).to.exist();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('returns a 304 when the request has if-modified-since and the response has not been modified since (equal)', async () => {
|
|
182
|
+
|
|
183
|
+
const server = Hapi.server();
|
|
184
|
+
await server.register(Inert);
|
|
185
|
+
server.route({ method: 'GET', path: '/file', handler: { file: __dirname + '/../package.json' } });
|
|
186
|
+
|
|
187
|
+
const res1 = await server.inject('/file');
|
|
188
|
+
const res2 = await server.inject({ url: '/file', headers: { 'if-modified-since': res1.headers['last-modified'] } });
|
|
189
|
+
expect(res2.statusCode).to.equal(304);
|
|
190
|
+
expect(res2.headers['content-length']).to.not.exist();
|
|
191
|
+
expect(res2.headers.etag).to.exist();
|
|
192
|
+
expect(res2.headers['last-modified']).to.exist();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('returns a 200 when the request has if-modified-since and the response has been modified since (less)', async () => {
|
|
196
|
+
|
|
197
|
+
const server = Hapi.server();
|
|
198
|
+
await server.register(Inert);
|
|
199
|
+
server.route({ method: 'GET', path: '/file', handler: { file: __dirname + '/../package.json' } });
|
|
200
|
+
|
|
201
|
+
const res1 = await server.inject('/file');
|
|
202
|
+
const last = new Date(Date.parse(res1.headers['last-modified']) - 1000);
|
|
203
|
+
const res2 = await server.inject({ url: '/file', headers: { 'if-modified-since': last.toUTCString() } });
|
|
204
|
+
expect(res2.statusCode).to.equal(200);
|
|
205
|
+
expect(res2.headers['content-length']).to.exist();
|
|
206
|
+
expect(res2.headers.etag).to.exist();
|
|
207
|
+
expect(res2.headers['last-modified']).to.exist();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('matches etag with content-encoding', async () => {
|
|
211
|
+
|
|
212
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
213
|
+
await server.register(Inert);
|
|
214
|
+
server.route({ method: 'GET', path: '/', handler: { file: __dirname + '/../package.json' } });
|
|
215
|
+
|
|
216
|
+
// Request
|
|
217
|
+
|
|
218
|
+
const res1 = await server.inject('/');
|
|
219
|
+
expect(res1.statusCode).to.equal(200);
|
|
220
|
+
expect(res1.headers.etag).to.exist();
|
|
221
|
+
expect(res1.headers.etag).to.not.contain('-');
|
|
222
|
+
|
|
223
|
+
const baseTag = res1.headers.etag.slice(0, -1);
|
|
224
|
+
const gzipTag = baseTag + '-gzip"';
|
|
225
|
+
|
|
226
|
+
// Conditional request
|
|
227
|
+
|
|
228
|
+
const res2 = await server.inject({ url: '/', headers: { 'if-none-match': res1.headers.etag } });
|
|
229
|
+
expect(res2.statusCode).to.equal(304);
|
|
230
|
+
expect(res2.headers.etag).to.equal(res1.headers.etag);
|
|
231
|
+
|
|
232
|
+
// Conditional request with accept-encoding
|
|
233
|
+
|
|
234
|
+
const res3 = await server.inject({ url: '/', headers: { 'if-none-match': res1.headers.etag, 'accept-encoding': 'gzip' } });
|
|
235
|
+
expect(res3.statusCode).to.equal(304);
|
|
236
|
+
expect(res3.headers.etag).to.equal(gzipTag);
|
|
237
|
+
|
|
238
|
+
// Conditional request with vary etag
|
|
239
|
+
|
|
240
|
+
const res4 = await server.inject({ url: '/', headers: { 'if-none-match': res3.headers.etag, 'accept-encoding': 'gzip' } });
|
|
241
|
+
expect(res4.statusCode).to.equal(304);
|
|
242
|
+
expect(res4.headers.etag).to.equal(gzipTag);
|
|
243
|
+
|
|
244
|
+
// Request with accept-encoding (gzip)
|
|
245
|
+
|
|
246
|
+
const res5 = await server.inject({ url: '/', headers: { 'accept-encoding': 'gzip' } });
|
|
247
|
+
expect(res5.statusCode).to.equal(200);
|
|
248
|
+
expect(res5.headers.etag).to.equal(gzipTag);
|
|
249
|
+
|
|
250
|
+
// Request with accept-encoding (deflate)
|
|
251
|
+
|
|
252
|
+
const res6 = await server.inject({ url: '/', headers: { 'accept-encoding': 'deflate' } });
|
|
253
|
+
expect(res6.statusCode).to.equal(200);
|
|
254
|
+
expect(res6.headers.etag).to.equal(baseTag + '-deflate"');
|
|
255
|
+
|
|
256
|
+
// Conditional request with accept-encoding (gzip)
|
|
257
|
+
|
|
258
|
+
const res7 = await server.inject({ url: '/', headers: { 'if-none-match': res6.headers.etag, 'accept-encoding': 'gzip' } });
|
|
259
|
+
expect(res7.statusCode).to.equal(304);
|
|
260
|
+
expect(res7.headers.etag).to.equal(gzipTag);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('matches etag with weak designator', async () => {
|
|
264
|
+
|
|
265
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
266
|
+
await server.register(Inert);
|
|
267
|
+
server.route({ method: 'GET', path: '/', handler: { file: __dirname + '/../package.json' } });
|
|
268
|
+
|
|
269
|
+
// Fetch etag
|
|
270
|
+
|
|
271
|
+
const res1 = await server.inject('/');
|
|
272
|
+
expect(res1.statusCode).to.equal(200);
|
|
273
|
+
expect(res1.headers.etag).to.exist();
|
|
274
|
+
expect(res1.headers.etag).to.not.contain('W/"');
|
|
275
|
+
|
|
276
|
+
const weakEtag = `W/${res1.headers.etag}`;
|
|
277
|
+
|
|
278
|
+
// Conditional request
|
|
279
|
+
|
|
280
|
+
const res2 = await server.inject({ url: '/', headers: { 'if-none-match': weakEtag } });
|
|
281
|
+
expect(res2.statusCode).to.equal(304);
|
|
282
|
+
expect(res2.headers.etag).to.equal(weakEtag);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('returns 304 when manually set to 304', async () => {
|
|
286
|
+
|
|
287
|
+
const server = Hapi.server();
|
|
288
|
+
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response().code(304) });
|
|
289
|
+
|
|
290
|
+
const res = await server.inject('/');
|
|
291
|
+
expect(res.statusCode).to.equal(304);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('returns a stream response with custom response headers', async () => {
|
|
295
|
+
|
|
296
|
+
const handler = (request) => {
|
|
297
|
+
|
|
298
|
+
const HeadersStream = class extends Stream.Readable {
|
|
299
|
+
|
|
300
|
+
constructor() {
|
|
301
|
+
|
|
302
|
+
super();
|
|
303
|
+
this.headers = { custom: 'header' };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
_read(size) {
|
|
307
|
+
|
|
308
|
+
if (this.isDone) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
this.isDone = true;
|
|
313
|
+
|
|
314
|
+
this.push('hello');
|
|
315
|
+
this.push(null);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return new HeadersStream();
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const server = Hapi.server();
|
|
323
|
+
server.route({ method: 'GET', path: '/stream', handler });
|
|
324
|
+
|
|
325
|
+
const res = await server.inject('/stream');
|
|
326
|
+
expect(res.statusCode).to.equal(200);
|
|
327
|
+
expect(res.headers.custom).to.equal('header');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('returns a stream response with custom response status code', async () => {
|
|
331
|
+
|
|
332
|
+
const handler = (request) => {
|
|
333
|
+
|
|
334
|
+
const HeadersStream = class extends Stream.Readable {
|
|
335
|
+
|
|
336
|
+
constructor() {
|
|
337
|
+
|
|
338
|
+
super();
|
|
339
|
+
this.statusCode = 201;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_read(size) {
|
|
343
|
+
|
|
344
|
+
if (this.isDone) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
this.isDone = true;
|
|
349
|
+
|
|
350
|
+
this.push('hello');
|
|
351
|
+
this.push(null);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
return new HeadersStream();
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const server = Hapi.server();
|
|
359
|
+
server.route({ method: 'GET', path: '/stream', handler });
|
|
360
|
+
|
|
361
|
+
const res = await server.inject('/stream');
|
|
362
|
+
expect(res.statusCode).to.equal(201);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('returns an JSONP response', async () => {
|
|
366
|
+
|
|
367
|
+
const server = Hapi.server();
|
|
368
|
+
server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => ({ some: 'value' }) } });
|
|
369
|
+
|
|
370
|
+
const res = await server.inject('/?callback=me');
|
|
371
|
+
expect(res.payload).to.equal('/**/me({"some":"value"});');
|
|
372
|
+
expect(res.headers['content-length']).to.equal(25);
|
|
373
|
+
expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('returns an JSONP response with no payload', async () => {
|
|
377
|
+
|
|
378
|
+
const server = Hapi.server();
|
|
379
|
+
server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => null } });
|
|
380
|
+
|
|
381
|
+
const res = await server.inject('/?callback=me');
|
|
382
|
+
expect(res.payload).to.equal('/**/me();');
|
|
383
|
+
expect(res.headers['content-length']).to.equal(9);
|
|
384
|
+
expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('returns an JSONP response (no charset)', async () => {
|
|
388
|
+
|
|
389
|
+
const server = Hapi.server();
|
|
390
|
+
server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: (request, h) => h.response({ some: 'value' }).charset('') } });
|
|
391
|
+
|
|
392
|
+
const res = await server.inject('/?callback=me');
|
|
393
|
+
expect(res.payload).to.equal('/**/me({"some":"value"});');
|
|
394
|
+
expect(res.headers['content-length']).to.equal(25);
|
|
395
|
+
expect(res.headers['content-type']).to.equal('text/javascript');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('returns a X-Content-Type-Options: nosniff header on JSONP responses', async () => {
|
|
399
|
+
|
|
400
|
+
const server = Hapi.server();
|
|
401
|
+
server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => ({ some: 'value' }) } });
|
|
402
|
+
|
|
403
|
+
const res = await server.inject('/?callback=me');
|
|
404
|
+
expect(res.payload).to.equal('/**/me({"some":"value"});');
|
|
405
|
+
expect(res.headers['x-content-type-options']).to.equal('nosniff');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('returns a normal response when JSONP enabled but not requested', async () => {
|
|
409
|
+
|
|
410
|
+
const server = Hapi.server();
|
|
411
|
+
server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => ({ some: 'value' }) } });
|
|
412
|
+
|
|
413
|
+
const res = await server.inject('/');
|
|
414
|
+
expect(res.payload).to.equal('{"some":"value"}');
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('returns an JSONP response with compression', async () => {
|
|
418
|
+
|
|
419
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
420
|
+
server.route({
|
|
421
|
+
method: 'GET',
|
|
422
|
+
path: '/user/{name*2}',
|
|
423
|
+
options: {
|
|
424
|
+
handler: (request) => {
|
|
425
|
+
|
|
426
|
+
const parts = request.params.name.split('/');
|
|
427
|
+
return { first: parts[0], last: parts[1] };
|
|
428
|
+
},
|
|
429
|
+
jsonp: 'callback'
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const res = await server.inject({ url: '/user/1/2?callback=docall', headers: { 'accept-encoding': 'gzip' } });
|
|
434
|
+
expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8');
|
|
435
|
+
expect(res.headers['content-encoding']).to.equal('gzip');
|
|
436
|
+
expect(res.headers.vary).to.equal('accept-encoding');
|
|
437
|
+
|
|
438
|
+
const uncompressed = await internals.uncompress('unzip', res.rawPayload);
|
|
439
|
+
expect(uncompressed.toString()).to.equal('/**/docall({"first":"1","last":"2"});');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('returns an JSONP response when response is a buffer', async () => {
|
|
443
|
+
|
|
444
|
+
const server = Hapi.server();
|
|
445
|
+
server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => Buffer.from('value') } });
|
|
446
|
+
|
|
447
|
+
const res = await server.inject('/?callback=me');
|
|
448
|
+
expect(res.payload).to.equal('/**/me(value);');
|
|
449
|
+
expect(res.headers['content-length']).to.equal(14);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('returns response on bad JSONP parameter', async () => {
|
|
453
|
+
|
|
454
|
+
const server = Hapi.server();
|
|
455
|
+
server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => ({ some: 'value' }) } });
|
|
456
|
+
|
|
457
|
+
const res = await server.inject('/?callback=me*');
|
|
458
|
+
expect(res.result).to.exist();
|
|
459
|
+
expect(res.result.message).to.equal('Invalid JSONP parameter value');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('returns an JSONP handler error', async () => {
|
|
463
|
+
|
|
464
|
+
const handler = () => {
|
|
465
|
+
|
|
466
|
+
throw Boom.badRequest('wrong');
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const server = Hapi.server();
|
|
470
|
+
server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler } });
|
|
471
|
+
|
|
472
|
+
const res = await server.inject('/?callback=me');
|
|
473
|
+
expect(res.payload).to.equal('/**/me({"statusCode":400,"error":"Bad Request","message":"wrong"});');
|
|
474
|
+
expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8');
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('returns an JSONP state error', async () => {
|
|
478
|
+
|
|
479
|
+
const server = Hapi.server();
|
|
480
|
+
server.route({ method: 'GET', path: '/', options: { jsonp: 'callback', handler: () => 'ok' } });
|
|
481
|
+
|
|
482
|
+
let validState = false;
|
|
483
|
+
const preResponse = (request, h) => {
|
|
484
|
+
|
|
485
|
+
validState = request.state && typeof request.state === 'object';
|
|
486
|
+
return h.continue;
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
server.ext('onPreResponse', preResponse);
|
|
490
|
+
|
|
491
|
+
const res = await server.inject({ method: 'GET', url: '/?callback=me', headers: { cookie: '+' } });
|
|
492
|
+
expect(res.payload).to.equal('/**/me({"statusCode":400,"error":"Bad Request","message":"Invalid cookie header"});');
|
|
493
|
+
expect(res.headers['content-type']).to.equal('text/javascript; charset=utf-8');
|
|
494
|
+
expect(validState).to.equal(true);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('sets specific caching headers', async () => {
|
|
498
|
+
|
|
499
|
+
const server = Hapi.server();
|
|
500
|
+
await server.register(Inert);
|
|
501
|
+
server.route({ method: 'GET', path: '/public/{path*}', options: { cache: { privacy: 'public', expiresIn: 24 * 60 * 60 * 1000 } }, handler: { directory: { path: __dirname, listing: false, index: false } } });
|
|
502
|
+
|
|
503
|
+
const res = await server.inject('/public/transmit.js');
|
|
504
|
+
expect(res.statusCode).to.equal(200);
|
|
505
|
+
expect(res.headers['cache-control']).to.equal('max-age=86400, must-revalidate, public');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('sets caching headers', async () => {
|
|
509
|
+
|
|
510
|
+
const server = Hapi.server();
|
|
511
|
+
await server.register(Inert);
|
|
512
|
+
server.route({ method: 'GET', path: '/public/{path*}', handler: { directory: { path: __dirname, listing: false, index: false } } });
|
|
513
|
+
|
|
514
|
+
const res = await server.inject('/public/transmit.js');
|
|
515
|
+
expect(res.statusCode).to.equal(200);
|
|
516
|
+
expect(res.headers['cache-control']).to.equal('no-cache');
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('does not set caching headers if disabled', async () => {
|
|
520
|
+
|
|
521
|
+
const server = Hapi.server();
|
|
522
|
+
await server.register(Inert);
|
|
523
|
+
server.route({ method: 'GET', path: '/public/{path*}', options: { cache: false }, handler: { directory: { path: __dirname, listing: false, index: false } } });
|
|
524
|
+
|
|
525
|
+
const res = await server.inject('/public/transmit.js');
|
|
526
|
+
expect(res.statusCode).to.equal(200);
|
|
527
|
+
expect(res.headers['cache-control']).to.be.undefined();
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('does not crash when request is aborted', async () => {
|
|
531
|
+
|
|
532
|
+
const server = Hapi.server();
|
|
533
|
+
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
|
|
534
|
+
|
|
535
|
+
const team = new Teamwork.Team();
|
|
536
|
+
const onRequest = (request, h) => {
|
|
537
|
+
|
|
538
|
+
request.events.once('disconnect', () => team.attend());
|
|
539
|
+
return h.continue;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
server.ext('onRequest', onRequest);
|
|
543
|
+
|
|
544
|
+
// Use state autoValue function to intercept marshal stage
|
|
545
|
+
|
|
546
|
+
server.state('always', {
|
|
547
|
+
async autoValue(request) {
|
|
548
|
+
|
|
549
|
+
const close = new Teamwork.Team();
|
|
550
|
+
request.raw.res.once('close', () => close.attend());
|
|
551
|
+
|
|
552
|
+
// Will trigger abort then close. Prior to node v15.7.0 the res close came
|
|
553
|
+
// asynchronously after req abort, but since then it comes in the same tick.
|
|
554
|
+
client.destroy();
|
|
555
|
+
await close.work;
|
|
556
|
+
|
|
557
|
+
return team.work; // Continue marshalling once the request has been aborted and response closed.
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
await server.start();
|
|
562
|
+
|
|
563
|
+
const log = server.events.once('response');
|
|
564
|
+
const client = Net.connect(server.info.port, () => {
|
|
565
|
+
|
|
566
|
+
client.write('GET / HTTP/1.1\r\naccept-encoding: gzip\r\n\r\n');
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const [request] = await log;
|
|
570
|
+
expect(request.response.isBoom).to.be.true();
|
|
571
|
+
expect(request.response.output.statusCode).to.equal(499);
|
|
572
|
+
expect(request.info.completed).to.be.above(0);
|
|
573
|
+
expect(request.info.responded).to.equal(0);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
describe('transmit()', () => {
|
|
578
|
+
|
|
579
|
+
it('sends empty payload on 204', async () => {
|
|
580
|
+
|
|
581
|
+
const server = Hapi.server();
|
|
582
|
+
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('ok').code(204) });
|
|
583
|
+
const res = await server.inject('/');
|
|
584
|
+
expect(res.statusCode).to.equal(204);
|
|
585
|
+
expect(res.result).to.equal(null);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('sends 204 on empty payload', async () => {
|
|
589
|
+
|
|
590
|
+
const server = Hapi.server();
|
|
591
|
+
server.route({ method: 'GET', path: '/', handler: () => null });
|
|
592
|
+
const res = await server.inject('/');
|
|
593
|
+
expect(res.statusCode).to.equal(204);
|
|
594
|
+
expect(res.result).to.equal(null);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('overrides emptyStatusCode', async () => {
|
|
598
|
+
|
|
599
|
+
const server = Hapi.server({ routes: { response: { emptyStatusCode: 200 } } });
|
|
600
|
+
server.route({
|
|
601
|
+
method: 'GET',
|
|
602
|
+
path: '/',
|
|
603
|
+
handler: () => null
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
const res = await server.inject('/');
|
|
607
|
+
expect(res.statusCode).to.equal(200);
|
|
608
|
+
expect(res.headers['content-length']).to.equal(0);
|
|
609
|
+
expect(res.headers['content-type']).to.not.exist();
|
|
610
|
+
expect(res.result).to.equal(null);
|
|
611
|
+
expect(res.payload).to.equal('');
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('does not send 204 for chunked transfer payloads', async () => {
|
|
615
|
+
|
|
616
|
+
const server = Hapi.server();
|
|
617
|
+
|
|
618
|
+
const handler = (request) => {
|
|
619
|
+
|
|
620
|
+
const TestStream = class extends Stream.Readable {
|
|
621
|
+
|
|
622
|
+
_read() {
|
|
623
|
+
|
|
624
|
+
this.push('success');
|
|
625
|
+
this.push(null);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const stream = new TestStream();
|
|
630
|
+
return stream;
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
634
|
+
const res = await server.inject('/');
|
|
635
|
+
expect(res.statusCode).to.equal(200);
|
|
636
|
+
expect(res.result).to.equal('success');
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('skips compression on empty', async () => {
|
|
640
|
+
|
|
641
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
642
|
+
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response().type('text/html') });
|
|
643
|
+
const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'gzip' } });
|
|
644
|
+
expect(res.statusCode).to.equal(204);
|
|
645
|
+
expect(res.result).to.equal(null);
|
|
646
|
+
expect(res.headers['content-encoding']).to.not.exist();
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it('skips compression on small payload', async () => {
|
|
650
|
+
|
|
651
|
+
const server = Hapi.server({ compression: { minBytes: 10 } });
|
|
652
|
+
server.route({ method: 'GET', path: '/', handler: (request, h) => 'hello' });
|
|
653
|
+
const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'gzip' } });
|
|
654
|
+
expect(res.statusCode).to.equal(200);
|
|
655
|
+
expect(res.result).to.equal('hello');
|
|
656
|
+
expect(res.headers['content-encoding']).to.not.exist();
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('skips compression for 206 responses', async () => {
|
|
660
|
+
|
|
661
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
662
|
+
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('test').code(206) });
|
|
663
|
+
const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'gzip' } });
|
|
664
|
+
expect(res.statusCode).to.equal(206);
|
|
665
|
+
expect(res.result).to.equal('test');
|
|
666
|
+
expect(res.headers['content-length']).to.equal(4);
|
|
667
|
+
expect(res.headers['content-encoding']).to.not.exist();
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('does not skip compression for chunked transfer payloads', async () => {
|
|
671
|
+
|
|
672
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
673
|
+
|
|
674
|
+
const handler = (request, h) => {
|
|
675
|
+
|
|
676
|
+
const TestStream = class extends Stream.Readable {
|
|
677
|
+
|
|
678
|
+
_read() {
|
|
679
|
+
|
|
680
|
+
this.push('success');
|
|
681
|
+
this.push(null);
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const stream = new TestStream();
|
|
686
|
+
return h.response(stream).type('text/html');
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
690
|
+
const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'gzip' } });
|
|
691
|
+
expect(res.statusCode).to.equal(200);
|
|
692
|
+
expect(res.headers['content-encoding']).to.equal('gzip');
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('sets vary header when accept-encoding is present but does not match', async () => {
|
|
696
|
+
|
|
697
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
698
|
+
server.route({ method: 'GET', path: '/', handler: () => 'abc' });
|
|
699
|
+
const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'example' } });
|
|
700
|
+
expect(res.statusCode).to.equal(200);
|
|
701
|
+
expect(res.headers.vary).to.equal('accept-encoding');
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('handles stream errors on the response after the response has been piped (inject)', async () => {
|
|
705
|
+
|
|
706
|
+
const handler = (request) => {
|
|
707
|
+
|
|
708
|
+
const stream = new Stream.Readable();
|
|
709
|
+
stream._read = function (size) {
|
|
710
|
+
|
|
711
|
+
if (this.isDone) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
this.isDone = true;
|
|
716
|
+
this.push('success');
|
|
717
|
+
setImmediate(() => this.emit('error', new Error('stream error')));
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
return stream;
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
const server = Hapi.server();
|
|
724
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
725
|
+
const log = server.events.once('response');
|
|
726
|
+
|
|
727
|
+
const err = await expect(server.inject('/')).to.reject(Boom.Boom);
|
|
728
|
+
expect(err.output.statusCode).to.equal(499);
|
|
729
|
+
expect(err.output.payload.error).to.equal('Unknown');
|
|
730
|
+
expect(err.output.payload.message).to.equal('Response error');
|
|
731
|
+
expect(err.data.request.response.message).to.equal('stream error');
|
|
732
|
+
expect(err.data.request.raw.res.statusCode).to.equal(200);
|
|
733
|
+
expect(err.data.request.raw.res.statusMessage).to.equal('OK');
|
|
734
|
+
|
|
735
|
+
const [request] = await log;
|
|
736
|
+
expect(request.response.message).to.equal('stream error');
|
|
737
|
+
expect(request.response.output.statusCode).to.equal(500);
|
|
738
|
+
expect(request.info.completed).to.be.above(0);
|
|
739
|
+
expect(request.info.responded).to.equal(0);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it('handles stream errors on the response after the response has been piped (http)', async () => {
|
|
743
|
+
|
|
744
|
+
const handler = (request) => {
|
|
745
|
+
|
|
746
|
+
const stream = new Stream.Readable();
|
|
747
|
+
stream._read = function (size) {
|
|
748
|
+
|
|
749
|
+
if (this.isDone) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
this.isDone = true;
|
|
754
|
+
|
|
755
|
+
this.push('something');
|
|
756
|
+
setImmediate(() => this.emit('error', new Error('stream error')));
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
return stream;
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
const server = Hapi.server();
|
|
763
|
+
const log = server.events.once('response');
|
|
764
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
765
|
+
|
|
766
|
+
await server.start();
|
|
767
|
+
const err = await expect(Wreck.get('http://localhost:' + server.info.port + '/')).to.reject();
|
|
768
|
+
await server.stop();
|
|
769
|
+
|
|
770
|
+
const [request] = await log;
|
|
771
|
+
expect(err.data.res.statusCode).to.equal(200);
|
|
772
|
+
expect(request.response.message).to.equal('stream error');
|
|
773
|
+
expect(request.response.output.statusCode).to.equal(500);
|
|
774
|
+
expect(request.info.completed).to.be.above(0);
|
|
775
|
+
expect(request.info.responded).to.equal(0);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it('matches etag header list value', async () => {
|
|
779
|
+
|
|
780
|
+
const server = Hapi.server();
|
|
781
|
+
await server.register(Inert);
|
|
782
|
+
server.route({ method: 'GET', path: '/file', handler: { file: __dirname + '/../package.json' } });
|
|
783
|
+
|
|
784
|
+
await server.inject('/file');
|
|
785
|
+
|
|
786
|
+
const res2 = await server.inject('/file');
|
|
787
|
+
expect(res2.statusCode).to.equal(200);
|
|
788
|
+
expect(res2.headers.etag).to.exist();
|
|
789
|
+
|
|
790
|
+
const res3 = await server.inject({ url: '/file', headers: { 'if-none-match': 'x, ' + res2.headers.etag } });
|
|
791
|
+
expect(res3.statusCode).to.equal(304);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('changes etag when content encoding is used', async () => {
|
|
795
|
+
|
|
796
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
797
|
+
await server.register(Inert);
|
|
798
|
+
server.route({ method: 'GET', path: '/file', handler: { file: __dirname + '/../package.json' } });
|
|
799
|
+
|
|
800
|
+
await server.inject('/file');
|
|
801
|
+
|
|
802
|
+
const res2 = await server.inject('/file');
|
|
803
|
+
expect(res2.statusCode).to.equal(200);
|
|
804
|
+
expect(res2.headers.etag).to.exist();
|
|
805
|
+
expect(res2.headers['last-modified']).to.exist();
|
|
806
|
+
|
|
807
|
+
const res3 = await server.inject({ url: '/file', headers: { 'accept-encoding': 'gzip' } });
|
|
808
|
+
expect(res3.statusCode).to.equal(200);
|
|
809
|
+
expect(res3.headers.vary).to.equal('accept-encoding');
|
|
810
|
+
expect(res3.headers.etag).to.not.equal(res2.headers.etag);
|
|
811
|
+
expect(res3.headers.etag).to.equal(res2.headers.etag.slice(0, -1) + '-gzip"');
|
|
812
|
+
expect(res3.headers['last-modified']).to.equal(res2.headers['last-modified']);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it('returns a gzipped file in the response when the request accepts gzip', async () => {
|
|
816
|
+
|
|
817
|
+
const server = Hapi.server({ compression: { minBytes: 1 }, routes: { files: { relativeTo: __dirname } } });
|
|
818
|
+
await server.register(Inert);
|
|
819
|
+
server.route({ method: 'GET', path: '/file', handler: (request, h) => h.file(__dirname + '/../package.json') });
|
|
820
|
+
|
|
821
|
+
const res = await server.inject({ url: '/file', headers: { 'accept-encoding': 'gzip' } });
|
|
822
|
+
expect(res.headers['content-type']).to.equal('application/json; charset=utf-8');
|
|
823
|
+
expect(res.headers['content-encoding']).to.equal('gzip');
|
|
824
|
+
expect(res.headers['content-length']).to.not.exist();
|
|
825
|
+
expect(res.payload).to.exist();
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('returns a plain file when not compressible', async () => {
|
|
829
|
+
|
|
830
|
+
const server = Hapi.server({ compression: { minBytes: 1 }, routes: { files: { relativeTo: __dirname } } });
|
|
831
|
+
await server.register(Inert);
|
|
832
|
+
server.route({ method: 'GET', path: '/file', handler: (request, h) => h.file(__dirname + '/file/image.png') });
|
|
833
|
+
|
|
834
|
+
const res = await server.inject({ url: '/file', headers: { 'accept-encoding': 'gzip' } });
|
|
835
|
+
expect(res.headers['content-type']).to.equal('image/png');
|
|
836
|
+
expect(res.headers['content-encoding']).to.not.exist();
|
|
837
|
+
expect(res.headers['content-length']).to.equal(42010);
|
|
838
|
+
expect(res.headers.vary).to.not.exist();
|
|
839
|
+
expect(res.payload).to.exist();
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it('returns a plain file when compression disabled', async () => {
|
|
843
|
+
|
|
844
|
+
const server = Hapi.server({ routes: { files: { relativeTo: __dirname } }, compression: false });
|
|
845
|
+
await server.register(Inert);
|
|
846
|
+
server.route({ method: 'GET', path: '/file', handler: (request, h) => h.file(__dirname + '/../package.json') });
|
|
847
|
+
|
|
848
|
+
const res = await server.inject({ url: '/file', headers: { 'accept-encoding': 'gzip' } });
|
|
849
|
+
expect(res.headers['content-type']).to.equal('application/json; charset=utf-8');
|
|
850
|
+
expect(res.headers['content-encoding']).to.not.exist();
|
|
851
|
+
expect(res.payload).to.exist();
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
it('returns a deflated file in the response when the request accepts deflate', async () => {
|
|
855
|
+
|
|
856
|
+
const server = Hapi.server({ compression: { minBytes: 1 }, routes: { files: { relativeTo: __dirname } } });
|
|
857
|
+
await server.register(Inert);
|
|
858
|
+
server.route({ method: 'GET', path: '/file', handler: (request, h) => h.file(__dirname + '/../package.json') });
|
|
859
|
+
|
|
860
|
+
const res = await server.inject({ url: '/file', headers: { 'accept-encoding': 'deflate' } });
|
|
861
|
+
expect(res.headers['content-type']).to.equal('application/json; charset=utf-8');
|
|
862
|
+
expect(res.headers['content-encoding']).to.equal('deflate');
|
|
863
|
+
expect(res.headers['content-length']).to.not.exist();
|
|
864
|
+
expect(res.payload).to.exist();
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it('returns a gzipped stream response without a content-length header when accept-encoding is gzip', async () => {
|
|
868
|
+
|
|
869
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
870
|
+
server.route({ method: 'GET', path: '/stream', handler: () => new internals.TimerStream() });
|
|
871
|
+
|
|
872
|
+
const res = await server.inject({ url: '/stream', headers: { 'Content-Type': 'application/json', 'accept-encoding': 'gzip' } });
|
|
873
|
+
expect(res.statusCode).to.equal(200);
|
|
874
|
+
expect(res.headers['content-length']).to.not.exist();
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it('returns a deflated stream response without a content-length header when accept-encoding is deflate', async () => {
|
|
878
|
+
|
|
879
|
+
const server = Hapi.server();
|
|
880
|
+
server.route({ method: 'GET', path: '/stream', handler: () => new internals.TimerStream() });
|
|
881
|
+
|
|
882
|
+
const res = await server.inject({ url: '/stream', headers: { 'Content-Type': 'application/json', 'accept-encoding': 'deflate' } });
|
|
883
|
+
expect(res.statusCode).to.equal(200);
|
|
884
|
+
expect(res.headers['content-length']).to.not.exist();
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
it('returns a gzip response on a post request when accept-encoding: gzip is requested', async () => {
|
|
888
|
+
|
|
889
|
+
const data = '{"test":"true"}';
|
|
890
|
+
|
|
891
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
892
|
+
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
|
|
893
|
+
await server.start();
|
|
894
|
+
|
|
895
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
896
|
+
const zipped = await internals.compress('gzip', Buffer.from(data));
|
|
897
|
+
|
|
898
|
+
const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'gzip' }, payload: data });
|
|
899
|
+
expect(payload.toString()).to.equal(zipped.toString());
|
|
900
|
+
await server.stop();
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it('returns a gzip response on a get request when accept-encoding: gzip is requested', async () => {
|
|
904
|
+
|
|
905
|
+
const data = '{"test":"true"}';
|
|
906
|
+
|
|
907
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
908
|
+
server.route({ method: 'GET', path: '/', handler: () => data });
|
|
909
|
+
await server.start();
|
|
910
|
+
|
|
911
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
912
|
+
const zipped = await internals.compress('gzip', Buffer.from(data));
|
|
913
|
+
const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'gzip' } });
|
|
914
|
+
expect(payload.toString()).to.equal(zipped.toString());
|
|
915
|
+
await server.stop();
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
it('returns a gzip response on a post request when accept-encoding: * is requested', async () => {
|
|
919
|
+
|
|
920
|
+
const data = '{"test":"true"}';
|
|
921
|
+
|
|
922
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
923
|
+
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
|
|
924
|
+
await server.start();
|
|
925
|
+
|
|
926
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
927
|
+
const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': '*' }, payload: data });
|
|
928
|
+
expect(payload.toString()).to.equal(data);
|
|
929
|
+
await server.stop();
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
it('returns a gzip response on a get request when accept-encoding: * is requested', async () => {
|
|
933
|
+
|
|
934
|
+
const data = '{"test":"true"}';
|
|
935
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
936
|
+
server.route({ method: 'GET', path: '/', handler: () => data });
|
|
937
|
+
await server.start();
|
|
938
|
+
|
|
939
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
940
|
+
const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': '*' } });
|
|
941
|
+
expect(payload.toString()).to.equal(data);
|
|
942
|
+
await server.stop();
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it('returns a deflate response on a post request when accept-encoding: deflate is requested', async () => {
|
|
946
|
+
|
|
947
|
+
const data = '{"test":"true"}';
|
|
948
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
949
|
+
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
|
|
950
|
+
await server.start();
|
|
951
|
+
|
|
952
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
953
|
+
const deflated = await internals.compress('deflate', Buffer.from(data));
|
|
954
|
+
const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'deflate' }, payload: data });
|
|
955
|
+
expect(payload.toString()).to.equal(deflated.toString());
|
|
956
|
+
await server.stop();
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it('returns a deflate response on a get request when accept-encoding: deflate is requested', async () => {
|
|
960
|
+
|
|
961
|
+
const data = '{"test":"true"}';
|
|
962
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
963
|
+
server.route({ method: 'GET', path: '/', handler: () => data });
|
|
964
|
+
await server.start();
|
|
965
|
+
|
|
966
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
967
|
+
const deflated = await internals.compress('deflate', Buffer.from(data));
|
|
968
|
+
const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'deflate' } });
|
|
969
|
+
expect(payload.toString()).to.equal(deflated.toString());
|
|
970
|
+
await server.stop();
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
it('returns a gzip response on a post request when accept-encoding: gzip;q=1, deflate;q=0.5 is requested', async () => {
|
|
974
|
+
|
|
975
|
+
const data = '{"test":"true"}';
|
|
976
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
977
|
+
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
|
|
978
|
+
await server.start();
|
|
979
|
+
|
|
980
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
981
|
+
const zipped = await internals.compress('gzip', Buffer.from(data));
|
|
982
|
+
const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'gzip;q=1, deflate;q=0.5' }, payload: data });
|
|
983
|
+
expect(payload.toString()).to.equal(zipped.toString());
|
|
984
|
+
await server.stop();
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it('returns a gzip response on a get request when accept-encoding: gzip;q=1, deflate;q=0.5 is requested', async () => {
|
|
988
|
+
|
|
989
|
+
const data = '{"test":"true"}';
|
|
990
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
991
|
+
server.route({ method: 'GET', path: '/', handler: () => data });
|
|
992
|
+
await server.start();
|
|
993
|
+
|
|
994
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
995
|
+
const zipped = await internals.compress('gzip', Buffer.from(data));
|
|
996
|
+
const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'gzip;q=1, deflate;q=0.5' } });
|
|
997
|
+
expect(payload.toString()).to.equal(zipped.toString());
|
|
998
|
+
await server.stop();
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
it('returns a deflate response on a post request when accept-encoding: deflate;q=1, gzip;q=0.5 is requested', async () => {
|
|
1002
|
+
|
|
1003
|
+
const data = '{"test":"true"}';
|
|
1004
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
1005
|
+
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
|
|
1006
|
+
await server.start();
|
|
1007
|
+
|
|
1008
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
1009
|
+
const deflated = await internals.compress('deflate', Buffer.from(data));
|
|
1010
|
+
const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'deflate;q=1, gzip;q=0.5' }, payload: data });
|
|
1011
|
+
expect(payload.toString()).to.equal(deflated.toString());
|
|
1012
|
+
await server.stop();
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
it('returns a deflate response on a get request when accept-encoding: deflate;q=1, gzip;q=0.5 is requested', async () => {
|
|
1016
|
+
|
|
1017
|
+
const data = '{"test":"true"}';
|
|
1018
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
1019
|
+
server.route({ method: 'GET', path: '/', handler: () => data });
|
|
1020
|
+
await server.start();
|
|
1021
|
+
|
|
1022
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
1023
|
+
const deflated = await internals.compress('deflate', Buffer.from(data));
|
|
1024
|
+
const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'deflate;q=1, gzip;q=0.5' } });
|
|
1025
|
+
expect(payload.toString()).to.equal(deflated.toString());
|
|
1026
|
+
await server.stop();
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
it('returns a gzip response on a post request when accept-encoding: deflate, gzip is requested', async () => {
|
|
1030
|
+
|
|
1031
|
+
const data = '{"test":"true"}';
|
|
1032
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
1033
|
+
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
|
|
1034
|
+
await server.start();
|
|
1035
|
+
|
|
1036
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
1037
|
+
const zipped = await internals.compress('gzip', Buffer.from(data));
|
|
1038
|
+
const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'deflate, gzip' }, payload: data });
|
|
1039
|
+
expect(payload.toString()).to.equal(zipped.toString());
|
|
1040
|
+
await server.stop();
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
it('returns a gzip response on a get request when accept-encoding: deflate, gzip is requested', async () => {
|
|
1044
|
+
|
|
1045
|
+
const data = '{"test":"true"}';
|
|
1046
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
1047
|
+
server.route({ method: 'GET', path: '/', handler: () => data });
|
|
1048
|
+
await server.start();
|
|
1049
|
+
|
|
1050
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
1051
|
+
const zipped = await internals.compress('gzip', Buffer.from(data));
|
|
1052
|
+
const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'deflate, gzip' } });
|
|
1053
|
+
expect(payload.toString()).to.equal(zipped.toString());
|
|
1054
|
+
await server.stop();
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
it('boom object reused does not affect encoding header.', async () => {
|
|
1058
|
+
|
|
1059
|
+
const error = Boom.badRequest();
|
|
1060
|
+
const data = JSON.stringify(error.output.payload);
|
|
1061
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
1062
|
+
|
|
1063
|
+
const handler = () => {
|
|
1064
|
+
|
|
1065
|
+
throw error;
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
1069
|
+
await server.start();
|
|
1070
|
+
|
|
1071
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
1072
|
+
const zipped = await internals.compress('gzip', Buffer.from(data));
|
|
1073
|
+
const err1 = await expect(Wreck.get(uri, { headers: { 'accept-encoding': 'gzip' } })).to.reject();
|
|
1074
|
+
expect(err1.data.payload.toString()).to.equal(zipped.toString());
|
|
1075
|
+
|
|
1076
|
+
const err2 = await expect(Wreck.get(uri, { headers: { 'accept-encoding': 'gzip' } })).to.reject();
|
|
1077
|
+
expect(err2.data.payload.toString()).to.equal(zipped.toString());
|
|
1078
|
+
await server.stop();
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it('Error reused does not affect encoding header.', async () => {
|
|
1082
|
+
|
|
1083
|
+
const error = new Error('something went wrong');
|
|
1084
|
+
const wrappedError = Boom.boomify(error);
|
|
1085
|
+
const data = JSON.stringify(wrappedError.output.payload);
|
|
1086
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
1087
|
+
|
|
1088
|
+
const handler = () => {
|
|
1089
|
+
|
|
1090
|
+
throw error;
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
1094
|
+
await server.start();
|
|
1095
|
+
|
|
1096
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
1097
|
+
const zipped = await internals.compress('gzip', Buffer.from(data));
|
|
1098
|
+
const err1 = await expect(Wreck.get(uri, { headers: { 'accept-encoding': 'gzip' } })).to.reject();
|
|
1099
|
+
expect(err1.data.payload.toString()).to.equal(zipped.toString());
|
|
1100
|
+
|
|
1101
|
+
const err2 = await expect(Wreck.get(uri, { headers: { 'accept-encoding': 'gzip' } })).to.reject();
|
|
1102
|
+
expect(err2.data.payload.toString()).to.equal(zipped.toString());
|
|
1103
|
+
await server.stop();
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
it('returns an identity response on a post request when accept-encoding is missing', async () => {
|
|
1107
|
+
|
|
1108
|
+
const data = '{"test":"true"}';
|
|
1109
|
+
|
|
1110
|
+
const server = Hapi.server();
|
|
1111
|
+
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
|
|
1112
|
+
await server.start();
|
|
1113
|
+
|
|
1114
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
1115
|
+
const { payload } = await Wreck.post(uri, { payload: data });
|
|
1116
|
+
expect(payload.toString()).to.equal(data);
|
|
1117
|
+
await server.stop();
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
it('returns an identity response on a get request when accept-encoding is missing', async () => {
|
|
1121
|
+
|
|
1122
|
+
const data = '{"test":"true"}';
|
|
1123
|
+
|
|
1124
|
+
const server = Hapi.server();
|
|
1125
|
+
server.route({
|
|
1126
|
+
method: 'GET',
|
|
1127
|
+
path: '/',
|
|
1128
|
+
handler: () => data
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
await server.start();
|
|
1132
|
+
|
|
1133
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
1134
|
+
const { payload } = await Wreck.get(uri);
|
|
1135
|
+
expect(payload.toString().toString()).to.equal(data);
|
|
1136
|
+
await server.stop();
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
it('returns a gzip response when forced by the handler', async () => {
|
|
1140
|
+
|
|
1141
|
+
const data = '{"test":"true"}';
|
|
1142
|
+
const zipped = await internals.compress('gzip', Buffer.from(data));
|
|
1143
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
1144
|
+
server.route({ method: 'POST', path: '/', handler: (request, h) => h.response(zipped).type('text/plain').header('content-encoding', 'gzip') });
|
|
1145
|
+
await server.start();
|
|
1146
|
+
|
|
1147
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
1148
|
+
const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'gzip' }, payload: data });
|
|
1149
|
+
expect(payload.toString()).to.equal(zipped.toString());
|
|
1150
|
+
await server.stop();
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
it('does not open file stream on 304', async () => {
|
|
1154
|
+
|
|
1155
|
+
const server = Hapi.server();
|
|
1156
|
+
await server.register(Inert);
|
|
1157
|
+
server.route({ method: 'GET', path: '/file', handler: { file: __dirname + '/../package.json' } });
|
|
1158
|
+
|
|
1159
|
+
const res1 = await server.inject('/file');
|
|
1160
|
+
|
|
1161
|
+
const preResponse = (request, h) => {
|
|
1162
|
+
|
|
1163
|
+
request.response._marshal = function () {
|
|
1164
|
+
|
|
1165
|
+
throw new Error('not called');
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
return h.continue;
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
server.ext('onPreResponse', preResponse);
|
|
1172
|
+
|
|
1173
|
+
const res2 = await server.inject({ url: '/file', headers: { 'if-modified-since': res1.headers.date } });
|
|
1174
|
+
expect(res2.statusCode).to.equal(304);
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
it('object listeners are maintained after transmission is complete', async () => {
|
|
1178
|
+
|
|
1179
|
+
const server = Hapi.server();
|
|
1180
|
+
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
|
|
1181
|
+
|
|
1182
|
+
let response;
|
|
1183
|
+
let log;
|
|
1184
|
+
|
|
1185
|
+
const preResponse = (request, h) => {
|
|
1186
|
+
|
|
1187
|
+
response = request.response;
|
|
1188
|
+
response.events.registerEvent('special');
|
|
1189
|
+
log = response.events.once('special');
|
|
1190
|
+
return h.continue;
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
server.ext('onPreResponse', preResponse);
|
|
1194
|
+
await server.inject('/');
|
|
1195
|
+
response.events.emit('special');
|
|
1196
|
+
await log;
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
it('stops processing the stream when the connection closes', async () => {
|
|
1200
|
+
|
|
1201
|
+
let stream;
|
|
1202
|
+
|
|
1203
|
+
const ErrStream = class extends Stream.Readable {
|
|
1204
|
+
|
|
1205
|
+
constructor(request) {
|
|
1206
|
+
|
|
1207
|
+
super();
|
|
1208
|
+
this.request = request;
|
|
1209
|
+
this.reads = 0;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
_read(size) {
|
|
1213
|
+
|
|
1214
|
+
if (this.reads === 0) {
|
|
1215
|
+
this.push('here is the response');
|
|
1216
|
+
this.request.raw.res.destroy();
|
|
1217
|
+
}
|
|
1218
|
+
else {
|
|
1219
|
+
// "Inifitely" push more content
|
|
1220
|
+
|
|
1221
|
+
process.nextTick(() => {
|
|
1222
|
+
|
|
1223
|
+
this.push('.');
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
++this.reads;
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
const server = Hapi.server();
|
|
1232
|
+
const log = server.events.once('response');
|
|
1233
|
+
server.route({ method: 'GET', path: '/stream', handler: (request, h) => {
|
|
1234
|
+
|
|
1235
|
+
stream = new ErrStream(request);
|
|
1236
|
+
return h.response(stream).bytes(0);
|
|
1237
|
+
} });
|
|
1238
|
+
|
|
1239
|
+
const err = await expect(server.inject({ url: '/stream', headers: { 'Accept-Encoding': 'gzip' } })).to.reject(Boom.Boom);
|
|
1240
|
+
expect(err.output.statusCode).to.equal(499);
|
|
1241
|
+
expect(err.output.payload.error).to.equal('Unknown');
|
|
1242
|
+
expect(err.output.payload.message).to.equal('Request close');
|
|
1243
|
+
expect(err.data.request.raw.res.statusCode).to.equal(204);
|
|
1244
|
+
expect(err.data.request.raw.res.statusMessage).to.equal('No Content');
|
|
1245
|
+
|
|
1246
|
+
const [request] = await log;
|
|
1247
|
+
expect(request.response.output.statusCode).to.equal(499);
|
|
1248
|
+
expect(request.info.completed).to.be.above(0);
|
|
1249
|
+
expect(request.info.responded).to.equal(0);
|
|
1250
|
+
|
|
1251
|
+
expect(stream.reads).to.equal(2);
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
it('does not truncate the response when stream finishes before response is done', async () => {
|
|
1255
|
+
|
|
1256
|
+
const chunkTimes = 10;
|
|
1257
|
+
const filePath = __dirname + '/response.js';
|
|
1258
|
+
const block = Fs.readFileSync(filePath).toString();
|
|
1259
|
+
|
|
1260
|
+
let expectedBody = '';
|
|
1261
|
+
for (let i = 0; i < chunkTimes; ++i) {
|
|
1262
|
+
expectedBody += block;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const handler = (request) => {
|
|
1266
|
+
|
|
1267
|
+
const fileStream = new Stream.Readable();
|
|
1268
|
+
|
|
1269
|
+
let readTimes = 0;
|
|
1270
|
+
fileStream._read = function (size) {
|
|
1271
|
+
|
|
1272
|
+
++readTimes;
|
|
1273
|
+
if (readTimes > chunkTimes) {
|
|
1274
|
+
return this.push(null);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
this.push(block);
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
return fileStream;
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
const server = Hapi.server();
|
|
1284
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
1285
|
+
await server.start();
|
|
1286
|
+
|
|
1287
|
+
const { payload } = await Wreck.get('http://localhost:' + server.info.port);
|
|
1288
|
+
expect(payload.toString()).to.equal(expectedBody);
|
|
1289
|
+
await server.stop();
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
it('does not truncate the response when stream finishes before response is done using https', async () => {
|
|
1293
|
+
|
|
1294
|
+
const chunkTimes = 10;
|
|
1295
|
+
const filePath = __dirname + '/response.js';
|
|
1296
|
+
const block = Fs.readFileSync(filePath).toString();
|
|
1297
|
+
|
|
1298
|
+
let expectedBody = '';
|
|
1299
|
+
for (let i = 0; i < chunkTimes; ++i) {
|
|
1300
|
+
expectedBody += block;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const handler = (request) => {
|
|
1304
|
+
|
|
1305
|
+
const fileStream = new Stream.Readable();
|
|
1306
|
+
|
|
1307
|
+
let readTimes = 0;
|
|
1308
|
+
fileStream._read = function (size) {
|
|
1309
|
+
|
|
1310
|
+
++readTimes;
|
|
1311
|
+
if (readTimes > chunkTimes) {
|
|
1312
|
+
return this.push(null);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
this.push(block);
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
return fileStream;
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
const config = {
|
|
1322
|
+
tls: {
|
|
1323
|
+
key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0UqyXDCqWDKpoNQQK/fdr0OkG4gW6DUafxdufH9GmkX/zoKz\ng/SFLrPipzSGINKWtyMvo7mPjXqqVgE10LDI3VFV8IR6fnART+AF8CW5HMBPGt/s\nfQW4W4puvBHkBxWSW1EvbecgNEIS9hTGvHXkFzm4xJ2e9DHp2xoVAjREC73B7JbF\nhc5ZGGchKw+CFmAiNysU0DmBgQcac0eg2pWoT+YGmTeQj6sRXO67n2xy/hA1DuN6\nA4WBK3wM3O4BnTG0dNbWUEbe7yAbV5gEyq57GhJIeYxRvveVDaX90LoAqM4cUH06\n6rciON0UbDHV2LP/JaH5jzBjUyCnKLLo5snlbwIDAQABAoIBAQDJm7YC3pJJUcxb\nc8x8PlHbUkJUjxzZ5MW4Zb71yLkfRYzsxrTcyQA+g+QzA4KtPY8XrZpnkgm51M8e\n+B16AcIMiBxMC6HgCF503i16LyyJiKrrDYfGy2rTK6AOJQHO3TXWJ3eT3BAGpxuS\n12K2Cq6EvQLCy79iJm7Ks+5G6EggMZPfCVdEhffRm2Epl4T7LpIAqWiUDcDfS05n\nNNfAGxxvALPn+D+kzcSF6hpmCVrFVTf9ouhvnr+0DpIIVPwSK/REAF3Ux5SQvFuL\njPmh3bGwfRtcC5d21QNrHdoBVSN2UBLmbHUpBUcOBI8FyivAWJhRfKnhTvXMFG8L\nwaXB51IZAoGBAP/E3uz6zCyN7l2j09wmbyNOi1AKvr1WSmuBJveITouwblnRSdvc\nsYm4YYE0Vb94AG4n7JIfZLKtTN0xvnCo8tYjrdwMJyGfEfMGCQQ9MpOBXAkVVZvP\ne2k4zHNNsfvSc38UNSt7K0HkVuH5BkRBQeskcsyMeu0qK4wQwdtiCoBDAoGBANF7\nFMppYxSW4ir7Jvkh0P8bP/Z7AtaSmkX7iMmUYT+gMFB5EKqFTQjNQgSJxS/uHVDE\nSC5co8WGHnRk7YH2Pp+Ty1fHfXNWyoOOzNEWvg6CFeMHW2o+/qZd4Z5Fep6qCLaa\nFvzWWC2S5YslEaaP8DQ74aAX4o+/TECrxi0z2lllAoGAdRB6qCSyRsI/k4Rkd6Lv\nw00z3lLMsoRIU6QtXaZ5rN335Awyrfr5F3vYxPZbOOOH7uM/GDJeOJmxUJxv+cia\nPQDflpPJZU4VPRJKFjKcb38JzO6C3Gm+po5kpXGuQQA19LgfDeO2DNaiHZOJFrx3\nm1R3Zr/1k491lwokcHETNVkCgYBPLjrZl6Q/8BhlLrG4kbOx+dbfj/euq5NsyHsX\n1uI7bo1Una5TBjfsD8nYdUr3pwWltcui2pl83Ak+7bdo3G8nWnIOJ/WfVzsNJzj7\n/6CvUzR6sBk5u739nJbfgFutBZBtlSkDQPHrqA7j3Ysibl3ZIJlULjMRKrnj6Ans\npCDwkQKBgQCM7gu3p7veYwCZaxqDMz5/GGFUB1My7sK0hcT7/oH61yw3O8pOekee\nuctI1R3NOudn1cs5TAy/aypgLDYTUGQTiBRILeMiZnOrvQQB9cEf7TFgDoRNCcDs\nV/ZWiegVB/WY7H0BkCekuq5bHwjgtJTpvHGqQ9YD7RhE8RSYOhdQ/Q==\n-----END RSA PRIVATE KEY-----\n',
|
|
1324
|
+
cert: '-----BEGIN CERTIFICATE-----\nMIIDBjCCAe4CCQDvLNml6smHlTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0\ncyBQdHkgTHRkMB4XDTE0MDEyNTIxMjIxOFoXDTE1MDEyNTIxMjIxOFowRTELMAkG\nA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0\nIFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nANFKslwwqlgyqaDUECv33a9DpBuIFug1Gn8Xbnx/RppF/86Cs4P0hS6z4qc0hiDS\nlrcjL6O5j416qlYBNdCwyN1RVfCEen5wEU/gBfAluRzATxrf7H0FuFuKbrwR5AcV\nkltRL23nIDRCEvYUxrx15Bc5uMSdnvQx6dsaFQI0RAu9weyWxYXOWRhnISsPghZg\nIjcrFNA5gYEHGnNHoNqVqE/mBpk3kI+rEVzuu59scv4QNQ7jegOFgSt8DNzuAZ0x\ntHTW1lBG3u8gG1eYBMquexoSSHmMUb73lQ2l/dC6AKjOHFB9Ouq3IjjdFGwx1diz\n/yWh+Y8wY1Mgpyiy6ObJ5W8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAoSc6Skb4\ng1e0ZqPKXBV2qbx7hlqIyYpubCl1rDiEdVzqYYZEwmst36fJRRrVaFuAM/1DYAmT\nWMhU+yTfA+vCS4tql9b9zUhPw/IDHpBDWyR01spoZFBF/hE1MGNpCSXXsAbmCiVf\naxrIgR2DNketbDxkQx671KwF1+1JOMo9ffXp+OhuRo5NaGIxhTsZ+f/MA4y084Aj\nDI39av50sTRTWWShlN+J7PtdQVA5SZD97oYbeUeL7gI18kAJww9eUdmT0nEjcwKs\nxsQT1fyKbo7AlZBY4KSlUMuGnn0VnAsB9b+LxtXlDfnjyM8bVQx1uAfRo0DO8p/5\n3J5DTjAU55deBQ==\n-----END CERTIFICATE-----\n'
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
const server = Hapi.server(config);
|
|
1329
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
1330
|
+
await server.start();
|
|
1331
|
+
|
|
1332
|
+
const { payload } = await Wreck.get('https://localhost:' + server.info.port, { rejectUnauthorized: false });
|
|
1333
|
+
expect(payload.toString()).to.equal(expectedBody);
|
|
1334
|
+
await server.stop();
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
it('destroy() stream when request aborts before stream drains', async () => {
|
|
1338
|
+
|
|
1339
|
+
const server = Hapi.server();
|
|
1340
|
+
|
|
1341
|
+
const team = new Teamwork.Team();
|
|
1342
|
+
const handler = (request) => {
|
|
1343
|
+
|
|
1344
|
+
return new Stream.Readable({
|
|
1345
|
+
read(size) {
|
|
1346
|
+
|
|
1347
|
+
const chunk = new Array(size).join('x');
|
|
1348
|
+
|
|
1349
|
+
setTimeout(() => {
|
|
1350
|
+
|
|
1351
|
+
this.push(chunk);
|
|
1352
|
+
}, 10);
|
|
1353
|
+
},
|
|
1354
|
+
destroy() {
|
|
1355
|
+
|
|
1356
|
+
team.attend();
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1361
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
1362
|
+
|
|
1363
|
+
await server.start();
|
|
1364
|
+
|
|
1365
|
+
const res = await Wreck.request('GET', 'http://localhost:' + server.info.port);
|
|
1366
|
+
res.once('data', (chunk) => {
|
|
1367
|
+
|
|
1368
|
+
res.destroy();
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
await team.work;
|
|
1372
|
+
await server.stop();
|
|
1373
|
+
|
|
1374
|
+
expect(res.statusCode).to.equal(200);
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
it('destroy() stream when request timeouts before stream drains', async () => {
|
|
1378
|
+
|
|
1379
|
+
const server = Hapi.server({ routes: { timeout: { server: 20, socket: 40 }, payload: { timeout: false } } });
|
|
1380
|
+
const team = new Teamwork.Team();
|
|
1381
|
+
|
|
1382
|
+
const handler = (request) => {
|
|
1383
|
+
|
|
1384
|
+
let count = 0;
|
|
1385
|
+
const stream = new Stream.Readable({
|
|
1386
|
+
read(size) {
|
|
1387
|
+
|
|
1388
|
+
const timeout = 10 * count++; // Must have back off here to hit the socket timeout
|
|
1389
|
+
|
|
1390
|
+
setTimeout(() => {
|
|
1391
|
+
|
|
1392
|
+
if (request._isFinalized) {
|
|
1393
|
+
stream.push(null);
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
stream.push(new Array(size).join('x'));
|
|
1398
|
+
|
|
1399
|
+
}, timeout);
|
|
1400
|
+
},
|
|
1401
|
+
destroy() {
|
|
1402
|
+
|
|
1403
|
+
team.attend();
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
return stream;
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
1411
|
+
|
|
1412
|
+
await server.start();
|
|
1413
|
+
|
|
1414
|
+
const res = await Wreck.request('GET', 'http://localhost:' + server.info.port);
|
|
1415
|
+
res.on('data', (chunk) => { });
|
|
1416
|
+
|
|
1417
|
+
await team.work;
|
|
1418
|
+
await server.stop();
|
|
1419
|
+
|
|
1420
|
+
expect(res.statusCode).to.equal(200);
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
it('destroy() stream when request aborts before stream drains', async () => {
|
|
1424
|
+
|
|
1425
|
+
const server = Hapi.server();
|
|
1426
|
+
|
|
1427
|
+
const team = new Teamwork.Team();
|
|
1428
|
+
const handler = async (request) => {
|
|
1429
|
+
|
|
1430
|
+
clientRequest.abort();
|
|
1431
|
+
|
|
1432
|
+
const stream = new Stream.Readable({
|
|
1433
|
+
read(size) {
|
|
1434
|
+
|
|
1435
|
+
const chunk = new Array(size).join('x');
|
|
1436
|
+
|
|
1437
|
+
setTimeout(() => {
|
|
1438
|
+
|
|
1439
|
+
this.push(chunk);
|
|
1440
|
+
}, 10);
|
|
1441
|
+
},
|
|
1442
|
+
destroy() {
|
|
1443
|
+
|
|
1444
|
+
team.attend();
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
await Hoek.wait(100);
|
|
1449
|
+
return stream;
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
1453
|
+
|
|
1454
|
+
await server.start();
|
|
1455
|
+
|
|
1456
|
+
const clientRequest = Http.request({
|
|
1457
|
+
hostname: 'localhost',
|
|
1458
|
+
port: server.info.port,
|
|
1459
|
+
method: 'GET'
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
clientRequest.on('error', Hoek.ignore);
|
|
1463
|
+
clientRequest.end();
|
|
1464
|
+
|
|
1465
|
+
await team.work;
|
|
1466
|
+
await server.stop();
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
it('close() stream when no destroy() method', async () => {
|
|
1470
|
+
|
|
1471
|
+
const server = Hapi.server();
|
|
1472
|
+
|
|
1473
|
+
const team = new Teamwork.Team();
|
|
1474
|
+
const handler = (request) => {
|
|
1475
|
+
|
|
1476
|
+
const stream = new LegacyReadableStream.Readable();
|
|
1477
|
+
stream._read = function (size) {
|
|
1478
|
+
|
|
1479
|
+
const chunk = new Array(size).join('x');
|
|
1480
|
+
|
|
1481
|
+
setTimeout(() => {
|
|
1482
|
+
|
|
1483
|
+
this.push(chunk);
|
|
1484
|
+
}, 10);
|
|
1485
|
+
};
|
|
1486
|
+
|
|
1487
|
+
stream.close = () => team.attend();
|
|
1488
|
+
|
|
1489
|
+
return stream;
|
|
1490
|
+
};
|
|
1491
|
+
|
|
1492
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
1493
|
+
|
|
1494
|
+
await server.start();
|
|
1495
|
+
|
|
1496
|
+
const res = await Wreck.request('GET', 'http://localhost:' + server.info.port);
|
|
1497
|
+
res.once('data', (chunk) => {
|
|
1498
|
+
|
|
1499
|
+
res.destroy();
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
await team.work;
|
|
1503
|
+
await server.stop();
|
|
1504
|
+
|
|
1505
|
+
expect(res.statusCode).to.equal(200);
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
it('unpipe() stream when no destroy() or close() method', async () => {
|
|
1509
|
+
|
|
1510
|
+
const server = Hapi.server();
|
|
1511
|
+
|
|
1512
|
+
const team = new Teamwork.Team();
|
|
1513
|
+
const handler = (request) => {
|
|
1514
|
+
|
|
1515
|
+
const stream = new LegacyReadableStream.Readable();
|
|
1516
|
+
stream._read = function (size) {
|
|
1517
|
+
|
|
1518
|
+
const chunk = new Array(size).join('x');
|
|
1519
|
+
|
|
1520
|
+
setTimeout(() => {
|
|
1521
|
+
|
|
1522
|
+
this.push(chunk);
|
|
1523
|
+
}, 10);
|
|
1524
|
+
};
|
|
1525
|
+
|
|
1526
|
+
stream.unpipe = () => team.attend();
|
|
1527
|
+
|
|
1528
|
+
return stream;
|
|
1529
|
+
};
|
|
1530
|
+
|
|
1531
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
1532
|
+
|
|
1533
|
+
await server.start();
|
|
1534
|
+
|
|
1535
|
+
const res = await Wreck.request('GET', 'http://localhost:' + server.info.port);
|
|
1536
|
+
res.once('data', (chunk) => {
|
|
1537
|
+
|
|
1538
|
+
res.destroy();
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
await team.work;
|
|
1542
|
+
await server.stop();
|
|
1543
|
+
|
|
1544
|
+
expect(res.statusCode).to.equal(200);
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
it('changes etag when content-encoding set manually', async () => {
|
|
1548
|
+
|
|
1549
|
+
const payload = new Array(1000).fill('x').join();
|
|
1550
|
+
const server = Hapi.server();
|
|
1551
|
+
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response(payload).header('content-encoding', 'gzip').etag('abc') });
|
|
1552
|
+
|
|
1553
|
+
const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'gzip' } });
|
|
1554
|
+
expect(res.statusCode).to.equal(200);
|
|
1555
|
+
expect(res.headers.etag).to.exist();
|
|
1556
|
+
expect(res.headers.etag).to.match(/-gzip"$/);
|
|
1557
|
+
expect(res.headers.vary).to.equal('accept-encoding');
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
it('changes etag without vary when content-encoding set via compressed', async () => {
|
|
1561
|
+
|
|
1562
|
+
const payload = new Array(1000).fill('x').join();
|
|
1563
|
+
const server = Hapi.server();
|
|
1564
|
+
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response(payload).compressed('gzip').etag('abc') });
|
|
1565
|
+
|
|
1566
|
+
const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'gzip' } });
|
|
1567
|
+
expect(res.statusCode).to.equal(200);
|
|
1568
|
+
expect(res.headers.etag).to.exist();
|
|
1569
|
+
expect(res.headers.etag).to.equal('"abc-gzip"');
|
|
1570
|
+
expect(res.headers['content-encoding']).to.equal('gzip');
|
|
1571
|
+
expect(res.headers.vary).to.not.exist();
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
it('head request retains content-length header', async () => {
|
|
1575
|
+
|
|
1576
|
+
const server = Hapi.server();
|
|
1577
|
+
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('x').bytes(1) });
|
|
1578
|
+
|
|
1579
|
+
const res = await server.inject({ method: 'HEAD', url: '/' });
|
|
1580
|
+
expect(res.statusCode).to.equal(200);
|
|
1581
|
+
expect(res.headers['content-length']).to.equal(1);
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1584
|
+
it('does not set accept-encoding multiple times', async () => {
|
|
1585
|
+
|
|
1586
|
+
const upstream = Hapi.server();
|
|
1587
|
+
upstream.route({ method: 'GET', path: '/headers', handler: (request, h) => h.response({ status: 'success' }).vary('X-Custom3') });
|
|
1588
|
+
await upstream.start();
|
|
1589
|
+
|
|
1590
|
+
const proxyHandler = async (request, h) => {
|
|
1591
|
+
|
|
1592
|
+
const options = {};
|
|
1593
|
+
options.headers = Hoek.clone(request.headers);
|
|
1594
|
+
delete options.headers.host;
|
|
1595
|
+
|
|
1596
|
+
const res = await Wreck.request(request.method, 'http://localhost:' + upstream.info.port + '/headers', options);
|
|
1597
|
+
return h.response(res).code(res.statusCode);
|
|
1598
|
+
};
|
|
1599
|
+
|
|
1600
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
1601
|
+
server.route({ method: 'GET', path: '/headers', handler: proxyHandler });
|
|
1602
|
+
|
|
1603
|
+
const res = await server.inject({ url: '/headers', headers: { 'accept-encoding': 'gzip' } });
|
|
1604
|
+
expect(res.statusCode).to.equal(200);
|
|
1605
|
+
expect(res.headers.vary).to.equal('X-Custom3,accept-encoding');
|
|
1606
|
+
|
|
1607
|
+
await upstream.stop();
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
it('ends response stream once', async () => {
|
|
1611
|
+
|
|
1612
|
+
const server = Hapi.server();
|
|
1613
|
+
|
|
1614
|
+
let count = 0;
|
|
1615
|
+
const onRequest = (request, h) => {
|
|
1616
|
+
|
|
1617
|
+
const res = request.raw.res;
|
|
1618
|
+
const orig = res.end;
|
|
1619
|
+
|
|
1620
|
+
res.end = function () {
|
|
1621
|
+
|
|
1622
|
+
++count;
|
|
1623
|
+
return orig.call(res);
|
|
1624
|
+
};
|
|
1625
|
+
|
|
1626
|
+
return h.continue;
|
|
1627
|
+
};
|
|
1628
|
+
|
|
1629
|
+
server.ext('onRequest', onRequest);
|
|
1630
|
+
await server.inject('/');
|
|
1631
|
+
expect(count).to.equal(1);
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
describe('response range', () => {
|
|
1635
|
+
|
|
1636
|
+
const fileStreamHandler = (request, h) => {
|
|
1637
|
+
|
|
1638
|
+
const filePath = Path.join(__dirname, 'file', 'image.png');
|
|
1639
|
+
return h.response(Fs.createReadStream(filePath)).bytes(Fs.statSync(filePath).size).etag('some-tag');
|
|
1640
|
+
};
|
|
1641
|
+
|
|
1642
|
+
it('returns a subset of a fileStream (start)', async () => {
|
|
1643
|
+
|
|
1644
|
+
const server = Hapi.server();
|
|
1645
|
+
server.route({ method: 'GET', path: '/file', handler: fileStreamHandler });
|
|
1646
|
+
|
|
1647
|
+
const res = await server.inject({ url: '/file', headers: { 'range': 'bytes=0-4' } });
|
|
1648
|
+
expect(res.statusCode).to.equal(206);
|
|
1649
|
+
expect(res.headers['content-length']).to.equal(5);
|
|
1650
|
+
expect(res.headers['content-range']).to.equal('bytes 0-4/42010');
|
|
1651
|
+
expect(res.headers['accept-ranges']).to.equal('bytes');
|
|
1652
|
+
expect(res.rawPayload.toString('binary')).to.equal('\x89PNG\r');
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
it('ignores range request when disabled', async () => {
|
|
1656
|
+
|
|
1657
|
+
const server = Hapi.server();
|
|
1658
|
+
server.route({ method: 'GET', path: '/file', handler: fileStreamHandler, options: { response: { ranges: false } } });
|
|
1659
|
+
|
|
1660
|
+
const res = await server.inject({ url: '/file', headers: { 'range': 'bytes=0-4' } });
|
|
1661
|
+
expect(res.statusCode).to.equal(200);
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
it('returns a subset of a fileStream (middle)', async () => {
|
|
1665
|
+
|
|
1666
|
+
const server = Hapi.server();
|
|
1667
|
+
server.route({ method: 'GET', path: '/file', handler: fileStreamHandler });
|
|
1668
|
+
|
|
1669
|
+
const res = await server.inject({ url: '/file', headers: { 'range': 'bytes=1-5' } });
|
|
1670
|
+
expect(res.statusCode).to.equal(206);
|
|
1671
|
+
expect(res.headers['content-length']).to.equal(5);
|
|
1672
|
+
expect(res.headers['content-range']).to.equal('bytes 1-5/42010');
|
|
1673
|
+
expect(res.headers['accept-ranges']).to.equal('bytes');
|
|
1674
|
+
expect(res.payload).to.equal('PNG\r\n');
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
it('returns a subset of a fileStream (-to)', async () => {
|
|
1678
|
+
|
|
1679
|
+
const server = Hapi.server();
|
|
1680
|
+
server.route({ method: 'GET', path: '/file', handler: fileStreamHandler });
|
|
1681
|
+
|
|
1682
|
+
const res = await server.inject({ url: '/file', headers: { 'range': 'bytes=-5' } });
|
|
1683
|
+
expect(res.statusCode).to.equal(206);
|
|
1684
|
+
expect(res.headers['content-length']).to.equal(5);
|
|
1685
|
+
expect(res.headers['content-range']).to.equal('bytes 42005-42009/42010');
|
|
1686
|
+
expect(res.headers['accept-ranges']).to.equal('bytes');
|
|
1687
|
+
expect(res.rawPayload.toString('binary')).to.equal('D\xAEB\x60\x82');
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
it('returns a subset of a fileStream (from-)', async () => {
|
|
1691
|
+
|
|
1692
|
+
const server = Hapi.server();
|
|
1693
|
+
server.route({ method: 'GET', path: '/file', handler: fileStreamHandler });
|
|
1694
|
+
|
|
1695
|
+
const res = await server.inject({ url: '/file', headers: { 'range': 'bytes=42005-' } });
|
|
1696
|
+
expect(res.statusCode).to.equal(206);
|
|
1697
|
+
expect(res.headers['content-length']).to.equal(5);
|
|
1698
|
+
expect(res.headers['content-range']).to.equal('bytes 42005-42009/42010');
|
|
1699
|
+
expect(res.headers['accept-ranges']).to.equal('bytes');
|
|
1700
|
+
expect(res.rawPayload.toString('binary')).to.equal('D\xAEB\x60\x82');
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
it('returns a subset of a fileStream (beyond end)', async () => {
|
|
1704
|
+
|
|
1705
|
+
const server = Hapi.server();
|
|
1706
|
+
server.route({ method: 'GET', path: '/file', handler: fileStreamHandler });
|
|
1707
|
+
|
|
1708
|
+
const res = await server.inject({ url: '/file', headers: { 'range': 'bytes=42005-42011' } });
|
|
1709
|
+
expect(res.statusCode).to.equal(206);
|
|
1710
|
+
expect(res.headers['content-length']).to.equal(5);
|
|
1711
|
+
expect(res.headers['content-range']).to.equal('bytes 42005-42009/42010');
|
|
1712
|
+
expect(res.headers['accept-ranges']).to.equal('bytes');
|
|
1713
|
+
expect(res.rawPayload.toString('binary')).to.equal('D\xAEB\x60\x82');
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1716
|
+
it('returns a subset of a fileStream (if-range)', async () => {
|
|
1717
|
+
|
|
1718
|
+
const server = Hapi.server();
|
|
1719
|
+
server.route({ method: 'GET', path: '/file', handler: fileStreamHandler });
|
|
1720
|
+
|
|
1721
|
+
await server.inject('/file');
|
|
1722
|
+
const res1 = await server.inject('/file');
|
|
1723
|
+
const res2 = await server.inject({ url: '/file', headers: { 'range': 'bytes=42005-42011', 'if-range': res1.headers.etag } });
|
|
1724
|
+
expect(res2.statusCode).to.equal(206);
|
|
1725
|
+
expect(res2.headers['content-length']).to.equal(5);
|
|
1726
|
+
expect(res2.headers['content-range']).to.equal('bytes 42005-42009/42010');
|
|
1727
|
+
expect(res2.headers['accept-ranges']).to.equal('bytes');
|
|
1728
|
+
expect(res2.rawPayload.toString('binary')).to.equal('D\xAEB\x60\x82');
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
it('returns 200 on incorrect if-range', async () => {
|
|
1732
|
+
|
|
1733
|
+
const server = Hapi.server();
|
|
1734
|
+
server.route({ method: 'GET', path: '/file', handler: fileStreamHandler });
|
|
1735
|
+
|
|
1736
|
+
const res = await server.inject({ url: '/file', headers: { 'range': 'bytes=42005-42011', 'if-range': 'abc' } });
|
|
1737
|
+
expect(res.statusCode).to.equal(200);
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
it('returns 416 on invalid range (unit)', async () => {
|
|
1741
|
+
|
|
1742
|
+
const server = Hapi.server();
|
|
1743
|
+
server.route({ method: 'GET', path: '/file', handler: fileStreamHandler });
|
|
1744
|
+
|
|
1745
|
+
const res = await server.inject({ url: '/file', headers: { 'range': 'horses=1-5' } });
|
|
1746
|
+
expect(res.statusCode).to.equal(416);
|
|
1747
|
+
expect(res.headers['content-range']).to.equal('bytes */42010');
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
it('returns 416 on invalid range (inversed)', async () => {
|
|
1751
|
+
|
|
1752
|
+
const server = Hapi.server();
|
|
1753
|
+
server.route({ method: 'GET', path: '/file', handler: fileStreamHandler });
|
|
1754
|
+
|
|
1755
|
+
const res = await server.inject({ url: '/file', headers: { 'range': 'bytes=5-1' } });
|
|
1756
|
+
expect(res.statusCode).to.equal(416);
|
|
1757
|
+
expect(res.headers['content-range']).to.equal('bytes */42010');
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
it('returns 416 on invalid range (format)', async () => {
|
|
1761
|
+
|
|
1762
|
+
const server = Hapi.server();
|
|
1763
|
+
server.route({ method: 'GET', path: '/file', handler: fileStreamHandler });
|
|
1764
|
+
|
|
1765
|
+
const res = await server.inject({ url: '/file', headers: { 'range': 'bytes 1-5' } });
|
|
1766
|
+
expect(res.statusCode).to.equal(416);
|
|
1767
|
+
expect(res.headers['content-range']).to.equal('bytes */42010');
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
it('returns 416 on invalid range (empty range)', async () => {
|
|
1771
|
+
|
|
1772
|
+
const server = Hapi.server();
|
|
1773
|
+
server.route({ method: 'GET', path: '/file', handler: fileStreamHandler });
|
|
1774
|
+
|
|
1775
|
+
const res = await server.inject({ url: '/file', headers: { 'range': 'bytes=-' } });
|
|
1776
|
+
expect(res.statusCode).to.equal(416);
|
|
1777
|
+
expect(res.headers['content-range']).to.equal('bytes */42010');
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
it('returns 200 on multiple ranges', async () => {
|
|
1781
|
+
|
|
1782
|
+
const server = Hapi.server();
|
|
1783
|
+
server.route({ method: 'GET', path: '/file', handler: fileStreamHandler });
|
|
1784
|
+
|
|
1785
|
+
const res = await server.inject({ url: '/file', headers: { 'range': 'bytes=1-5,7-10' } });
|
|
1786
|
+
expect(res.statusCode).to.equal(200);
|
|
1787
|
+
expect(res.headers['content-length']).to.equal(42010);
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
it('returns a subset of a stream', async () => {
|
|
1791
|
+
|
|
1792
|
+
const TestStream = class extends Stream.Readable {
|
|
1793
|
+
|
|
1794
|
+
constructor() {
|
|
1795
|
+
|
|
1796
|
+
super();
|
|
1797
|
+
this._count = -1;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
_read(size) {
|
|
1801
|
+
|
|
1802
|
+
this._count++;
|
|
1803
|
+
|
|
1804
|
+
if (this._count > 10) {
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
if (this._count === 10) {
|
|
1809
|
+
this.push(null);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
this.push(this._count.toString());
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
size() {
|
|
1817
|
+
|
|
1818
|
+
return 10;
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
|
|
1822
|
+
const server = Hapi.server();
|
|
1823
|
+
server.route({ method: 'GET', path: '/', handler: () => new TestStream() });
|
|
1824
|
+
|
|
1825
|
+
const res = await server.inject({ url: '/', headers: { 'range': 'bytes=2-4' } });
|
|
1826
|
+
expect(res.statusCode).to.equal(206);
|
|
1827
|
+
expect(res.headers['content-length']).to.equal(3);
|
|
1828
|
+
expect(res.headers['content-range']).to.equal('bytes 2-4/10');
|
|
1829
|
+
expect(res.headers['accept-ranges']).to.equal('bytes');
|
|
1830
|
+
expect(res.payload).to.equal('234');
|
|
1831
|
+
});
|
|
1832
|
+
|
|
1833
|
+
it('returns a consolidated range', async () => {
|
|
1834
|
+
|
|
1835
|
+
const TestStream = class extends Stream.Readable {
|
|
1836
|
+
|
|
1837
|
+
constructor() {
|
|
1838
|
+
|
|
1839
|
+
super();
|
|
1840
|
+
this._count = -1;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
_read(size) {
|
|
1844
|
+
|
|
1845
|
+
this._count++;
|
|
1846
|
+
|
|
1847
|
+
if (this._count > 10) {
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
if (this._count === 10) {
|
|
1852
|
+
this.push(null);
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
this.push(this._count.toString());
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
size() {
|
|
1860
|
+
|
|
1861
|
+
return 10;
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1865
|
+
const server = Hapi.server();
|
|
1866
|
+
server.route({ method: 'GET', path: '/', handler: () => new TestStream() });
|
|
1867
|
+
|
|
1868
|
+
const res = await server.inject({ url: '/', headers: { 'range': 'bytes=0-1,1-2, 3-5' } });
|
|
1869
|
+
expect(res.statusCode).to.equal(206);
|
|
1870
|
+
expect(res.headers['content-length']).to.equal(6);
|
|
1871
|
+
expect(res.headers['content-range']).to.equal('bytes 0-5/10');
|
|
1872
|
+
expect(res.headers['accept-ranges']).to.equal('bytes');
|
|
1873
|
+
expect(res.payload).to.equal('012345');
|
|
1874
|
+
});
|
|
1875
|
+
});
|
|
1876
|
+
|
|
1877
|
+
it('skips undefined header values', async () => {
|
|
1878
|
+
|
|
1879
|
+
const server = Hapi.server();
|
|
1880
|
+
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('ok').header('x', undefined) });
|
|
1881
|
+
const res = await server.inject('/');
|
|
1882
|
+
expect(res.statusCode).to.equal(200);
|
|
1883
|
+
expect(res.headers.x).to.not.exist();
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
it('does not add connection close header to normal requests', async () => {
|
|
1887
|
+
|
|
1888
|
+
const server = Hapi.server();
|
|
1889
|
+
server.route({ method: 'GET', path: '/', handler: () => 'ok' });
|
|
1890
|
+
const res = await server.inject('/');
|
|
1891
|
+
expect(res.statusCode).to.equal(200);
|
|
1892
|
+
expect(res.headers.connection).to.not.equal('close');
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
it('returns 500 when node rejects a header', async () => {
|
|
1896
|
+
|
|
1897
|
+
const server = Hapi.server();
|
|
1898
|
+
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response('ok').header('x', '1').header('', 'test') });
|
|
1899
|
+
const res = await server.inject('/');
|
|
1900
|
+
expect(res.statusCode).to.equal(500);
|
|
1901
|
+
expect(res.headers.x).to.not.exist();
|
|
1902
|
+
});
|
|
1903
|
+
|
|
1904
|
+
it('returns 500 for out of range status code', async () => {
|
|
1905
|
+
|
|
1906
|
+
const server = Hapi.server();
|
|
1907
|
+
|
|
1908
|
+
const handler = (request, h) => {
|
|
1909
|
+
|
|
1910
|
+
// Patch writeHead to always fail on out of range headers
|
|
1911
|
+
|
|
1912
|
+
const origWriteHead = request.raw.res.writeHead;
|
|
1913
|
+
request.raw.res.writeHead = function (statusCode, ...args) {
|
|
1914
|
+
|
|
1915
|
+
statusCode |= 0;
|
|
1916
|
+
if (statusCode < 100 || statusCode > 999) {
|
|
1917
|
+
throw new RangeError(`Invalid status code: ${statusCode}`);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
return origWriteHead.call(this, statusCode, ...args);
|
|
1921
|
+
};
|
|
1922
|
+
|
|
1923
|
+
return h.response('ok').code(1);
|
|
1924
|
+
};
|
|
1925
|
+
|
|
1926
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
1927
|
+
const res = await server.inject('/');
|
|
1928
|
+
expect(res.statusCode).to.equal(500);
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
it('permits ending reading request stream while transmitting response.', async (flags) => {
|
|
1932
|
+
|
|
1933
|
+
const server = Hapi.server();
|
|
1934
|
+
|
|
1935
|
+
server.route({
|
|
1936
|
+
method: 'post',
|
|
1937
|
+
path: '/',
|
|
1938
|
+
options: {
|
|
1939
|
+
payload: {
|
|
1940
|
+
output: 'stream'
|
|
1941
|
+
}
|
|
1942
|
+
},
|
|
1943
|
+
handler: (request, h) => {
|
|
1944
|
+
|
|
1945
|
+
const stream = new Stream.PassThrough();
|
|
1946
|
+
|
|
1947
|
+
// Start transmitting stream response...
|
|
1948
|
+
stream.push('hello ');
|
|
1949
|
+
|
|
1950
|
+
Bounce.background(async () => {
|
|
1951
|
+
|
|
1952
|
+
await Events.once(request.raw.res, 'pipe');
|
|
1953
|
+
|
|
1954
|
+
// ...but also only read and end the request once the response is transmitting...
|
|
1955
|
+
request.raw.req.on('data', Hoek.ignore);
|
|
1956
|
+
await Events.once(request.raw.req, 'end');
|
|
1957
|
+
|
|
1958
|
+
// ...and finally end the intended response once the request stream has ended.
|
|
1959
|
+
stream.end('world');
|
|
1960
|
+
});
|
|
1961
|
+
|
|
1962
|
+
return h.response(stream);
|
|
1963
|
+
}
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
flags.onCleanup = () => server.stop();
|
|
1967
|
+
await server.start();
|
|
1968
|
+
|
|
1969
|
+
const req = Http.request({
|
|
1970
|
+
hostname: 'localhost',
|
|
1971
|
+
port: server.info.port,
|
|
1972
|
+
method: 'post'
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1975
|
+
req.end('{}');
|
|
1976
|
+
|
|
1977
|
+
const [res] = await Events.once(req, 'response');
|
|
1978
|
+
|
|
1979
|
+
let result = '';
|
|
1980
|
+
for await (const chunk of res) {
|
|
1981
|
+
result += chunk.toString();
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
// If not permitted then result will be "hello " without "world"
|
|
1985
|
+
expect(result).to.equal('hello world');
|
|
1986
|
+
});
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1989
|
+
describe('length()', () => {
|
|
1990
|
+
|
|
1991
|
+
it('ignores NaN content-length', async () => {
|
|
1992
|
+
|
|
1993
|
+
const server = Hapi.server();
|
|
1994
|
+
server.route({ method: 'GET', path: '/', options: { handler: (request, h) => h.response().header('Content-Length', 'x') } });
|
|
1995
|
+
|
|
1996
|
+
const res = await server.inject('/');
|
|
1997
|
+
expect(res.statusCode).to.equal(200);
|
|
1998
|
+
expect(res.headers['content-length']).to.not.exist();
|
|
1999
|
+
});
|
|
2000
|
+
});
|
|
2001
|
+
|
|
2002
|
+
describe('encoding()', () => {
|
|
2003
|
+
|
|
2004
|
+
it('passes compressor to stream', async () => {
|
|
2005
|
+
|
|
2006
|
+
const handler = (request, h) => {
|
|
2007
|
+
|
|
2008
|
+
const TestStream = class extends Stream.Readable {
|
|
2009
|
+
|
|
2010
|
+
_read(size) {
|
|
2011
|
+
|
|
2012
|
+
if (this.isDone) {
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
this.isDone = true;
|
|
2017
|
+
|
|
2018
|
+
this.push('some payload');
|
|
2019
|
+
this._compressor.flush();
|
|
2020
|
+
|
|
2021
|
+
setTimeout(() => {
|
|
2022
|
+
|
|
2023
|
+
this.push(' and some other payload');
|
|
2024
|
+
this.push(null);
|
|
2025
|
+
}, 10);
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
setCompressor(compressor) {
|
|
2029
|
+
|
|
2030
|
+
this._compressor = compressor;
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2033
|
+
|
|
2034
|
+
return h.response(new TestStream()).type('text/html');
|
|
2035
|
+
};
|
|
2036
|
+
|
|
2037
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
2038
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
2039
|
+
|
|
2040
|
+
const res = await server.inject({ url: '/', headers: { 'accept-encoding': 'gzip' } });
|
|
2041
|
+
const uncompressed = await internals.uncompress('unzip', res.rawPayload);
|
|
2042
|
+
expect(uncompressed.toString()).to.equal('some payload and some other payload');
|
|
2043
|
+
});
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
describe('writeHead()', () => {
|
|
2047
|
+
|
|
2048
|
+
it('set custom statusMessage', async () => {
|
|
2049
|
+
|
|
2050
|
+
const server = Hapi.server();
|
|
2051
|
+
server.route({ method: 'GET', path: '/', handler: (request, h) => h.response({}).message('Great') });
|
|
2052
|
+
await server.start();
|
|
2053
|
+
|
|
2054
|
+
const uri = 'http://localhost:' + server.info.port;
|
|
2055
|
+
const { res } = await Wreck.get(uri);
|
|
2056
|
+
expect(res.statusMessage).to.equal('Great');
|
|
2057
|
+
await server.stop();
|
|
2058
|
+
});
|
|
2059
|
+
});
|
|
2060
|
+
|
|
2061
|
+
describe('chain()', () => {
|
|
2062
|
+
|
|
2063
|
+
it('handles stream errors on the response after the response has been piped', async () => {
|
|
2064
|
+
|
|
2065
|
+
const handler = (request, h) => {
|
|
2066
|
+
|
|
2067
|
+
const stream = new Stream.Readable();
|
|
2068
|
+
stream._read = function (size) {
|
|
2069
|
+
|
|
2070
|
+
if (this.isDone) {
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
this.isDone = true;
|
|
2075
|
+
|
|
2076
|
+
this.push('something');
|
|
2077
|
+
this.emit('error', new Error());
|
|
2078
|
+
};
|
|
2079
|
+
|
|
2080
|
+
return h.response(stream).type('text/html');
|
|
2081
|
+
};
|
|
2082
|
+
|
|
2083
|
+
const server = Hapi.server({ compression: { minBytes: 1 } });
|
|
2084
|
+
server.route({ method: 'GET', path: '/', handler });
|
|
2085
|
+
|
|
2086
|
+
const err = await expect(server.inject({ url: '/', headers: { 'accept-encoding': 'gzip' } })).to.reject(Boom.Boom);
|
|
2087
|
+
expect(err.output.statusCode).to.equal(499);
|
|
2088
|
+
});
|
|
2089
|
+
});
|
|
2090
|
+
});
|
|
2091
|
+
|
|
2092
|
+
|
|
2093
|
+
internals.TimerStream = class extends Stream.Readable {
|
|
2094
|
+
|
|
2095
|
+
_read(size) {
|
|
2096
|
+
|
|
2097
|
+
if (this.isDone) {
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
this.isDone = true;
|
|
2102
|
+
|
|
2103
|
+
setTimeout(() => {
|
|
2104
|
+
|
|
2105
|
+
this.push('hi');
|
|
2106
|
+
this.push(null);
|
|
2107
|
+
}, 5);
|
|
2108
|
+
}
|
|
2109
|
+
};
|
|
2110
|
+
|
|
2111
|
+
|
|
2112
|
+
internals.compress = function (encoder, value) {
|
|
2113
|
+
|
|
2114
|
+
return new Promise((resolve) => Zlib[encoder](value, (ignoreErr, compressed) => resolve(compressed)));
|
|
2115
|
+
};
|
|
2116
|
+
|
|
2117
|
+
|
|
2118
|
+
internals.uncompress = function (decoder, value) {
|
|
2119
|
+
|
|
2120
|
+
return new Promise((resolve) => Zlib[decoder](value, (ignoreErr, uncompressed) => resolve(uncompressed)));
|
|
2121
|
+
};
|