webhoster 0.1.0 → 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 -43
- package/middleware/ContentWriterMiddleware.js +5 -11
- package/middleware/HashMiddleware.js +68 -40
- package/middleware/HeadMethodMiddleware.js +24 -22
- package/middleware/MethodMiddleware.js +29 -36
- package/middleware/PathMiddleware.js +49 -66
- package/middleware/ReadFormData.js +99 -0
- package/middleware/SendHeadersMiddleware.js +0 -2
- 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 -3200
- package/test/constants.js +0 -4
- /package/{test → .test}/cookietester.js +0 -0
package/lib/HttpHandler.js
CHANGED
|
@@ -1,46 +1,34 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
2
3
|
|
|
3
|
-
import
|
|
4
|
-
import { noop } from '../utils/function.js';
|
|
4
|
+
import { isWritable } from '../utils/stream.js';
|
|
5
5
|
|
|
6
6
|
import HttpRequest from './HttpRequest.js';
|
|
7
7
|
import HttpResponse from './HttpResponse.js';
|
|
8
|
+
import HttpTransaction from './HttpTransaction.js';
|
|
8
9
|
|
|
9
10
|
/** @typedef {import('../types').Middleware} Middleware */
|
|
10
11
|
/** @typedef {import('../types').MiddlewareErrorHandler} MiddlewareErrorHandler */
|
|
11
|
-
/** @typedef {import('../types').
|
|
12
|
-
/** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
|
|
13
|
-
/** @typedef {import('../types').MiddlewareFunctionResultType} MiddlewareFunctionResultType */
|
|
12
|
+
/** @typedef {import('../types').MiddlewareFlowInstruction} MiddlewareFlowInstruction */
|
|
14
13
|
/** @typedef {import('../types').RequestMethod} RequestMethod */
|
|
15
|
-
/** @typedef {import('../types/index.js').HandlerState} HandlerState */
|
|
16
14
|
|
|
17
15
|
/** @type {HttpHandler} */
|
|
18
16
|
let defaultInstance = null;
|
|
19
17
|
|
|
20
|
-
/**
|
|
21
|
-
* @param {Middleware} middleware
|
|
22
|
-
* @return {boolean}
|
|
23
|
-
*/
|
|
24
|
-
function isErrorHandler(middleware) {
|
|
25
|
-
return !!middleware && typeof middleware === 'object' && 'onError' in middleware && middleware.onError != null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
18
|
/**
|
|
29
19
|
* @typedef {Object} HttpHandlerOptions
|
|
30
|
-
* @prop {Middleware[]} [
|
|
31
|
-
* @prop {Set<Middleware>} [middleware]
|
|
20
|
+
* @prop {Middleware[]} [middleware]
|
|
32
21
|
* @prop {MiddlewareErrorHandler[]} [errorHandlers]
|
|
33
22
|
*/
|
|
34
23
|
export default class HttpHandler {
|
|
35
|
-
/** @
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
24
|
+
/** @type {true} */
|
|
25
|
+
static CONTINUE = true;
|
|
26
|
+
|
|
27
|
+
/** @type {false} */
|
|
28
|
+
static BREAK = false;
|
|
29
|
+
|
|
30
|
+
/** @type {0} */
|
|
31
|
+
static END = 0;
|
|
44
32
|
|
|
45
33
|
/** @return {HttpHandler} */
|
|
46
34
|
static get defaultInstance() {
|
|
@@ -51,329 +39,511 @@ export default class HttpHandler {
|
|
|
51
39
|
}
|
|
52
40
|
|
|
53
41
|
/**
|
|
54
|
-
* @param {
|
|
55
|
-
* @
|
|
56
|
-
* @param {HttpResponse} params.res
|
|
57
|
-
* @return {Promise<HttpResponse>}
|
|
42
|
+
* @param {any} result
|
|
43
|
+
* @return {?MiddlewareFlowInstruction}
|
|
58
44
|
*/
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
/** @type {MiddlewareFunctionResult} */
|
|
77
|
-
let returnValue;
|
|
78
|
-
try {
|
|
79
|
-
returnValue = /** @type {MiddlewareErrorHandler} */ (middleware)
|
|
80
|
-
.onError({
|
|
81
|
-
req, res, state, err: caughtError,
|
|
82
|
-
});
|
|
83
|
-
} catch (err) {
|
|
84
|
-
isInErrorState = true;
|
|
85
|
-
caughtError = err;
|
|
86
|
-
returnValue = 'continue';
|
|
87
|
-
}
|
|
88
|
-
return Promise.resolve().then(() => returnValue);
|
|
89
|
-
}
|
|
90
|
-
if (middleware !== context.errorHandlers) {
|
|
91
|
-
// Consume and advance
|
|
92
|
-
return Promise.resolve();
|
|
93
|
-
}
|
|
94
|
-
} else if (isMiddlewareAnErrorHandler) {
|
|
95
|
-
// Don't run error handler if not in error state.
|
|
96
|
-
return Promise.resolve();
|
|
97
|
-
}
|
|
45
|
+
static ParseResultSync(result) {
|
|
46
|
+
// Fast return
|
|
47
|
+
switch (result) {
|
|
48
|
+
case true:
|
|
49
|
+
case null:
|
|
50
|
+
case undefined:
|
|
51
|
+
case HttpHandler.CONTINUE:
|
|
52
|
+
return HttpHandler.CONTINUE;
|
|
53
|
+
case false:
|
|
54
|
+
case HttpHandler.BREAK:
|
|
55
|
+
return HttpHandler.BREAK;
|
|
56
|
+
case 0:
|
|
57
|
+
return HttpHandler.END;
|
|
58
|
+
default:
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
98
62
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} scheme
|
|
65
|
+
* @param {string} authority
|
|
66
|
+
* @param {string} path
|
|
67
|
+
*/
|
|
68
|
+
static parseURL(scheme, authority, path) {
|
|
69
|
+
let query = '';
|
|
70
|
+
let fragment = '';
|
|
105
71
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
72
|
+
const authoritySplit = authority.split(':');
|
|
73
|
+
let pathname = '';
|
|
74
|
+
let search = '';
|
|
75
|
+
let hash = '';
|
|
76
|
+
const queryIndex = path.indexOf('?');
|
|
77
|
+
const fragmentIndex = path.indexOf('#');
|
|
78
|
+
const hasQuery = queryIndex !== -1;
|
|
79
|
+
const hasFragment = fragmentIndex !== -1;
|
|
115
80
|
|
|
116
|
-
|
|
117
|
-
|
|
81
|
+
// URL variables
|
|
82
|
+
pathname = path;
|
|
83
|
+
if (hasQuery && hasFragment) {
|
|
84
|
+
// Both ? and # present
|
|
85
|
+
if (queryIndex < fragmentIndex) {
|
|
86
|
+
pathname = path.slice(0, queryIndex);
|
|
87
|
+
search = path.slice(queryIndex, fragmentIndex);
|
|
88
|
+
hash = path.slice(fragmentIndex);
|
|
89
|
+
query = search.slice(1);
|
|
90
|
+
fragment = hash.slice(1);
|
|
91
|
+
} else {
|
|
92
|
+
// # comes before ?, treat as no query
|
|
93
|
+
pathname = path.slice(0, fragmentIndex);
|
|
94
|
+
hash = path.slice(fragmentIndex);
|
|
95
|
+
fragment = hash.slice(1);
|
|
118
96
|
}
|
|
97
|
+
} else if (hasQuery) {
|
|
98
|
+
pathname = path.slice(0, queryIndex);
|
|
99
|
+
search = path.slice(queryIndex);
|
|
100
|
+
query = search.slice(1);
|
|
101
|
+
} else if (hasFragment) {
|
|
102
|
+
pathname = path.slice(0, fragmentIndex);
|
|
103
|
+
hash = path.slice(fragmentIndex);
|
|
104
|
+
fragment = hash.slice(1);
|
|
105
|
+
} else {
|
|
106
|
+
pathname = path;
|
|
107
|
+
}
|
|
108
|
+
// Remove dot segments
|
|
109
|
+
pathname = posix.normalize(pathname);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
href: `${scheme}://${authority}${pathname}${search}${hash}`,
|
|
113
|
+
origin: `${scheme}://${authority}`,
|
|
114
|
+
protocol: `${scheme}:`,
|
|
115
|
+
username: '',
|
|
116
|
+
password: '',
|
|
117
|
+
host: authority,
|
|
118
|
+
hostname: authoritySplit[0],
|
|
119
|
+
port: authoritySplit[1] ?? '',
|
|
120
|
+
pathname,
|
|
121
|
+
search,
|
|
122
|
+
hash,
|
|
123
|
+
query,
|
|
124
|
+
fragment,
|
|
125
|
+
url: `${scheme}://${authority}${path}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
119
128
|
|
|
120
|
-
|
|
121
|
-
|
|
129
|
+
/** @param {HttpHandlerOptions} options */
|
|
130
|
+
constructor(options = {}) {
|
|
131
|
+
this.middleware = options.middleware || [];
|
|
132
|
+
this.errorHandlers = options.errorHandlers || [];
|
|
133
|
+
this.handleTransaction = this.handleTransaction.bind(this);
|
|
134
|
+
this.handleHttp1Request = this.handleHttp1Request.bind(this);
|
|
135
|
+
this.handleHttp2Stream = this.handleHttp2Stream.bind(this);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {HttpTransaction} transaction
|
|
140
|
+
* @param {Middleware} middleware
|
|
141
|
+
* @return {Promise<MiddlewareFlowInstruction>}
|
|
142
|
+
*/
|
|
143
|
+
async processMiddleware(transaction, middleware) {
|
|
144
|
+
if (middleware == null) return HttpHandler.CONTINUE;
|
|
145
|
+
|
|
146
|
+
// Check if error handler
|
|
147
|
+
const isErrorHandler = (typeof middleware === 'object'
|
|
148
|
+
&& typeof middleware.onError === 'function');
|
|
149
|
+
|
|
150
|
+
let value = middleware;
|
|
151
|
+
if (transaction.error) {
|
|
152
|
+
if (isErrorHandler) {
|
|
153
|
+
value = middleware.onError;
|
|
154
|
+
} else if (!transaction.isErrorHandlerState()) {
|
|
155
|
+
if (middleware !== this.errorHandlers) return HttpHandler.CONTINUE;
|
|
156
|
+
transaction.setErrorHandlerState();
|
|
122
157
|
}
|
|
158
|
+
} else if (isErrorHandler) {
|
|
159
|
+
return HttpHandler.CONTINUE;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let syncResult = HttpHandler.ParseResultSync(value);
|
|
163
|
+
if (syncResult != null) {
|
|
164
|
+
return syncResult;
|
|
165
|
+
}
|
|
123
166
|
|
|
124
|
-
|
|
125
|
-
|
|
167
|
+
/** @type {?MiddlewareFlowInstruction} */
|
|
168
|
+
let result;
|
|
169
|
+
switch (typeof value) {
|
|
170
|
+
case 'number':
|
|
171
|
+
transaction.response.status = value;
|
|
172
|
+
try {
|
|
173
|
+
return transaction.response.end();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
transaction.error = error;
|
|
176
|
+
return HttpHandler.CONTINUE;
|
|
177
|
+
}
|
|
178
|
+
case 'function':
|
|
179
|
+
try {
|
|
180
|
+
result = value.constructor.name === 'AsyncFunction'
|
|
181
|
+
? await value(transaction)
|
|
182
|
+
: value(transaction);
|
|
183
|
+
if (result == null) {
|
|
184
|
+
if (isErrorHandler) transaction.error = null;
|
|
185
|
+
return HttpHandler.CONTINUE;
|
|
186
|
+
}
|
|
126
187
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const chainLoop = (chainResult) => {
|
|
132
|
-
if (chainResult === 'end') {
|
|
133
|
-
return Promise.resolve('end');
|
|
188
|
+
// Sync operation returned Promise
|
|
189
|
+
if (typeof result === 'object' && typeof result.then === 'function') {
|
|
190
|
+
if (isErrorHandler) transaction.error = null;
|
|
191
|
+
result = await result;
|
|
134
192
|
}
|
|
135
|
-
|
|
136
|
-
|
|
193
|
+
syncResult = HttpHandler.ParseResultSync(result);
|
|
194
|
+
if (syncResult != null) {
|
|
195
|
+
if (isErrorHandler) transaction.error = null;
|
|
196
|
+
return syncResult;
|
|
137
197
|
}
|
|
138
|
-
|
|
139
|
-
|
|
198
|
+
|
|
199
|
+
// Slip in support for functions that return an Array
|
|
200
|
+
if (Array.isArray(result)) {
|
|
201
|
+
result = transaction.response.end(result);
|
|
202
|
+
if (isErrorHandler) transaction.error = null;
|
|
203
|
+
return result;
|
|
140
204
|
}
|
|
141
|
-
const chainIteration = iterator.next();
|
|
142
|
-
if (chainIteration.done) return Promise.resolve();
|
|
143
|
-
state.treeIndex[state.treeIndex.length - 1] += 1;
|
|
144
|
-
/** @type {Middleware} */
|
|
145
|
-
const innerMiddleware = chainIteration.value;
|
|
146
|
-
return handleMiddleware(innerMiddleware).then(chainLoop);
|
|
147
|
-
};
|
|
148
205
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return chainLoop().then((result) => {
|
|
152
|
-
state.treeIndex.pop();
|
|
206
|
+
if (isErrorHandler) transaction.error = null;
|
|
207
|
+
result = await this.processMiddleware(transaction, result);
|
|
153
208
|
return result;
|
|
154
|
-
})
|
|
155
|
-
|
|
209
|
+
} catch (error) {
|
|
210
|
+
// console.warn('Caught runtime error', err.message, err.stack);
|
|
211
|
+
transaction.error = error;
|
|
212
|
+
return HttpHandler.CONTINUE;
|
|
213
|
+
}
|
|
214
|
+
case 'object':
|
|
215
|
+
if (Array.isArray(value)) {
|
|
216
|
+
const { treeIndex } = transaction.state;
|
|
217
|
+
treeIndex.push(-1);
|
|
218
|
+
const { length } = value;
|
|
219
|
+
for (let index = 0; index < length; index++) {
|
|
220
|
+
const innerMiddleware = value[index];
|
|
221
|
+
treeIndex[treeIndex.length - 1] += 1;
|
|
222
|
+
if (innerMiddleware == null) continue;
|
|
223
|
+
result = HttpHandler.ParseResultSync(innerMiddleware);
|
|
224
|
+
if (result == null) {
|
|
225
|
+
// eslint-disable-next-line no-await-in-loop
|
|
226
|
+
result = await this.processMiddleware(transaction, innerMiddleware);
|
|
227
|
+
}
|
|
156
228
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
229
|
+
if (result === HttpHandler.END) {
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
if (result === HttpHandler.BREAK) {
|
|
233
|
+
// Break from branch and continue in parent
|
|
234
|
+
result = HttpHandler.CONTINUE;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
// Continue in branch
|
|
161
238
|
}
|
|
162
|
-
|
|
239
|
+
treeIndex.pop();
|
|
240
|
+
return result;
|
|
163
241
|
}
|
|
164
|
-
return handleMiddleware(Object.values(middleware));
|
|
165
|
-
}
|
|
166
242
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
243
|
+
if ('execute' in value && typeof value.execute === 'function') {
|
|
244
|
+
return await this.processMiddleware(transaction, value.execute.bind(value));
|
|
245
|
+
}
|
|
246
|
+
// Static caller
|
|
247
|
+
if ('Execute' in value && typeof value.Execute === 'function') {
|
|
248
|
+
return await this.processMiddleware(transaction, value.Execute);
|
|
249
|
+
}
|
|
250
|
+
if ('then' in value && typeof value.then === 'function') {
|
|
251
|
+
return await this.processMiddleware(transaction, await value);
|
|
252
|
+
}
|
|
253
|
+
// Fallthrough for Objects
|
|
254
|
+
case 'string':
|
|
255
|
+
try {
|
|
256
|
+
transaction.response.status ??= value ? 200 : 204;
|
|
257
|
+
return transaction.response.end(value);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
transaction.error = error;
|
|
260
|
+
return HttpHandler.CONTINUE;
|
|
261
|
+
}
|
|
262
|
+
default:
|
|
263
|
+
console.warn('Unknown middleware', value);
|
|
264
|
+
return HttpHandler.CONTINUE;
|
|
176
265
|
}
|
|
266
|
+
}
|
|
177
267
|
|
|
268
|
+
/**
|
|
269
|
+
* @param {HttpTransaction} transaction
|
|
270
|
+
* @return {Promise<HttpTransaction>}
|
|
271
|
+
*/
|
|
272
|
+
async handleTransaction(transaction) {
|
|
178
273
|
const allMiddleware = [
|
|
179
|
-
this.preprocessors,
|
|
180
274
|
this.middleware,
|
|
181
275
|
this.errorHandlers,
|
|
182
276
|
];
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
277
|
+
|
|
278
|
+
const finalFinalResponse = await this.processMiddleware(transaction, allMiddleware);
|
|
279
|
+
if (finalFinalResponse !== HttpHandler.END) {
|
|
280
|
+
// console.warn(transaction.request.url, "Middleware resolution did not complete with 'end'");
|
|
281
|
+
} else if (!transaction.response.wasEndCalled()) {
|
|
282
|
+
transaction.response.end();
|
|
283
|
+
}
|
|
284
|
+
if (transaction.error) {
|
|
285
|
+
console.warn('Webhoster did not find error handler. Crash prevented.', transaction.request.path, transaction.error);
|
|
286
|
+
if (!transaction.response.wasEndCalled()) {
|
|
287
|
+
// Use generic error response and don't expose error
|
|
288
|
+
transaction.response.status = 500;
|
|
289
|
+
transaction.response.headers['content-type'] = 'text/plain';
|
|
290
|
+
transaction.response.end('Internal Server Error');
|
|
187
291
|
}
|
|
188
|
-
|
|
189
|
-
|
|
292
|
+
}
|
|
293
|
+
return transaction;
|
|
190
294
|
}
|
|
191
295
|
|
|
192
296
|
/**
|
|
193
297
|
* @param {import('http').IncomingMessage} incomingMessage
|
|
194
298
|
* @param {import('http').ServerResponse} serverResponse
|
|
195
|
-
* @return {Promise<
|
|
299
|
+
* @return {Promise<HttpTransaction>}
|
|
196
300
|
*/
|
|
197
|
-
handleHttp1Request(incomingMessage, serverResponse) {
|
|
301
|
+
async handleHttp1Request(incomingMessage, serverResponse) {
|
|
302
|
+
/** @throws {Error} */
|
|
303
|
+
function onMalformed() {
|
|
304
|
+
const error = new Error('PROTOCOL_ERROR');
|
|
305
|
+
incomingMessage.destroy(error);
|
|
306
|
+
serverResponse.destroy(error);
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const {
|
|
311
|
+
method, headers, socket, url: path,
|
|
312
|
+
} = incomingMessage;
|
|
313
|
+
|
|
314
|
+
if (!method) onMalformed();
|
|
198
315
|
// @ts-ignore If TLSSocketLike
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
316
|
+
const scheme = socket.encrypted ? 'https' : 'http';
|
|
317
|
+
const authority = headers.host;
|
|
318
|
+
if (!authority) onMalformed();
|
|
319
|
+
if (authority.includes('@')) onMalformed();
|
|
320
|
+
|
|
321
|
+
let urlOptions;
|
|
322
|
+
if (method === 'CONNECT') {
|
|
323
|
+
if (scheme || path) onMalformed();
|
|
324
|
+
} else {
|
|
325
|
+
if (!scheme || !path) onMalformed();
|
|
326
|
+
if (path === '*') {
|
|
327
|
+
// asterisk-form
|
|
328
|
+
if (method !== 'OPTIONS') onMalformed();
|
|
329
|
+
} else {
|
|
330
|
+
urlOptions = HttpHandler.parseURL(scheme, authority, path);
|
|
331
|
+
}
|
|
208
332
|
}
|
|
209
333
|
|
|
210
|
-
const
|
|
211
|
-
headers
|
|
212
|
-
method
|
|
334
|
+
const request = new HttpRequest({
|
|
335
|
+
headers,
|
|
336
|
+
method,
|
|
213
337
|
stream: incomingMessage,
|
|
214
|
-
|
|
215
|
-
|
|
338
|
+
|
|
339
|
+
scheme,
|
|
340
|
+
authority,
|
|
341
|
+
path,
|
|
342
|
+
|
|
343
|
+
...urlOptions,
|
|
216
344
|
});
|
|
217
345
|
|
|
218
|
-
const
|
|
346
|
+
const response = new HttpResponse({
|
|
219
347
|
stream: serverResponse,
|
|
220
|
-
|
|
348
|
+
headers: {},
|
|
349
|
+
request,
|
|
221
350
|
onHeadersSent() {
|
|
222
351
|
return serverResponse.headersSent;
|
|
223
352
|
},
|
|
224
|
-
onSendHeaders(flush) {
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
353
|
+
onSendHeaders(flush, end) {
|
|
354
|
+
if (response.status == null) {
|
|
355
|
+
throw new Error('NO_STATUS');
|
|
356
|
+
}
|
|
357
|
+
if (!isWritable(serverResponse)) return false;
|
|
358
|
+
serverResponse.writeHead(response.status, response.headers);
|
|
359
|
+
if (end) {
|
|
360
|
+
serverResponse.end();
|
|
361
|
+
} else if (flush) {
|
|
228
362
|
serverResponse.flushHeaders();
|
|
229
363
|
}
|
|
230
|
-
return
|
|
364
|
+
return true;
|
|
231
365
|
},
|
|
232
366
|
});
|
|
233
367
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
368
|
+
const transaction = new HttpTransaction({
|
|
369
|
+
httpVersion: '2.0',
|
|
370
|
+
request,
|
|
371
|
+
response,
|
|
372
|
+
socket,
|
|
373
|
+
canPing: false,
|
|
374
|
+
canPushPath: false,
|
|
240
375
|
});
|
|
376
|
+
|
|
377
|
+
await this.handleTransaction(transaction);
|
|
378
|
+
|
|
379
|
+
if (isWritable(serverResponse)) {
|
|
380
|
+
setTimeout(() => {
|
|
381
|
+
if (isWritable(serverResponse)) {
|
|
382
|
+
console.warn('Respond stream end lagging more than 60s. Did you forget to call `.end()`?', request.url);
|
|
383
|
+
}
|
|
384
|
+
}, 60_000);
|
|
385
|
+
}
|
|
386
|
+
return transaction;
|
|
241
387
|
}
|
|
242
388
|
|
|
243
389
|
/**
|
|
244
390
|
* @param {import('http2').ServerHttp2Stream} stream
|
|
245
391
|
* @param {import('http2').IncomingHttpHeaders} headers
|
|
246
|
-
* @param {Partial<import('./
|
|
247
|
-
* @return {Promise<
|
|
392
|
+
* @param {Partial<import('./HttpTransaction.js').HttpTransactionOptions<unknown>>} [transactionOptions]
|
|
393
|
+
* @return {Promise<HttpTransaction>}
|
|
248
394
|
*/
|
|
249
|
-
handleHttp2Stream(stream, headers,
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
395
|
+
async handleHttp2Stream(stream, headers, transactionOptions = {}) {
|
|
396
|
+
/** @throws {Error} */
|
|
397
|
+
function onMalformed() {
|
|
398
|
+
const error = new Error('PROTOCOL_ERROR');
|
|
399
|
+
stream.destroy(error);
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const {
|
|
404
|
+
':method': method,
|
|
405
|
+
':scheme': scheme,
|
|
406
|
+
':authority': authorityHeader,
|
|
407
|
+
host: hostHeader,
|
|
408
|
+
} = headers;
|
|
409
|
+
|
|
410
|
+
if (!method) onMalformed();
|
|
411
|
+
// HTTP/2 to HTTP/1 translation
|
|
412
|
+
const authority = /** @type {string} */ (authorityHeader || hostHeader);
|
|
413
|
+
if (!authority) onMalformed();
|
|
414
|
+
if (authority.includes('@')) onMalformed();
|
|
415
|
+
|
|
416
|
+
const path = headers[':path'];
|
|
417
|
+
|
|
418
|
+
let urlOptions;
|
|
419
|
+
if (method === 'CONNECT') {
|
|
420
|
+
if (scheme || path) onMalformed();
|
|
421
|
+
} else {
|
|
422
|
+
if (!scheme || !path) onMalformed();
|
|
423
|
+
if (path === '*') {
|
|
424
|
+
// asterisk-form
|
|
425
|
+
if (method !== 'OPTIONS') onMalformed();
|
|
426
|
+
} else {
|
|
427
|
+
urlOptions = HttpHandler.parseURL(scheme, authority, path);
|
|
428
|
+
}
|
|
261
429
|
}
|
|
262
430
|
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
431
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
432
|
+
const context = this;
|
|
433
|
+
|
|
434
|
+
/** @type {Promise<any>[]} */
|
|
435
|
+
const pendingStreamLocks = [];
|
|
436
|
+
|
|
437
|
+
const request = new HttpRequest({
|
|
266
438
|
headers,
|
|
267
|
-
|
|
268
|
-
method: /** @type {RequestMethod} */ (headers[':method']),
|
|
439
|
+
method,
|
|
269
440
|
stream,
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
reject(err);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
resolve(duration);
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
},
|
|
441
|
+
|
|
442
|
+
scheme,
|
|
443
|
+
authority,
|
|
444
|
+
path,
|
|
445
|
+
|
|
446
|
+
...urlOptions,
|
|
283
447
|
});
|
|
284
448
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const res = new HttpResponse({
|
|
449
|
+
const response = new HttpResponse({
|
|
450
|
+
request,
|
|
288
451
|
stream,
|
|
289
|
-
socket: stream.session.socket,
|
|
290
|
-
canPushPath: stream.pushAllowed,
|
|
291
452
|
onHeadersSent() {
|
|
292
453
|
return stream.headersSent;
|
|
293
454
|
},
|
|
294
|
-
onSendHeaders() {
|
|
295
|
-
if (
|
|
296
|
-
if (
|
|
455
|
+
onSendHeaders(flush, end) {
|
|
456
|
+
if (response.headers[':status'] == null) {
|
|
457
|
+
if (response.status == null) {
|
|
297
458
|
throw new Error('NO_STATUS');
|
|
298
459
|
}
|
|
299
|
-
|
|
460
|
+
response.headers[':status'] = response.status;
|
|
300
461
|
}
|
|
301
|
-
stream
|
|
462
|
+
if (!isWritable(stream)) return false;
|
|
463
|
+
stream.respond(response.headers, { endStream: end });
|
|
464
|
+
return true;
|
|
302
465
|
},
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const transaction = new HttpTransaction({
|
|
469
|
+
httpVersion: '2.0',
|
|
470
|
+
request,
|
|
471
|
+
response,
|
|
472
|
+
socket: stream.session.socket,
|
|
473
|
+
canPing: true,
|
|
474
|
+
onPing: promisify(stream.session.ping),
|
|
475
|
+
canPushPath: () => stream.pushAllowed,
|
|
476
|
+
onPushPath: async (pushPath) => {
|
|
477
|
+
if (!stream.pushAllowed) {
|
|
478
|
+
throw new Error('PUSH_NOT_ALLOWED');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const newHeaders = {
|
|
482
|
+
':scheme': headers[':scheme'],
|
|
483
|
+
':authority': headers[':authority'],
|
|
484
|
+
':path': pushPath,
|
|
485
|
+
':method': 'GET',
|
|
486
|
+
};
|
|
487
|
+
for (const passedHeader of [
|
|
488
|
+
'accept',
|
|
489
|
+
'accept-encoding',
|
|
490
|
+
'accept-language',
|
|
491
|
+
'user-agent',
|
|
492
|
+
'cache-control',
|
|
493
|
+
]) {
|
|
494
|
+
if (passedHeader in headers) {
|
|
326
495
|
// @ts-ignore Coerce
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
496
|
+
newHeaders[passedHeader] = headers[passedHeader];
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Build promise function
|
|
501
|
+
const promiseFunction = async () => {
|
|
502
|
+
try {
|
|
503
|
+
const pushStream = await new Promise((resolve, reject) => {
|
|
504
|
+
stream.pushStream(newHeaders, ((error, newStream) => (error ? reject(error) : resolve(newStream))));
|
|
505
|
+
});
|
|
506
|
+
pushStream.addListener('error', (error) => {
|
|
507
|
+
if (error?.code === 'ECONNRESET') {
|
|
508
|
+
console.warn('HTTP/2 stream connection reset.', headers[':path']);
|
|
509
|
+
} else {
|
|
510
|
+
console.error('HTTP/2 stream error', headers, error);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
await context.handleHttp2Stream(pushStream, newHeaders, { canPushPath: false });
|
|
514
|
+
} catch (error) {
|
|
515
|
+
console.error('onPushFailed', error, error.stack);
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// Schedule microtask
|
|
521
|
+
const promiseExecution = promiseFunction();
|
|
522
|
+
|
|
523
|
+
// Add as stream lock
|
|
524
|
+
pendingStreamLocks.push(promiseExecution);
|
|
525
|
+
|
|
526
|
+
// Wait for promise to complete before returning
|
|
527
|
+
await promiseExecution;
|
|
344
528
|
},
|
|
345
|
-
...
|
|
529
|
+
...transactionOptions,
|
|
346
530
|
});
|
|
347
531
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
function sendPing() {
|
|
354
|
-
if (stream.session) stream.session.ping(noop);
|
|
532
|
+
await this.handleTransaction(transaction);
|
|
533
|
+
|
|
534
|
+
if (pendingStreamLocks.length) {
|
|
535
|
+
// Wait for all child push streams to terminate before we return.
|
|
536
|
+
await Promise.allSettled(pendingStreamLocks);
|
|
355
537
|
}
|
|
356
|
-
const autoPingStream = new PassThrough({
|
|
357
|
-
read(...args) {
|
|
358
|
-
clearTimeout(pingTimeout);
|
|
359
|
-
pingTimeout = setTimeout(sendPing, STREAM_WAIT_MS);
|
|
360
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
361
|
-
return PassThrough.prototype._read.call(this, ...args);
|
|
362
|
-
},
|
|
363
|
-
});
|
|
364
|
-
stream.pipe(autoPingStream);
|
|
365
|
-
req.replaceStream(autoPingStream);
|
|
366
538
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
})
|
|
377
|
-
.then(() => res);
|
|
539
|
+
if (isWritable(stream)) {
|
|
540
|
+
setTimeout(() => {
|
|
541
|
+
if (isWritable(stream)) {
|
|
542
|
+
console.warn('Respond stream end lagging more than 60s. Did you forget to call `.end()`?', request.url);
|
|
543
|
+
}
|
|
544
|
+
}, 60_000);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return transaction;
|
|
378
548
|
}
|
|
379
549
|
}
|