webhoster 0.1.1 → 0.3.1

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.
Files changed (87) hide show
  1. package/.eslintrc.json +74 -58
  2. package/.github/copilot-instructions.md +100 -0
  3. package/.github/workflows/test-matrix.yml +37 -0
  4. package/.test/benchmark.js +28 -0
  5. package/.test/constants.js +4 -0
  6. package/{test → .test}/http2server.js +1 -1
  7. package/{test → .test}/httpserver.js +1 -1
  8. package/{test → .test}/index.js +178 -192
  9. package/.test/multipromise.js +32 -0
  10. package/{test → .test}/tls.js +3 -3
  11. package/{test → .test}/urlencoded.js +3 -0
  12. package/.vscode/launch.json +24 -3
  13. package/README.md +116 -90
  14. package/data/CookieObject.js +14 -14
  15. package/errata/socketio.js +6 -11
  16. package/examples/starter.js +11 -0
  17. package/helpers/HeadersParser.js +7 -8
  18. package/helpers/HttpListener.js +387 -42
  19. package/helpers/RequestHeaders.js +43 -36
  20. package/helpers/RequestReader.js +27 -26
  21. package/helpers/ResponseHeaders.js +47 -36
  22. package/jsconfig.json +1 -1
  23. package/lib/HttpHandler.js +447 -277
  24. package/lib/HttpRequest.js +383 -39
  25. package/lib/HttpResponse.js +316 -52
  26. package/lib/HttpTransaction.js +146 -0
  27. package/middleware/AutoHeadersMiddleware.js +73 -0
  28. package/middleware/CORSMiddleware.js +45 -47
  29. package/middleware/CaseInsensitiveHeadersMiddleware.js +5 -11
  30. package/middleware/ContentDecoderMiddleware.js +81 -35
  31. package/middleware/ContentEncoderMiddleware.js +179 -132
  32. package/middleware/ContentLengthMiddleware.js +66 -41
  33. package/middleware/ContentWriterMiddleware.js +5 -5
  34. package/middleware/HashMiddleware.js +68 -40
  35. package/middleware/HeadMethodMiddleware.js +24 -21
  36. package/middleware/MethodMiddleware.js +29 -36
  37. package/middleware/PathMiddleware.js +49 -66
  38. package/middleware/ReadFormData.js +99 -0
  39. package/middleware/SendJsonMiddleware.js +131 -0
  40. package/middleware/SendStringMiddleware.js +87 -0
  41. package/package.json +38 -29
  42. package/polyfill/FormData.js +164 -0
  43. package/rollup.config.js +0 -1
  44. package/scripts/check-teapot.mjs +40 -0
  45. package/scripts/test-all-sync.sh +6 -0
  46. package/scripts/test-all.sh +7 -0
  47. package/templates/starter.js +55 -0
  48. package/test/fixtures/stream.js +68 -0
  49. package/test/helpers/HttpListener/construct.js +18 -0
  50. package/test/helpers/HttpListener/customOptions.js +22 -0
  51. package/test/helpers/HttpListener/doubleCreate.js +40 -0
  52. package/test/helpers/HttpListener/events.js +77 -0
  53. package/test/helpers/HttpListener/http.js +31 -0
  54. package/test/helpers/HttpListener/http2.js +41 -0
  55. package/test/helpers/HttpListener/https.js +38 -0
  56. package/test/helpers/HttpListener/startAll.js +31 -0
  57. package/test/helpers/HttpListener/stopNotStarted.js +23 -0
  58. package/test/lib/HttpHandler/class.js +8 -0
  59. package/test/lib/HttpHandler/handleRequest.js +11 -0
  60. package/test/lib/HttpHandler/middleware.js +941 -0
  61. package/test/lib/HttpHandler/parse.js +41 -0
  62. package/test/lib/HttpRequest/class.js +8 -0
  63. package/test/lib/HttpRequest/downstream.js +171 -0
  64. package/test/lib/HttpRequest/properties.js +101 -0
  65. package/test/lib/HttpRequest/read.js +518 -0
  66. package/test/lib/HttpResponse/class.js +8 -0
  67. package/test/lib/HttpResponse/properties.js +59 -0
  68. package/test/lib/HttpResponse/send.js +275 -0
  69. package/test/lib/HttpTransaction/class.js +8 -0
  70. package/test/lib/HttpTransaction/ping.js +50 -0
  71. package/test/lib/HttpTransaction/push.js +89 -0
  72. package/test/middleware/SendJsonMiddleware.js +222 -0
  73. package/test/sanity.js +10 -0
  74. package/test/templates/error-teapot.js +47 -0
  75. package/test/templates/starter.js +93 -0
  76. package/tsconfig.json +12 -0
  77. package/types/index.js +61 -34
  78. package/types/typings.d.ts +8 -9
  79. package/utils/AsyncObject.js +6 -3
  80. package/utils/CaseInsensitiveObject.js +2 -3
  81. package/utils/function.js +1 -7
  82. package/utils/headers.js +42 -0
  83. package/utils/qualityValues.js +1 -1
  84. package/utils/stream.js +4 -20
  85. package/index.cjs +0 -3190
  86. package/test/constants.js +0 -4
  87. /package/{test → .test}/cookietester.js +0 -0
