@interopio/gateway-server 0.4.0-beta
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/changelog.md +94 -0
- package/dist/gateway-ent.cjs +305 -0
- package/dist/gateway-ent.cjs.map +7 -0
- package/dist/gateway-ent.js +277 -0
- package/dist/gateway-ent.js.map +7 -0
- package/dist/index.cjs +1713 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.js +1682 -0
- package/dist/index.js.map +7 -0
- package/dist/metrics-rest.cjs +21440 -0
- package/dist/metrics-rest.cjs.map +7 -0
- package/dist/metrics-rest.js +21430 -0
- package/dist/metrics-rest.js.map +7 -0
- package/gateway-server.d.ts +69 -0
- package/package.json +66 -0
- package/readme.md +9 -0
- package/src/common/compose.ts +40 -0
- package/src/gateway/ent/config.ts +174 -0
- package/src/gateway/ent/index.ts +18 -0
- package/src/gateway/ent/logging.ts +89 -0
- package/src/gateway/ent/server.ts +34 -0
- package/src/gateway/metrics/rest.ts +20 -0
- package/src/gateway/ws/core.ts +90 -0
- package/src/index.ts +3 -0
- package/src/logger.ts +6 -0
- package/src/mesh/connections.ts +101 -0
- package/src/mesh/rest-directory/routes.ts +38 -0
- package/src/mesh/ws/broker/core.ts +163 -0
- package/src/mesh/ws/cluster/core.ts +107 -0
- package/src/mesh/ws/relays/core.ts +159 -0
- package/src/metrics/routes.ts +86 -0
- package/src/server/address.ts +47 -0
- package/src/server/cors.ts +311 -0
- package/src/server/exchange.ts +379 -0
- package/src/server/monitoring.ts +167 -0
- package/src/server/types.ts +69 -0
- package/src/server/ws-client-verify.ts +79 -0
- package/src/server.ts +316 -0
- package/src/utils.ts +10 -0
- package/types/gateway-ent.d.ts +212 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HeaderValue,
|
|
3
|
+
HeaderValues,
|
|
4
|
+
HttpRequest, HttpResponse,
|
|
5
|
+
MutableHttpHeaders,
|
|
6
|
+
ReadonlyHttpHeaders,
|
|
7
|
+
ServerHttpRequest,
|
|
8
|
+
ServerHttpResponse,
|
|
9
|
+
WebExchange
|
|
10
|
+
} from './types.js';
|
|
11
|
+
import http from 'node:http';
|
|
12
|
+
import http2 from 'node:http2';
|
|
13
|
+
|
|
14
|
+
function requestToProtocol(request: ServerHttpRequest, defaultProtocol: string): string {
|
|
15
|
+
let proto = request.headers.get('x-forwarded-proto');
|
|
16
|
+
|
|
17
|
+
if (Array.isArray(proto)) {
|
|
18
|
+
proto = proto[0];
|
|
19
|
+
}
|
|
20
|
+
if (proto !== undefined) {
|
|
21
|
+
return (proto as string).split(',', 1)[0].trim();
|
|
22
|
+
}
|
|
23
|
+
return defaultProtocol;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function requestToHost(request: ServerHttpRequest, defaultHost?: string): string | undefined {
|
|
27
|
+
let host = request.headers.get('x-forwarded-for');
|
|
28
|
+
if (host === undefined) {
|
|
29
|
+
host = request.headers.get('x-forwarded-host');
|
|
30
|
+
if (Array.isArray(host)) {
|
|
31
|
+
host = host[0];
|
|
32
|
+
}
|
|
33
|
+
if (host) {
|
|
34
|
+
const port = request.headers.one('x-forwarded-port');
|
|
35
|
+
if (port) {
|
|
36
|
+
host = `${host}:${port}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (host === undefined) {
|
|
40
|
+
host = request.headers.one('host');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(host)) {
|
|
44
|
+
host = host[0];
|
|
45
|
+
}
|
|
46
|
+
if (host) {
|
|
47
|
+
return (host as string).split(',', 1)[0].trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
return defaultHost;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class HttpServerRequest implements ServerHttpRequest {
|
|
56
|
+
private _body: Promise<Blob> | undefined;
|
|
57
|
+
private _url: URL | undefined;
|
|
58
|
+
private readonly _headers: ReadonlyHttpHeaders;
|
|
59
|
+
constructor(readonly _req: http.IncomingMessage) {
|
|
60
|
+
this._headers = new IncomingMessageHeaders(_req);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get http2(): boolean {
|
|
64
|
+
return this._req.httpVersionMajor >= 2;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get headers() {
|
|
68
|
+
return this._headers;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get path() {
|
|
72
|
+
return this.URL?.pathname;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get URL() {
|
|
76
|
+
this._url ??= new URL(this._req.url!, `${this.protocol}://${this.host}`);
|
|
77
|
+
return this._url;
|
|
78
|
+
}
|
|
79
|
+
get query() {
|
|
80
|
+
return this.URL?.search;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get method() {
|
|
84
|
+
return this._req.method;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get host() {
|
|
88
|
+
let dh: string | undefined = undefined;
|
|
89
|
+
if (this._req.httpVersionMajor >= 2) {
|
|
90
|
+
dh = (this._req?.headers as http2.IncomingHttpHeaders)[':authority'];
|
|
91
|
+
}
|
|
92
|
+
if (dh === undefined) {
|
|
93
|
+
dh = this._req?.socket.remoteAddress;
|
|
94
|
+
}
|
|
95
|
+
return requestToHost(this, dh);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get protocol() {
|
|
99
|
+
let dp: string | undefined = undefined;
|
|
100
|
+
if (this._req.httpVersionMajor > 2) {
|
|
101
|
+
dp = (this._req.headers as http2.IncomingHttpHeaders)[':scheme'];
|
|
102
|
+
}
|
|
103
|
+
if (dp === undefined) {
|
|
104
|
+
dp = this._req?.socket['encrypted'] ? 'https' : 'http'
|
|
105
|
+
}
|
|
106
|
+
return requestToProtocol(this, dp);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get socket() {
|
|
110
|
+
return this._req.socket;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get body() {
|
|
114
|
+
this._body ??= new Promise((resolve, reject) => {
|
|
115
|
+
const chunks: Uint8Array[] = [];
|
|
116
|
+
this._req
|
|
117
|
+
.on('error', (err: Error) => reject(err))
|
|
118
|
+
.on('data', (chunk) => chunks.push(chunk))
|
|
119
|
+
.on('end', () => {
|
|
120
|
+
resolve(new Blob(chunks));
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
return this._body;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
get text() {
|
|
127
|
+
return this.body.then(async (blob) => await blob.text());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get json() {
|
|
131
|
+
return this.body.then(async (blob) => {
|
|
132
|
+
const json = JSON.parse(await blob.text());
|
|
133
|
+
return json;
|
|
134
|
+
// try {
|
|
135
|
+
// } catch (e) {
|
|
136
|
+
// reject(e instanceof Error ? e : new Error(`parse failed :${e}`));
|
|
137
|
+
// }
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
class IncomingMessageHeaders implements ReadonlyHttpHeaders {
|
|
143
|
+
constructor(private readonly _msg: http.IncomingMessage) {
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
has(name: string): boolean {
|
|
147
|
+
return this._msg.headers[name] !== undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get(name: string): string | (readonly string[]) | undefined {
|
|
151
|
+
return this._msg.headers[name];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
list(name: string): string[] {
|
|
155
|
+
return toList(this._msg.headers[name]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
one(name: string): string | undefined {
|
|
159
|
+
const value = this._msg.headers[name];
|
|
160
|
+
if (Array.isArray(value)) {
|
|
161
|
+
return value[0];
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
keys() {
|
|
166
|
+
return Object.keys(this._msg.headers).values();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
class OutgoingMessageHeaders implements MutableHttpHeaders {
|
|
171
|
+
constructor(private readonly _msg: http.OutgoingMessage) {
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
has(name: string): boolean {
|
|
175
|
+
return this._msg.hasHeader(name);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
keys() {
|
|
179
|
+
return this._msg.getHeaderNames().values();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
get(name: string): HeaderValues {
|
|
183
|
+
return this._msg.getHeader(name);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
one(name: string): HeaderValue {
|
|
187
|
+
const value = this._msg.getHeader(name);
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
return value[0];
|
|
190
|
+
}
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
set(name: string, value: HeaderValues): this {
|
|
195
|
+
if (!this._msg.headersSent) {
|
|
196
|
+
if (Array.isArray(value)) {
|
|
197
|
+
value = value.map(v => typeof v === 'number' ? String(v) : v);
|
|
198
|
+
} else if (typeof value === 'number') {
|
|
199
|
+
value = String(value);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (value) {
|
|
203
|
+
this._msg.setHeader(name, value);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
this._msg.removeHeader(name);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
add(name: string, value: string | (readonly string[])): this {
|
|
213
|
+
if (!this._msg.headersSent) {
|
|
214
|
+
this._msg.appendHeader(name, value);
|
|
215
|
+
}
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
list(name: string): string[] {
|
|
220
|
+
const values = this.get(name);
|
|
221
|
+
return toList(values);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export class HttpServerResponse implements ServerHttpResponse {
|
|
226
|
+
private readonly _headers: MutableHttpHeaders;
|
|
227
|
+
constructor(readonly _res: http.ServerResponse) {
|
|
228
|
+
this._headers = new OutgoingMessageHeaders(_res);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
get statusCode(): number {
|
|
232
|
+
return this._res.statusCode;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
set statusCode(value: number) {
|
|
236
|
+
if (this._res.headersSent) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this._res.statusCode = value;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
get headers() {
|
|
243
|
+
return this._headers;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export class DefaultWebExchange<Request extends ServerHttpRequest, Response extends ServerHttpResponse> extends WebExchange<Request, Response> {
|
|
248
|
+
constructor(readonly request: Request, readonly response: Response) {
|
|
249
|
+
super();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function toList(values: number | string | (readonly string[]) | undefined): string[] {
|
|
254
|
+
if (typeof values === 'string') {
|
|
255
|
+
values = [values];
|
|
256
|
+
}
|
|
257
|
+
if (typeof values === 'number') {
|
|
258
|
+
values = [String(values)];
|
|
259
|
+
}
|
|
260
|
+
const list: string[] = [];
|
|
261
|
+
if (values) {
|
|
262
|
+
for (const value of values) {
|
|
263
|
+
if (value) {
|
|
264
|
+
list.push(...parseHeader(value));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return list;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function parseHeader(value: string): string[] {
|
|
272
|
+
const list: string[] = [];
|
|
273
|
+
{
|
|
274
|
+
let start = 0;
|
|
275
|
+
let end = 0;
|
|
276
|
+
|
|
277
|
+
for (let i = 0; i < value.length; i++) {
|
|
278
|
+
switch (value.charCodeAt(i)) {
|
|
279
|
+
case 0x20: // space
|
|
280
|
+
if (start === end) {
|
|
281
|
+
start = end = i + 1;
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
case 0x2c: // comma
|
|
285
|
+
list.push(value.slice(start, end));
|
|
286
|
+
start = end = i + 1;
|
|
287
|
+
break;
|
|
288
|
+
default:
|
|
289
|
+
end = end + 1;
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
list.push(value.slice(start, end));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return list;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export class MapHttpHeaders extends Map<string, (readonly string[])> implements MutableHttpHeaders {
|
|
300
|
+
|
|
301
|
+
get(name: string) {
|
|
302
|
+
return super.get(name.toLowerCase());
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
one(name: string): string | undefined {
|
|
306
|
+
return this.get(name)?.[0];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
list(name: string) {
|
|
310
|
+
const values = super.get(name.toLowerCase());
|
|
311
|
+
return toList(values);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
set(name: string, value: number | string | (readonly string[]) | undefined): this {
|
|
315
|
+
if (typeof value === 'number') {
|
|
316
|
+
value = String(value);
|
|
317
|
+
}
|
|
318
|
+
if (typeof value === 'string') {
|
|
319
|
+
value = [value];
|
|
320
|
+
}
|
|
321
|
+
if (value) {
|
|
322
|
+
return super.set(name.toLowerCase(), value);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
super.delete(name.toLowerCase());
|
|
326
|
+
return this;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
add(name: string, value: string | (readonly string[])) {
|
|
331
|
+
const prev = super.get(name.toLowerCase());
|
|
332
|
+
if (typeof value === 'string') {
|
|
333
|
+
value = [value];
|
|
334
|
+
}
|
|
335
|
+
if (prev) {
|
|
336
|
+
value = prev.concat(value);
|
|
337
|
+
}
|
|
338
|
+
this.set(name, value);
|
|
339
|
+
return this;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export class MockHttpRequest implements HttpRequest<MutableHttpHeaders> {
|
|
344
|
+
private _body?: Blob;
|
|
345
|
+
readonly headers = new MapHttpHeaders();
|
|
346
|
+
|
|
347
|
+
constructor(private readonly url: URL, method?: string) {
|
|
348
|
+
this.method = method ?? 'GET';
|
|
349
|
+
this.headers.set('Host', url.hostname);
|
|
350
|
+
this.path = url?.pathname ?? '/';
|
|
351
|
+
}
|
|
352
|
+
method: string;
|
|
353
|
+
path: string;
|
|
354
|
+
|
|
355
|
+
get host() {
|
|
356
|
+
return requestToHost(this, this.url.host);
|
|
357
|
+
}
|
|
358
|
+
get protocol() {
|
|
359
|
+
return requestToProtocol(this, this.url.protocol.slice(0, -1));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
get body(): Promise<Blob> {
|
|
363
|
+
const body = this._body;
|
|
364
|
+
return body ? Promise.resolve(body): Promise.reject(new Error(`no body set`));
|
|
365
|
+
}
|
|
366
|
+
set body(value: Blob) {
|
|
367
|
+
this._body = value;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
get URL() {
|
|
371
|
+
return new URL(this.path, `${this.protocol}://${this.host}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export class MockHttpResponse implements HttpResponse<MutableHttpHeaders> {
|
|
376
|
+
statusCode!: number;
|
|
377
|
+
headers = new MapHttpHeaders();
|
|
378
|
+
|
|
379
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import getLogger from '../logger.js';
|
|
2
|
+
import {getHeapStatistics, writeHeapSnapshot, HeapInfo} from 'node:v8';
|
|
3
|
+
import {PathLike} from 'node:fs';
|
|
4
|
+
import {access, mkdir, rename, unlink} from 'node:fs/promises';
|
|
5
|
+
|
|
6
|
+
const log = getLogger('monitoring');
|
|
7
|
+
|
|
8
|
+
export type Options = typeof DEFAULT_OPTIONS;
|
|
9
|
+
|
|
10
|
+
export type Command = 'run' | 'dump' | 'stop';
|
|
11
|
+
export type Channel = (command?: Command) => Promise<boolean>;
|
|
12
|
+
|
|
13
|
+
const DEFAULT_OPTIONS = {
|
|
14
|
+
memoryLimit: 1024 * 1024 * 1024, // 1GB
|
|
15
|
+
reportInterval: 10 * 60 * 1000, // 10 min
|
|
16
|
+
dumpLocation: '.', // current folder
|
|
17
|
+
maxBackups: 10,
|
|
18
|
+
dumpPrefix: 'Heap'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function fetchStats(): HeapInfo {
|
|
22
|
+
return getHeapStatistics();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function dumpHeap(opts: Options) {
|
|
26
|
+
const prefix = opts.dumpPrefix ?? 'Heap';
|
|
27
|
+
const target = `${opts.dumpLocation}/${prefix}.heapsnapshot`;
|
|
28
|
+
if (log.enabledFor('debug')) {
|
|
29
|
+
log.debug(`starting heap dump in ${target}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await fileExists(opts.dumpLocation)
|
|
33
|
+
.catch(async (_) => {
|
|
34
|
+
if (log.enabledFor('debug')) {
|
|
35
|
+
log.debug(`dump location ${opts.dumpLocation} does not exists. Will try to create it`);
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
await mkdir(opts.dumpLocation, {recursive: true});
|
|
39
|
+
log.info(`dump location dir ${opts.dumpLocation} successfully created`);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
log.error(`failed to create dump location ${opts.dumpLocation}`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const dumpFileName = writeHeapSnapshot(target);
|
|
45
|
+
log.info(`heap dumped`);
|
|
46
|
+
try {
|
|
47
|
+
log.debug(`rolling snapshot backups`);
|
|
48
|
+
const lastFileName = `${opts.dumpLocation}/${prefix}.${opts.maxBackups}.heapsnapshot`;
|
|
49
|
+
await fileExists(lastFileName)
|
|
50
|
+
.then(async () => {
|
|
51
|
+
if (log.enabledFor('debug')) {
|
|
52
|
+
log.debug(`deleting ${lastFileName}`);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
await unlink(lastFileName);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
log.warn(`failed to delete ${lastFileName}`, e);
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
.catch(() => {
|
|
61
|
+
/* do nothing*/
|
|
62
|
+
});
|
|
63
|
+
for (let i = opts.maxBackups - 1; i > 0; i--) {
|
|
64
|
+
const currentFileName = `${opts.dumpLocation}/${prefix}.${i}.heapsnapshot`;
|
|
65
|
+
const nextFileName = `${opts.dumpLocation}/${prefix}.${i + 1}.heapsnapshot`;
|
|
66
|
+
await fileExists(currentFileName)
|
|
67
|
+
.then(async () => {
|
|
68
|
+
try {
|
|
69
|
+
await rename(currentFileName, nextFileName);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
log.warn(`failed to rename ${currentFileName} to ${nextFileName}`, e);
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
.catch(() => {
|
|
75
|
+
/* do nothing*/
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const firstFileName = `${opts.dumpLocation}/${prefix}.${1}.heapsnapshot`;
|
|
79
|
+
try {
|
|
80
|
+
await rename(dumpFileName, firstFileName);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
log.warn(`failed to rename ${dumpFileName} to ${firstFileName}`, e);
|
|
83
|
+
}
|
|
84
|
+
log.debug('snapshots rolled');
|
|
85
|
+
} catch (e) {
|
|
86
|
+
log.error('error rolling backups', e);
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function fileExists(path: PathLike): Promise<void> {
|
|
92
|
+
log.trace(`checking file ${path}`);
|
|
93
|
+
await access(path);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function processStats(stats: HeapInfo, state: {
|
|
97
|
+
memoryLimitExceeded: boolean,
|
|
98
|
+
snapshot?: boolean
|
|
99
|
+
}, opts: Options) {
|
|
100
|
+
if (log.enabledFor('debug')) {
|
|
101
|
+
log.debug(`processing heap stats ${JSON.stringify(stats)}`);
|
|
102
|
+
}
|
|
103
|
+
const limit = Math.min(opts.memoryLimit, (0.95 * stats.heap_size_limit));
|
|
104
|
+
const used = stats.used_heap_size;
|
|
105
|
+
log.info(`heap stats ${JSON.stringify(stats)}`);
|
|
106
|
+
if (used >= limit) {
|
|
107
|
+
log.warn(`used heap ${used} bytes exceeds memory limit ${limit} bytes`);
|
|
108
|
+
if (state.memoryLimitExceeded) {
|
|
109
|
+
delete state.snapshot;
|
|
110
|
+
} else {
|
|
111
|
+
state.memoryLimitExceeded = true;
|
|
112
|
+
state.snapshot = true;
|
|
113
|
+
}
|
|
114
|
+
await dumpHeap(opts);
|
|
115
|
+
} else {
|
|
116
|
+
state.memoryLimitExceeded = false;
|
|
117
|
+
delete state.snapshot;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function start(opts?: Partial<Options>): Options & { channel: Channel } {
|
|
122
|
+
const merged: Options = {...DEFAULT_OPTIONS, ...opts};
|
|
123
|
+
|
|
124
|
+
let stopped = false;
|
|
125
|
+
const state = {memoryLimitExceeded: false};
|
|
126
|
+
const report = async () => {
|
|
127
|
+
const stats = fetchStats();
|
|
128
|
+
await processStats(stats, state, merged);
|
|
129
|
+
}
|
|
130
|
+
const interval = setInterval(report, merged.reportInterval);
|
|
131
|
+
const channel = async (command?: Command) => {
|
|
132
|
+
if (!stopped) {
|
|
133
|
+
command ??= 'run';
|
|
134
|
+
switch (command) {
|
|
135
|
+
case 'run': {
|
|
136
|
+
await report();
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case 'dump': {
|
|
140
|
+
await dumpHeap(merged);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'stop': {
|
|
144
|
+
stopped = true;
|
|
145
|
+
clearInterval(interval);
|
|
146
|
+
log.info('exit memory diagnostic');
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
}
|
|
152
|
+
return stopped;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {...merged, channel};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function run({channel}: { channel: Channel }, command?: Command) {
|
|
159
|
+
if (!await channel(command)) {
|
|
160
|
+
log.warn(`cannot execute command "${command}" already closed`)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
export async function stop(m: { channel: Channel }) {
|
|
166
|
+
return await run(m, 'stop');
|
|
167
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type Middleware<Request extends ServerHttpRequest = ServerHttpRequest, Response extends ServerHttpResponse = ServerHttpResponse> = ((context: WebExchange<Request, Response>, next: () => Promise<void>) => Promise<void>)[];
|
|
2
|
+
|
|
3
|
+
export abstract class WebExchange<Request extends ServerHttpRequest = ServerHttpRequest, Response extends ServerHttpResponse = ServerHttpResponse> {
|
|
4
|
+
abstract readonly request: Request
|
|
5
|
+
abstract readonly response: Response
|
|
6
|
+
|
|
7
|
+
get method(): string | undefined {
|
|
8
|
+
return this.request.method;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get path(): string | null | undefined {
|
|
12
|
+
return this.request.path;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export type ReadonlyHeaderValue = string | undefined;
|
|
16
|
+
export type HeaderValue = number | ReadonlyHeaderValue;
|
|
17
|
+
export type ReadonlyHeaderValues = (readonly string[]) | ReadonlyHeaderValue
|
|
18
|
+
export type HeaderValues = (readonly string[]) | HeaderValue
|
|
19
|
+
|
|
20
|
+
export interface HttpHeaders {
|
|
21
|
+
get(name: string): HeaderValues
|
|
22
|
+
list(name: string): string[]
|
|
23
|
+
one(name: string): HeaderValue
|
|
24
|
+
has(name: string): boolean
|
|
25
|
+
keys(): IteratorObject<string>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ReadonlyHttpHeaders extends HttpHeaders {
|
|
29
|
+
get(name: string): ReadonlyHeaderValues
|
|
30
|
+
one(name: string): string | undefined
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
export interface MutableHttpHeaders extends HttpHeaders {
|
|
34
|
+
get(name: string): HeaderValues
|
|
35
|
+
one(name: string): HeaderValue
|
|
36
|
+
set(name: string, value: HeaderValues): this
|
|
37
|
+
add(name: string, value: string | (readonly string[])): this
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface HttpMessage<Headers = HttpHeaders> {
|
|
41
|
+
readonly headers: Headers
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface HttpRequest<Headers = HttpHeaders> extends HttpMessage<Headers> {
|
|
45
|
+
readonly path: string,
|
|
46
|
+
readonly method?: string
|
|
47
|
+
readonly URL: URL
|
|
48
|
+
readonly protocol: string
|
|
49
|
+
/**
|
|
50
|
+
* hostname[:port]
|
|
51
|
+
*/
|
|
52
|
+
readonly host?: string
|
|
53
|
+
readonly body: Promise<Blob>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface HttpResponse<Headers = HttpHeaders> extends HttpMessage<Headers> {
|
|
57
|
+
readonly statusCode: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type ServerHttpRequest = HttpRequest<ReadonlyHttpHeaders> /* & {
|
|
61
|
+
// readonly text: Promise<string>
|
|
62
|
+
// readonly json: Promise<unknown>
|
|
63
|
+
// readonly socket: http.IncomingMessage['socket']
|
|
64
|
+
// readonly _req: http.IncomingMessage
|
|
65
|
+
}*/
|
|
66
|
+
|
|
67
|
+
export interface ServerHttpResponse extends HttpResponse<MutableHttpHeaders> {
|
|
68
|
+
statusCode: number
|
|
69
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {IOGateway} from '@interopio/gateway';
|
|
2
|
+
import {GatewayServer} from '../../gateway-server';
|
|
3
|
+
import getLogger from '../logger.js';
|
|
4
|
+
|
|
5
|
+
const log = getLogger('gateway.ws.client-verify');
|
|
6
|
+
|
|
7
|
+
export type ProcessedOriginFilters
|
|
8
|
+
= Required<Omit<GatewayServer.OriginFilters, 'blacklist' | 'whitelist'>>
|
|
9
|
+
//| Required<Omit<GatewayServer.OriginFilters, 'block' | 'allow'>>
|
|
10
|
+
;
|
|
11
|
+
|
|
12
|
+
function acceptsMissing(originFilters: ProcessedOriginFilters): boolean {
|
|
13
|
+
switch (originFilters.missing) {
|
|
14
|
+
case 'allow': // fall-through
|
|
15
|
+
case 'whitelist':
|
|
16
|
+
return true;
|
|
17
|
+
case 'block': // fall-through
|
|
18
|
+
case 'blacklist':
|
|
19
|
+
return false;
|
|
20
|
+
default:
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function tryMatch(originFilters: ProcessedOriginFilters, origin: string): boolean | undefined {
|
|
26
|
+
const block = originFilters.block ?? originFilters['blacklist'];
|
|
27
|
+
const allow = originFilters.allow ?? originFilters['whitelist'];
|
|
28
|
+
if (block.length > 0 && IOGateway.Filtering.valuesMatch(block, origin)) {
|
|
29
|
+
log.warn(`origin ${origin} matches block filter`);
|
|
30
|
+
return false;
|
|
31
|
+
} else if (allow.length > 0 && IOGateway.Filtering.valuesMatch(allow, origin)) {
|
|
32
|
+
if (log.enabledFor('debug')) {
|
|
33
|
+
log.debug(`origin ${origin} matches allow filter`);
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function acceptsNonMatched(originFilters: ProcessedOriginFilters): boolean {
|
|
40
|
+
switch (originFilters.non_matched) {
|
|
41
|
+
case 'allow': // fall-through
|
|
42
|
+
case 'whitelist':
|
|
43
|
+
return true;
|
|
44
|
+
case 'block': // fall-through
|
|
45
|
+
case 'blacklist':
|
|
46
|
+
return false;
|
|
47
|
+
default:
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function acceptsOrigin(origin?: string, originFilters?: ProcessedOriginFilters): boolean {
|
|
53
|
+
if (!originFilters) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (!origin) {
|
|
57
|
+
return acceptsMissing(originFilters);
|
|
58
|
+
} else {
|
|
59
|
+
const matchResult: boolean | undefined = tryMatch(originFilters, origin);
|
|
60
|
+
if (matchResult) {
|
|
61
|
+
return matchResult;
|
|
62
|
+
} else {
|
|
63
|
+
return acceptsNonMatched(originFilters);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function regexifyOriginFilters(originFilters?: GatewayServer.OriginFilters): ProcessedOriginFilters | undefined {
|
|
69
|
+
if (originFilters) {
|
|
70
|
+
const block = (originFilters.block ?? originFilters.blacklist ?? []).map(IOGateway.Filtering.regexify);
|
|
71
|
+
const allow = (originFilters.allow ?? originFilters.whitelist ?? []).map(IOGateway.Filtering.regexify);
|
|
72
|
+
return {
|
|
73
|
+
non_matched: originFilters.non_matched ?? 'allow',
|
|
74
|
+
missing: originFilters.missing ?? 'allow',
|
|
75
|
+
allow,
|
|
76
|
+
block,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|