webhoster 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +74 -58
- package/.github/copilot-instructions.md +100 -0
- package/.github/workflows/test-matrix.yml +37 -0
- package/.test/benchmark.js +28 -0
- package/.test/constants.js +4 -0
- package/{test → .test}/http2server.js +1 -1
- package/{test → .test}/httpserver.js +1 -1
- package/{test → .test}/index.js +178 -192
- package/.test/multipromise.js +32 -0
- package/{test → .test}/tls.js +3 -3
- package/{test → .test}/urlencoded.js +3 -0
- package/.vscode/launch.json +24 -3
- package/README.md +116 -90
- package/data/CookieObject.js +14 -14
- package/errata/socketio.js +6 -11
- package/examples/starter.js +11 -0
- package/helpers/HeadersParser.js +7 -8
- package/helpers/HttpListener.js +387 -42
- package/helpers/RequestHeaders.js +43 -36
- package/helpers/RequestReader.js +27 -26
- package/helpers/ResponseHeaders.js +47 -36
- package/jsconfig.json +1 -1
- package/lib/HttpHandler.js +447 -277
- package/lib/HttpRequest.js +383 -39
- package/lib/HttpResponse.js +316 -52
- package/lib/HttpTransaction.js +146 -0
- package/middleware/AutoHeadersMiddleware.js +73 -0
- package/middleware/CORSMiddleware.js +45 -47
- package/middleware/CaseInsensitiveHeadersMiddleware.js +5 -11
- package/middleware/ContentDecoderMiddleware.js +81 -35
- package/middleware/ContentEncoderMiddleware.js +179 -132
- package/middleware/ContentLengthMiddleware.js +66 -41
- package/middleware/ContentWriterMiddleware.js +5 -5
- package/middleware/HashMiddleware.js +68 -40
- package/middleware/HeadMethodMiddleware.js +24 -21
- package/middleware/MethodMiddleware.js +29 -36
- package/middleware/PathMiddleware.js +49 -66
- package/middleware/ReadFormData.js +99 -0
- package/middleware/SendJsonMiddleware.js +131 -0
- package/middleware/SendStringMiddleware.js +87 -0
- package/package.json +38 -29
- package/polyfill/FormData.js +164 -0
- package/rollup.config.js +0 -1
- package/scripts/test-all-sync.sh +6 -0
- package/scripts/test-all.sh +7 -0
- package/templates/starter.js +53 -0
- package/test/fixtures/stream.js +68 -0
- package/test/helpers/HttpListener/construct.js +18 -0
- package/test/helpers/HttpListener/customOptions.js +22 -0
- package/test/helpers/HttpListener/doubleCreate.js +40 -0
- package/test/helpers/HttpListener/events.js +77 -0
- package/test/helpers/HttpListener/http.js +31 -0
- package/test/helpers/HttpListener/http2.js +41 -0
- package/test/helpers/HttpListener/https.js +38 -0
- package/test/helpers/HttpListener/startAll.js +31 -0
- package/test/helpers/HttpListener/stopNotStarted.js +23 -0
- package/test/lib/HttpHandler/class.js +8 -0
- package/test/lib/HttpHandler/handleRequest.js +11 -0
- package/test/lib/HttpHandler/middleware.js +941 -0
- package/test/lib/HttpHandler/parse.js +41 -0
- package/test/lib/HttpRequest/class.js +8 -0
- package/test/lib/HttpRequest/downstream.js +171 -0
- package/test/lib/HttpRequest/properties.js +101 -0
- package/test/lib/HttpRequest/read.js +518 -0
- package/test/lib/HttpResponse/class.js +8 -0
- package/test/lib/HttpResponse/properties.js +59 -0
- package/test/lib/HttpResponse/send.js +275 -0
- package/test/lib/HttpTransaction/class.js +8 -0
- package/test/lib/HttpTransaction/ping.js +50 -0
- package/test/lib/HttpTransaction/push.js +89 -0
- package/test/middleware/SendJsonMiddleware.js +222 -0
- package/test/sanity.js +10 -0
- package/test/templates/starter.js +93 -0
- package/tsconfig.json +12 -0
- package/types/index.js +61 -34
- package/types/typings.d.ts +8 -9
- package/utils/AsyncObject.js +6 -3
- package/utils/CaseInsensitiveObject.js +2 -3
- package/utils/function.js +1 -7
- package/utils/headers.js +42 -0
- package/utils/qualityValues.js +1 -1
- package/utils/stream.js +4 -20
- package/index.cjs +0 -3190
- package/test/constants.js +0 -4
- /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;
|