package/index.cjs DELETED
@@ -1,3190 +0,0 @@
1
- 'use strict';
2
-
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
- function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
6
-
7
- var stream = require('stream');
8
- var http = _interopDefault(require('http'));
9
- var https = _interopDefault(require('https'));
10
- var http2 = _interopDefault(require('http2'));
11
- var util = require('util');
12
- var zlib = require('zlib');
13
- var crypto = _interopDefault(require('crypto'));
14
- var path = require('path');
15
-
16
- /** @typedef {import('stream').Readable} Readable */
17
- /** @typedef {import('../types').RequestMethod} RequestMethod */
18
- /** @typedef {import('http').IncomingHttpHeaders} IncomingHttpHeaders */
19
-
20
- /**
21
- * @typedef {Object} HttpRequestOptions
22
- * @prop {RequestMethod} method always uppercase
23
- * @prop {URL} url
24
- * @prop {!IncomingHttpHeaders} headers
25
- * @prop {Object<string,any>} [locals]
26
- * @prop {Readable} stream
27
- * @prop {import('net').Socket|import('tls').TLSSocket} [socket]
28
- * @prop {boolean} [canPing]
29
- * @prop {function():Promise<any>} [onPing]
30
- * @prop {boolean} [unsealed]
31
- */
32
-
33
- class HttpRequest {
34
- #canPing = false;
35
-
36
- /** @type {function():Promise<any>} */
37
- #onPing = null;
38
-
39
- /** @param {HttpRequestOptions} options */
40
- constructor(options) {
41
- /** @type {RequestMethod} */
42
- this.method = (options.method.toUpperCase());
43
- this.url = options.url;
44
- /** @type {IncomingHttpHeaders} */
45
- this.headers = options.headers;
46
- this.stream = options.stream;
47
- this.socket = options.socket;
48
- this.#canPing = options.canPing ?? false;
49
- this.#onPing = options.onPing;
50
- this.locals = options.locals || {};
51
- this.unsealed = options.unsealed ?? false;
52
- if (!this.unsealed) {
53
- Object.seal(this);
54
- }
55
- }
56
-
57
- /**
58
- * @param {Readable} stream
59
- * @return {Readable} previousStream
60
- */
61
- replaceStream(stream) {
62
- const previousStream = this.stream;
63
- this.stream = stream;
64
- return previousStream;
65
- }
66
-
67
- get canPing() {
68
- return this.#canPing;
69
- }
70
-
71
- /**
72
- * @return {Promise<any>}
73
- */
74
- ping() {
75
- if (!this.#onPing) {
76
- return Promise.reject(new Error('NOT_IMPLEMENTED'));
77
- }
78
- if (!this.#canPing) {
79
- return Promise.reject(new Error('NOT_SUPPORTED'));
80
- }
81
- return this.#onPing();
82
- }
83
- }
84
-
85
- /** @typedef {import('stream').Writable} Writable */
86
-
87
- /** @typedef {import('http').OutgoingHttpHeaders} OutgoingHttpHeaders */
88
-
89
- /**
90
- * @typedef {Object} HttpResponseOptions
91
- * @prop {OutgoingHttpHeaders} [headers]
92
- * @prop {function():boolean} [onHeadersSent]
93
- * @prop {function(boolean):void} [onSendHeaders]
94
- * @prop {number} [status]
95
- * @prop {Writable} stream
96
- * @prop {import('net').Socket|import('tls').TLSSocket} [socket]
97
- * @prop {boolean} [canPushPath]
98
- * @prop {function(string):Promise<any>} [onPushPath]
99
- * @prop {Object<string,any>} [locals]
100
- * @prop {boolean} [unsealed]
101
- */
102
-
103
- class HttpResponse {
104
- /** @type {function():boolean} */
105
- #onHeadersSent = null;
106
-
107
- /** @type {function(boolean):void} */
108
- #onSendHeaders = null;
109
-
110
- /** @type {function(string):Promise<any>} */
111
- #onPushPath = null;
112
-
113
- /** @type {boolean} */
114
- #headersSent = false;
115
-
116
- /** @type {Array<string>} */
117
- #pushedPaths = [];
118
-
119
- #canPushPath = false;
120
-
121
- /** @param {HttpResponseOptions} options */
122
- constructor(options) {
123
- /** @type {OutgoingHttpHeaders} */
124
- this.headers = options.headers || {};
125
- this.stream = options.stream;
126
- this.socket = options.socket;
127
- this.status = options.status;
128
- this.#onPushPath = options.onPushPath;
129
- this.#onHeadersSent = options.onHeadersSent;
130
- this.#onSendHeaders = options.onSendHeaders;
131
- this.#canPushPath = options.canPushPath ?? false;
132
- this.locals = options.locals || {};
133
- this.unsealed = options.unsealed ?? false;
134
- if (!this.unsealed) {
135
- Object.seal(this);
136
- }
137
- }
138
-
139
- get headersSent() {
140
- if (this.#onHeadersSent) {
141
- return this.#onHeadersSent();
142
- }
143
- return this.#headersSent;
144
- }
145
-
146
- /**
147
- * @param {Writable} stream
148
- * @return {Writable} previousStream
149
- */
150
- replaceStream(stream) {
151
- const previousStream = this.stream;
152
- this.stream = stream;
153
- return previousStream;
154
- }
155
-
156
- /**
157
- * @param {boolean} [flush]
158
- * @return {void}
159
- */
160
- sendHeaders(flush) {
161
- if (this.headersSent) {
162
- throw new Error('ALREADY_SENT');
163
- }
164
- if (!this.#onSendHeaders) {
165
- throw new Error('NOT_IMPLEMENTED');
166
- }
167
- this.#onSendHeaders(flush);
168
- this.#headersSent = true;
169
- }
170
-
171
- get pushedPaths() {
172
- return this.#pushedPaths;
173
- }
174
-
175
- get canPushPath() {
176
- return this.#canPushPath;
177
- }
178
-
179
- /**
180
- * @param {string} [path]
181
- * @return {Promise<any>}
182
- */
183
- pushPath(path) {
184
- if (this.#pushedPaths.includes(path)) {
185
- return Promise.reject(new Error('ALREADY_PUSHED'));
186
- }
187
- if (!this.#onPushPath) {
188
- return Promise.reject(new Error('NOT_IMPLEMENTED'));
189
- }
190
- if (!this.#canPushPath) {
191
- return Promise.reject(new Error('NOT_SUPPORTED'));
192
- }
193
- return this.#onPushPath(path);
194
- }
195
- }
196
-
197
- /**
198
- * @template {any} T
199
- * @class AsyncObject<T>
200
- */
201
- class AsyncObject {
202
- /** @param {T} [value] */
203
- constructor(value) {
204
- this.value = value;
205
- this.ready = false;
206
- this.busy = false;
207
- /** @type {{resolve:function(T):void, reject:function(Error?):void}[]} */
208
- this.pendingPromises = [];
209
- }
210
-
211
- hasValue() {
212
- return this.ready;
213
- }
214
-
215
- isBusy() {
216
- return this.busy;
217
- }
218
-
219
- /** @return {Promise<T>} */
220
- get() {
221
- if (!this.isBusy() && this.hasValue()) {
222
- return Promise.resolve(this.value);
223
- }
224
- return new Promise((resolve, reject) => {
225
- this.pendingPromises.push({ resolve, reject });
226
- });
227
- }
228
-
229
- /**
230
- * @param {T} [value]
231
- * @return {Promise<T>}
232
- */
233
- set(value) {
234
- this.value = value;
235
- this.busy = false;
236
- this.ready = true;
237
- while (this.pendingPromises.length) {
238
- this.pendingPromises.shift().resolve(value);
239
- }
240
- return Promise.resolve(value);
241
- }
242
-
243
- /**
244
- * @param {Error} [error] Error passed to pending promises
245
- * @return {void}
246
- */
247
- reset(error) {
248
- this.value = undefined;
249
- this.busy = false;
250
- this.ready = false;
251
- while (this.pendingPromises.length) {
252
- this.pendingPromises.shift().reject(error);
253
- }
254
- }
255
-
256
- /** @return {void} */
257
- prepare() {
258
- this.value = undefined;
259
- this.busy = true;
260
- this.ready = false;
261
- }
262
-
263
- /** @return {void} */
264
- setBusy() {
265
- this.busy = true;
266
- }
267
- }
268
-
269
- // eslint-disable-next-line jsdoc/require-returns-check
270
- /**
271
- * @param {any} [args]
272
- * @return {any}
273
- */
274
- // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
275
- function noop(...args) {}
276
-
277
- /** @typedef {import('../types').Middleware} Middleware */
278
- /** @typedef {import('../types').MiddlewareErrorHandler} MiddlewareErrorHandler */
279
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
280
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
281
- /** @typedef {import('../types').MiddlewareFunctionResultType} MiddlewareFunctionResultType */
282
- /** @typedef {import('../types').RequestMethod} RequestMethod */
283
- /** @typedef {import('../types/index.js').HandlerState} HandlerState */
284
-
285
- /** @type {HttpHandler} */
286
- let defaultInstance = null;
287
-
288
- /**
289
- * @param {Middleware} middleware
290
- * @return {boolean}
291
- */
292
- function isErrorHandler(middleware) {
293
- return !!middleware && typeof middleware === 'object' && 'onError' in middleware && middleware.onError != null;
294
- }
295
-
296
- /**
297
- * @typedef {Object} HttpHandlerOptions
298
- * @prop {Middleware[]} [preprocessors]
299
- * @prop {Set<Middleware>} [middleware]
300
- * @prop {MiddlewareErrorHandler[]} [errorHandlers]
301
- */
302
- class HttpHandler {
303
- /** @param {HttpHandlerOptions} options */
304
- constructor(options = {}) {
305
- this.preprocessors = options.preprocessors || [];
306
- this.middleware = options.middleware || new Set();
307
- this.errorHandlers = options.errorHandlers || [];
308
- this.handleRequest = this.handleRequest.bind(this);
309
- this.handleHttp1Request = this.handleHttp1Request.bind(this);
310
- this.handleHttp2Stream = this.handleHttp2Stream.bind(this);
311
- }
312
-
313
- /** @return {HttpHandler} */
314
- static get defaultInstance() {
315
- if (!defaultInstance) {
316
- defaultInstance = new HttpHandler();
317
- }
318
- return defaultInstance;
319
- }
320
-
321
- /**
322
- * @param {Object} params
323
- * @param {HttpRequest} params.req
324
- * @param {HttpResponse} params.res
325
- * @return {Promise<HttpResponse>}
326
- */
327
- handleRequest({ req, res }) {
328
- /** @type {HandlerState} */
329
- const state = { treeIndex: [] };
330
- let isInErrorState = false;
331
- /** @type {any} */
332
- let caughtError = null;
333
- // eslint-disable-next-line @typescript-eslint/no-this-alias
334
- const context = this;
335
- /**
336
- * @param {Middleware} middleware
337
- * @return {Promise<MiddlewareFunctionResult>}
338
- */
339
- function handleMiddleware(middleware) {
340
- const isMiddlewareAnErrorHandler = isErrorHandler(middleware);
341
- if (isInErrorState) {
342
- if (isMiddlewareAnErrorHandler) {
343
- isInErrorState = false;
344
- /** @type {MiddlewareFunctionResult} */
345
- let returnValue;
346
- try {
347
- returnValue = /** @type {MiddlewareErrorHandler} */ (middleware)
348
- .onError({
349
- req, res, state, err: caughtError,
350
- });
351
- } catch (err) {
352
- isInErrorState = true;
353
- caughtError = err;
354
- returnValue = 'continue';
355
- }
356
- return Promise.resolve().then(() => returnValue);
357
- }
358
- if (middleware !== context.errorHandlers) {
359
- // Consume and advance
360
- return Promise.resolve();
361
- }
362
- } else if (isMiddlewareAnErrorHandler) {
363
- // Don't run error handler if not in error state.
364
- return Promise.resolve();
365
- }
366
-
367
- if (typeof middleware === 'boolean') {
368
- if (middleware === false) {
369
- return Promise.resolve('break');
370
- }
371
- return Promise.resolve();
372
- }
373
-
374
- switch (middleware) {
375
- case 'break':
376
- return Promise.resolve('break');
377
- case 'end':
378
- return Promise.resolve('end');
379
- case 'continue':
380
- return Promise.resolve();
381
- }
382
-
383
- if (middleware === null || typeof middleware === 'string' || typeof middleware === 'undefined') {
384
- return Promise.resolve();
385
- }
386
-
387
- if (middleware instanceof Map) {
388
- return handleMiddleware(middleware.values());
389
- }
390
-
391
- if (Symbol.iterator in middleware) {
392
- const iterator = (/** @type {Iterable<Middleware>} */ (middleware))[Symbol.iterator]();
393
-
394
- /**
395
- * @param {MiddlewareFunctionResult} [chainResult]
396
- * @return {Promise<MiddlewareFunctionResult>}
397
- */
398
- const chainLoop = (chainResult) => {
399
- if (chainResult === 'end') {
400
- return Promise.resolve('end');
401
- }
402
- if (chainResult === false || chainResult === 'break') {
403
- return Promise.resolve();
404
- }
405
- if (chainResult != null && chainResult !== true) {
406
- return handleMiddleware(chainResult).then(chainLoop);
407
- }
408
- const chainIteration = iterator.next();
409
- if (chainIteration.done) return Promise.resolve();
410
- state.treeIndex[state.treeIndex.length - 1] += 1;
411
- /** @type {Middleware} */
412
- const innerMiddleware = chainIteration.value;
413
- return handleMiddleware(innerMiddleware).then(chainLoop);
414
- };
415
-
416
- // Start looping
417
- state.treeIndex.push(-1);
418
- return chainLoop().then((result) => {
419
- state.treeIndex.pop();
420
- return result;
421
- });
422
- }
423
-
424
- if (middleware && typeof middleware === 'object') {
425
- if ('execute' in middleware) {
426
- if (typeof middleware.execute === 'function') {
427
- return handleMiddleware(middleware.execute.bind(middleware));
428
- }
429
- return handleMiddleware(middleware.execute);
430
- }
431
- return handleMiddleware(Object.values(middleware));
432
- }
433
-
434
- if (typeof middleware !== 'function') {
435
- console.warn('Unknown middleware', middleware);
436
- return Promise.resolve();
437
- }
438
- return Promise.resolve().then(() => middleware({ req, res, state })).catch((err) => {
439
- isInErrorState = true;
440
- caughtError = err;
441
- return /** @type {'continue'} */ ('continue');
442
- });
443
- }
444
-
445
- const allMiddleware = [
446
- this.preprocessors,
447
- this.middleware,
448
- this.errorHandlers,
449
- ];
450
- return handleMiddleware(allMiddleware).then(() => {
451
- if (isInErrorState) {
452
- isInErrorState = false;
453
- throw caughtError;
454
- }
455
- return res;
456
- });
457
- }
458
-
459
- /**
460
- * @param {import('http').IncomingMessage} incomingMessage
461
- * @param {import('http').ServerResponse} serverResponse
462
- * @return {Promise<HttpResponse>}
463
- */
464
- handleHttp1Request(incomingMessage, serverResponse) {
465
- // @ts-ignore If TLSSocketLike
466
- const protocol = incomingMessage.socket.encrypted ? 'https:' : 'http:';
467
-
468
- let url;
469
- try {
470
- url = new URL(`${protocol}//${incomingMessage.headers.host}${incomingMessage.url}`);
471
- } catch (error) {
472
- serverResponse.writeHead(400);
473
- serverResponse.end();
474
- return Promise.reject(error);
475
- }
476
-
477
- const req = new HttpRequest({
478
- headers: incomingMessage.headers,
479
- method: /** @type {RequestMethod} */ (incomingMessage.method?.toUpperCase()),
480
- stream: incomingMessage,
481
- socket: incomingMessage.socket,
482
- url,
483
- });
484
-
485
- const res = new HttpResponse({
486
- stream: serverResponse,
487
- socket: serverResponse.socket,
488
- onHeadersSent() {
489
- return serverResponse.headersSent;
490
- },
491
- onSendHeaders(flush) {
492
- if (res.status == null) return Promise.reject(new Error('NO_STATUS'));
493
- serverResponse.writeHead(res.status, this.headers);
494
- if (flush) {
495
- serverResponse.flushHeaders();
496
- }
497
- return Promise.resolve();
498
- },
499
- });
500
-
501
- return this.handleRequest({ req, res }).then(() => {
502
- if (!res.stream.writableEnded) {
503
- console.warn('Response stream was not ended.');
504
- res.stream.end();
505
- }
506
- return res;
507
- });
508
- }
509
-
510
- /**
511
- * @param {import('http2').ServerHttp2Stream} stream
512
- * @param {import('http2').IncomingHttpHeaders} headers
513
- * @param {Partial<import('./HttpResponse.js').HttpResponseOptions>} [responseOptions]
514
- * @return {Promise<HttpResponse>}
515
- */
516
- handleHttp2Stream(stream$1, headers, responseOptions = {}) {
517
- let url;
518
- try {
519
- url = new URL([
520
- headers[':scheme'] ?? '',
521
- '://',
522
- headers[':authority'] ?? '',
523
- headers[':path'] ?? '',
524
- ].join(''));
525
- } catch (error) {
526
- stream$1.respond({ ':status': 400 }, { endStream: true });
527
- return Promise.reject(error);
528
- }
529
-
530
- /** @type {Set<AsyncObject<any>>} */
531
- const pendingPushSyncLocks = new Set();
532
- const req = new HttpRequest({
533
- headers,
534
- url,
535
- method: /** @type {RequestMethod} */ (headers[':method']),
536
- stream: stream$1,
537
- socket: stream$1.session.socket,
538
- canPing: true,
539
- onPing() {
540
- return new Promise((resolve, reject) => {
541
- stream$1.session.ping((err, duration) => {
542
- if (err) {
543
- reject(err);
544
- return;
545
- }
546
- resolve(duration);
547
- });
548
- });
549
- },
550
- });
551
-
552
- // eslint-disable-next-line @typescript-eslint/no-this-alias
553
- const context = this;
554
- const res = new HttpResponse({
555
- stream: stream$1,
556
- socket: stream$1.session.socket,
557
- canPushPath: stream$1.pushAllowed,
558
- onHeadersSent() {
559
- return stream$1.headersSent;
560
- },
561
- onSendHeaders() {
562
- if (this.headers[':status'] == null) {
563
- if (res.status == null) {
564
- throw new Error('NO_STATUS');
565
- }
566
- this.headers[':status'] = res.status;
567
- }
568
- stream$1.respond(this.headers);
569
- },
570
- onPushPath(path) {
571
- const syncLock = new AsyncObject();
572
- pendingPushSyncLocks.add(syncLock);
573
- syncLock.prepare();
574
- return new Promise((resolve, reject) => {
575
- if (!stream$1.pushAllowed) {
576
- reject(new Error('PUSH_NOT_ALLOWED'));
577
- return;
578
- }
579
- const newHeaders = {
580
- ':scheme': headers[':scheme'],
581
- ':authority': headers[':authority'],
582
- ':path': path,
583
- ':method': 'GET',
584
- };
585
- [
586
- 'accept',
587
- 'accept-encoding',
588
- 'accept-language',
589
- 'user-agent',
590
- 'cache-control',
591
- ].forEach((passedHeader) => {
592
- if (passedHeader in headers) {
593
- // @ts-ignore Coerce
594
- newHeaders[passedHeader] = headers[passedHeader];
595
- }
596
- });
597
- stream$1.pushStream(newHeaders, ((err, pushStream) => {
598
- if (err) {
599
- pendingPushSyncLocks.delete(syncLock);
600
- syncLock.set(null);
601
- reject(err);
602
- return;
603
- }
604
- context.handleHttp2Stream(pushStream, newHeaders, { canPushPath: false })
605
- .then(resolve).catch(reject).finally(() => {
606
- pendingPushSyncLocks.delete(syncLock);
607
- syncLock.set(null);
608
- });
609
- }));
610
- });
611
- },
612
- ...responseOptions,
613
- });
614
-
615
- // Workaround for https://github.com/nodejs/node/issues/31309
616
- const STREAM_WAIT_MS = 0;
617
- /** @type {NodeJS.Timeout} */
618
- let pingTimeout = null;
619
- /** @return {void} */
620
- function sendPing() {
621
- if (stream$1.session) stream$1.session.ping(noop);
622
- }
623
- const autoPingStream = new stream.PassThrough({
624
- read(...args) {
625
- clearTimeout(pingTimeout);
626
- pingTimeout = setTimeout(sendPing, STREAM_WAIT_MS);
627
- // eslint-disable-next-line no-underscore-dangle
628
- return stream.PassThrough.prototype._read.call(this, ...args);
629
- },
630
- });
631
- stream$1.pipe(autoPingStream);
632
- req.replaceStream(autoPingStream);
633
-
634
- stream$1.on('error', (err) => {
635
- console.error(err);
636
- console.error(headers[':path']);
637
- });
638
- return this.handleRequest({ req, res })
639
- .then(() => Promise.all([...pendingPushSyncLocks.values()]
640
- .map((syncLock) => syncLock.get().catch(noop))))
641
- .then(() => {
642
- res.stream.end();
643
- })
644
- .then(() => res);
645
- }
646
- }
647
-
648
- var index = /*#__PURE__*/Object.freeze({
649
- __proto__: null,
650
- HttpRequest: HttpRequest,
651
- HttpResponse: HttpResponse,
652
- HttpHandler: HttpHandler
653
- });
654
-
655
- const SERVER_ALREADY_CREATED = 'SERVER_ALREADY_CREATED';
656
-
657
- /** @typedef {import('tls').TlsOptions} TlsOptions */
658
-
659
- /**
660
- * @typedef {Object} HttpListenerOptions
661
- * @prop {HttpHandler} [httpHandler]
662
- * @prop {number} [insecurePort=8080]
663
- * @prop {string} [insecureHost] blank defaults to '::' or '0.0.0.0'
664
- * @prop {number} [securePort=8443]
665
- * @prop {string} [secureHost] blank defaults to '::' or '0.0.0.0'
666
- * @prop {boolean} [useHttp=true]
667
- * @prop {boolean} [useHttps=false]
668
- * @prop {boolean} [useHttp2=true]
669
- * @prop {TlsOptions} [tlsOptions]
670
- */
671
-
672
- /** @type {HttpListener} */
673
- let defaultInstance$1;
674
-
675
- class HttpListener {
676
- /** @param {HttpListenerOptions} options */
677
- constructor(options = {}) {
678
- this.httpHandler = options.httpHandler ?? HttpHandler.defaultInstance;
679
- this.insecurePort = options.insecurePort ?? 8080;
680
- this.insecureHost = options.insecureHost;
681
- this.securePort = options.securePort ?? 8443;
682
- this.secureHost = options.secureHost;
683
- this.useHttp = options.useHttp !== false;
684
- this.useHttps = options.useHttps === true;
685
- this.useHttp2 = options.useHttp2 !== false;
686
- this.tlsOptions = options.tlsOptions ?? {};
687
- }
688
-
689
- /** @return {HttpListener} */
690
- static get defaultInstance() {
691
- if (!defaultInstance$1) {
692
- defaultInstance$1 = new HttpListener();
693
- }
694
- return defaultInstance$1;
695
- }
696
-
697
- /**
698
- * @param {http.ServerOptions} [options]
699
- * @return {http.Server}
700
- */
701
- createHttpServer(options = {}) {
702
- if (!this.httpServer) {
703
- this.httpServer = http.createServer(options);
704
- } else if (Object.keys(options).length) {
705
- throw new Error(SERVER_ALREADY_CREATED);
706
- }
707
- return this.httpServer;
708
- }
709
-
710
- /**
711
- * @param {https.ServerOptions} [options]
712
- * @return {https.Server}
713
- */
714
- createHttpsServer(options = {}) {
715
- if (!this.httpsServer) {
716
- this.httpsServer = https.createServer({
717
- ...this.tlsOptions,
718
- ...options,
719
- });
720
- } else if (Object.keys(options).length) {
721
- throw new Error(SERVER_ALREADY_CREATED);
722
- }
723
- return this.httpsServer;
724
- }
725
-
726
- /**
727
- * @param {http2.ServerOptions} [options]
728
- * @return {http2.Http2SecureServer}
729
- */
730
- createHttp2Server(options = {}) {
731
- if (!this.http2Server) {
732
- this.http2Server = http2.createSecureServer({
733
- allowHTTP1: true,
734
- ...this.tlsOptions,
735
- ...options,
736
- });
737
- } else if (Object.keys(options).length) {
738
- throw new Error(SERVER_ALREADY_CREATED);
739
- }
740
- return this.http2Server;
741
- }
742
-
743
- /** @return {Promise<http.Server>} */
744
- startHttpServer() {
745
- return new Promise((resolve, reject) => {
746
- this.createHttpServer();
747
- this.httpServer.listen({
748
- port: this.insecurePort,
749
- host: this.insecureHost,
750
- }, () => {
751
- this.httpServer.removeListener('error', reject);
752
- this.httpServer.addListener('request', this.httpHandler.handleHttp1Request);
753
- resolve(this.httpServer);
754
- });
755
- this.httpServer.addListener('error', reject);
756
- });
757
- }
758
-
759
- /** @return {Promise<https.Server>} */
760
- startHttpsServer() {
761
- return new Promise((resolve, reject) => {
762
- this.createHttpsServer();
763
- this.httpsServer.listen({
764
- port: this.securePort,
765
- host: this.secureHost,
766
- }, () => {
767
- this.httpsServer.removeListener('error', reject);
768
- this.httpsServer.addListener('request', this.httpHandler.handleHttp1Request);
769
- resolve(this.httpsServer);
770
- });
771
- this.httpsServer.addListener('error', reject);
772
- });
773
- }
774
-
775
- /** @return {Promise<http2.Http2SecureServer>} */
776
- startHttp2Server() {
777
- return new Promise((resolve, reject) => {
778
- this.createHttp2Server();
779
- this.http2Server.listen({
780
- port: this.securePort,
781
- host: this.secureHost,
782
- }, () => {
783
- this.http2Server.removeListener('error', reject);
784
- this.http2Server.addListener('stream', this.httpHandler.handleHttp2Stream);
785
- this.http2Server.addListener('request', (req, res) => {
786
- if (req.httpVersionMajor >= 2) return;
787
- // @ts-ignore Ignore typings
788
- this.httpHandler.handleHttp1Request(req, res);
789
- });
790
- resolve(this.http2Server);
791
- });
792
- this.http2Server.addListener('error', reject);
793
- });
794
- }
795
-
796
- /** @return {Promise<void>} */
797
- stopHttpServer() {
798
- return new Promise((resolve, reject) => {
799
- if (!this.httpServer) {
800
- resolve();
801
- return;
802
- }
803
- this.httpServer.close((err) => {
804
- if (err) {
805
- reject(err);
806
- return;
807
- }
808
- resolve();
809
- });
810
- });
811
- }
812
-
813
- /** @return {Promise<void>} */
814
- stopHttpsServer() {
815
- return new Promise((resolve, reject) => {
816
- if (!this.httpsServer) {
817
- resolve();
818
- return;
819
- }
820
- this.httpsServer.close((err) => {
821
- if (err) {
822
- reject(err);
823
- return;
824
- }
825
- resolve();
826
- });
827
- });
828
- }
829
-
830
- /** @return {Promise<void>} */
831
- stopHttp2Server() {
832
- return new Promise((resolve, reject) => {
833
- if (!this.http2Server) {
834
- resolve();
835
- return;
836
- }
837
- this.http2Server.close((err) => {
838
- if (err) {
839
- reject(err);
840
- return;
841
- }
842
- resolve();
843
- });
844
- });
845
- }
846
-
847
- /**
848
- * @return {Promise<[http.Server, https.Server, http2.Http2SecureServer]>}
849
- */
850
- startAll() {
851
- return Promise.all([
852
- this.useHttp ? this.startHttpServer() : Promise.resolve(null),
853
- this.useHttps ? this.startHttpsServer() : Promise.resolve(null),
854
- this.useHttp2 ? this.startHttp2Server() : Promise.resolve(null),
855
- ]);
856
- }
857
-
858
- /**
859
- * @return {Promise<void>}
860
- */
861
- stopAll() {
862
- return Promise.all([
863
- this.useHttp ? this.stopHttpServer() : Promise.resolve(null),
864
- this.useHttps ? this.stopHttpsServer() : Promise.resolve(null),
865
- this.useHttp2 ? this.stopHttp2Server() : Promise.resolve(null),
866
- ]).then(() => null);
867
- }
868
- }
869
-
870
- class HeadersParser {
871
- constructor(headers = {}) {
872
- /** @type {Object<string,any>} */
873
- this.headers = headers;
874
- }
875
-
876
- /** @return {string} */
877
- get contentType() {
878
- return this.headers['content-type'];
879
- }
880
-
881
- /**
882
- * The `media-type` directive of `Content-Type`.
883
- * The MIME type of the resource or the data.
884
- * (Always lowercase)
885
- * @return {string}
886
- */
887
- get mediaType() {
888
- return this.contentType?.split(';')[0].trim().toLowerCase();
889
- }
890
-
891
- /**
892
- * The `charset` direct of `Content-Type`.
893
- * The character encoding standard.
894
- * (Always lowercase)
895
- * @return {string} */
896
- get charset() {
897
- let value = null;
898
- // eslint-disable-next-line no-unused-expressions
899
- this.contentType?.split(';').some((directive) => {
900
- const parameters = directive.split('=');
901
- if (parameters[0].trim().toLowerCase() !== 'charset') {
902
- return false;
903
- }
904
- value = parameters[1]?.trim().toLowerCase();
905
- const firstQuote = value.indexOf('"');
906
- const lastQuote = value.lastIndexOf('"');
907
- if (firstQuote !== -1 && lastQuote !== -1) {
908
- value = value.substring(firstQuote + 1, lastQuote);
909
- }
910
- return true;
911
- });
912
- return value;
913
- }
914
-
915
- /** @return {string} */
916
- get boundary() {
917
- let value = null;
918
- // eslint-disable-next-line no-unused-expressions
919
- this.contentType?.split(';').some((directive) => {
920
- const parameters = directive.split('=');
921
- if (parameters[0].trim().toLowerCase() !== 'boundary') {
922
- return false;
923
- }
924
- value = parameters[1]?.trim().toLowerCase();
925
- const firstQuote = value.indexOf('"');
926
- const lastQuote = value.lastIndexOf('"');
927
- if (firstQuote !== -1 && lastQuote !== -1) {
928
- value = value.substring(firstQuote + 1, lastQuote);
929
- }
930
- return true;
931
- });
932
- return value;
933
- }
934
-
935
-
936
- /** @return {number} */
937
- get contentLength() {
938
- return parseInt(this.headers['content-length'], 10) || null;
939
- }
940
- }
941
-
942
- /** @typedef {import('../types').HttpRequest} HttpRequest */
943
-
944
- /**
945
- * @param {string} cookieString
946
- * @return {[string,string][]}
947
- */
948
- function getEntriesFromCookie(cookieString) {
949
- return cookieString.split(';').map((pair) => {
950
- const indexOfEquals = pair.indexOf('=');
951
- let name;
952
- let value;
953
- if (indexOfEquals === -1) {
954
- name = '';
955
- value = pair.trim();
956
- } else {
957
- name = pair.substr(0, indexOfEquals).trim();
958
- value = pair.substr(indexOfEquals + 1).trim();
959
- }
960
- const firstQuote = value.indexOf('"');
961
- const lastQuote = value.lastIndexOf('"');
962
- if (firstQuote !== -1 && lastQuote !== -1) {
963
- value = value.substring(firstQuote + 1, lastQuote);
964
- }
965
- return [name, value];
966
- });
967
- }
968
-
969
- class RequestHeaders extends HeadersParser {
970
- /** @param {HttpRequest} req */
971
- constructor(req) {
972
- super(req.headers);
973
- }
974
-
975
- /** @type {Object<string,string[]>} */
976
- #cookiesProxy = null;
977
-
978
- get cookies() {
979
- // eslint-disable-next-line @typescript-eslint/no-this-alias
980
- const instance = this;
981
- return {
982
- /**
983
- * @param {string} name
984
- * @return {string}
985
- */
986
- get(name) {
987
- return instance.cookieEntries[name]?.[0];
988
- },
989
- /**
990
- * @param {string} name
991
- * @return {string[]}
992
- */
993
- all(name) {
994
- return instance.cookieEntries[name] ?? [];
995
- },
996
- };
997
- }
998
-
999
- /** @return {Object<string,string[]>} */
1000
- get cookieEntries() {
1001
- if (!this.#cookiesProxy) {
1002
- // eslint-disable-next-line @typescript-eslint/no-this-alias
1003
- const instance = this;
1004
- /** @type {Map<string,string[]>} */
1005
- const arrayProxyMap = new Map();
1006
- this.#cookiesProxy = new Proxy({}, {
1007
- get(cookieTarget, cookieName) {
1008
- if (typeof cookieName !== 'string') return undefined;
1009
- if (arrayProxyMap.has(cookieName)) {
1010
- return arrayProxyMap.get(cookieName);
1011
- }
1012
- const cookieString = (instance.headers.cookie ?? '');
1013
- const split = cookieString.split(';');
1014
- const values = [];
1015
- for (let i = 0; i < split.length; i += 1) {
1016
- const [key, value] = split[i].split('=');
1017
- if (key.trim() === cookieName) values.push(value);
1018
- }
1019
- const arrayProxy = new Proxy(values, {
1020
- get: (arrayTarget, arrayProp, receiver) => {
1021
- if (typeof arrayProp !== 'string') {
1022
- return Reflect.get(arrayTarget, arrayProp, receiver);
1023
- }
1024
- if (arrayProp === 'length') {
1025
- return getEntriesFromCookie(instance.headers.cookie ?? '')
1026
- .filter(([key]) => (key === cookieName)).length;
1027
- }
1028
- if (Number.isNaN(parseInt(arrayProp, 10))) {
1029
- return Reflect.get(arrayTarget, arrayProp, receiver);
1030
- }
1031
- const entries = getEntriesFromCookie(instance.headers.cookie ?? '');
1032
- let count = 0;
1033
- for (let i = 0; i < entries.length; i += 1) {
1034
- const entry = entries[i];
1035
- if (entry[0] === cookieName) {
1036
- if (arrayProp === count.toString()) {
1037
- return entry[1];
1038
- }
1039
- count += 1;
1040
- }
1041
- }
1042
- return Reflect.get(arrayTarget, arrayProp, receiver);
1043
- },
1044
- set: (arrayTarget, arrayProp, value, receiver) => {
1045
- Reflect.set(arrayTarget, arrayProp, value, receiver);
1046
- if (typeof arrayProp !== 'string') return true;
1047
- const result = getEntriesFromCookie(instance.headers.cookie ?? '').reduce((prev, curr) => {
1048
- if (!curr[0]) return prev;
1049
- if (curr[0] === cookieName) return prev;
1050
- return `${prev};${curr[0]}=${curr[1]}`;
1051
- }, arrayTarget.map((v) => `${cookieName}=${v}`).join(';'));
1052
- instance.headers.cookie = result;
1053
- return true;
1054
- },
1055
- });
1056
- arrayProxyMap.set(cookieName, arrayProxy);
1057
- return arrayProxy;
1058
- },
1059
- ownKeys() {
1060
- const cookieString = (instance.headers.cookie || '');
1061
- const split = cookieString.split(';');
1062
- /** @type {string[]} */
1063
- const keys = [];
1064
- for (let i = 0; i < split.length; i += 1) {
1065
- const [key] = split[i].split('=');
1066
- const trimmed = key?.trim();
1067
- if (trimmed && !keys.includes(trimmed)) {
1068
- keys.push(trimmed);
1069
- }
1070
- }
1071
- return keys;
1072
- },
1073
- has(target, p) {
1074
- return instance.headers.cookie?.split(';')
1075
- .some((/** @type string */ cookie) => cookie.split('=')[0]?.trim() === p) ?? false;
1076
- },
1077
- getOwnPropertyDescriptor() {
1078
- return {
1079
- enumerable: true,
1080
- configurable: true,
1081
- };
1082
- },
1083
- });
1084
- }
1085
- return this.#cookiesProxy;
1086
- }
1087
- }
1088
-
1089
- /** @typedef {import('../types').HttpRequest} HttpRequest */
1090
-
1091
- /**
1092
- * @typedef {Object} RequestReaderOptions
1093
- * @prop {boolean} [cache=true]
1094
- */
1095
-
1096
- const BUFFER_SIZE = 4096;
1097
- const STREAM_WAIT_MS = 0;
1098
-
1099
- /** @type {WeakMap<HttpRequest, RequestReader>} */
1100
- const cache = new WeakMap();
1101
-
1102
- class RequestReader {
1103
- /** @type {AsyncObject<Buffer>} */
1104
- #buffer = new AsyncObject(null);
1105
-
1106
- /**
1107
- * @param {HttpRequest} request
1108
- * @param {RequestReaderOptions} [options]
1109
- */
1110
- constructor(request, options) {
1111
- const o = {
1112
- cache: true,
1113
- ...options,
1114
- };
1115
- if (o.cache !== false) {
1116
- if (cache.has(request)) {
1117
- return cache.get(request);
1118
- }
1119
- cache.set(request, this);
1120
- }
1121
- this.request = request;
1122
- }
1123
-
1124
- /** @return {Promise<Buffer>} */
1125
- readBuffer() {
1126
- if (this.#buffer.isBusy() || this.#buffer.hasValue()) return this.#buffer.get();
1127
- this.#buffer.prepare();
1128
- const hp = new RequestHeaders(this.request);
1129
- let data = Buffer.alloc(Math.min(BUFFER_SIZE, hp.contentLength || BUFFER_SIZE));
1130
- let bytesWritten = 0;
1131
- /** @type {NodeJS.Timeout} */
1132
- let sendPingTimeout = null;
1133
- this.request.stream.on('readable', () => {
1134
- let chunk;
1135
- // eslint-disable-next-line no-cond-assign
1136
- while (chunk = this.request.stream.read(Math.min(BUFFER_SIZE, this.request.stream.readableLength))) {
1137
- /** @type {Buffer} */
1138
- let buffer;
1139
- if (typeof chunk === 'string') {
1140
- console.warn('Unexpected string type on chunk!', this.request.stream.readableEncoding);
1141
- buffer = Buffer.from(chunk, this.request.stream.readableEncoding);
1142
- } else {
1143
- buffer = chunk;
1144
- }
1145
- if ((buffer.length + bytesWritten) > data.length) {
1146
- let newLength = data.length * 2;
1147
- while (newLength < buffer.length + data.length) {
1148
- newLength *= 2;
1149
- }
1150
- const newBuffer = Buffer.alloc(newLength);
1151
- data.copy(newBuffer);
1152
- data = newBuffer;
1153
- }
1154
- bytesWritten += buffer.copy(data, bytesWritten);
1155
- }
1156
- clearTimeout(sendPingTimeout);
1157
- if (this.request.canPing) {
1158
- sendPingTimeout = setTimeout(() => {
1159
- this.request.ping().catch(noop);
1160
- }, STREAM_WAIT_MS);
1161
- }
1162
- });
1163
- this.request.stream.on('end', () => {
1164
- clearTimeout(sendPingTimeout);
1165
- if (data.length > bytesWritten) {
1166
- data = data.subarray(0, bytesWritten);
1167
- }
1168
- this.#buffer.set(data);
1169
- });
1170
- this.request.stream.on('error', (err) => {
1171
- this.#buffer.reset(err);
1172
- });
1173
- return this.#buffer.get();
1174
- }
1175
-
1176
- /** @return {Promise<string>} */
1177
- readString() {
1178
- return this.readBuffer().then((buffer) => {
1179
- const reqHeaders = new RequestHeaders(this.request);
1180
- // TODO: Compare TextDecoder, Buffer.from(), and StringDecoder performance
1181
- const decoder = new util.TextDecoder(reqHeaders.charset || 'utf-8');
1182
- return decoder.decode(buffer);
1183
- });
1184
- }
1185
-
1186
- /** @return {Promise<Object<string,any>>} */
1187
- readJSON() {
1188
- return this.readString().then(JSON.parse);
1189
- }
1190
-
1191
- /**
1192
- * The application/x-www-form-urlencoded format is in many ways an aberrant monstrosity,
1193
- * the result of many years of implementation accidents and compromises leading to a set of
1194
- * requirements necessary for interoperability, but in no way representing good design practices.
1195
- * In particular, readers are cautioned to pay close attention to the twisted details
1196
- * involving repeated (and in some cases nested) conversions between character encodings and byte sequences.
1197
- * Unfortunately the format is in widespread use due to the prevalence of HTML forms. [HTML]
1198
- * @return {Promise<[string, string][]>}
1199
- */
1200
- readUrlEncoded() {
1201
- // https://url.spec.whatwg.org/#urlencoded-parsing
1202
- const reqHeaders = new RequestHeaders(this.request);
1203
- const decoder = new util.TextDecoder(reqHeaders.charset || 'utf-8');
1204
- return this.readBuffer().then((buffer) => {
1205
- const sequences = [];
1206
- let startIndex = 0;
1207
- for (let i = 0; i < buffer.length; i += 1) {
1208
- if (buffer[i] === 0x26) {
1209
- sequences.push(buffer.subarray(startIndex, i));
1210
- startIndex = i + 1;
1211
- }
1212
- if (i === buffer.length - 1) {
1213
- sequences.push(buffer.subarray(startIndex, i));
1214
- }
1215
- }
1216
- /** @type {[string, string][]} */
1217
- const output = [];
1218
- sequences.forEach((bytes) => {
1219
- if (!bytes.length) return;
1220
-
1221
- // Find 0x3D and replace 0x2B in one loop for better performance
1222
- let indexOf0x3D = -1;
1223
- for (let i = 0; i < bytes.length; i += 1) {
1224
- switch (bytes[i]) {
1225
- case 0x3D:
1226
- if (indexOf0x3D === -1) {
1227
- indexOf0x3D = i;
1228
- }
1229
- break;
1230
- case 0x2B:
1231
- // Replace bytes on original stream for memory conservation
1232
- // eslint-disable-next-line no-param-reassign
1233
- bytes[i] = 0x20;
1234
- break;
1235
- }
1236
- }
1237
- let name;
1238
- let value;
1239
- if (indexOf0x3D === -1) {
1240
- name = bytes;
1241
- value = bytes.subarray(bytes.length, 0);
1242
- } else {
1243
- name = bytes.subarray(0, indexOf0x3D);
1244
- value = bytes.subarray(indexOf0x3D + 1);
1245
- }
1246
- const nameString = decodeURIComponent(decoder.decode(name));
1247
- const valueString = decodeURIComponent(decoder.decode(value));
1248
- output.push([nameString, valueString]);
1249
- });
1250
- return output;
1251
- });
1252
- }
1253
-
1254
- /** @return {Promise<Map<string,string>>} */
1255
- readUrlEncodedAsMap() {
1256
- return this.readUrlEncoded().then((tupleArray) => new Map(tupleArray));
1257
- }
1258
-
1259
- /** @return {Promise<Object<string,string>>} */
1260
- readUrlEncodedAsObject() {
1261
- return this.readUrlEncoded().then((tupleArray) => Object.fromEntries(tupleArray));
1262
- }
1263
-
1264
- /**
1265
- * Returns `readJSON()`, `readUrlEncodedAsObject`, or `Promise<null>` based on Content-Type
1266
- * @return {Promise<Object<string, any>|null>}
1267
- */
1268
- readObject() {
1269
- const reqHeaders = new RequestHeaders(this.request);
1270
- const mediaType = reqHeaders.mediaType?.toLowerCase() ?? '';
1271
- switch (mediaType) {
1272
- case 'application/json':
1273
- return this.readJSON();
1274
- case 'application/x-www-form-urlencoded':
1275
- return this.readUrlEncodedAsObject();
1276
- default:
1277
- return Promise.resolve(null);
1278
- }
1279
- }
1280
-
1281
- /**
1282
- * Returns `readJSON()`, `readUrlEncoded`, `readBuffer()`, or `readString()` based on Content-Type
1283
- * @return {Promise<Object<string,any>|string|Buffer>}
1284
- */
1285
- read() {
1286
- const reqHeaders = new RequestHeaders(this.request);
1287
- const mediaType = reqHeaders.mediaType?.toLowerCase() ?? '';
1288
- switch (mediaType) {
1289
- case 'application/json':
1290
- return this.readJSON();
1291
- case 'application/x-www-form-urlencoded':
1292
- return this.readUrlEncoded();
1293
- case 'application/octet-stream':
1294
- case '':
1295
- return this.readBuffer();
1296
- default:
1297
- if (mediaType.startsWith('text')) {
1298
- return this.readString();
1299
- }
1300
- return this.readBuffer();
1301
- }
1302
- }
1303
- }
1304
-
1305
- /** @typedef {import('../types').CookieDetails} CookieDetails */
1306
-
1307
- /** @private */
1308
- class CookieObject {
1309
- /** @param {CookieDetails|string} options */
1310
- constructor(options) {
1311
- if (typeof options === 'string') {
1312
- return CookieObject.parse(options);
1313
- }
1314
- this.name = options.name;
1315
- this.value = options.value;
1316
- this.expires = options.expires;
1317
- this.maxAge = options.maxAge;
1318
- this.domain = options.domain;
1319
- this.path = options.path;
1320
- this.secure = options.secure;
1321
- this.httpOnly = options.httpOnly;
1322
- this.sameSite = options.sameSite;
1323
- }
1324
-
1325
- /**
1326
- * @param {string} cookieString
1327
- * @return {CookieObject}
1328
- */
1329
- static parse(cookieString) {
1330
- /** @type {Partial<CookieDetails>} */
1331
- const options = {};
1332
- cookieString.split(';').forEach((pair, index) => {
1333
- const indexOfEquals = pair.indexOf('=');
1334
- let key;
1335
- let value;
1336
- if (indexOfEquals === -1) {
1337
- key = '';
1338
- value = pair.trim();
1339
- } else {
1340
- key = pair.substr(0, indexOfEquals).trim();
1341
- value = pair.substr(indexOfEquals + 1).trim();
1342
- }
1343
- const firstQuote = value.indexOf('"');
1344
- const lastQuote = value.lastIndexOf('"');
1345
- if (firstQuote !== -1 && lastQuote !== -1) {
1346
- value = value.substring(firstQuote + 1, lastQuote);
1347
- }
1348
- if (index === 0) {
1349
- options.name = key;
1350
- if (value != null) {
1351
- options.value = value;
1352
- }
1353
- return;
1354
- }
1355
- switch (key.toLowerCase()) {
1356
- case 'expires':
1357
- options.expires = new Date(value);
1358
- return;
1359
- case 'max-age':
1360
- options.maxAge = parseInt(value, 10);
1361
- return;
1362
- case 'domain':
1363
- options.domain = value;
1364
- break;
1365
- case 'path':
1366
- options.path = value;
1367
- break;
1368
- case 'secure':
1369
- options.secure = true;
1370
- break;
1371
- case 'httponly':
1372
- options.httpOnly = true;
1373
- break;
1374
- case 'samesite':
1375
- // @ts-ignore No cast
1376
- options.sameSite = value;
1377
- break;
1378
- }
1379
- });
1380
- return new CookieObject(options);
1381
- }
1382
-
1383
- toString() {
1384
- // eslint-disable-next-line prefer-template
1385
- return (`${this.name ?? ''}=${this.value ?? ''}`)
1386
- + (this.expires != null ? `; Expires=${this.expires.toUTCString()}` : '')
1387
- + (this.maxAge != null ? `; Max-Age=${this.maxAge}` : '')
1388
- + (this.domain != null ? `; Domain=${this.domain}` : '')
1389
- + (this.path != null ? `; Path=${this.path}` : '')
1390
- + (this.secure ? '; Secure' : '')
1391
- + (this.httpOnly ? '; HttpOnly' : '')
1392
- + (this.sameSite ? `; SameSite=${this.sameSite}` : '');
1393
- }
1394
-
1395
- toJSON() {
1396
- return {
1397
- name: this.name,
1398
- value: this.value,
1399
- expires: this.expires,
1400
- maxAge: this.maxAge,
1401
- domain: this.domain,
1402
- path: this.path,
1403
- secure: this.secure || undefined,
1404
- httpOnly: this.httpOnly || undefined,
1405
- sameSite: this.sameSite,
1406
- };
1407
- }
1408
- }
1409
-
1410
- /** @typedef {import('../types').HttpResponse} HttpResponse */
1411
- /** @typedef {import('../types').CookieDetails} CookieDetails */
1412
-
1413
- /** @type {(keyof CookieDetails)[]} */
1414
- const COOKIE_DETAIL_KEYS = [
1415
- 'name',
1416
- 'value',
1417
- 'expires',
1418
- 'maxAge',
1419
- 'domain',
1420
- 'path',
1421
- 'secure',
1422
- 'httpOnly',
1423
- 'sameSite',
1424
- ];
1425
-
1426
- class ResponseHeaders extends HeadersParser {
1427
- /** @param {HttpResponse} res */
1428
- constructor(res) {
1429
- super(res.headers);
1430
- }
1431
-
1432
- /** @type {CookieObject[]} */
1433
- #setCookiesProxy = null;
1434
-
1435
- /** @type {ProxyHandler<CookieObject>} */
1436
- #cookieObjectProxyHandler = {
1437
- set: (cookieTarget, cookieProp, cookieValue, receiver) => {
1438
- Reflect.set(cookieTarget, cookieProp, cookieValue, receiver);
1439
- const index = this.cookieEntries.findIndex((entry) => entry.toString() === cookieTarget.toString());
1440
- if (index !== -1) {
1441
- // Force reflection
1442
- Reflect.set(this.cookieEntries, index, cookieTarget);
1443
- }
1444
- return true;
1445
- },
1446
- };
1447
-
1448
- get contentType() {
1449
- return super.contentType;
1450
- }
1451
-
1452
- /** @param {string} contentType */
1453
- set contentType(contentType) {
1454
- this.headers['content-type'] = contentType;
1455
- }
1456
-
1457
- get mediaType() {
1458
- return super.mediaType;
1459
- }
1460
-
1461
- /** @param {string} mediaType */
1462
- set mediaType(mediaType) {
1463
- const { charset, boundary } = this;
1464
- this.contentType = [
1465
- mediaType ?? '',
1466
- charset ? `charset=${charset}` : null,
1467
- boundary ? `boundary=${boundary}` : null,
1468
- ].filter((s) => s != null).join(';');
1469
- }
1470
-
1471
- get charset() {
1472
- return super.charset;
1473
- }
1474
-
1475
- /** @param {string} charset */
1476
- set charset(charset) {
1477
- const { mediaType, boundary } = this;
1478
- this.contentType = [
1479
- mediaType ?? '',
1480
- charset ? `charset=${charset}` : null,
1481
- boundary ? `boundary=${boundary}` : null,
1482
- ].filter((s) => s != null).join(';');
1483
- }
1484
-
1485
- /** @return {BufferEncoding} */
1486
- get charsetAsBufferEncoding() {
1487
- const { charset } = this;
1488
- switch (charset) {
1489
- case 'iso-8859-1':
1490
- case 'ascii':
1491
- case 'binary':
1492
- case 'latin1':
1493
- return 'latin1';
1494
- case 'utf-16le':
1495
- case 'ucs-2':
1496
- case 'ucs2':
1497
- case 'utf16le':
1498
- return 'utf16le';
1499
- case 'utf-8':
1500
- case 'utf8':
1501
- return 'utf-8';
1502
- default:
1503
- case 'base64':
1504
- case 'hex':
1505
- return /** @type {BufferEncoding} */ (charset);
1506
- }
1507
- }
1508
-
1509
- /** @param {BufferEncoding} bufferEncoding */
1510
- set charsetAsBufferEncoding(bufferEncoding) {
1511
- switch (bufferEncoding) {
1512
- case 'ascii':
1513
- case 'binary':
1514
- case 'latin1':
1515
- this.charset = 'iso-8859-1';
1516
- break;
1517
- case 'ucs-2':
1518
- case 'ucs2':
1519
- case 'utf16le':
1520
- this.charset = 'utf-16le';
1521
- break;
1522
- case 'utf-8':
1523
- case 'utf8':
1524
- this.charset = 'utf-8';
1525
- break;
1526
- default:
1527
- case 'base64':
1528
- case 'hex':
1529
- this.charset = bufferEncoding;
1530
- }
1531
- }
1532
-
1533
- get boundary() {
1534
- return super.boundary;
1535
- }
1536
-
1537
- /** @param {string} boundary */
1538
- set boundary(boundary) {
1539
- const { mediaType, charset } = this;
1540
- this.contentType = [
1541
- mediaType ?? '',
1542
- charset ? `charset=${charset}` : null,
1543
- boundary ? `boundary=${boundary}` : null,
1544
- ].filter((s) => s != null).join(';');
1545
- }
1546
-
1547
- get contentLength() {
1548
- return super.contentLength;
1549
- }
1550
-
1551
- /** @param {number} value */
1552
- set contentLength(value) {
1553
- this.headers['content-length'] = value;
1554
- }
1555
-
1556
- get cookies() {
1557
- // eslint-disable-next-line @typescript-eslint/no-this-alias
1558
- const instance = this;
1559
- return {
1560
- /**
1561
- * @param {string|CookieDetails|CookieObject} nameOrDetails
1562
- * @return {boolean}
1563
- */
1564
- has(nameOrDetails) {
1565
- return !!this.getAll(nameOrDetails)[0];
1566
- },
1567
- /**
1568
- * @param {string|CookieDetails} partial
1569
- * @return {CookieObject}
1570
- */
1571
- get(partial) {
1572
- return this.getAll(partial)[0];
1573
- },
1574
- /**
1575
- * @param {string|CookieDetails} [partial]
1576
- * @return {CookieObject[]}
1577
- */
1578
- getAll(partial) {
1579
- const details = typeof partial === 'string' ? new CookieObject({ name: partial }) : partial ?? {};
1580
- /** @type {CookieDetails} */
1581
- const searchDetails = { name: details.name };
1582
- if (details.path) {
1583
- searchDetails.path = details.path;
1584
- }
1585
- return this.findAll(searchDetails);
1586
- },
1587
- /**
1588
- * @param {CookieDetails} details
1589
- * @return {CookieObject}
1590
- */
1591
- find(details) {
1592
- return this.findAll(details)[0];
1593
- },
1594
- /**
1595
- * @param {CookieDetails} [details]
1596
- * @return {CookieObject[]}
1597
- */
1598
- findAll(details = {}) {
1599
- return instance.cookieEntries
1600
- .filter((cookieObject) => COOKIE_DETAIL_KEYS.every((key) => {
1601
- if ((key in details) === false) {
1602
- return true;
1603
- }
1604
- switch (key) {
1605
- case 'expires':
1606
- return details.expires?.getTime() === cookieObject.expires?.getTime();
1607
- case 'path':
1608
- return (details.path ?? '') === (cookieObject.path ?? '');
1609
- default:
1610
- return details[key] === cookieObject[key];
1611
- }
1612
- }))
1613
- .sort((a, b) => (b?.path?.length ?? 0 - a?.path?.length ?? 0));
1614
- },
1615
- /**
1616
- * @param {string|CookieDetails} cookie
1617
- * @return {CookieObject}
1618
- */
1619
- set(cookie) {
1620
- const details = typeof cookie === 'string' ? new CookieObject(cookie) : cookie ?? {};
1621
- let cookieObject = this.find({
1622
- path: '',
1623
- ...details,
1624
- });
1625
- if (!cookieObject) {
1626
- cookieObject = new Proxy(new CookieObject(details), instance.#cookieObjectProxyHandler);
1627
- instance.cookieEntries.push(cookieObject);
1628
- } else {
1629
- COOKIE_DETAIL_KEYS.forEach((key) => {
1630
- // @ts-ignore Coerce
1631
- cookieObject[key] = details[key];
1632
- });
1633
- }
1634
- return cookieObject;
1635
- },
1636
- /**
1637
- * @param {string|CookieDetails} partial
1638
- * @return {number} count
1639
- */
1640
- remove(partial) {
1641
- const items = this.getAll(partial);
1642
- const count = items.length;
1643
- items.forEach((item) => {
1644
- instance.cookieEntries.splice(instance.cookieEntries.indexOf(item), 1);
1645
- });
1646
- return count;
1647
- },
1648
- /**
1649
- * @param {string|CookieDetails} partial name or details
1650
- * @return {CookieObject}
1651
- */
1652
- expire(partial) {
1653
- const details = typeof partial === 'string' ? new CookieObject({ name: partial }) : partial ?? {};
1654
- let object = this.get(details);
1655
- if (!object) {
1656
- object = new Proxy(new CookieObject(details), instance.#cookieObjectProxyHandler);
1657
- instance.cookieEntries.push(object);
1658
- }
1659
- delete object.expires;
1660
- object.maxAge = 0;
1661
- object.value = '';
1662
- return object;
1663
- },
1664
- /**
1665
- * @param {string|CookieDetails} [partial]
1666
- * @return {CookieObject[]}
1667
- */
1668
- expireAll(partial) {
1669
- const items = this.getAll(partial);
1670
- items.forEach((item) => {
1671
- /* eslint-disable no-param-reassign */
1672
- item.expires = null;
1673
- item.maxAge = 0;
1674
- item.value = '';
1675
- /* eslint-enable no-param-reassign */
1676
- });
1677
- return items;
1678
- },
1679
- };
1680
- }
1681
-
1682
- /** @return {Array<CookieObject>} */
1683
- get cookieEntries() {
1684
- if (!this.#setCookiesProxy) {
1685
- if (!this.headers['set-cookie']) {
1686
- this.headers['set-cookie'] = [];
1687
- }
1688
- // eslint-disable-next-line @typescript-eslint/no-this-alias
1689
- const instance = this;
1690
- /** @type {CookieObject[]} */
1691
- const values = instance.headers['set-cookie']
1692
- .map((/** @type {string} */ setCookie) => new Proxy(
1693
- new CookieObject(setCookie),
1694
- instance.#cookieObjectProxyHandler,
1695
- ));
1696
- this.#setCookiesProxy = new Proxy(values, {
1697
- get: (arrayTarget, arrayProp, receiver) => {
1698
- if (typeof arrayProp !== 'string') {
1699
- return Reflect.get(arrayTarget, arrayProp, receiver);
1700
- }
1701
- if (arrayProp === 'length') {
1702
- return instance.headers['set-cookie'].length;
1703
- }
1704
- if (Number.isNaN(parseInt(arrayProp, 10))) {
1705
- return Reflect.get(arrayTarget, arrayProp, receiver);
1706
- }
1707
- const entry = instance.headers['set-cookie'][arrayProp];
1708
- if (typeof entry === 'undefined') {
1709
- return entry;
1710
- }
1711
- if (arrayProp in arrayTarget === false) {
1712
- Reflect.set(
1713
- arrayTarget,
1714
- arrayProp,
1715
- new Proxy(new CookieObject(entry), instance.#cookieObjectProxyHandler),
1716
- );
1717
- }
1718
- return Reflect.get(arrayTarget, arrayProp, receiver);
1719
- },
1720
- set: (arrayTarget, arrayProp, value, receiver) => {
1721
- Reflect.set(arrayTarget, arrayProp, value, receiver);
1722
- if (typeof arrayProp !== 'string') return true;
1723
- if (arrayProp === 'length') {
1724
- Reflect.set(instance.headers['set-cookie'], arrayProp, value);
1725
- }
1726
- if (value instanceof CookieObject) {
1727
- instance.headers['set-cookie'][arrayProp] = value.toString();
1728
- }
1729
- return true;
1730
- },
1731
- });
1732
- }
1733
- return this.#setCookiesProxy;
1734
- }
1735
- }
1736
-
1737
- var index$1 = /*#__PURE__*/Object.freeze({
1738
- __proto__: null,
1739
- HttpListener: HttpListener,
1740
- HeadersParser: HeadersParser,
1741
- RequestHeaders: RequestHeaders,
1742
- RequestReader: RequestReader,
1743
- ResponseHeaders: ResponseHeaders
1744
- });
1745
-
1746
- class CaseInsensitiveObject {
1747
- /** @param {Object} [object] */
1748
- constructor(object) {
1749
- if (object && object instanceof CaseInsensitiveObject) {
1750
- return object;
1751
- }
1752
- // eslint-disable-next-line @typescript-eslint/no-this-alias
1753
- const instance = this;
1754
- const proxy = new Proxy(instance, CaseInsensitiveObject.defaultProxyHandler);
1755
- Object.entries(object).forEach(([key, value]) => {
1756
- // @ts-ignore Coerce
1757
- this[key] = value;
1758
- });
1759
- return proxy;
1760
- }
1761
- }
1762
-
1763
- /** @type {ProxyHandler<Object>} */
1764
- CaseInsensitiveObject.defaultProxyHandler = {
1765
- get(target, p, receiver) {
1766
- return Reflect.get(target, typeof p === 'string' ? p.toLowerCase() : p, receiver);
1767
- },
1768
- set(target, p, receiver) {
1769
- return Reflect.set(target, typeof p === 'string' ? p.toLowerCase() : p, receiver);
1770
- },
1771
- has(target, p) {
1772
- return Reflect.has(target, typeof p === 'string' ? p.toLowerCase() : p);
1773
- },
1774
- deleteProperty(target, p) {
1775
- return Reflect.deleteProperty(target, typeof p === 'string' ? p.toLowerCase() : p);
1776
- },
1777
- };
1778
-
1779
- /** @typedef {import('../types').IMiddleware} IMiddleware */
1780
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
1781
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
1782
-
1783
- /**
1784
- * @typedef {Object} CaseInsensitiveHeadersMiddlewareOptions
1785
- * @prop {boolean} [request=false] Mutate request headers to be case-insensitive
1786
- * @prop {boolean} [response=false] Mutate response headers to be case-insensistive
1787
- */
1788
-
1789
- /** @implements {IMiddleware} */
1790
- class CaseInsensitiveHeadersMiddleware {
1791
- /** @param {CaseInsensitiveHeadersMiddlewareOptions} options */
1792
- constructor(options) {
1793
- this.request = options.request === true;
1794
- this.response = options.response === true;
1795
- }
1796
-
1797
- /**
1798
- * @param {!MiddlewareFunctionParams} params
1799
- * @return {MiddlewareFunctionResult}
1800
- */
1801
- execute({ req, res }) {
1802
- if (this.request) {
1803
- // @ts-ignore Coerce
1804
- req.headers = new CaseInsensitiveObject(req.headers || {});
1805
- }
1806
- if (this.response) {
1807
- // @ts-ignore Coerce
1808
- res.headers = new CaseInsensitiveObject(res.headers || {});
1809
- }
1810
- return 'continue';
1811
- }
1812
- }
1813
-
1814
- /** @typedef {import('../types').IMiddleware} IMiddleware */
1815
- /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
1816
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
1817
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
1818
-
1819
- /**
1820
- * @typedef ContentDecoderMiddlewareOptions
1821
- * @prop {number} [chunkSize]
1822
- * @prop {boolean} [respondNotAcceptable=false]
1823
- */
1824
-
1825
- /**
1826
- * Implements `Accept-Encoding`
1827
- * https://tools.ietf.org/html/rfc7231#section-5.3.4
1828
- * @implements {IMiddleware}
1829
- */
1830
- class ContentDecoderMiddleware {
1831
- /** @param {ContentDecoderMiddlewareOptions} [options] */
1832
- constructor(options = {}) {
1833
- this.chunkSize = options.chunkSize;
1834
- this.respondNotAcceptable = options.respondNotAcceptable === true;
1835
- }
1836
-
1837
- /**
1838
- * @param {!MiddlewareFunctionParams} params
1839
- * @return {MiddlewareFunctionResult}
1840
- */
1841
- execute({ req, res }) {
1842
- switch (req.method) {
1843
- case 'HEAD':
1844
- case 'GET':
1845
- return 'continue';
1846
- }
1847
-
1848
- res.headers['accept-encoding'] = 'gzip, deflate, br';
1849
- const contentEncoding = (req.headers['content-encoding'] ?? '').trim().toLowerCase();
1850
-
1851
- switch (contentEncoding) {
1852
- case '':
1853
- case 'identity':
1854
- return 'continue';
1855
- case 'gzip':
1856
- case 'br':
1857
- case 'deflate':
1858
- break;
1859
- default:
1860
- if (this.respondNotAcceptable) {
1861
- res.status = 406;
1862
- return 'end';
1863
- }
1864
- return 'continue';
1865
- }
1866
-
1867
- const source = req.stream;
1868
- let initialized = false;
1869
- const { chunkSize } = this;
1870
- const newReadable = new stream.PassThrough({
1871
- read(...args) {
1872
- if (!initialized) {
1873
- /** @type {import("zlib").Gzip} */
1874
- let gzipStream;
1875
- switch (contentEncoding) {
1876
- case 'deflate':
1877
- gzipStream = zlib.createInflate({ chunkSize });
1878
- break;
1879
- case 'gzip':
1880
- gzipStream = zlib.createGunzip({ chunkSize });
1881
- break;
1882
- case 'br':
1883
- gzipStream = zlib.createBrotliDecompress({ chunkSize });
1884
- break;
1885
- default:
1886
- throw new Error('UNKNOWN_ENCODING');
1887
- }
1888
- source.pipe(gzipStream).pipe(this);
1889
- initialized = true;
1890
- }
1891
- if (source.isPaused()) source.resume();
1892
- // eslint-disable-next-line no-underscore-dangle
1893
- stream.Transform.prototype._read.call(this, ...args);
1894
- },
1895
- });
1896
- source.pause();
1897
- req.replaceStream(newReadable);
1898
- return 'continue';
1899
- }
1900
- }
1901
-
1902
- /** @typedef {{q:number?} & {[key:string]:string}} ParsedQualityValues */
1903
-
1904
- /**
1905
- * @param {string} input
1906
- * @return {Map<string, ParsedQualityValues>}
1907
- */
1908
- function parseQualityValues(input) {
1909
- if (!input || !input.trim()) {
1910
- return new Map();
1911
- }
1912
- const tupleArray = input
1913
- .split(',')
1914
- .map((values) => {
1915
- const [value, ...specifiers] = values.split(';');
1916
- return /** @type {[string, ParsedQualityValues]} */ ([
1917
- value.trim(),
1918
- {
1919
- ...Object.assign({}, ...specifiers.map((pair) => {
1920
- const [specifier, sValue] = pair.split('=');
1921
- const trimmedSpec = specifier?.trim();
1922
- const trimmedSValue = sValue?.trim();
1923
- if (trimmedSpec === 'q') {
1924
- const parsedQ = parseFloat(trimmedSValue);
1925
- return { q: Number.isNaN(parsedQ) ? 1 : parsedQ };
1926
- }
1927
- return { [trimmedSpec]: trimmedSValue };
1928
- })),
1929
- },
1930
- ]);
1931
- }).sort((a, b) => (b?.[1]?.q ?? 1) - (a?.[1]?.q ?? 1));
1932
- return new Map(tupleArray);
1933
- }
1934
-
1935
- /** @typedef {import('../types').IMiddleware} IMiddleware */
1936
- /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
1937
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
1938
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
1939
-
1940
- /** @typedef {'br'|'gzip'|'deflate'|'identity'|'*'} COMPATIBLE_ENCODING */
1941
-
1942
- const DEFAULT_MINIMUM_SIZE = 256;
1943
-
1944
- /**
1945
- * @typedef ContentEncoderMiddlewareOptions
1946
- * @prop {number} [chunkSize]
1947
- * @prop {boolean} [respondNotAcceptable=false]
1948
- * @prop {'br'|'gzip'|'deflate'|'identity'} [preferredEncoding='identity']
1949
- * @prop {number} [minimumSize=DEFAULT_MINIMUM_SIZE]
1950
- */
1951
-
1952
- /** @type {COMPATIBLE_ENCODING[]} */
1953
- const COMPATIBLE_ENCODINGS = ['br', 'gzip', 'deflate', 'identity', '*'];
1954
-
1955
- /** @implements {IMiddleware} */
1956
- class ContentEncoderMiddleware {
1957
- /** @param {ContentEncoderMiddlewareOptions} [options] */
1958
- constructor(options = {}) {
1959
- this.chunkSize = options.chunkSize;
1960
- this.respondNotAcceptable = options.respondNotAcceptable === true;
1961
- this.preferredEncoding = options.preferredEncoding ?? 'identity';
1962
- this.minimumSize = options.minimumSize ?? DEFAULT_MINIMUM_SIZE;
1963
- }
1964
-
1965
- /**
1966
- * @param {import('../types/index.js').HttpRequest} req
1967
- * @throws {NotAcceptableException} Error with `NOT_ACCEPTIBLE` message
1968
- * @return {COMPATIBLE_ENCODING}
1969
- */
1970
- static chooseEncoding(req) {
1971
- /**
1972
- * A request without an Accept-Encoding header field implies that the
1973
- * user agent has no preferences regarding content-codings. Although
1974
- * this allows the server to use any content-coding in a response, it
1975
- * does not imply that the user agent will be able to correctly process
1976
- * all encodings.
1977
- */
1978
- if ('accept-encoding' in req.headers === false) {
1979
- return '*';
1980
- }
1981
-
1982
- /** @type {string} */
1983
- const acceptString = (req.headers['accept-encoding']);
1984
- const encodings = parseQualityValues(acceptString?.toLowerCase());
1985
- if (!encodings.size) {
1986
- /**
1987
- * An Accept-Encoding header field with a combined field-value that is
1988
- * empty implies that the user agent does not want any content-coding in
1989
- * response.
1990
- */
1991
- return 'identity';
1992
- }
1993
- let encoding = COMPATIBLE_ENCODINGS[0];
1994
- const allowWildcards = (encodings.get('*')?.q !== 0);
1995
- const encodingEntries = [...encodings.entries()];
1996
- // @ts-ignore Cannot cast to COMPATIBLE_ENCODINGS
1997
- encoding = (encodingEntries.find(([value, spec]) => spec.q !== 0 && COMPATIBLE_ENCODINGS.includes(value))?.[0]);
1998
- if (allowWildcards && (encoding === '*' || !encoding)) {
1999
- // Server preference
2000
- // Get first compatible encoding not specified
2001
- encoding = COMPATIBLE_ENCODINGS.find((value) => !encodings.has(value));
2002
- }
2003
- if (allowWildcards && !encoding) {
2004
- // Get highest q'd compatible encoding not q=0 or '*'
2005
- // @ts-ignore Cannot cast to COMPATIBLE_ENCODINGS
2006
- encoding = encodingEntries
2007
- // @ts-ignore Cannot cast to COMPATIBLE_ENCODINGS
2008
- .find(([value, spec]) => spec.q !== 0 && value !== '*' && COMPATIBLE_ENCODINGS.includes(value))?.[0];
2009
- }
2010
- if (!encoding) {
2011
- throw new Error('NOT_ACCEPTABLE');
2012
- }
2013
- return encoding;
2014
- }
2015
-
2016
- /**
2017
- * Implements `Accept-Encoding`
2018
- * https://tools.ietf.org/html/rfc7231#section-5.3.4
2019
- * @param {MiddlewareFunctionParams} params
2020
- * @return {MiddlewareFunctionResult}
2021
- */
2022
- execute({ req, res }) {
2023
- if (req.method === 'HEAD') {
2024
- // Never needs content-encoding
2025
- return 'continue';
2026
- }
2027
-
2028
- /** @type {COMPATIBLE_ENCODING} */
2029
- let parsedEncoding;
2030
- if (this.respondNotAcceptable) {
2031
- // Parse now to catch the error;
2032
- try {
2033
- parsedEncoding = ContentEncoderMiddleware.chooseEncoding(req);
2034
- } catch (error) {
2035
- if (error?.message === 'NOT_ACCEPTABLE') {
2036
- res.status = 406;
2037
- return 'end';
2038
- }
2039
- // Unknown error
2040
- throw error;
2041
- }
2042
- }
2043
-
2044
- /** @return {string} */
2045
- const getContentEncoding = () => {
2046
- if (!parsedEncoding) {
2047
- try {
2048
- parsedEncoding = ContentEncoderMiddleware.chooseEncoding(req);
2049
- } catch (error) {
2050
- if (error?.message !== 'NOT_ACCEPTABLE') {
2051
- throw error;
2052
- }
2053
- }
2054
- }
2055
- if (!parsedEncoding || parsedEncoding === '*') {
2056
- parsedEncoding = this.preferredEncoding || 'identity';
2057
- }
2058
- res.headers['content-encoding'] = parsedEncoding;
2059
- return parsedEncoding;
2060
- };
2061
-
2062
- let finalCalled = false;
2063
- let transformCount = 0;
2064
- let inputLength = 0;
2065
- const newStream = new stream.Transform({
2066
- transform(chunk, encoding, callback) {
2067
- transformCount += 1;
2068
- inputLength += chunk.length;
2069
- // Stall to see if more chunks are in transit
2070
- process.nextTick(() => {
2071
- this.push(chunk);
2072
- });
2073
- callback();
2074
- },
2075
- final(callback) {
2076
- finalCalled = true;
2077
- callback();
2078
- },
2079
- });
2080
- const destination = res.replaceStream(newStream);
2081
-
2082
- /**
2083
- * @param {'br'|'gzip'|'deflate'} encoding
2084
- * @return {import("zlib").Gzip}
2085
- */
2086
- const buildGzipStream = (encoding) => {
2087
- /** @type {import("zlib").Gzip} */
2088
- let gzipStream;
2089
- switch (encoding) {
2090
- case 'deflate':
2091
- gzipStream = zlib.createDeflate({ chunkSize: this.chunkSize });
2092
- break;
2093
- case 'gzip':
2094
- gzipStream = zlib.createGzip({ chunkSize: this.chunkSize });
2095
- break;
2096
- case 'br':
2097
- gzipStream = zlib.createBrotliCompress({ chunkSize: this.chunkSize });
2098
- break;
2099
- default:
2100
- throw new Error('UNKNOWN_ENCODING');
2101
- }
2102
-
2103
- /** @type {Buffer[]} */
2104
- const pendingChunks = [];
2105
-
2106
- gzipStream.on('data', (chunk) => {
2107
- if (finalCalled) {
2108
- pendingChunks.push(chunk);
2109
- } else {
2110
- let previousChunk;
2111
- // eslint-disable-next-line no-cond-assign
2112
- while (previousChunk = pendingChunks.shift()) {
2113
- destination.write(previousChunk);
2114
- }
2115
- destination.write(chunk);
2116
- }
2117
- });
2118
- gzipStream.on('end', () => {
2119
- let chunk;
2120
- // eslint-disable-next-line no-cond-assign
2121
- while (chunk = pendingChunks.shift()) {
2122
- destination.write(chunk);
2123
- }
2124
- destination.end();
2125
- });
2126
-
2127
- return gzipStream;
2128
- };
2129
-
2130
- // Don't do any work until first chunk is received (if at all).
2131
- // This allows middleware to set `Content-Encoding` manually,
2132
- // prevents allocation memory for a gzip stream unnecessarily, and
2133
- // prevents polluting 204 responses.
2134
-
2135
- const onEnd = () => destination.end();
2136
- newStream.once('data', (chunk) => {
2137
- // Will be handled by .pipe() or .end() call
2138
- newStream.off('end', onEnd);
2139
-
2140
- /** @type {string} */
2141
- let encoding = (res.headers['content-encoding']);
2142
- if (encoding == null) {
2143
- // Only continue if unset. Blank is still considered set.
2144
- // This allows forced encoding (eg: use gzip regardless of size; always identity)
2145
- if (inputLength > (this.minimumSize ?? DEFAULT_MINIMUM_SIZE) || transformCount > 1) {
2146
- // If we're getting data in chunks, assume larger than minimum
2147
- encoding = getContentEncoding().toLowerCase?.();
2148
- } else {
2149
- encoding = 'identity';
2150
- }
2151
- }
2152
-
2153
- let next;
2154
- switch (encoding) {
2155
- case 'br':
2156
- case 'gzip':
2157
- case 'deflate':
2158
- next = buildGzipStream(encoding);
2159
- break;
2160
- default:
2161
- next = destination;
2162
- }
2163
- next.write(chunk);
2164
- newStream.pipe(next);
2165
- });
2166
-
2167
- // In case no data is passed
2168
- newStream.on('end', onEnd);
2169
-
2170
- return 'continue';
2171
- }
2172
- }
2173
-
2174
- /** @typedef {import('../types').IMiddleware} IMiddleware */
2175
- /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
2176
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
2177
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
2178
-
2179
- /**
2180
- * @typedef {Object} ContentLengthMiddlewareOptions
2181
- * @prop {boolean} [delayCycle=true]
2182
- * Delays writing to stream by one I/O cycle.
2183
- * If `.end()` is called on the same event loop as write, then the
2184
- * content length can be still calculated despite receiving data in chunks.
2185
- * Compared to no delay, chunks are held in memory for two event loops instead
2186
- * of just one.
2187
- * @prop {boolean} [overrideHeader=false]
2188
- * Always replace `Content-Length` header
2189
- */
2190
-
2191
- /** @implements {IMiddleware} */
2192
- class ContentLengthMiddleware {
2193
- /** @param {ContentLengthMiddlewareOptions} [options] */
2194
- constructor(options = {}) {
2195
- this.delayCycle = options.delayCycle !== false;
2196
- this.overrideHeader = options.overrideHeader !== true;
2197
- }
2198
-
2199
- /**
2200
- * @param {MiddlewareFunctionParams} params
2201
- * @return {MiddlewareFunctionResult}
2202
- */
2203
- execute({ req, res }) {
2204
- if (req.method === 'HEAD') {
2205
- return 'continue';
2206
- }
2207
-
2208
- let length = 0;
2209
- /** @type {Buffer[]} */
2210
- const pendingChunks = [];
2211
- let delayPending = false;
2212
- const { delayCycle, overrideHeader } = this;
2213
- const newWritable = new stream.Transform({
2214
- transform(chunk, encoding, callback) {
2215
- length += chunk.length;
2216
- if (delayCycle === false) {
2217
- callback(null, chunk);
2218
- return;
2219
- }
2220
-
2221
- pendingChunks.push(chunk);
2222
- if (!delayPending) {
2223
- delayPending = true;
2224
- process.nextTick(() => setImmediate(() => {
2225
- delayPending = false;
2226
- pendingChunks.splice(0, pendingChunks.length)
2227
- .forEach((buffer) => this.push(buffer));
2228
- }));
2229
- }
2230
- callback();
2231
- },
2232
- flush(callback) {
2233
- if (!res.headersSent) {
2234
- /**
2235
- * Any response message which "MUST NOT" include a message-body
2236
- * (such as the 1xx, 204, and 304 responses and any response to a HEAD request)
2237
- * is always terminated by the first empty line after the header fields,
2238
- * regardless of the entity-header fields present in the message.
2239
- * https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
2240
- */
2241
- if ((res.status >= 100 && res.status < 200) || res.status === 204 || res.status === 304) {
2242
- if (overrideHeader) {
2243
- delete res.headers['content-length'];
2244
- }
2245
- } else if (overrideHeader === true || res.headers['content-length'] == null) {
2246
- res.headers['content-length'] = length;
2247
- }
2248
- }
2249
- pendingChunks.splice(0, pendingChunks.length)
2250
- .forEach((buffer) => this.push(buffer));
2251
- callback();
2252
- },
2253
- });
2254
-
2255
- const destination = res.replaceStream(newWritable);
2256
- newWritable.pipe(destination);
2257
-
2258
- return 'continue';
2259
- }
2260
- }
2261
-
2262
- /** @typedef {import('stream').Readable} Readable */
2263
- /** @typedef {import('../types').IMiddleware} IMiddleware */
2264
- /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
2265
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
2266
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
2267
-
2268
- /**
2269
- * @typedef {Object} ContentReaderMiddlewareOptions
2270
- * @prop {string} [defaultMediaType]
2271
- * Assumed mediatype if not specified
2272
- * @prop {boolean} [parseJSON=false]
2273
- * Automatically parses JSON if `application/json` mediatype
2274
- * @prop {'entries'|'object'|'string'|'none'} [formURLEncodedFormat='none']
2275
- * Automatically converts to object if `application/x-www-form-urlencoded` mediatype
2276
- * @prop {boolean} [buildString=false]
2277
- * Automatically builds string into one `read()` response
2278
- * @prop {boolean|string} [cache=false]
2279
- * Caches content in req.local.content or req.local[cacheName]
2280
- */
2281
-
2282
- /** @implements {IMiddleware} */
2283
- class ContentReaderMiddleware {
2284
- /** @param {ContentReaderMiddlewareOptions} [options] */
2285
- constructor(options = {}) {
2286
- this.defaultMediaType = options.defaultMediaType;
2287
- this.parseJSON = options.parseJSON === true;
2288
- this.formURLEncodedFormat = options.formURLEncodedFormat || 'none';
2289
- this.buildString = options.buildString === true;
2290
- this.cache = options.cache;
2291
- }
2292
-
2293
- /**
2294
- * @param {string} charset
2295
- * @return {BufferEncoding}
2296
- */
2297
- static charsetAsBufferEncoding(charset) {
2298
- switch (charset) {
2299
- case 'iso-8859-1':
2300
- case 'ascii':
2301
- case 'binary':
2302
- case 'latin1':
2303
- return 'latin1';
2304
- case 'utf-16le':
2305
- case 'ucs-2':
2306
- case 'ucs2':
2307
- case 'utf16le':
2308
- return 'utf16le';
2309
- default:
2310
- case 'utf-8':
2311
- case 'utf8':
2312
- return 'utf-8';
2313
- case 'base64':
2314
- case 'hex':
2315
- return /** @type {BufferEncoding} */ (charset);
2316
- }
2317
- }
2318
-
2319
- /**
2320
- * The application/x-www-form-urlencoded format is in many ways an aberrant monstrosity,
2321
- * the result of many years of implementation accidents and compromises leading to a set of
2322
- * requirements necessary for interoperability, but in no way representing good design practices.
2323
- * In particular, readers are cautioned to pay close attention to the twisted details
2324
- * involving repeated (and in some cases nested) conversions between character encodings and byte sequences.
2325
- * Unfortunately the format is in widespread use due to the prevalence of HTML forms. [HTML]
2326
- * @param {Buffer} buffer
2327
- * @param {string} charset
2328
- * @return {[string, string][]} Tuple
2329
- */
2330
- static readUrlEncoded(buffer, charset) {
2331
- // https://url.spec.whatwg.org/#urlencoded-parsing
2332
- const bufferEncoding = ContentReaderMiddleware.charsetAsBufferEncoding(charset);
2333
-
2334
- const sequences = [];
2335
- let startIndex = 0;
2336
- for (let i = 0; i < buffer.length; i += 1) {
2337
- if (buffer[i] === 0x26) {
2338
- sequences.push(buffer.subarray(startIndex, i));
2339
- startIndex = i + 1;
2340
- }
2341
- if (i === buffer.length - 1) {
2342
- sequences.push(buffer.subarray(startIndex, i + 1));
2343
- break;
2344
- }
2345
- }
2346
- /** @type {[string, string][]} */
2347
- const output = [];
2348
- sequences.forEach((bytes) => {
2349
- if (!bytes.length) return;
2350
-
2351
- // Find 0x3D and replace 0x2B in one loop for better performance
2352
- let indexOf0x3D = -1;
2353
- for (let i = 0; i < bytes.length; i += 1) {
2354
- switch (bytes[i]) {
2355
- case 0x3D:
2356
- if (indexOf0x3D === -1) {
2357
- indexOf0x3D = i;
2358
- }
2359
- break;
2360
- case 0x2B:
2361
- // Replace bytes on original stream for memory conservation
2362
- // eslint-disable-next-line no-param-reassign
2363
- bytes[i] = 0x20;
2364
- break;
2365
- }
2366
- }
2367
- let name;
2368
- let value;
2369
- if (indexOf0x3D === -1) {
2370
- name = bytes;
2371
- value = bytes.subarray(bytes.length, 0);
2372
- } else {
2373
- name = bytes.subarray(0, indexOf0x3D);
2374
- value = bytes.subarray(indexOf0x3D + 1);
2375
- }
2376
- const nameString = decodeURIComponent(name.toString(bufferEncoding));
2377
- const valueString = decodeURIComponent(value.toString(bufferEncoding));
2378
- output.push([nameString, valueString]);
2379
- });
2380
- return output;
2381
- }
2382
-
2383
- /**
2384
- * @param {MiddlewareFunctionParams} params
2385
- * @return {MiddlewareFunctionResult}
2386
- */
2387
- execute({ req }) {
2388
- switch (req.method) {
2389
- case 'HEAD':
2390
- case 'GET':
2391
- return 'continue';
2392
- }
2393
-
2394
- const contentType = (req.headers['content-type']);
2395
- /** @type {string} */
2396
- let mediaType;
2397
- /** @type {string} */
2398
- let charset;
2399
- if (contentType) {
2400
- contentType.split(';').forEach((directive) => {
2401
- const parameters = directive.split('=');
2402
- if (parameters.length === 1) {
2403
- mediaType = directive.trim().toLowerCase();
2404
- return;
2405
- }
2406
- if (parameters[0].trim().toLowerCase() !== 'charset') {
2407
- return;
2408
- }
2409
- charset = parameters[1]?.trim().toLowerCase();
2410
- const firstQuote = charset.indexOf('"');
2411
- const lastQuote = charset.lastIndexOf('"');
2412
- if (firstQuote !== -1 && lastQuote !== -1) {
2413
- charset = charset.substring(firstQuote + 1, lastQuote);
2414
- }
2415
- });
2416
- }
2417
-
2418
- if (!mediaType) {
2419
- mediaType = this.defaultMediaType;
2420
- }
2421
- const isFormUrlEncoded = mediaType === 'application/x-www-form-urlencoded';
2422
- const isJSON = /application\/(.+\+)?json/i.test(mediaType);
2423
- if (!charset) {
2424
- if (!mediaType) {
2425
- return 'continue';
2426
- }
2427
- if (isFormUrlEncoded && (this.formURLEncodedFormat || 'none') === 'none') {
2428
- return 'continue';
2429
- }
2430
- if (!isFormUrlEncoded && !isJSON && !mediaType.startsWith('text/')) {
2431
- return 'continue';
2432
- }
2433
- }
2434
-
2435
- const readAll = isJSON || isFormUrlEncoded;
2436
-
2437
- let fullString = '';
2438
- /** @type {Buffer[]} */
2439
- const pendingChunks = [];
2440
- const source = req.stream;
2441
- const {
2442
- buildString, formURLEncodedFormat, cache, parseJSON,
2443
- } = this;
2444
- const newReadable = new stream.Transform({
2445
- objectMode: true,
2446
- read(...args) {
2447
- if (source.isPaused()) source.resume();
2448
- // eslint-disable-next-line no-underscore-dangle
2449
- stream.Transform.prototype._read.call(this, ...args);
2450
- },
2451
- transform(chunk, encoding, callback) {
2452
- if (typeof chunk === 'string') {
2453
- if (readAll || buildString) {
2454
- fullString += chunk;
2455
- } else {
2456
- this.push(chunk);
2457
- }
2458
- } else if (isFormUrlEncoded) {
2459
- pendingChunks.push(chunk);
2460
- } else {
2461
- this.push(chunk);
2462
- }
2463
- callback();
2464
- },
2465
- flush(callback) {
2466
- let result = null;
2467
- if (isFormUrlEncoded) {
2468
- const combinedBuffer = Buffer.concat(pendingChunks);
2469
- if (formURLEncodedFormat === 'object') {
2470
- result = Object.fromEntries(ContentReaderMiddleware.readUrlEncoded(combinedBuffer, charset));
2471
- } else if (formURLEncodedFormat === 'string') {
2472
- result = combinedBuffer.toString(ContentReaderMiddleware.charsetAsBufferEncoding(charset));
2473
- } else {
2474
- result = ContentReaderMiddleware.readUrlEncoded(combinedBuffer, charset);
2475
- }
2476
- } else if (isJSON && parseJSON) {
2477
- try {
2478
- result = JSON.parse(fullString);
2479
- } catch {
2480
- result = fullString;
2481
- }
2482
- } else if (fullString) {
2483
- result = fullString;
2484
- }
2485
- if (cache && result) {
2486
- const cacheName = cache === true ? 'content' : cache;
2487
- req.locals[cacheName] = result;
2488
- }
2489
- callback(null, result);
2490
- },
2491
- });
2492
- req.replaceStream(newReadable);
2493
- if (!isFormUrlEncoded) {
2494
- // Data read from source will be decoded as a string
2495
- const encoding = ContentReaderMiddleware.charsetAsBufferEncoding(charset);
2496
- const stringDecoder = new stream.PassThrough({ encoding });
2497
- newReadable.setDefaultEncoding(encoding);
2498
- source.pipe(stringDecoder).pipe(newReadable);
2499
- } else {
2500
- source.pipe(newReadable);
2501
- }
2502
- source.pause();
2503
-
2504
- return 'continue';
2505
- }
2506
- }
2507
-
2508
- /** @typedef {import('../types').IMiddleware} IMiddleware */
2509
- /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
2510
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
2511
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
2512
-
2513
- /**
2514
- * @typedef {Object} ContentWriterMiddlewareOptions
2515
- * @prop {string} [defaultCharset='utf-8']
2516
- * @prop {boolean} [setCharset=false]
2517
- * Automatically applies charset in `Content-Type`
2518
- * @prop {boolean} [setJSON=false]
2519
- * Automatically applies `application/json` mediatype in `Content-Type`
2520
- * @prop {boolean|string} [cache=false]
2521
- * Caches content in res.local.content or res.local[cacheName]
2522
- */
2523
-
2524
- /** @implements {IMiddleware} */
2525
- class ContentWriterMiddleware {
2526
- /** @param {ContentWriterMiddlewareOptions} [options] */
2527
- constructor(options = {}) {
2528
- this.defaultCharset = options.defaultCharset || 'utf-8';
2529
- this.setCharset = options.setCharset === true;
2530
- this.setJSON = options.setJSON === true;
2531
- this.cache = options.cache;
2532
- }
2533
-
2534
- /**
2535
- * @param {string} charset
2536
- * @return {BufferEncoding}
2537
- */
2538
- static charsetAsBufferEncoding(charset) {
2539
- switch (charset) {
2540
- case 'iso-8859-1':
2541
- case 'ascii':
2542
- case 'binary':
2543
- case 'latin1':
2544
- return 'latin1';
2545
- case 'utf-16le':
2546
- case 'ucs-2':
2547
- case 'ucs2':
2548
- case 'utf16le':
2549
- return 'utf16le';
2550
- default:
2551
- case 'utf-8':
2552
- case 'utf8':
2553
- return 'utf-8';
2554
- case 'base64':
2555
- case 'hex':
2556
- return /** @type {BufferEncoding} */ (charset);
2557
- }
2558
- }
2559
-
2560
- /**
2561
- * @param {MiddlewareFunctionParams} params
2562
- * @return {MiddlewareFunctionResult}
2563
- */
2564
- execute({ req, res }) {
2565
- if (req.method === 'HEAD') {
2566
- return 'continue';
2567
- }
2568
-
2569
- /** @type {string} */
2570
- let charset = null;
2571
- /** @type {BufferEncoding} */
2572
- let encoding = null;
2573
- let hasSetJSON = false;
2574
-
2575
- /** @return {string} */
2576
- const parseCharset = () => {
2577
- if (charset) return charset;
2578
- /** @type {string} */
2579
- const contentType = (res.headers['content-type']);
2580
- if (contentType) {
2581
- contentType.split(';').some((directive) => {
2582
- const parameters = directive.split('=');
2583
- if (parameters[0].trim().toLowerCase() !== 'charset') {
2584
- return false;
2585
- }
2586
- charset = parameters[1]?.trim().toLowerCase();
2587
- const firstQuote = charset.indexOf('"');
2588
- const lastQuote = charset.lastIndexOf('"');
2589
- if (firstQuote !== -1 && lastQuote !== -1) {
2590
- charset = charset.substring(firstQuote + 1, lastQuote);
2591
- }
2592
- return true;
2593
- });
2594
- }
2595
- if (!charset) {
2596
- charset = this.defaultCharset || 'utf-8';
2597
- if (this.setCharset && !res.headersSent) {
2598
- res.headers['content-type'] = `${contentType || ''};charset=${charset}`;
2599
- }
2600
- }
2601
- return charset;
2602
- };
2603
-
2604
- /** @return {void} */
2605
- const setJSONMediaType = () => {
2606
- if (hasSetJSON) return;
2607
- /** @type {string} */
2608
- const contentType = (res.headers['content-type']);
2609
- res.headers['content-type'] = (contentType || '')
2610
- .split(';')
2611
- .map((directive) => {
2612
- const isKeyPair = directive.includes('=');
2613
- if (isKeyPair) return directive;
2614
- return 'application/json';
2615
- })
2616
- .join(';');
2617
-
2618
- hasSetJSON = true;
2619
- };
2620
-
2621
- const newWritable = new stream.Transform({
2622
- writableObjectMode: true,
2623
- transform: (chunk, e, callback) => {
2624
- if (Buffer.isBuffer(chunk)) {
2625
- callback(null, chunk);
2626
- return;
2627
- }
2628
- const cacheName = this.cache && (this.cache === true ? 'content' : this.cache);
2629
- if (typeof chunk === 'string') {
2630
- if (!encoding) {
2631
- encoding = ContentWriterMiddleware.charsetAsBufferEncoding(parseCharset());
2632
- }
2633
- if (cacheName) {
2634
- if (typeof res.locals[cacheName] === 'string') {
2635
- res.locals[cacheName] += chunk;
2636
- } else {
2637
- res.locals[cacheName] = chunk;
2638
- }
2639
- }
2640
- const callbackData = Buffer.from(chunk, encoding);
2641
- callback(null, callbackData);
2642
- return;
2643
- }
2644
- if (cacheName) {
2645
- res.locals[cacheName] = chunk;
2646
- }
2647
- if (typeof chunk === 'object') {
2648
- if (!encoding) {
2649
- encoding = ContentWriterMiddleware.charsetAsBufferEncoding(parseCharset());
2650
- }
2651
- if (this.setJSON && !hasSetJSON && !res.headersSent) {
2652
- setJSONMediaType();
2653
- }
2654
- callback(null, Buffer.from(JSON.stringify(chunk), encoding));
2655
- return;
2656
- }
2657
-
2658
- callback(null, chunk);
2659
- },
2660
- });
2661
- const destination = res.replaceStream(newWritable);
2662
- newWritable.pipe(destination);
2663
-
2664
- return 'continue';
2665
- }
2666
- }
2667
-
2668
- /** @typedef {import('../types').IMiddleware} IMiddleware */
2669
- /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
2670
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
2671
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
2672
- /** @typedef {import('../types').RequestMethod} RequestMethod */
2673
-
2674
- /**
2675
- * @typedef CORSMiddlewareOptions
2676
- * @prop {(string|RegExp)[]} [allowOrigin]
2677
- * Indicates whether the response can be shared, via returning the literal
2678
- * value of the `Origin` request header (which can be `null`) or `*` in a response.
2679
- * @prop {boolean} [allowCredentials]
2680
- * Indicates whether the response can be shared when request’s credentials mode is "include".
2681
- * @prop {RequestMethod[]} [allowMethods]
2682
- * Indicates which methods are supported by the response’s URL for the purposes of the CORS protocol.
2683
- * @prop {string[]} [allowHeaders]
2684
- * Indicates which headers are supported by the response’s URL for the purposes of the CORS protocol.
2685
- * @prop {number} [maxAge]
2686
- * Indicates the number of seconds (5 by default) the information provided by the
2687
- * `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` headers can be cached.
2688
- * @prop {string[]} [exposeHeaders]
2689
- * Indicates which headers can be exposed as part of the response by listing their names.
2690
- */
2691
-
2692
- /** @implements {IMiddleware} */
2693
- class CORSMiddleware {
2694
- /** @param {CORSMiddlewareOptions} [options] */
2695
- constructor(options = {}) {
2696
- this.allowOrigin = options.allowOrigin;
2697
- this.allowCredentials = options.allowCredentials === true;
2698
- this.allowMethods = options.allowMethods;
2699
- this.allowHeaders = options.allowHeaders;
2700
- this.maxAge = options.maxAge ?? 5;
2701
- this.exposeHeaders = options.exposeHeaders;
2702
- }
2703
-
2704
- /**
2705
- * @param {MiddlewareFunctionParams} params
2706
- * @return {MiddlewareFunctionResult}
2707
- */
2708
- execute({ req, res }) {
2709
- if (('origin' in req.headers) === false) {
2710
- // not CORS
2711
- return 'continue';
2712
- }
2713
- if (!this.allowOrigin) {
2714
- // Unspecified default of '*'
2715
- res.headers['access-control-allow-origin'] = '*';
2716
- } else {
2717
- this.allowOrigin.some((origin) => {
2718
- if (origin === '*') {
2719
- res.headers['access-control-allow-origin'] = '*';
2720
- return true;
2721
- }
2722
- if (typeof origin === 'string') {
2723
- if (req.headers.origin?.toLowerCase() === origin.toLowerCase()) {
2724
- res.headers['access-control-allow-origin'] = req.headers.origin;
2725
- return true;
2726
- }
2727
- return false;
2728
- }
2729
- if (origin.test(req.headers.origin)) {
2730
- res.headers['access-control-allow-origin'] = req.headers.origin;
2731
- return true;
2732
- }
2733
- return false;
2734
- });
2735
- }
2736
- if (this.allowCredentials) {
2737
- res.headers['access-control-allow-credentials'] = 'true';
2738
- }
2739
- if (req.method === 'OPTIONS') {
2740
- if (this.allowMethods) {
2741
- res.headers['access-control-allow-methods'] = this.allowMethods.join(',');
2742
- } else {
2743
- res.headers['access-control-allow-methods'] = [
2744
- 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'TRACE', 'PATCH',
2745
- ].join(',');
2746
- }
2747
- if (this.allowHeaders) {
2748
- res.headers['access-control-allow-headers'] = this.allowHeaders.join(',');
2749
- } else {
2750
- res.headers['access-control-allow-headers'] = req.headers['access-control-request-headers'];
2751
- }
2752
- if (this.maxAge != null) {
2753
- res.headers['access-control-max-age'] = this.maxAge.toString(10);
2754
- }
2755
- // 200 instead of 204 for compatibility
2756
- res.status = 200;
2757
- res.stream.end('OK');
2758
- return 'end';
2759
- }
2760
-
2761
- if (this.exposeHeaders) {
2762
- res.headers['access-control-expose-headers'] = this.exposeHeaders.join(',');
2763
- }
2764
- return 'continue';
2765
- }
2766
- }
2767
-
2768
- /** @typedef {import('../types').IMiddleware} IMiddleware */
2769
- /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
2770
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
2771
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
2772
-
2773
- const DEFAULT_ALGORITHM = 'sha1';
2774
- /** @type {crypto.HexBase64Latin1Encoding} */
2775
- const DEFAULT_DIGEST = 'base64';
2776
-
2777
- /**
2778
- * @typedef {Object} HashMiddlewareOptions
2779
- * @prop {'md5'|'sha1'|'sha256'|'sha512'} [algorithm=DEFAULT_ALGORITHM]
2780
- * @prop {crypto.HexBase64Latin1Encoding} [digest=DEFAULT_DIGEST]
2781
- */
2782
-
2783
- /** @implements {IMiddleware} */
2784
- class HashMiddleware {
2785
- /** @param {HashMiddlewareOptions} options */
2786
- constructor(options = {}) {
2787
- this.algorithm = options.algorithm || DEFAULT_ALGORITHM;
2788
- this.digest = options.digest || DEFAULT_DIGEST;
2789
- }
2790
-
2791
- /**
2792
- * @param {MiddlewareFunctionParams} params
2793
- * @return {MiddlewareFunctionResult}
2794
- */
2795
- execute({ res }) {
2796
- const { algorithm, digest } = this;
2797
- let hasData = false;
2798
- let length = 0;
2799
- let abort = false;
2800
- const hashStream = crypto.createHash(algorithm);
2801
- const newWritable = new stream.Transform({
2802
- transform(chunk, encoding, callback) {
2803
- length += chunk.length;
2804
- hasData = true;
2805
- if (!abort && res.headersSent) {
2806
- abort = true;
2807
- hashStream.destroy();
2808
- }
2809
- if (abort) {
2810
- callback(null, chunk);
2811
- return;
2812
- }
2813
- // Manually pipe
2814
- const needsDrain = !hashStream.write(chunk);
2815
- if (needsDrain) {
2816
- hashStream.once('drain', () => {
2817
- callback(null, chunk);
2818
- });
2819
- } else {
2820
- callback(null, chunk);
2821
- }
2822
- },
2823
- flush(callback) {
2824
- if (!abort && hasData && res.status !== 206 && !res.headersSent) {
2825
- const hash = hashStream.digest(digest);
2826
- // https://tools.ietf.org/html/rfc7232#section-2.3
2827
- if (res.headers.etag == null) {
2828
- res.headers.etag = `${algorithm === 'md5' ? 'W/' : ''}"${length.toString(16)}-${hash}"`;
2829
- }
2830
- if (digest === 'base64') {
2831
- res.headers.digest = `${algorithm}=${hash}`;
2832
- if ((algorithm === 'md5')) {
2833
- res.headers['content-md5'] = hash;
2834
- }
2835
- }
2836
- }
2837
- callback();
2838
- },
2839
- });
2840
-
2841
- const destination = res.replaceStream(newWritable);
2842
- newWritable.pipe(destination);
2843
- return 'continue';
2844
- }
2845
- }
2846
-
2847
- /** @typedef {import('../lib').HttpRequest} HttpRequest */
2848
- /** @typedef {import('../types').IMiddleware} IMiddleware */
2849
- /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
2850
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
2851
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
2852
- /** @typedef {import('../types').RequestMethod} RequestMethod */
2853
-
2854
- /** @typedef {RegExp|RequestMethod} MethodEntry */
2855
-
2856
- /**
2857
- * @typedef {Object} MethodMiddlewareOptions
2858
- * @prop {MethodEntry|MethodEntry[]} method
2859
- */
2860
-
2861
- /** @implements {IMiddleware} */
2862
- class MethodMiddleware {
2863
- /** @param {MethodMiddlewareOptions|MethodEntry|MethodEntry[]} options */
2864
- constructor(options) {
2865
- if (Array.isArray(options)) {
2866
- this.method = options;
2867
- } else if (typeof options === 'string' || options instanceof RegExp) {
2868
- this.method = [options];
2869
- } else {
2870
- this.method = Array.isArray(options.method) ? options.method : [options.method];
2871
- }
2872
- }
2873
-
2874
- /** @type {Map<RequestMethod, MethodMiddleware>} */
2875
- static cache = new Map();
2876
-
2877
- /**
2878
- * @param {RequestMethod} name
2879
- * @return {MethodMiddleware}
2880
- */
2881
- static byMethod(name) {
2882
- let m = MethodMiddleware.cache.get(name);
2883
- if (m) return m;
2884
- m = new MethodMiddleware(name);
2885
- MethodMiddleware.cache.set(name, m);
2886
- return m;
2887
- }
2888
-
2889
- /**
2890
- * @param {RequestMethod} method
2891
- * @param {RegExp | string} input
2892
- * @return {boolean}
2893
- */
2894
- static test(method, input) {
2895
- if (typeof input === 'string') {
2896
- return method === input;
2897
- }
2898
- return input.test(method) === true;
2899
- }
2900
-
2901
- /**
2902
- * @param {MiddlewareFunctionParams} params
2903
- * @return {MiddlewareFunctionResult}
2904
- */
2905
- execute({ req }) {
2906
- for (let i = 0; i < this.method.length; i++) {
2907
- if (MethodMiddleware.test(req.method, this.method[i])) {
2908
- return 'continue';
2909
- }
2910
- }
2911
- return 'break';
2912
- }
2913
-
2914
- static get CONNECT() { return MethodMiddleware.byMethod('CONNECT'); }
2915
-
2916
- static get DELETE() { return MethodMiddleware.byMethod('DELETE'); }
2917
-
2918
- static get GET() { return MethodMiddleware.byMethod('GET'); }
2919
-
2920
- static get OPTIONS() { return MethodMiddleware.byMethod('OPTIONS'); }
2921
-
2922
- static get HEAD() { return MethodMiddleware.byMethod('HEAD'); }
2923
-
2924
- static get PATCH() { return MethodMiddleware.byMethod('PATCH'); }
2925
-
2926
- static get POST() { return MethodMiddleware.byMethod('POST'); }
2927
-
2928
- static get PUT() { return MethodMiddleware.byMethod('PUT'); }
2929
-
2930
- static get TRACE() { return MethodMiddleware.byMethod('TRACE'); }
2931
- }
2932
-
2933
- /** @typedef {import('../types').HttpRequest} HttpRequest */
2934
- /** @typedef {import('../types').IMiddleware} IMiddleware */
2935
- /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
2936
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
2937
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
2938
- /** @typedef {import('../types').RequestMethod} RequestMethod */
2939
-
2940
- /** @typedef {RegExp|string} PathEntry */
2941
-
2942
- /**
2943
- * @typedef {Object} PathHistoryEntry hello?
2944
- * @prop {string} base
2945
- * @prop {number[]} treeIndex
2946
- */
2947
-
2948
- /**
2949
- * @typedef {Object} PathState
2950
- * @prop {PathHistoryEntry[]} history
2951
- * @prop {string} currentPath
2952
- */
2953
-
2954
- /**
2955
- * @typedef {Object} PathMiddlewareOptions
2956
- * @prop {PathEntry|PathEntry[]} [path]
2957
- * @prop {string} [key='path']
2958
- * @prop {boolean} [absolute=false]
2959
- * Path is not relative to previous PathMiddleware. Defaults to `false`.
2960
- * @prop {boolean} [subPath=false]
2961
- * Path values are subpaths. Default to `false`;
2962
- */
2963
-
2964
- /** @implements {IMiddleware} */
2965
- class PathMiddleware {
2966
- /** @param {PathMiddlewareOptions|PathEntry|PathEntry[]} options */
2967
- constructor(options) {
2968
- if (Array.isArray(options)) {
2969
- this.path = options;
2970
- this.key = 'path';
2971
- this.absolute = false;
2972
- this.subPath = false;
2973
- } else if (typeof options === 'string' || options instanceof RegExp) {
2974
- this.path = [options];
2975
- this.key = 'path';
2976
- this.absolute = false;
2977
- this.subPath = false;
2978
- } else {
2979
- this.path = Array.isArray(options.path) ? options.path : [options.path];
2980
- this.key = options.key || 'path';
2981
- this.absolute = options.absolute === true;
2982
- this.subPath = options.subPath === true;
2983
- }
2984
- }
2985
-
2986
- /**
2987
- * @param {PathEntry|PathEntry[]} entry
2988
- */
2989
- static SUBPATH(entry) {
2990
- const path = Array.isArray(entry) ? entry : [entry];
2991
- return new PathMiddleware({
2992
- path: path.map((p) => (typeof p === 'string' ? RegExp(`^(${p})/*.*$`) : p)),
2993
- subPath: true,
2994
- });
2995
- }
2996
-
2997
- /**
2998
- * @param {string} path
2999
- * @param {RegExp | string} input
3000
- * @return {?string}
3001
- */
3002
- static test(path, input) {
3003
- if (typeof input === 'string') {
3004
- return (path === input ? input : null);
3005
- }
3006
- const result = input.exec(path);
3007
- if (!result) return null;
3008
- if (result.length === 1) {
3009
- return result[0];
3010
- }
3011
- return result[1] ?? result[0];
3012
- }
3013
-
3014
- /**
3015
- * @param {HttpRequest} req
3016
- * @param {string} base new base subpath
3017
- * @param {number[]} treeIndex this node's treeIndex
3018
- * @param {string} currentPath
3019
- * @return {void}
3020
- */
3021
- writePathState(req, base, treeIndex, currentPath) {
3022
- /** @type {PathState} */
3023
- let state = req.locals[this.key];
3024
- if (!state) {
3025
- state = { history: [], currentPath };
3026
- req.locals[this.key] = state;
3027
- } else if (!state.history) {
3028
- state.history = [];
3029
- }
3030
- state.history.push({ base, treeIndex: [...treeIndex] });
3031
- state.currentPath = currentPath;
3032
- }
3033
-
3034
- /**
3035
- * @param {HttpRequest} req
3036
- * @param {number[]} treeIndex this node's treeIndex
3037
- * @return {string} joined base path
3038
- */
3039
- readPathState(req, treeIndex) {
3040
- /** @type {PathState} */
3041
- const state = req.locals[this.key];
3042
- if (!state || !state.history || !state.history.length) {
3043
- return '/';
3044
- }
3045
- const paths = [];
3046
- let newLength = 0;
3047
- /* eslint-disable no-labels, no-restricted-syntax */
3048
- historyLoop: {
3049
- for (let i = 0; i < state.history.length; i++) {
3050
- const item = state.history[i];
3051
- if (item.treeIndex.length >= treeIndex.length) break;
3052
- for (let j = 0; j < item.treeIndex.length - 1; j++) {
3053
- if (item.treeIndex[j] !== treeIndex[j]) break historyLoop;
3054
- }
3055
- paths.push(item.base);
3056
- newLength++;
3057
- }
3058
- }
3059
- if (state.history.length !== newLength) {
3060
- state.history.length = newLength;
3061
- }
3062
- if (!paths.length) {
3063
- return '/';
3064
- }
3065
- return path.join(...paths);
3066
- }
3067
-
3068
- /**
3069
- * @param {MiddlewareFunctionParams} params
3070
- * @return {MiddlewareFunctionResult}
3071
- */
3072
- execute({ req, state }) {
3073
- const currentPath = this.absolute ? '' : this.readPathState(req, state.treeIndex);
3074
- const comparison = this.absolute ? req.url.pathname : `/${path.relative(currentPath, req.url.pathname)}`;
3075
-
3076
- for (let i = 0; i < this.path.length; i++) {
3077
- const path$1 = this.path[i];
3078
- const result = PathMiddleware.test(comparison, path$1);
3079
- if (result) {
3080
- if (this.subPath) {
3081
- this.writePathState(req, result, state.treeIndex, path.join(currentPath, result));
3082
- }
3083
- return 'continue';
3084
- }
3085
- }
3086
-
3087
- return 'break';
3088
- }
3089
- }
3090
-
3091
- /** @typedef {import('../types').IMiddleware} IMiddleware */
3092
- /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
3093
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
3094
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
3095
-
3096
- /**
3097
- * @typedef {Object} SendHeadersMiddlewareOptions
3098
- * @prop {boolean} [setStatus=false]
3099
- * Automatically set `200` or `204` status if not set
3100
- */
3101
-
3102
- /** @implements {IMiddleware} */
3103
- class SendHeadersMiddleware {
3104
- /** @param {SendHeadersMiddlewareOptions} options */
3105
- constructor(options = {}) {
3106
- this.setStatus = options.setStatus === true;
3107
- }
3108
-
3109
- /**
3110
- * @param {MiddlewareFunctionParams} params
3111
- * @return {MiddlewareFunctionResult}
3112
- */
3113
- execute({ res }) {
3114
- const newWritable = new stream.PassThrough();
3115
- const destination = res.replaceStream(newWritable);
3116
- newWritable.once('data', () => {
3117
- if (!res.headersSent) {
3118
- if (this.setStatus && res.status == null) {
3119
- res.status = 200;
3120
- }
3121
- res.sendHeaders(false);
3122
- }
3123
- });
3124
- newWritable.on('end', () => {
3125
- if (!res.headersSent) {
3126
- if (this.setStatus && res.status == null) {
3127
- res.status = 204;
3128
- }
3129
- res.sendHeaders(false);
3130
- }
3131
- });
3132
- newWritable.pipe(destination);
3133
- return 'continue';
3134
- }
3135
- }
3136
-
3137
- var index$2 = /*#__PURE__*/Object.freeze({
3138
- __proto__: null,
3139
- CaseInsensitiveHeadersMiddleware: CaseInsensitiveHeadersMiddleware,
3140
- ContentDecoderMiddleware: ContentDecoderMiddleware,
3141
- ContentEncoderMiddleware: ContentEncoderMiddleware,
3142
- ContentLengthMiddleware: ContentLengthMiddleware,
3143
- ContentReaderMiddleware: ContentReaderMiddleware,
3144
- ContentWriterMiddleware: ContentWriterMiddleware,
3145
- CorsMiddleware: CORSMiddleware,
3146
- HashMiddleware: HashMiddleware,
3147
- MethodMiddleware: MethodMiddleware,
3148
- PathMiddleware: PathMiddleware,
3149
- SendHeadersMiddleware: SendHeadersMiddleware
3150
- });
3151
-
3152
- /* eslint-disable import/prefer-default-export */
3153
-
3154
- /** @typedef {import('../lib/HttpHandler').default} HttpHandler */
3155
- /** @typedef {import('http2').Http2SecureServer} Http2SecureServer */
3156
-
3157
- /**
3158
- * @param {HttpHandler} httpHandler
3159
- * @param {RegExp} [socketioPath] /^\/socket.io\//i
3160
- * @return {void}
3161
- */
3162
- function addHttp2Support(httpHandler, socketioPath = /^\/socket.io\//i) {
3163
- const fn = httpHandler.handleHttp2Stream;
3164
- /** @type {fn} */
3165
- const newFunction = (...args) => {
3166
- const headers = args[1];
3167
- if (headers?.[':path']?.match(socketioPath)) {
3168
- return Promise.resolve(null);
3169
- }
3170
- return fn.call(httpHandler, ...args);
3171
- };
3172
- // @ts-ignore
3173
- // eslint-disable-next-line no-param-reassign
3174
- httpHandler.handleHttp2Stream = newFunction;
3175
- }
3176
-
3177
- var socketio = /*#__PURE__*/Object.freeze({
3178
- __proto__: null,
3179
- addHttp2Support: addHttp2Support
3180
- });
3181
-
3182
- var index$3 = /*#__PURE__*/Object.freeze({
3183
- __proto__: null,
3184
- socketio: socketio
3185
- });
3186
-
3187
- exports.errata = index$3;
3188
- exports.helpers = index$1;
3189
- exports.lib = index;
3190
- exports.middleware = index$2;