webhoster 0.1.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/check-teapot.mjs +40 -0
- package/scripts/test-all-sync.sh +6 -0
- package/scripts/test-all.sh +7 -0
- package/templates/starter.js +55 -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/error-teapot.js +47 -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/helpers/HttpListener.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import { createServer as createHttpServer } from 'node:http';
|
|
2
|
+
import { createSecureServer as createHttp2Server } from 'node:http2';
|
|
3
|
+
import { createServer as createHttpsServer } from 'node:https';
|
|
4
|
+
import { Server, Socket } from 'node:net';
|
|
5
|
+
|
|
4
6
|
import HttpHandler from '../lib/HttpHandler.js';
|
|
5
7
|
|
|
6
8
|
export const SERVER_ALREADY_CREATED = 'SERVER_ALREADY_CREATED';
|
|
7
9
|
|
|
8
10
|
/** @typedef {import('tls').TlsOptions} TlsOptions */
|
|
11
|
+
/** @typedef {import('http2').Http2Session} Http2Session */
|
|
12
|
+
/** @typedef {import('http2').Http2Stream} Http2Stream */
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* @typedef {Object} HttpListenerOptions
|
|
@@ -24,8 +28,57 @@ export const SERVER_ALREADY_CREATED = 'SERVER_ALREADY_CREATED';
|
|
|
24
28
|
let defaultInstance;
|
|
25
29
|
|
|
26
30
|
export default class HttpListener {
|
|
31
|
+
/** @return {HttpListener} */
|
|
32
|
+
static get defaultInstance() {
|
|
33
|
+
if (!defaultInstance) {
|
|
34
|
+
defaultInstance = new HttpListener();
|
|
35
|
+
}
|
|
36
|
+
return defaultInstance;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @type {(err: Error) => void} */
|
|
40
|
+
#httpErrorListener;
|
|
41
|
+
|
|
42
|
+
/** @type {(err: Error, socket: import('node:net').Socket) => void} */
|
|
43
|
+
#httpClientErrorListener;
|
|
44
|
+
|
|
45
|
+
/** @type {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => void} */
|
|
46
|
+
#httpRequestListener;
|
|
47
|
+
|
|
48
|
+
/** @type {(err: Error) => void} */
|
|
49
|
+
#httpsErrorListener;
|
|
50
|
+
|
|
51
|
+
/** @type {(err: Error, socket: import('node:net').Socket) => void} */
|
|
52
|
+
#httpsClientErrorListener;
|
|
53
|
+
|
|
54
|
+
/** @type {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => void} */
|
|
55
|
+
#httpsRequestListener;
|
|
56
|
+
|
|
57
|
+
/** @type {(err: Error) => void} */
|
|
58
|
+
#http2ErrorListener;
|
|
59
|
+
|
|
60
|
+
/** @type {(err: Error, socket: import('node:net').Socket) => void} */
|
|
61
|
+
#http2ClientErrorListener;
|
|
62
|
+
|
|
63
|
+
/** @type {(err: Error, session: import('node:http2').Http2Session) => void} */
|
|
64
|
+
#http2SessionErrorListener;
|
|
65
|
+
|
|
66
|
+
/** @type {(session: import('node:http2').Http2Session) => void} */
|
|
67
|
+
#http2SessionListener;
|
|
68
|
+
|
|
69
|
+
/** @type {(stream: import('node:http2').Http2Stream, headers: import('node:http2').IncomingHttpHeaders) => void} */
|
|
70
|
+
#http2StreamListener;
|
|
71
|
+
|
|
72
|
+
/** @type {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => void} */
|
|
73
|
+
#http2RequestListener;
|
|
74
|
+
|
|
27
75
|
/** @param {HttpListenerOptions} options */
|
|
28
76
|
constructor(options = {}) {
|
|
77
|
+
this.configure(options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @param {HttpListenerOptions} options */
|
|
81
|
+
configure(options = {}) {
|
|
29
82
|
this.httpHandler = options.httpHandler ?? HttpHandler.defaultInstance;
|
|
30
83
|
this.insecurePort = options.insecurePort ?? 8080;
|
|
31
84
|
this.insecureHost = options.insecureHost;
|
|
@@ -37,21 +90,13 @@ export default class HttpListener {
|
|
|
37
90
|
this.tlsOptions = options.tlsOptions ?? {};
|
|
38
91
|
}
|
|
39
92
|
|
|
40
|
-
/** @return {HttpListener} */
|
|
41
|
-
static get defaultInstance() {
|
|
42
|
-
if (!defaultInstance) {
|
|
43
|
-
defaultInstance = new HttpListener();
|
|
44
|
-
}
|
|
45
|
-
return defaultInstance;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
93
|
/**
|
|
49
|
-
* @param {http.ServerOptions} [options]
|
|
50
|
-
* @return {http.Server}
|
|
94
|
+
* @param {import('node:http').ServerOptions} [options]
|
|
95
|
+
* @return {import('node:http').Server}
|
|
51
96
|
*/
|
|
52
97
|
createHttpServer(options = {}) {
|
|
53
98
|
if (!this.httpServer) {
|
|
54
|
-
this.httpServer =
|
|
99
|
+
this.httpServer = createHttpServer(options);
|
|
55
100
|
} else if (Object.keys(options).length) {
|
|
56
101
|
throw new Error(SERVER_ALREADY_CREATED);
|
|
57
102
|
}
|
|
@@ -59,12 +104,12 @@ export default class HttpListener {
|
|
|
59
104
|
}
|
|
60
105
|
|
|
61
106
|
/**
|
|
62
|
-
* @param {https.ServerOptions} [options]
|
|
63
|
-
* @return {https.Server}
|
|
107
|
+
* @param {import('node:https').ServerOptions} [options]
|
|
108
|
+
* @return {import('node:https').Server}
|
|
64
109
|
*/
|
|
65
110
|
createHttpsServer(options = {}) {
|
|
66
111
|
if (!this.httpsServer) {
|
|
67
|
-
this.httpsServer =
|
|
112
|
+
this.httpsServer = createHttpsServer({
|
|
68
113
|
...this.tlsOptions,
|
|
69
114
|
...options,
|
|
70
115
|
});
|
|
@@ -75,12 +120,12 @@ export default class HttpListener {
|
|
|
75
120
|
}
|
|
76
121
|
|
|
77
122
|
/**
|
|
78
|
-
* @param {http2.ServerOptions} [options]
|
|
79
|
-
* @return {http2.Http2SecureServer}
|
|
123
|
+
* @param {import('node:http2').ServerOptions} [options]
|
|
124
|
+
* @return {import('node:http2').Http2SecureServer}
|
|
80
125
|
*/
|
|
81
126
|
createHttp2Server(options = {}) {
|
|
82
127
|
if (!this.http2Server) {
|
|
83
|
-
this.http2Server =
|
|
128
|
+
this.http2Server = createHttp2Server({
|
|
84
129
|
allowHTTP1: true,
|
|
85
130
|
...this.tlsOptions,
|
|
86
131
|
...options,
|
|
@@ -91,7 +136,7 @@ export default class HttpListener {
|
|
|
91
136
|
return this.http2Server;
|
|
92
137
|
}
|
|
93
138
|
|
|
94
|
-
/** @return {Promise<http.Server>} */
|
|
139
|
+
/** @return {Promise<import('node:http').Server>} */
|
|
95
140
|
startHttpServer() {
|
|
96
141
|
return new Promise((resolve, reject) => {
|
|
97
142
|
this.createHttpServer();
|
|
@@ -100,14 +145,44 @@ export default class HttpListener {
|
|
|
100
145
|
host: this.insecureHost,
|
|
101
146
|
}, () => {
|
|
102
147
|
this.httpServer.removeListener('error', reject);
|
|
103
|
-
|
|
148
|
+
|
|
149
|
+
this.httpServer.keepAliveTimeout = 5000;
|
|
150
|
+
this.httpServer.requestTimeout = 300_000;
|
|
151
|
+
this.httpServer.setTimeout(120_000, (socket) => {
|
|
152
|
+
if (!socket) {
|
|
153
|
+
console.warn('HTTP socket (unknown) timed out.');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// const identity = `${socket.remoteFamily}:${socket.remoteAddress}:${socket.remotePort}`;
|
|
157
|
+
// console.warn(`HTTP socket ${identity} timed out.`);
|
|
158
|
+
socket.destroy(new Error('SOCKET_TIMEOUT'));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Save listeners as private members for later removal
|
|
162
|
+
this.httpServer.addListener('error', this.#httpErrorListener = (error) => {
|
|
163
|
+
// console.error('HTTP server error', err);
|
|
164
|
+
});
|
|
165
|
+
this.httpServer.addListener('clientError', this.#httpClientErrorListener = (error, socket) => {
|
|
166
|
+
if (error?.code === 'ECONNRESET') {
|
|
167
|
+
// console.warn('HTTP client connection reset.');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// console.error('HTTP client error', err);
|
|
171
|
+
if (socket.destroyed || socket.writableEnded) return;
|
|
172
|
+
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
173
|
+
});
|
|
174
|
+
this.httpServer.addListener('request', this.#httpRequestListener = (request, res) => {
|
|
175
|
+
this.httpHandler.handleHttp1Request(request, res).catch((error) => {
|
|
176
|
+
// console.error('HTTP1 handler failed', err);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
104
179
|
resolve(this.httpServer);
|
|
105
180
|
});
|
|
106
181
|
this.httpServer.addListener('error', reject);
|
|
107
182
|
});
|
|
108
183
|
}
|
|
109
184
|
|
|
110
|
-
/** @return {Promise<https.Server>} */
|
|
185
|
+
/** @return {Promise<import('node:https').Server>} */
|
|
111
186
|
startHttpsServer() {
|
|
112
187
|
return new Promise((resolve, reject) => {
|
|
113
188
|
this.createHttpsServer();
|
|
@@ -116,14 +191,44 @@ export default class HttpListener {
|
|
|
116
191
|
host: this.secureHost,
|
|
117
192
|
}, () => {
|
|
118
193
|
this.httpsServer.removeListener('error', reject);
|
|
119
|
-
|
|
194
|
+
|
|
195
|
+
this.httpsServer.keepAliveTimeout = 5000;
|
|
196
|
+
this.httpsServer.requestTimeout = 300_000;
|
|
197
|
+
this.httpsServer.setTimeout(120_000, (socket) => {
|
|
198
|
+
if (!socket) {
|
|
199
|
+
// console.warn('HTTPS socket (unknown) timed out.');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// const identity = `${socket.remoteFamily}:${socket.remoteAddress}:${socket.remotePort}`;
|
|
203
|
+
// console.error(`HTTPS socket ${identity} timed out.`);
|
|
204
|
+
socket.destroy(new Error('SOCKET_TIMEOUT'));
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
this.httpsServer.addListener('error', this.#httpsErrorListener = (error) => {
|
|
208
|
+
// console.error('HTTPS server error', err);
|
|
209
|
+
});
|
|
210
|
+
this.httpsServer.addListener('clientError', this.#httpsClientErrorListener = (error, socket) => {
|
|
211
|
+
if (error?.code === 'ECONNRESET') {
|
|
212
|
+
console.warn('HTTPS client connection reset.');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// console.error('HTTPS client error', err);
|
|
216
|
+
if (socket.destroyed) return;
|
|
217
|
+
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
this.httpsServer.addListener('request', this.#httpsRequestListener = (request, res) => {
|
|
221
|
+
this.httpHandler.handleHttp1Request(request, res).catch((error) => {
|
|
222
|
+
// console.error('HTTPS handler failed', err);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
120
225
|
resolve(this.httpsServer);
|
|
121
226
|
});
|
|
122
227
|
this.httpsServer.addListener('error', reject);
|
|
123
228
|
});
|
|
124
229
|
}
|
|
125
230
|
|
|
126
|
-
/** @return {Promise<http2.Http2SecureServer>} */
|
|
231
|
+
/** @return {Promise<import('node:http2').Http2SecureServer>} */
|
|
127
232
|
startHttp2Server() {
|
|
128
233
|
return new Promise((resolve, reject) => {
|
|
129
234
|
this.createHttp2Server();
|
|
@@ -132,12 +237,209 @@ export default class HttpListener {
|
|
|
132
237
|
host: this.secureHost,
|
|
133
238
|
}, () => {
|
|
134
239
|
this.http2Server.removeListener('error', reject);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
240
|
+
|
|
241
|
+
/** @type {Set<WeakRef<Http2Session>>} */
|
|
242
|
+
const sessions = new Set();
|
|
243
|
+
/** @type {WeakMap<Http2Session, {timestamp:number, identity:string}>} */
|
|
244
|
+
const sessionMetadata = new WeakMap();
|
|
245
|
+
/** @type {Set<WeakRef<Http2Stream>>} */
|
|
246
|
+
const streams = new Set();
|
|
247
|
+
/** @type {WeakMap<Http2Stream, {timestamp:number, identity:string, path:string}>} */
|
|
248
|
+
const streamMetadata = new WeakMap();
|
|
249
|
+
/** @return {void} */
|
|
250
|
+
function logUsage() {
|
|
251
|
+
if (global.gc) {
|
|
252
|
+
console.debug('Perfoming garbage collection.');
|
|
253
|
+
global.gc();
|
|
254
|
+
}
|
|
255
|
+
for (const reference of sessions) {
|
|
256
|
+
const session = reference.deref();
|
|
257
|
+
if (!session) {
|
|
258
|
+
sessions.delete(reference);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const metadata = sessionMetadata.get(session);
|
|
262
|
+
if (session.destroyed) {
|
|
263
|
+
console.warn('SESSION destroyed from', metadata.identity, 'since', metadata.timestamp);
|
|
264
|
+
} else {
|
|
265
|
+
console.debug('SESSION alive from', metadata.identity, 'since', metadata.timestamp);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const reference of streams) {
|
|
269
|
+
const stream = reference.deref();
|
|
270
|
+
if (!stream) {
|
|
271
|
+
streams.delete(reference);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const metadata = streamMetadata.get(stream);
|
|
275
|
+
if (stream.destroyed) {
|
|
276
|
+
console.warn('STREAM destroyed from', metadata.identity, 'since', metadata.timestamp, 'for', metadata.path);
|
|
277
|
+
} else {
|
|
278
|
+
console.debug('STREAM alive from', metadata.identity, 'since', metadata.timestamp, 'for', metadata.path);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (sessions.size) {
|
|
282
|
+
console.debug('Active sessions:', sessions.size);
|
|
283
|
+
}
|
|
284
|
+
if (streams.size) {
|
|
285
|
+
console.debug('Active streams:', streams.size);
|
|
286
|
+
}
|
|
287
|
+
if ('process' in globalThis) {
|
|
288
|
+
if ('getActiveResourcesInfo' in globalThis.process) {
|
|
289
|
+
console.dir('Active Resources', globalThis.process.getActiveResourcesInfo());
|
|
290
|
+
}
|
|
291
|
+
if ('_getActiveRequests' in globalThis.process) {
|
|
292
|
+
console.dir('Active Requests', globalThis.process._getActiveRequests());
|
|
293
|
+
}
|
|
294
|
+
if ('_getActiveHandles' in globalThis.process) {
|
|
295
|
+
const handles = globalThis.process._getActiveHandles();
|
|
296
|
+
for (const handle of handles) {
|
|
297
|
+
if (handle instanceof Socket) {
|
|
298
|
+
console.debug('Active Handle (Socket)', `${handle.localAddress}:${handle.localPort} <=> ${handle.remoteAddress}:${handle.remotePort}`);
|
|
299
|
+
} else if (handle instanceof Server) {
|
|
300
|
+
console.debug('Active Handle (Server)', `${handle.address()?.port}`);
|
|
301
|
+
} else {
|
|
302
|
+
console.debug('Active Handle (unknown)', handle);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// setInterval(logUsage, 300_000);
|
|
309
|
+
// logUsage();
|
|
310
|
+
|
|
311
|
+
this.http2Server.setTimeout(120_000, (socket) => {
|
|
312
|
+
if (!socket) {
|
|
313
|
+
console.warn('HTTP2 socket (unknown) timed out.');
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// const identity = `${socket.remoteFamily}:${socket.remoteAddress}:${socket.remotePort}`;
|
|
317
|
+
// console.warn(`HTTP2 socket ${identity} timed out.`);
|
|
318
|
+
socket.destroy(new Error('SOCKET_TIMEOUT'));
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Error Handlers
|
|
322
|
+
this.http2Server.addListener('error', this.#http2ErrorListener = (error) => {
|
|
323
|
+
console.error('HTTP/2 server error', error);
|
|
324
|
+
});
|
|
325
|
+
this.http2Server.addListener('clientError', this.#http2ClientErrorListener = (error, socket) => {
|
|
326
|
+
// No clear way to signal back to client why a socket has failed because
|
|
327
|
+
// it's unsure if it's HTTP1 or HTTP2. Just destroy and log server-side.
|
|
328
|
+
if (error?.code === 'ECONNRESET') {
|
|
329
|
+
// console.warn('HTTP/2 client connection reset.');
|
|
330
|
+
} else {
|
|
331
|
+
console.error('HTTP/2 client error', error);
|
|
332
|
+
}
|
|
333
|
+
if (!socket.destroyed) socket.destroy(error);
|
|
334
|
+
});
|
|
335
|
+
this.http2Server.addListener('sessionError', this.#http2SessionErrorListener = (error, session) => {
|
|
336
|
+
if (error?.code === 'ECONNRESET') {
|
|
337
|
+
// console.warn('HTTP/2 client connection reset.');
|
|
338
|
+
} else if (error?.message === 'SOCKET_TIMEOUT') {
|
|
339
|
+
// Server generated error
|
|
340
|
+
} else {
|
|
341
|
+
console.error('HTTP/2 sessionError error', error);
|
|
342
|
+
}
|
|
343
|
+
if (!session.destroyed) session.destroy(error);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
this.http2Server.addListener('session', this.#http2SessionListener = (session) => {
|
|
347
|
+
sessions.add(new WeakRef(session));
|
|
348
|
+
const identity = `${session.socket.remoteFamily}:${session.socket.remoteAddress}:${session.socket.remotePort}`;
|
|
349
|
+
sessionMetadata.set(session, {
|
|
350
|
+
timestamp: performance.now(),
|
|
351
|
+
identity,
|
|
352
|
+
});
|
|
353
|
+
session.setTimeout(60_000, () => {
|
|
354
|
+
// console.warn(`HTTP/2 session ${identity} timed out.`);
|
|
355
|
+
session.destroy(new Error('SESSION_TIMEOUT'));
|
|
356
|
+
});
|
|
357
|
+
const pingInterval = setInterval(() => {
|
|
358
|
+
if (session.destroyed) {
|
|
359
|
+
clearInterval(pingInterval);
|
|
360
|
+
} else {
|
|
361
|
+
session.ping((error) => {
|
|
362
|
+
if (!error) return;
|
|
363
|
+
if (session.destroyed) return;
|
|
364
|
+
if (error.code === 'ERR_HTTP2_PING_CANCEL') return;
|
|
365
|
+
console.error(`Ping to ${identity} failed.`, error);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}, 15_000);
|
|
369
|
+
session.addListener('close', () => {
|
|
370
|
+
clearInterval(pingInterval);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Logic handlers
|
|
375
|
+
this.http2Server.addListener('stream', this.#http2StreamListener = (stream, headers) => {
|
|
376
|
+
streams.add(new WeakRef(stream));
|
|
377
|
+
streamMetadata.set(stream, {
|
|
378
|
+
timestamp: performance.now(),
|
|
379
|
+
identity: `${stream.session?.socket.remoteFamily}:${stream.session?.socket.remoteAddress}:${stream.session?.socket.remotePort}`,
|
|
380
|
+
path: headers[':path'],
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
stream.setTimeout(300_000, () => {
|
|
384
|
+
stream.destroy(new Error('SOCKET_TIMEOUT'));
|
|
385
|
+
});
|
|
386
|
+
stream.addListener('error', (error) => {
|
|
387
|
+
if (error?.code === 'ECONNRESET') {
|
|
388
|
+
// console.warn('HTTP/2 stream connection reset.', headers[':path']);
|
|
389
|
+
} else {
|
|
390
|
+
console.error('HTTP/2 stream error', headers, error);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
this.httpHandler.handleHttp2Stream(stream, headers).catch((error) => {
|
|
394
|
+
console.error('HTTP2 handler failed.', error);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
this.http2Server.addListener('request', this.#http2RequestListener = (request, res) => {
|
|
398
|
+
if (request.httpVersionMajor >= 2) return;
|
|
138
399
|
// @ts-ignore Ignore typings
|
|
139
|
-
|
|
400
|
+
request.setTimeout(300_000, (socket) => {
|
|
401
|
+
if (!socket) {
|
|
402
|
+
// console.warn('HTTP1 in HTTP2 request (unknown) timed out.');
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
// const identity = `${socket.remoteFamily}:${socket.remoteAddress}:${socket.remotePort}`;
|
|
406
|
+
// console.warn(`HTTP1 in HTTP2 request ${identity} timed out.`);
|
|
407
|
+
socket.destroy(new Error('SOCKET_TIMEOUT'));
|
|
408
|
+
});
|
|
409
|
+
res.setTimeout(300_000, (socket) => {
|
|
410
|
+
if (!socket) {
|
|
411
|
+
// console.warn('HTTP1 in HTTP2 response (unknown) timed out.');
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const identity = `${socket.remoteFamily}:${socket.remoteAddress}:${socket.remotePort}`;
|
|
415
|
+
// console.warn(`HTTP1 in HTTP2 response ${identity} timed out.`);
|
|
416
|
+
socket.destroy(new Error('SOCKET_TIMEOUT'));
|
|
417
|
+
});
|
|
418
|
+
request.addListener('error', (error) => {
|
|
419
|
+
if (error?.code === 'ECONNRESET') {
|
|
420
|
+
// console.warn('Request stream connection reset.', req.url);
|
|
421
|
+
} else {
|
|
422
|
+
console.error('Request stream error.', request.url, request.headers, error);
|
|
423
|
+
}
|
|
424
|
+
if (!request.destroyed) {
|
|
425
|
+
request.destroy(error);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
res.addListener('error', (error) => {
|
|
429
|
+
if (error?.code === 'ECONNRESET') {
|
|
430
|
+
// console.warn('Response stream connection reset.', req.url);
|
|
431
|
+
} else {
|
|
432
|
+
console.error('Response stream error', request.url, request.headers, error);
|
|
433
|
+
}
|
|
434
|
+
if (!res.destroyed) {
|
|
435
|
+
res.destroy(error);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
this.httpHandler.handleHttp1Request(request, res).catch((error) => {
|
|
439
|
+
console.error('HTTP1 in HTTP2 handler failed.', error);
|
|
440
|
+
});
|
|
140
441
|
});
|
|
442
|
+
|
|
141
443
|
resolve(this.http2Server);
|
|
142
444
|
});
|
|
143
445
|
this.http2Server.addListener('error', reject);
|
|
@@ -151,11 +453,20 @@ export default class HttpListener {
|
|
|
151
453
|
resolve();
|
|
152
454
|
return;
|
|
153
455
|
}
|
|
154
|
-
this.httpServer.close((
|
|
155
|
-
if (
|
|
156
|
-
reject(
|
|
456
|
+
this.httpServer.close((error) => {
|
|
457
|
+
if (error) {
|
|
458
|
+
reject(error);
|
|
157
459
|
return;
|
|
158
460
|
}
|
|
461
|
+
if (this.#httpRequestListener) {
|
|
462
|
+
this.httpServer.removeListener('request', this.#httpRequestListener);
|
|
463
|
+
}
|
|
464
|
+
if (this.#httpErrorListener) {
|
|
465
|
+
this.httpServer.removeListener('error', this.#httpErrorListener);
|
|
466
|
+
}
|
|
467
|
+
if (this.#httpClientErrorListener) {
|
|
468
|
+
this.httpServer.removeListener('clientError', this.#httpClientErrorListener);
|
|
469
|
+
}
|
|
159
470
|
resolve();
|
|
160
471
|
});
|
|
161
472
|
});
|
|
@@ -168,11 +479,20 @@ export default class HttpListener {
|
|
|
168
479
|
resolve();
|
|
169
480
|
return;
|
|
170
481
|
}
|
|
171
|
-
this.httpsServer.close((
|
|
172
|
-
if (
|
|
173
|
-
reject(
|
|
482
|
+
this.httpsServer.close((error) => {
|
|
483
|
+
if (error) {
|
|
484
|
+
reject(error);
|
|
174
485
|
return;
|
|
175
486
|
}
|
|
487
|
+
if (this.#httpsRequestListener) {
|
|
488
|
+
this.httpsServer.removeListener('request', this.#httpsRequestListener);
|
|
489
|
+
}
|
|
490
|
+
if (this.#httpsErrorListener) {
|
|
491
|
+
this.httpsServer.removeListener('error', this.#httpsErrorListener);
|
|
492
|
+
}
|
|
493
|
+
if (this.#httpsClientErrorListener) {
|
|
494
|
+
this.httpsServer.removeListener('clientError', this.#httpsClientErrorListener);
|
|
495
|
+
}
|
|
176
496
|
resolve();
|
|
177
497
|
});
|
|
178
498
|
});
|
|
@@ -185,18 +505,43 @@ export default class HttpListener {
|
|
|
185
505
|
resolve();
|
|
186
506
|
return;
|
|
187
507
|
}
|
|
188
|
-
this.http2Server.close((
|
|
189
|
-
if (
|
|
190
|
-
reject(
|
|
508
|
+
this.http2Server.close((error) => {
|
|
509
|
+
if (error) {
|
|
510
|
+
reject(error);
|
|
191
511
|
return;
|
|
192
512
|
}
|
|
513
|
+
|
|
514
|
+
if (this.#http2ErrorListener) {
|
|
515
|
+
this.http2Server.removeListener('error', this.#http2ErrorListener);
|
|
516
|
+
}
|
|
517
|
+
if (this.#http2ClientErrorListener) {
|
|
518
|
+
this.http2Server.removeListener('clientError', this.#http2ClientErrorListener);
|
|
519
|
+
}
|
|
520
|
+
if (this.#http2SessionErrorListener) {
|
|
521
|
+
this.http2Server.removeListener('sessionError', this.#http2SessionErrorListener);
|
|
522
|
+
}
|
|
523
|
+
if (this.#http2SessionListener) {
|
|
524
|
+
this.http2Server.removeListener('session', this.#http2SessionListener);
|
|
525
|
+
}
|
|
526
|
+
if (this.#http2StreamListener) {
|
|
527
|
+
this.http2Server.removeListener('stream', this.#http2StreamListener);
|
|
528
|
+
}
|
|
529
|
+
if (this.#http2RequestListener) {
|
|
530
|
+
this.http2Server.removeListener('request', this.#http2RequestListener);
|
|
531
|
+
}
|
|
532
|
+
|
|
193
533
|
resolve();
|
|
194
534
|
});
|
|
195
535
|
});
|
|
196
536
|
}
|
|
197
537
|
|
|
198
538
|
/**
|
|
199
|
-
* @return {Promise<[
|
|
539
|
+
* @return {Promise<[
|
|
540
|
+
* import('node:http').Server,
|
|
541
|
+
* import('node:https').Server,
|
|
542
|
+
* import('node:http2').Http2SecureServer
|
|
543
|
+
* ]>
|
|
544
|
+
* }
|
|
200
545
|
*/
|
|
201
546
|
startAll() {
|
|
202
547
|
return Promise.all([
|
|
@@ -209,11 +554,11 @@ export default class HttpListener {
|
|
|
209
554
|
/**
|
|
210
555
|
* @return {Promise<void>}
|
|
211
556
|
*/
|
|
212
|
-
stopAll() {
|
|
213
|
-
|
|
557
|
+
async stopAll() {
|
|
558
|
+
await Promise.all([
|
|
214
559
|
this.useHttp ? this.stopHttpServer() : Promise.resolve(null),
|
|
215
560
|
this.useHttps ? this.stopHttpsServer() : Promise.resolve(null),
|
|
216
561
|
this.useHttp2 ? this.stopHttp2Server() : Promise.resolve(null),
|
|
217
|
-
])
|
|
562
|
+
]);
|
|
218
563
|
}
|
|
219
564
|
}
|