@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,1682 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/server.ts
|
|
8
|
+
var server_exports = {};
|
|
9
|
+
__export(server_exports, {
|
|
10
|
+
Factory: () => Factory
|
|
11
|
+
});
|
|
12
|
+
import { WebSocketServer } from "ws";
|
|
13
|
+
import http from "node:http";
|
|
14
|
+
import https from "node:https";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
|
+
import { IOGateway as IOGateway7 } from "@interopio/gateway";
|
|
18
|
+
|
|
19
|
+
// src/logger.ts
|
|
20
|
+
import { IOGateway } from "@interopio/gateway";
|
|
21
|
+
function getLogger(name) {
|
|
22
|
+
return IOGateway.Logging.getLogger(`gateway.server.${name}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/utils.ts
|
|
26
|
+
function socketKey(socket) {
|
|
27
|
+
const remoteIp = socket.remoteAddress;
|
|
28
|
+
if (!remoteIp) {
|
|
29
|
+
throw new Error("Socket has no remote address");
|
|
30
|
+
}
|
|
31
|
+
return `${remoteIp}:${socket.remotePort}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/server/types.ts
|
|
35
|
+
var WebExchange = class {
|
|
36
|
+
get method() {
|
|
37
|
+
return this.request.method;
|
|
38
|
+
}
|
|
39
|
+
get path() {
|
|
40
|
+
return this.request.path;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/server/exchange.ts
|
|
45
|
+
function requestToProtocol(request, defaultProtocol) {
|
|
46
|
+
let proto = request.headers.get("x-forwarded-proto");
|
|
47
|
+
if (Array.isArray(proto)) {
|
|
48
|
+
proto = proto[0];
|
|
49
|
+
}
|
|
50
|
+
if (proto !== void 0) {
|
|
51
|
+
return proto.split(",", 1)[0].trim();
|
|
52
|
+
}
|
|
53
|
+
return defaultProtocol;
|
|
54
|
+
}
|
|
55
|
+
function requestToHost(request, defaultHost) {
|
|
56
|
+
let host = request.headers.get("x-forwarded-for");
|
|
57
|
+
if (host === void 0) {
|
|
58
|
+
host = request.headers.get("x-forwarded-host");
|
|
59
|
+
if (Array.isArray(host)) {
|
|
60
|
+
host = host[0];
|
|
61
|
+
}
|
|
62
|
+
if (host) {
|
|
63
|
+
const port = request.headers.one("x-forwarded-port");
|
|
64
|
+
if (port) {
|
|
65
|
+
host = `${host}:${port}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (host === void 0) {
|
|
69
|
+
host = request.headers.one("host");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (Array.isArray(host)) {
|
|
73
|
+
host = host[0];
|
|
74
|
+
}
|
|
75
|
+
if (host) {
|
|
76
|
+
return host.split(",", 1)[0].trim();
|
|
77
|
+
}
|
|
78
|
+
return defaultHost;
|
|
79
|
+
}
|
|
80
|
+
var HttpServerRequest = class {
|
|
81
|
+
constructor(_req) {
|
|
82
|
+
this._req = _req;
|
|
83
|
+
this._headers = new IncomingMessageHeaders(_req);
|
|
84
|
+
}
|
|
85
|
+
_body;
|
|
86
|
+
_url;
|
|
87
|
+
_headers;
|
|
88
|
+
get http2() {
|
|
89
|
+
return this._req.httpVersionMajor >= 2;
|
|
90
|
+
}
|
|
91
|
+
get headers() {
|
|
92
|
+
return this._headers;
|
|
93
|
+
}
|
|
94
|
+
get path() {
|
|
95
|
+
return this.URL?.pathname;
|
|
96
|
+
}
|
|
97
|
+
get URL() {
|
|
98
|
+
this._url ??= new URL(this._req.url, `${this.protocol}://${this.host}`);
|
|
99
|
+
return this._url;
|
|
100
|
+
}
|
|
101
|
+
get query() {
|
|
102
|
+
return this.URL?.search;
|
|
103
|
+
}
|
|
104
|
+
get method() {
|
|
105
|
+
return this._req.method;
|
|
106
|
+
}
|
|
107
|
+
get host() {
|
|
108
|
+
let dh = void 0;
|
|
109
|
+
if (this._req.httpVersionMajor >= 2) {
|
|
110
|
+
dh = (this._req?.headers)[":authority"];
|
|
111
|
+
}
|
|
112
|
+
if (dh === void 0) {
|
|
113
|
+
dh = this._req?.socket.remoteAddress;
|
|
114
|
+
}
|
|
115
|
+
return requestToHost(this, dh);
|
|
116
|
+
}
|
|
117
|
+
get protocol() {
|
|
118
|
+
let dp = void 0;
|
|
119
|
+
if (this._req.httpVersionMajor > 2) {
|
|
120
|
+
dp = this._req.headers[":scheme"];
|
|
121
|
+
}
|
|
122
|
+
if (dp === void 0) {
|
|
123
|
+
dp = this._req?.socket["encrypted"] ? "https" : "http";
|
|
124
|
+
}
|
|
125
|
+
return requestToProtocol(this, dp);
|
|
126
|
+
}
|
|
127
|
+
get socket() {
|
|
128
|
+
return this._req.socket;
|
|
129
|
+
}
|
|
130
|
+
get body() {
|
|
131
|
+
this._body ??= new Promise((resolve, reject) => {
|
|
132
|
+
const chunks = [];
|
|
133
|
+
this._req.on("error", (err) => reject(err)).on("data", (chunk) => chunks.push(chunk)).on("end", () => {
|
|
134
|
+
resolve(new Blob(chunks));
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
return this._body;
|
|
138
|
+
}
|
|
139
|
+
get text() {
|
|
140
|
+
return this.body.then(async (blob) => await blob.text());
|
|
141
|
+
}
|
|
142
|
+
get json() {
|
|
143
|
+
return this.body.then(async (blob) => {
|
|
144
|
+
const json = JSON.parse(await blob.text());
|
|
145
|
+
return json;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
var IncomingMessageHeaders = class {
|
|
150
|
+
constructor(_msg) {
|
|
151
|
+
this._msg = _msg;
|
|
152
|
+
}
|
|
153
|
+
has(name) {
|
|
154
|
+
return this._msg.headers[name] !== void 0;
|
|
155
|
+
}
|
|
156
|
+
get(name) {
|
|
157
|
+
return this._msg.headers[name];
|
|
158
|
+
}
|
|
159
|
+
list(name) {
|
|
160
|
+
return toList(this._msg.headers[name]);
|
|
161
|
+
}
|
|
162
|
+
one(name) {
|
|
163
|
+
const value = this._msg.headers[name];
|
|
164
|
+
if (Array.isArray(value)) {
|
|
165
|
+
return value[0];
|
|
166
|
+
}
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
keys() {
|
|
170
|
+
return Object.keys(this._msg.headers).values();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
var OutgoingMessageHeaders = class {
|
|
174
|
+
constructor(_msg) {
|
|
175
|
+
this._msg = _msg;
|
|
176
|
+
}
|
|
177
|
+
has(name) {
|
|
178
|
+
return this._msg.hasHeader(name);
|
|
179
|
+
}
|
|
180
|
+
keys() {
|
|
181
|
+
return this._msg.getHeaderNames().values();
|
|
182
|
+
}
|
|
183
|
+
get(name) {
|
|
184
|
+
return this._msg.getHeader(name);
|
|
185
|
+
}
|
|
186
|
+
one(name) {
|
|
187
|
+
const value = this._msg.getHeader(name);
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
return value[0];
|
|
190
|
+
}
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
set(name, value) {
|
|
194
|
+
if (!this._msg.headersSent) {
|
|
195
|
+
if (Array.isArray(value)) {
|
|
196
|
+
value = value.map((v) => typeof v === "number" ? String(v) : v);
|
|
197
|
+
} else if (typeof value === "number") {
|
|
198
|
+
value = String(value);
|
|
199
|
+
}
|
|
200
|
+
if (value) {
|
|
201
|
+
this._msg.setHeader(name, value);
|
|
202
|
+
} else {
|
|
203
|
+
this._msg.removeHeader(name);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return this;
|
|
207
|
+
}
|
|
208
|
+
add(name, value) {
|
|
209
|
+
if (!this._msg.headersSent) {
|
|
210
|
+
this._msg.appendHeader(name, value);
|
|
211
|
+
}
|
|
212
|
+
return this;
|
|
213
|
+
}
|
|
214
|
+
list(name) {
|
|
215
|
+
const values = this.get(name);
|
|
216
|
+
return toList(values);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
var HttpServerResponse = class {
|
|
220
|
+
constructor(_res) {
|
|
221
|
+
this._res = _res;
|
|
222
|
+
this._headers = new OutgoingMessageHeaders(_res);
|
|
223
|
+
}
|
|
224
|
+
_headers;
|
|
225
|
+
get statusCode() {
|
|
226
|
+
return this._res.statusCode;
|
|
227
|
+
}
|
|
228
|
+
set statusCode(value) {
|
|
229
|
+
if (this._res.headersSent) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this._res.statusCode = value;
|
|
233
|
+
}
|
|
234
|
+
get headers() {
|
|
235
|
+
return this._headers;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
var DefaultWebExchange = class extends WebExchange {
|
|
239
|
+
constructor(request, response) {
|
|
240
|
+
super();
|
|
241
|
+
this.request = request;
|
|
242
|
+
this.response = response;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
function toList(values) {
|
|
246
|
+
if (typeof values === "string") {
|
|
247
|
+
values = [values];
|
|
248
|
+
}
|
|
249
|
+
if (typeof values === "number") {
|
|
250
|
+
values = [String(values)];
|
|
251
|
+
}
|
|
252
|
+
const list = [];
|
|
253
|
+
if (values) {
|
|
254
|
+
for (const value of values) {
|
|
255
|
+
if (value) {
|
|
256
|
+
list.push(...parseHeader(value));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return list;
|
|
261
|
+
}
|
|
262
|
+
function parseHeader(value) {
|
|
263
|
+
const list = [];
|
|
264
|
+
{
|
|
265
|
+
let start2 = 0;
|
|
266
|
+
let end = 0;
|
|
267
|
+
for (let i = 0; i < value.length; i++) {
|
|
268
|
+
switch (value.charCodeAt(i)) {
|
|
269
|
+
case 32:
|
|
270
|
+
if (start2 === end) {
|
|
271
|
+
start2 = end = i + 1;
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
case 44:
|
|
275
|
+
list.push(value.slice(start2, end));
|
|
276
|
+
start2 = end = i + 1;
|
|
277
|
+
break;
|
|
278
|
+
default:
|
|
279
|
+
end = end + 1;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
list.push(value.slice(start2, end));
|
|
284
|
+
}
|
|
285
|
+
return list;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/gateway/ws/core.ts
|
|
289
|
+
import { IOGateway as IOGateway2 } from "@interopio/gateway";
|
|
290
|
+
var GatewayEncoders = IOGateway2.Encoding;
|
|
291
|
+
var log = getLogger("ws");
|
|
292
|
+
var codec = GatewayEncoders.json();
|
|
293
|
+
function initClient(key, socket, host) {
|
|
294
|
+
const opts = {
|
|
295
|
+
key,
|
|
296
|
+
host,
|
|
297
|
+
codec,
|
|
298
|
+
onPing: () => {
|
|
299
|
+
socket.ping((err) => {
|
|
300
|
+
if (err) {
|
|
301
|
+
log.warn(`failed to ping ${key}`, err);
|
|
302
|
+
} else {
|
|
303
|
+
log.info(`ping sent to ${key}`);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
onDisconnect: (reason) => {
|
|
308
|
+
switch (reason) {
|
|
309
|
+
case "inactive": {
|
|
310
|
+
log.warn(`no heartbeat (ping) received from ${key}, closing socket`);
|
|
311
|
+
socket.close(4001, "ping expected");
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case "shutdown": {
|
|
315
|
+
socket.close(1001, "shutdown");
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
try {
|
|
322
|
+
return this.client((data) => socket.send(data), opts);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
log.warn(`${key} failed to create client`, err);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function create(server) {
|
|
328
|
+
log.info("start gateway");
|
|
329
|
+
await this.start({ endpoint: server.endpoint });
|
|
330
|
+
server.wss.on("error", (err) => {
|
|
331
|
+
log.error("error starting the gateway websocket server", err);
|
|
332
|
+
}).on("connection", (socket, req) => {
|
|
333
|
+
const request = new HttpServerRequest(req);
|
|
334
|
+
const key = socketKey(request.socket);
|
|
335
|
+
const host = request.host;
|
|
336
|
+
log.info(`${key} connected on gw from ${host}`);
|
|
337
|
+
const client = initClient.call(this, key, socket);
|
|
338
|
+
if (!client) {
|
|
339
|
+
log.error(`${key} gw client init failed`);
|
|
340
|
+
socket.terminate();
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
socket.on("error", (err) => {
|
|
344
|
+
log.error(`${key} websocket error: ${err}`, err);
|
|
345
|
+
});
|
|
346
|
+
socket.on("message", (data, _isBinary) => {
|
|
347
|
+
if (Array.isArray(data)) {
|
|
348
|
+
data = Buffer.concat(data);
|
|
349
|
+
}
|
|
350
|
+
client.send(data);
|
|
351
|
+
});
|
|
352
|
+
socket.on("close", (code) => {
|
|
353
|
+
log.info(`${key} disconnected from gw. code: ${code}`);
|
|
354
|
+
client.close();
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
return {
|
|
358
|
+
close: async () => {
|
|
359
|
+
server.wss.close();
|
|
360
|
+
await this.stop();
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
var core_default = create;
|
|
365
|
+
|
|
366
|
+
// src/mesh/connections.ts
|
|
367
|
+
var logger = getLogger("mesh.connections");
|
|
368
|
+
var InMemoryNodeConnections = class {
|
|
369
|
+
constructor(timeout = 6e4) {
|
|
370
|
+
this.timeout = timeout;
|
|
371
|
+
}
|
|
372
|
+
nodes = /* @__PURE__ */ new Map();
|
|
373
|
+
nodesByEndpoint = /* @__PURE__ */ new Map();
|
|
374
|
+
memberIds = 0;
|
|
375
|
+
announce(nodes) {
|
|
376
|
+
for (const node of nodes) {
|
|
377
|
+
const { node: nodeId, users, endpoint } = node;
|
|
378
|
+
const foundId = this.nodesByEndpoint.get(endpoint);
|
|
379
|
+
if (foundId) {
|
|
380
|
+
if (foundId !== nodeId) {
|
|
381
|
+
logger.warn(`endpoint ${endpoint} clash. replacing node ${foundId} with ${nodeId}`);
|
|
382
|
+
this.nodesByEndpoint.set(endpoint, nodeId);
|
|
383
|
+
this.nodes.delete(foundId);
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
logger.info(`endpoint ${endpoint} announced for ${nodeId}`);
|
|
387
|
+
this.nodesByEndpoint.set(endpoint, nodeId);
|
|
388
|
+
}
|
|
389
|
+
this.nodes.set(nodeId, this.updateNode(node, new Set(users ?? []), nodeId, this.nodes.get(nodeId)));
|
|
390
|
+
}
|
|
391
|
+
this.cleanupOldNodes();
|
|
392
|
+
const sortedNodes = Array.from(this.nodes.values()).sort((a, b) => a.memberId - b.memberId);
|
|
393
|
+
return nodes.map((e) => {
|
|
394
|
+
const node = e.node;
|
|
395
|
+
const connect = this.findConnections(sortedNodes, this.nodes.get(node));
|
|
396
|
+
return { node, connect };
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
remove(nodeId) {
|
|
400
|
+
const removed = this.nodes.get(nodeId);
|
|
401
|
+
if (removed) {
|
|
402
|
+
this.nodes.delete(nodeId);
|
|
403
|
+
const endpoint = removed.endpoint;
|
|
404
|
+
this.nodesByEndpoint.delete(endpoint);
|
|
405
|
+
logger.info(`endpoint ${endpoint} removed for ${nodeId}`);
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
updateNode(newNode, users, _key, oldNode) {
|
|
411
|
+
const node = !oldNode ? { ...newNode, memberId: this.memberIds++ } : oldNode;
|
|
412
|
+
return { ...node, users, lastAccess: Date.now() };
|
|
413
|
+
}
|
|
414
|
+
cleanupOldNodes() {
|
|
415
|
+
const threshold = Date.now() - this.timeout;
|
|
416
|
+
for (const [nodeId, v] of this.nodes) {
|
|
417
|
+
if (v.lastAccess < threshold) {
|
|
418
|
+
if (logger.enabledFor("debug")) {
|
|
419
|
+
logger.debug(`${nodeId} expired - no announcement since ${new Date(v.lastAccess).toISOString()}, timeout is ${this.timeout} ms.`);
|
|
420
|
+
}
|
|
421
|
+
this.nodes.delete(nodeId);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
findConnections(sortedNodes, node) {
|
|
426
|
+
return sortedNodes.reduce((l, c) => {
|
|
427
|
+
if (node !== void 0 && c.memberId < node.memberId) {
|
|
428
|
+
const intersection = new Set(c.users);
|
|
429
|
+
node.users.forEach((user) => {
|
|
430
|
+
if (!c.users.has(user)) {
|
|
431
|
+
intersection.delete(user);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
c.users.forEach((user) => {
|
|
435
|
+
if (!node.users.has(user)) {
|
|
436
|
+
intersection.delete(user);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
if (intersection.size > 0) {
|
|
440
|
+
const e = { node: c.node, endpoint: c.endpoint };
|
|
441
|
+
return l.concat(e);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return l;
|
|
445
|
+
}, new Array());
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// src/mesh/rest-directory/routes.ts
|
|
450
|
+
function routes(connections) {
|
|
451
|
+
return [
|
|
452
|
+
async (ctx, next) => {
|
|
453
|
+
if (ctx.method === "POST" && ctx.path === "/api/nodes") {
|
|
454
|
+
const json = await ctx.request.json;
|
|
455
|
+
if (!Array.isArray(json)) {
|
|
456
|
+
ctx.response.statusCode = 400;
|
|
457
|
+
ctx.response._res.end();
|
|
458
|
+
} else {
|
|
459
|
+
const nodes = json;
|
|
460
|
+
const result = connections.announce(nodes);
|
|
461
|
+
const body = JSON.stringify(result);
|
|
462
|
+
ctx.response.headers.set("content-type", "application/json");
|
|
463
|
+
ctx.response.statusCode = 200;
|
|
464
|
+
ctx.response._res.end(body);
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
await next();
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
async ({ method, path, response }, next) => {
|
|
471
|
+
if (method === "DELETE" && path?.startsWith("/api/nodes/")) {
|
|
472
|
+
const nodeId = path?.substring("/api/nodes/".length);
|
|
473
|
+
connections.remove(nodeId);
|
|
474
|
+
response.statusCode = 200;
|
|
475
|
+
response._res.end();
|
|
476
|
+
} else {
|
|
477
|
+
await next();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
];
|
|
481
|
+
}
|
|
482
|
+
var routes_default = routes;
|
|
483
|
+
|
|
484
|
+
// src/mesh/ws/broker/core.ts
|
|
485
|
+
import { IOGateway as IOGateway3 } from "@interopio/gateway";
|
|
486
|
+
var GatewayEncoders2 = IOGateway3.Encoding;
|
|
487
|
+
var logger2 = getLogger("mesh.ws.broker");
|
|
488
|
+
function broadcastNodeAdded(nodes, newSocket, newNodeId) {
|
|
489
|
+
Object.entries(nodes.nodes).forEach(([nodeId, socket]) => {
|
|
490
|
+
if (nodeId !== newNodeId) {
|
|
491
|
+
newSocket.send(codec2.encode({ type: "node-added", "node-id": newNodeId, "new-node": nodeId }));
|
|
492
|
+
socket.send(codec2.encode({ type: "node-added", "node-id": nodeId, "new-node": newNodeId }));
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
function broadcastNodeRemoved(nodes, removedNodeId) {
|
|
497
|
+
Object.entries(nodes.nodes).forEach(([nodeId, socket]) => {
|
|
498
|
+
if (nodeId !== removedNodeId) {
|
|
499
|
+
socket.send(codec2.encode({ type: "node-removed", "node-id": nodeId, "removed-node": removedNodeId }));
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
function onOpen(connectedNodes, key) {
|
|
504
|
+
logger2.info(`[${key}] connection accepted`);
|
|
505
|
+
}
|
|
506
|
+
function onClose(connectedNodes, key, code, reason) {
|
|
507
|
+
logger2.info(`[${key}] connected closed [${code}](${reason})`);
|
|
508
|
+
const nodeIds = connectedNodes.sockets[key];
|
|
509
|
+
if (nodeIds) {
|
|
510
|
+
delete connectedNodes.sockets[key];
|
|
511
|
+
for (const nodeId of nodeIds) {
|
|
512
|
+
delete connectedNodes.nodes[nodeId];
|
|
513
|
+
}
|
|
514
|
+
for (const nodeId of nodeIds) {
|
|
515
|
+
broadcastNodeRemoved(connectedNodes, nodeId);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function processMessage(connectedNodes, socket, key, msg) {
|
|
520
|
+
switch (msg.type) {
|
|
521
|
+
case "hello": {
|
|
522
|
+
const nodeId = msg["node-id"];
|
|
523
|
+
connectedNodes.nodes[nodeId] = socket;
|
|
524
|
+
connectedNodes.sockets[key] = connectedNodes.sockets[key] ?? [];
|
|
525
|
+
connectedNodes.sockets[key].push(nodeId);
|
|
526
|
+
logger2.info(`[${key}] node ${nodeId} added.`);
|
|
527
|
+
broadcastNodeAdded(connectedNodes, socket, nodeId);
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
case "bye": {
|
|
531
|
+
const nodeId = msg["node-id"];
|
|
532
|
+
delete connectedNodes[nodeId];
|
|
533
|
+
logger2.info(`[${key}] node ${nodeId} removed.`);
|
|
534
|
+
broadcastNodeRemoved(connectedNodes, nodeId);
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
case "data": {
|
|
538
|
+
const sourceNodeId = msg.from;
|
|
539
|
+
const targetNodeId = msg.to;
|
|
540
|
+
if ("all" === targetNodeId) {
|
|
541
|
+
Object.entries(connectedNodes.nodes).forEach(([nodeId, socket2]) => {
|
|
542
|
+
if (nodeId !== sourceNodeId) {
|
|
543
|
+
socket2.send(codec2.encode(msg));
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
} else {
|
|
547
|
+
const socket2 = connectedNodes.nodes[targetNodeId];
|
|
548
|
+
if (socket2) {
|
|
549
|
+
socket2.send(codec2.encode(msg));
|
|
550
|
+
} else {
|
|
551
|
+
logger2.warn(`unable to send to node ${targetNodeId} message ${JSON.stringify(msg)}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
default: {
|
|
557
|
+
logger2.warn(`[${key}] ignoring unknown message ${JSON.stringify(msg)}`);
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
var codec2 = GatewayEncoders2.transit({
|
|
563
|
+
keywordize: /* @__PURE__ */ new Map([
|
|
564
|
+
["/type", "*"],
|
|
565
|
+
["/message/body/type", "*"],
|
|
566
|
+
["/message/origin", "*"],
|
|
567
|
+
["/message/receiver/type", "*"],
|
|
568
|
+
["/message/source/type", "*"],
|
|
569
|
+
["/message/body/type", "*"]
|
|
570
|
+
])
|
|
571
|
+
});
|
|
572
|
+
function onMessage(connectedNodes, socket, key, msg) {
|
|
573
|
+
try {
|
|
574
|
+
const decoded = codec2.decode(msg);
|
|
575
|
+
if (logger2.enabledFor("debug")) {
|
|
576
|
+
logger2.debug(`[${key}] processing msg ${JSON.stringify(decoded)}`);
|
|
577
|
+
}
|
|
578
|
+
processMessage(connectedNodes, socket, key, decoded);
|
|
579
|
+
} catch (ex) {
|
|
580
|
+
logger2.error(`[${key}] unable to process message`, ex);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
var WebsocketBroker = class {
|
|
584
|
+
constructor(server) {
|
|
585
|
+
this.server = server;
|
|
586
|
+
}
|
|
587
|
+
async close() {
|
|
588
|
+
this.server.close();
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
async function create2(server) {
|
|
592
|
+
const connectedNodes = { nodes: {}, sockets: {} };
|
|
593
|
+
logger2.info(`mesh server is listening`);
|
|
594
|
+
server.wss.on("error", () => {
|
|
595
|
+
logger2.error(`error starting mesh server`);
|
|
596
|
+
}).on("connection", (socket, request) => {
|
|
597
|
+
const key = socketKey(request.socket);
|
|
598
|
+
onOpen(connectedNodes, key);
|
|
599
|
+
socket.on("error", (err) => {
|
|
600
|
+
logger2.error(`[${key}] websocket error: ${err}`, err);
|
|
601
|
+
});
|
|
602
|
+
socket.on("message", (data, isBinary) => {
|
|
603
|
+
if (Array.isArray(data)) {
|
|
604
|
+
data = Buffer.concat(data);
|
|
605
|
+
}
|
|
606
|
+
onMessage(connectedNodes, socket, key, data);
|
|
607
|
+
});
|
|
608
|
+
socket.on("close", (code, reason) => {
|
|
609
|
+
onClose(connectedNodes, key, code, reason);
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
return new WebsocketBroker(server.wss);
|
|
613
|
+
}
|
|
614
|
+
var core_default2 = create2;
|
|
615
|
+
|
|
616
|
+
// src/mesh/ws/relays/core.ts
|
|
617
|
+
import { IOGateway as IOGateway4 } from "@interopio/gateway";
|
|
618
|
+
var GatewayEncoders3 = IOGateway4.Encoding;
|
|
619
|
+
var logger3 = getLogger("mesh.ws.relay");
|
|
620
|
+
var codec3 = GatewayEncoders3.transit({
|
|
621
|
+
keywordize: /* @__PURE__ */ new Map([
|
|
622
|
+
["/type", "*"],
|
|
623
|
+
["/message/body/type", "*"],
|
|
624
|
+
["/message/origin", "*"],
|
|
625
|
+
["/message/receiver/type", "*"],
|
|
626
|
+
["/message/source/type", "*"],
|
|
627
|
+
["/message/body/type", "*"]
|
|
628
|
+
])
|
|
629
|
+
});
|
|
630
|
+
var InternalRelays = class {
|
|
631
|
+
// key -> socket
|
|
632
|
+
clients = /* @__PURE__ */ new Map();
|
|
633
|
+
// node -> key
|
|
634
|
+
links = /* @__PURE__ */ new Map();
|
|
635
|
+
onMsg;
|
|
636
|
+
onErr;
|
|
637
|
+
add(key, soc) {
|
|
638
|
+
this.clients.set(key, soc);
|
|
639
|
+
}
|
|
640
|
+
remove(key) {
|
|
641
|
+
this.clients.delete(key);
|
|
642
|
+
for (const [node, k] of this.links) {
|
|
643
|
+
if (k === key) {
|
|
644
|
+
this.links.delete(node);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
receive(key, msg) {
|
|
649
|
+
const node = this.link(key, msg);
|
|
650
|
+
if (node && this.onMsg) {
|
|
651
|
+
this.onMsg(key, node, msg);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
link(key, msg) {
|
|
655
|
+
try {
|
|
656
|
+
const decoded = codec3.decode(msg);
|
|
657
|
+
const { type, from, to } = decoded;
|
|
658
|
+
if (to === "all") {
|
|
659
|
+
switch (type) {
|
|
660
|
+
case "hello": {
|
|
661
|
+
if (logger3.enabledFor("debug")) {
|
|
662
|
+
logger3.debug(`${key} registers node ${from}`);
|
|
663
|
+
}
|
|
664
|
+
this.links.set(from, key);
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
case "bye": {
|
|
668
|
+
if (logger3.enabledFor("debug")) {
|
|
669
|
+
logger3.debug(`${key} unregisters node ${from}`);
|
|
670
|
+
}
|
|
671
|
+
this.links.delete(from);
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
return from;
|
|
678
|
+
} catch (e) {
|
|
679
|
+
if (this.onErr) {
|
|
680
|
+
this.onErr(key, e instanceof Error ? e : new Error(`link failed :${e}`));
|
|
681
|
+
} else {
|
|
682
|
+
logger3.warn(`${key} unable to process ${msg}`, e);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
send(key, node, msg, cb) {
|
|
687
|
+
const decoded = codec3.decode(msg);
|
|
688
|
+
if (logger3.enabledFor("debug")) {
|
|
689
|
+
logger3.debug(`${key} sending msg to ${node} ${JSON.stringify(decoded)}`);
|
|
690
|
+
}
|
|
691
|
+
const clientKey = this.links.get(node);
|
|
692
|
+
if (clientKey) {
|
|
693
|
+
const client = this.clients.get(clientKey);
|
|
694
|
+
if (client) {
|
|
695
|
+
client.send(msg, { binary: false }, (err) => {
|
|
696
|
+
cb(clientKey, err);
|
|
697
|
+
});
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
throw new Error(`${key} no active link for ${decoded.to}`);
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
var internal = new InternalRelays();
|
|
705
|
+
var relays = internal;
|
|
706
|
+
async function create3(server) {
|
|
707
|
+
logger3.info(`relays server is listening`);
|
|
708
|
+
server.wss.on("error", () => {
|
|
709
|
+
logger3.error(`error starting relays server`);
|
|
710
|
+
}).on("connection", (socket, request) => {
|
|
711
|
+
const key = socketKey(request.socket);
|
|
712
|
+
logger3.info(`${key} connected on relays`);
|
|
713
|
+
internal.add(key, socket);
|
|
714
|
+
socket.on("error", (err) => {
|
|
715
|
+
logger3.error(`[${key}] websocket error: ${err}`, err);
|
|
716
|
+
});
|
|
717
|
+
socket.on("message", (data, _isBinary) => {
|
|
718
|
+
if (Array.isArray(data)) {
|
|
719
|
+
data = Buffer.concat(data);
|
|
720
|
+
}
|
|
721
|
+
try {
|
|
722
|
+
internal.receive(key, data);
|
|
723
|
+
} catch (e) {
|
|
724
|
+
logger3.warn(`[${key}] error processing received data '${data}'`, e);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
socket.on("close", (code, reason) => {
|
|
728
|
+
internal.remove(key);
|
|
729
|
+
logger3.info(`${key} disconnected from relays`);
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
return {
|
|
733
|
+
close: async () => {
|
|
734
|
+
server.wss.close();
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
var core_default3 = create3;
|
|
739
|
+
|
|
740
|
+
// src/mesh/ws/cluster/core.ts
|
|
741
|
+
var logger4 = getLogger("mesh.ws.cluster");
|
|
742
|
+
function onMessage2(key, node, socketsByNodeId, msg) {
|
|
743
|
+
try {
|
|
744
|
+
relays.send(key, node, msg, (k, err) => {
|
|
745
|
+
if (err) {
|
|
746
|
+
logger4.warn(`${k} error writing msg ${msg}: ${err}`);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (logger4.enabledFor("debug")) {
|
|
750
|
+
logger4.debug(`${k} sent msg ${msg}`);
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
} catch (ex) {
|
|
754
|
+
logger4.error(`${key} unable to process message`, ex);
|
|
755
|
+
if (node) {
|
|
756
|
+
const socket = socketsByNodeId.get(node)?.get(key);
|
|
757
|
+
socket?.terminate();
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async function create4(server) {
|
|
762
|
+
const socketsByNodeId = /* @__PURE__ */ new Map();
|
|
763
|
+
relays.onMsg = (k, nodeId, msg) => {
|
|
764
|
+
try {
|
|
765
|
+
const sockets = socketsByNodeId.get(nodeId);
|
|
766
|
+
if (sockets && sockets.size > 0) {
|
|
767
|
+
for (const [key, socket] of sockets) {
|
|
768
|
+
socket.send(msg, { binary: false }, (err) => {
|
|
769
|
+
if (err) {
|
|
770
|
+
logger4.warn(`${key} error writing from ${k} msg ${msg}: ${err}`);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (logger4.enabledFor("debug")) {
|
|
774
|
+
logger4.debug(`${key} sent from ${k} msg ${msg}`);
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
} else {
|
|
779
|
+
logger4.warn(`${k} dropped msg ${msg}.`);
|
|
780
|
+
}
|
|
781
|
+
} catch (ex) {
|
|
782
|
+
logger4.error(`${k} unable to process message`, ex);
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
server.wss.on("error", () => {
|
|
786
|
+
logger4.error(`error starting mesh server`);
|
|
787
|
+
}).on("listening", () => {
|
|
788
|
+
logger4.info(`mesh server is listening`);
|
|
789
|
+
}).on("connection", (socket, req) => {
|
|
790
|
+
const request = new HttpServerRequest(req);
|
|
791
|
+
const key = socketKey(request.socket);
|
|
792
|
+
const query = new URLSearchParams(request.query ?? void 0);
|
|
793
|
+
logger4.info(`${key} connected on cluster with ${query}`);
|
|
794
|
+
const node = query.get("node");
|
|
795
|
+
if (node) {
|
|
796
|
+
let sockets = socketsByNodeId.get(node);
|
|
797
|
+
if (!sockets) {
|
|
798
|
+
sockets = /* @__PURE__ */ new Map();
|
|
799
|
+
socketsByNodeId.set(node, sockets);
|
|
800
|
+
}
|
|
801
|
+
sockets.set(key, socket);
|
|
802
|
+
} else {
|
|
803
|
+
socket.terminate();
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
socket.on("error", (err) => {
|
|
807
|
+
logger4.error(`${key} websocket error: ${err}`, err);
|
|
808
|
+
});
|
|
809
|
+
socket.on("message", (data, _isBinary) => {
|
|
810
|
+
if (Array.isArray(data)) {
|
|
811
|
+
data = Buffer.concat(data);
|
|
812
|
+
}
|
|
813
|
+
onMessage2(key, node, socketsByNodeId, data);
|
|
814
|
+
});
|
|
815
|
+
socket.on("close", (_code, _reason) => {
|
|
816
|
+
logger4.info(`${key} disconnected from cluster`);
|
|
817
|
+
const sockets = socketsByNodeId.get(node);
|
|
818
|
+
if (sockets) {
|
|
819
|
+
sockets.delete(key);
|
|
820
|
+
if (sockets.size === 0) {
|
|
821
|
+
socketsByNodeId.delete(node);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
return {
|
|
827
|
+
close: async () => {
|
|
828
|
+
server.wss.close();
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
var core_default4 = create4;
|
|
833
|
+
|
|
834
|
+
// src/metrics/routes.ts
|
|
835
|
+
var logger5 = getLogger("metrics");
|
|
836
|
+
var COOKIE_NAME = "GW_LOGIN";
|
|
837
|
+
function loggedIn(auth, ctx) {
|
|
838
|
+
if (auth) {
|
|
839
|
+
const cookieHeaderValue = ctx.request.headers.list("cookie");
|
|
840
|
+
const cookie = cookieHeaderValue?.join("; ").split("; ").find((value) => value.startsWith(`${COOKIE_NAME}=`));
|
|
841
|
+
return cookie && parseInt(cookie?.substring(COOKIE_NAME.length + 1)) > Date.now();
|
|
842
|
+
}
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
async function routes2(config) {
|
|
846
|
+
const { jsonFileAppender } = await import("@interopio/gateway/metrics/publisher/file");
|
|
847
|
+
const appender = jsonFileAppender(logger5);
|
|
848
|
+
await appender.open(config.file?.location ?? "metrics.ndjson", config.file?.append ?? true);
|
|
849
|
+
return [
|
|
850
|
+
async (ctx, next) => {
|
|
851
|
+
if (ctx.method === "GET" && ctx.path === "/api/metrics") {
|
|
852
|
+
if (loggedIn(config.auth, ctx)) {
|
|
853
|
+
ctx.response.statusCode = 200;
|
|
854
|
+
} else {
|
|
855
|
+
ctx.response.statusCode = 302;
|
|
856
|
+
ctx.response.headers.set("location", "/api/login?redirectTo=/api/metrics");
|
|
857
|
+
}
|
|
858
|
+
ctx.response._res.end();
|
|
859
|
+
} else {
|
|
860
|
+
await next();
|
|
861
|
+
}
|
|
862
|
+
},
|
|
863
|
+
async (ctx, next) => {
|
|
864
|
+
if (ctx.method === "GET" && ctx.path === "/api/login") {
|
|
865
|
+
const redirectTo = new URLSearchParams(ctx.request.query ?? void 0).get("redirectTo");
|
|
866
|
+
const expires = Date.now() + 180 * 1e3;
|
|
867
|
+
ctx.response.headers.add("set-cookie", `${COOKIE_NAME}=${expires}; Path=/api; SameSite=strict`);
|
|
868
|
+
if (redirectTo) {
|
|
869
|
+
ctx.response.statusCode = 302;
|
|
870
|
+
ctx.response.headers.set("location", redirectTo);
|
|
871
|
+
} else {
|
|
872
|
+
ctx.response.statusCode = 200;
|
|
873
|
+
}
|
|
874
|
+
ctx.response._res.end();
|
|
875
|
+
} else {
|
|
876
|
+
await next();
|
|
877
|
+
}
|
|
878
|
+
},
|
|
879
|
+
async (ctx, next) => {
|
|
880
|
+
if (ctx.method === "POST" && ctx.path === "/api/metrics") {
|
|
881
|
+
if (loggedIn(config.auth, ctx)) {
|
|
882
|
+
ctx.response.statusCode = 202;
|
|
883
|
+
ctx.response._res.end();
|
|
884
|
+
try {
|
|
885
|
+
const json = await ctx.request.json;
|
|
886
|
+
const update = json;
|
|
887
|
+
if (logger5.enabledFor("debug")) {
|
|
888
|
+
logger5.debug(`${JSON.stringify(update)}`);
|
|
889
|
+
}
|
|
890
|
+
if ((config.file?.status ?? false) || update.status === void 0) {
|
|
891
|
+
await appender.write(update);
|
|
892
|
+
}
|
|
893
|
+
} catch (e) {
|
|
894
|
+
logger5.error(`error processing metrics`, e);
|
|
895
|
+
}
|
|
896
|
+
} else {
|
|
897
|
+
ctx.response.statusCode = 401;
|
|
898
|
+
ctx.response._res.end();
|
|
899
|
+
}
|
|
900
|
+
} else {
|
|
901
|
+
await next();
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
];
|
|
905
|
+
}
|
|
906
|
+
var routes_default2 = routes2;
|
|
907
|
+
|
|
908
|
+
// src/common/compose.ts
|
|
909
|
+
function compose(...middleware) {
|
|
910
|
+
if (!Array.isArray(middleware)) {
|
|
911
|
+
throw new Error("middleware must be array!");
|
|
912
|
+
}
|
|
913
|
+
for (const fn of middleware) {
|
|
914
|
+
if (typeof fn !== "function") {
|
|
915
|
+
throw new Error("middleware must be compose of functions!");
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return async function(ctx, next) {
|
|
919
|
+
let index = -1;
|
|
920
|
+
return await dispatch(0);
|
|
921
|
+
async function dispatch(i) {
|
|
922
|
+
if (i < index) {
|
|
923
|
+
throw new Error("next() called multiple times");
|
|
924
|
+
}
|
|
925
|
+
index = i;
|
|
926
|
+
let fn;
|
|
927
|
+
if (i === middleware.length) {
|
|
928
|
+
fn = next;
|
|
929
|
+
} else {
|
|
930
|
+
fn = middleware[i];
|
|
931
|
+
}
|
|
932
|
+
if (!fn) {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
await fn(ctx, dispatch.bind(null, i + 1));
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// src/server/address.ts
|
|
941
|
+
import { networkInterfaces } from "node:os";
|
|
942
|
+
var PORT_RANGE_MATCHER = /^(\d+|(0x[\da-f]+))(-(\d+|(0x[\da-f]+)))?$/i;
|
|
943
|
+
function validPort(port) {
|
|
944
|
+
if (port > 65535) throw new Error(`bad port ${port}`);
|
|
945
|
+
return port;
|
|
946
|
+
}
|
|
947
|
+
function* portRange(port) {
|
|
948
|
+
if (typeof port === "string") {
|
|
949
|
+
for (const portRange2 of port.split(",")) {
|
|
950
|
+
const trimmed = portRange2.trim();
|
|
951
|
+
const matchResult = PORT_RANGE_MATCHER.exec(trimmed);
|
|
952
|
+
if (matchResult) {
|
|
953
|
+
const start2 = parseInt(matchResult[1]);
|
|
954
|
+
const end = parseInt(matchResult[4] ?? matchResult[1]);
|
|
955
|
+
for (let i = validPort(start2); i < validPort(end) + 1; i++) {
|
|
956
|
+
yield i;
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
throw new Error(`'${portRange2}' is not a valid port or range.`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
} else {
|
|
963
|
+
yield validPort(port);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
var localIp = (() => {
|
|
967
|
+
function first(a) {
|
|
968
|
+
return a.length > 0 ? a[0] : void 0;
|
|
969
|
+
}
|
|
970
|
+
const addresses = Object.values(networkInterfaces()).flatMap((details) => {
|
|
971
|
+
return (details ?? []).filter((info) => info.family === "IPv4");
|
|
972
|
+
}).reduce((acc, info) => {
|
|
973
|
+
acc[info.internal ? "internal" : "external"].push(info);
|
|
974
|
+
return acc;
|
|
975
|
+
}, { internal: [], external: [] });
|
|
976
|
+
return (first(addresses.internal) ?? first(addresses.external))?.address;
|
|
977
|
+
})();
|
|
978
|
+
|
|
979
|
+
// src/server/monitoring.ts
|
|
980
|
+
import { getHeapStatistics, writeHeapSnapshot } from "node:v8";
|
|
981
|
+
import { access, mkdir, rename, unlink } from "node:fs/promises";
|
|
982
|
+
var log2 = getLogger("monitoring");
|
|
983
|
+
var DEFAULT_OPTIONS = {
|
|
984
|
+
memoryLimit: 1024 * 1024 * 1024,
|
|
985
|
+
// 1GB
|
|
986
|
+
reportInterval: 10 * 60 * 1e3,
|
|
987
|
+
// 10 min
|
|
988
|
+
dumpLocation: ".",
|
|
989
|
+
// current folder
|
|
990
|
+
maxBackups: 10,
|
|
991
|
+
dumpPrefix: "Heap"
|
|
992
|
+
};
|
|
993
|
+
function fetchStats() {
|
|
994
|
+
return getHeapStatistics();
|
|
995
|
+
}
|
|
996
|
+
async function dumpHeap(opts) {
|
|
997
|
+
const prefix = opts.dumpPrefix ?? "Heap";
|
|
998
|
+
const target = `${opts.dumpLocation}/${prefix}.heapsnapshot`;
|
|
999
|
+
if (log2.enabledFor("debug")) {
|
|
1000
|
+
log2.debug(`starting heap dump in ${target}`);
|
|
1001
|
+
}
|
|
1002
|
+
await fileExists(opts.dumpLocation).catch(async (_) => {
|
|
1003
|
+
if (log2.enabledFor("debug")) {
|
|
1004
|
+
log2.debug(`dump location ${opts.dumpLocation} does not exists. Will try to create it`);
|
|
1005
|
+
}
|
|
1006
|
+
try {
|
|
1007
|
+
await mkdir(opts.dumpLocation, { recursive: true });
|
|
1008
|
+
log2.info(`dump location dir ${opts.dumpLocation} successfully created`);
|
|
1009
|
+
} catch (e) {
|
|
1010
|
+
log2.error(`failed to create dump location ${opts.dumpLocation}`);
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
const dumpFileName = writeHeapSnapshot(target);
|
|
1014
|
+
log2.info(`heap dumped`);
|
|
1015
|
+
try {
|
|
1016
|
+
log2.debug(`rolling snapshot backups`);
|
|
1017
|
+
const lastFileName = `${opts.dumpLocation}/${prefix}.${opts.maxBackups}.heapsnapshot`;
|
|
1018
|
+
await fileExists(lastFileName).then(async () => {
|
|
1019
|
+
if (log2.enabledFor("debug")) {
|
|
1020
|
+
log2.debug(`deleting ${lastFileName}`);
|
|
1021
|
+
}
|
|
1022
|
+
try {
|
|
1023
|
+
await unlink(lastFileName);
|
|
1024
|
+
} catch (e) {
|
|
1025
|
+
log2.warn(`failed to delete ${lastFileName}`, e);
|
|
1026
|
+
}
|
|
1027
|
+
}).catch(() => {
|
|
1028
|
+
});
|
|
1029
|
+
for (let i = opts.maxBackups - 1; i > 0; i--) {
|
|
1030
|
+
const currentFileName = `${opts.dumpLocation}/${prefix}.${i}.heapsnapshot`;
|
|
1031
|
+
const nextFileName = `${opts.dumpLocation}/${prefix}.${i + 1}.heapsnapshot`;
|
|
1032
|
+
await fileExists(currentFileName).then(async () => {
|
|
1033
|
+
try {
|
|
1034
|
+
await rename(currentFileName, nextFileName);
|
|
1035
|
+
} catch (e) {
|
|
1036
|
+
log2.warn(`failed to rename ${currentFileName} to ${nextFileName}`, e);
|
|
1037
|
+
}
|
|
1038
|
+
}).catch(() => {
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
const firstFileName = `${opts.dumpLocation}/${prefix}.${1}.heapsnapshot`;
|
|
1042
|
+
try {
|
|
1043
|
+
await rename(dumpFileName, firstFileName);
|
|
1044
|
+
} catch (e) {
|
|
1045
|
+
log2.warn(`failed to rename ${dumpFileName} to ${firstFileName}`, e);
|
|
1046
|
+
}
|
|
1047
|
+
log2.debug("snapshots rolled");
|
|
1048
|
+
} catch (e) {
|
|
1049
|
+
log2.error("error rolling backups", e);
|
|
1050
|
+
throw e;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
async function fileExists(path) {
|
|
1054
|
+
log2.trace(`checking file ${path}`);
|
|
1055
|
+
await access(path);
|
|
1056
|
+
}
|
|
1057
|
+
async function processStats(stats, state, opts) {
|
|
1058
|
+
if (log2.enabledFor("debug")) {
|
|
1059
|
+
log2.debug(`processing heap stats ${JSON.stringify(stats)}`);
|
|
1060
|
+
}
|
|
1061
|
+
const limit = Math.min(opts.memoryLimit, 0.95 * stats.heap_size_limit);
|
|
1062
|
+
const used = stats.used_heap_size;
|
|
1063
|
+
log2.info(`heap stats ${JSON.stringify(stats)}`);
|
|
1064
|
+
if (used >= limit) {
|
|
1065
|
+
log2.warn(`used heap ${used} bytes exceeds memory limit ${limit} bytes`);
|
|
1066
|
+
if (state.memoryLimitExceeded) {
|
|
1067
|
+
delete state.snapshot;
|
|
1068
|
+
} else {
|
|
1069
|
+
state.memoryLimitExceeded = true;
|
|
1070
|
+
state.snapshot = true;
|
|
1071
|
+
}
|
|
1072
|
+
await dumpHeap(opts);
|
|
1073
|
+
} else {
|
|
1074
|
+
state.memoryLimitExceeded = false;
|
|
1075
|
+
delete state.snapshot;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function start(opts) {
|
|
1079
|
+
const merged = { ...DEFAULT_OPTIONS, ...opts };
|
|
1080
|
+
let stopped = false;
|
|
1081
|
+
const state = { memoryLimitExceeded: false };
|
|
1082
|
+
const report = async () => {
|
|
1083
|
+
const stats = fetchStats();
|
|
1084
|
+
await processStats(stats, state, merged);
|
|
1085
|
+
};
|
|
1086
|
+
const interval = setInterval(report, merged.reportInterval);
|
|
1087
|
+
const channel = async (command) => {
|
|
1088
|
+
if (!stopped) {
|
|
1089
|
+
command ??= "run";
|
|
1090
|
+
switch (command) {
|
|
1091
|
+
case "run": {
|
|
1092
|
+
await report();
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
case "dump": {
|
|
1096
|
+
await dumpHeap(merged);
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
case "stop": {
|
|
1100
|
+
stopped = true;
|
|
1101
|
+
clearInterval(interval);
|
|
1102
|
+
log2.info("exit memory diagnostic");
|
|
1103
|
+
break;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return stopped;
|
|
1108
|
+
};
|
|
1109
|
+
return { ...merged, channel };
|
|
1110
|
+
}
|
|
1111
|
+
async function run({ channel }, command) {
|
|
1112
|
+
if (!await channel(command)) {
|
|
1113
|
+
log2.warn(`cannot execute command "${command}" already closed`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
async function stop(m) {
|
|
1117
|
+
return await run(m, "stop");
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/server/ws-client-verify.ts
|
|
1121
|
+
import { IOGateway as IOGateway5 } from "@interopio/gateway";
|
|
1122
|
+
var log3 = getLogger("gateway.ws.client-verify");
|
|
1123
|
+
function acceptsMissing(originFilters) {
|
|
1124
|
+
switch (originFilters.missing) {
|
|
1125
|
+
case "allow":
|
|
1126
|
+
// fall-through
|
|
1127
|
+
case "whitelist":
|
|
1128
|
+
return true;
|
|
1129
|
+
case "block":
|
|
1130
|
+
// fall-through
|
|
1131
|
+
case "blacklist":
|
|
1132
|
+
return false;
|
|
1133
|
+
default:
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function tryMatch(originFilters, origin) {
|
|
1138
|
+
const block = originFilters.block ?? originFilters["blacklist"];
|
|
1139
|
+
const allow = originFilters.allow ?? originFilters["whitelist"];
|
|
1140
|
+
if (block.length > 0 && IOGateway5.Filtering.valuesMatch(block, origin)) {
|
|
1141
|
+
log3.warn(`origin ${origin} matches block filter`);
|
|
1142
|
+
return false;
|
|
1143
|
+
} else if (allow.length > 0 && IOGateway5.Filtering.valuesMatch(allow, origin)) {
|
|
1144
|
+
if (log3.enabledFor("debug")) {
|
|
1145
|
+
log3.debug(`origin ${origin} matches allow filter`);
|
|
1146
|
+
}
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function acceptsNonMatched(originFilters) {
|
|
1151
|
+
switch (originFilters.non_matched) {
|
|
1152
|
+
case "allow":
|
|
1153
|
+
// fall-through
|
|
1154
|
+
case "whitelist":
|
|
1155
|
+
return true;
|
|
1156
|
+
case "block":
|
|
1157
|
+
// fall-through
|
|
1158
|
+
case "blacklist":
|
|
1159
|
+
return false;
|
|
1160
|
+
default:
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
function acceptsOrigin(origin, originFilters) {
|
|
1165
|
+
if (!originFilters) {
|
|
1166
|
+
return true;
|
|
1167
|
+
}
|
|
1168
|
+
if (!origin) {
|
|
1169
|
+
return acceptsMissing(originFilters);
|
|
1170
|
+
} else {
|
|
1171
|
+
const matchResult = tryMatch(originFilters, origin);
|
|
1172
|
+
if (matchResult) {
|
|
1173
|
+
return matchResult;
|
|
1174
|
+
} else {
|
|
1175
|
+
return acceptsNonMatched(originFilters);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
function regexifyOriginFilters(originFilters) {
|
|
1180
|
+
if (originFilters) {
|
|
1181
|
+
const block = (originFilters.block ?? originFilters.blacklist ?? []).map(IOGateway5.Filtering.regexify);
|
|
1182
|
+
const allow = (originFilters.allow ?? originFilters.whitelist ?? []).map(IOGateway5.Filtering.regexify);
|
|
1183
|
+
return {
|
|
1184
|
+
non_matched: originFilters.non_matched ?? "allow",
|
|
1185
|
+
missing: originFilters.missing ?? "allow",
|
|
1186
|
+
allow,
|
|
1187
|
+
block
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// src/server/cors.ts
|
|
1193
|
+
import { IOGateway as IOGateway6 } from "@interopio/gateway";
|
|
1194
|
+
function isSameOrigin(request) {
|
|
1195
|
+
const origin = request.headers.one("origin");
|
|
1196
|
+
if (origin === void 0) {
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
const url = request.URL;
|
|
1200
|
+
const actualProtocol = url.protocol;
|
|
1201
|
+
const actualHost = url.host;
|
|
1202
|
+
const originUrl = new URL(origin);
|
|
1203
|
+
const originHost = originUrl.host;
|
|
1204
|
+
const originProtocol = originUrl.protocol;
|
|
1205
|
+
return actualProtocol === originProtocol && actualHost === originHost;
|
|
1206
|
+
}
|
|
1207
|
+
function isCorsRequest(request) {
|
|
1208
|
+
return request.headers.has("origin") && !isSameOrigin(request);
|
|
1209
|
+
}
|
|
1210
|
+
function isPreFlightRequest(request) {
|
|
1211
|
+
return request.method === "OPTIONS" && request.headers.has("origin") && request.headers.has("access-control-request-method");
|
|
1212
|
+
}
|
|
1213
|
+
var VARY_HEADERS = ["Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"];
|
|
1214
|
+
function processRequest(exchange, config) {
|
|
1215
|
+
const { request, response } = exchange;
|
|
1216
|
+
const responseHeaders = response.headers;
|
|
1217
|
+
if (!responseHeaders.has("Vary")) {
|
|
1218
|
+
responseHeaders.set("Vary", VARY_HEADERS.join(", "));
|
|
1219
|
+
} else {
|
|
1220
|
+
const varyHeaders = responseHeaders.list("Vary");
|
|
1221
|
+
for (const header of VARY_HEADERS) {
|
|
1222
|
+
if (!varyHeaders.find((h) => h === header)) {
|
|
1223
|
+
varyHeaders.push(header);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
responseHeaders.set("Vary", varyHeaders.join(", "));
|
|
1227
|
+
}
|
|
1228
|
+
try {
|
|
1229
|
+
if (!isCorsRequest(request)) {
|
|
1230
|
+
return true;
|
|
1231
|
+
}
|
|
1232
|
+
} catch (e) {
|
|
1233
|
+
if (logger6.enabledFor("debug")) {
|
|
1234
|
+
logger6.debug(`reject: origin is malformed`);
|
|
1235
|
+
}
|
|
1236
|
+
rejectRequest(response);
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
if (responseHeaders.has("access-control-allow-origin")) {
|
|
1240
|
+
logger6.trace(`skip: already contains "Access-Control-Allow-Origin"`);
|
|
1241
|
+
return true;
|
|
1242
|
+
}
|
|
1243
|
+
const preFlightRequest = isPreFlightRequest(request);
|
|
1244
|
+
if (config) {
|
|
1245
|
+
return handleInternal(exchange, config, preFlightRequest);
|
|
1246
|
+
}
|
|
1247
|
+
if (preFlightRequest) {
|
|
1248
|
+
rejectRequest(response);
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
function validateConfig(config) {
|
|
1254
|
+
if (config) {
|
|
1255
|
+
const headers = config.headers;
|
|
1256
|
+
if (headers?.allow && headers.allow !== ALL) {
|
|
1257
|
+
headers.allow = headers.allow.map((header) => header.toLowerCase());
|
|
1258
|
+
}
|
|
1259
|
+
const origins = config.origins;
|
|
1260
|
+
if (origins?.allow && origins.allow !== ALL) {
|
|
1261
|
+
origins.allow = origins.allow.map((origin) => {
|
|
1262
|
+
if (typeof origin === "string") {
|
|
1263
|
+
return origin.toLowerCase();
|
|
1264
|
+
}
|
|
1265
|
+
return origin;
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
return config;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
var handler = (config) => {
|
|
1272
|
+
validateConfig(config);
|
|
1273
|
+
return async (ctx, next) => {
|
|
1274
|
+
const isValid = processRequest(ctx, config);
|
|
1275
|
+
if (!isValid || isPreFlightRequest(ctx.request)) {
|
|
1276
|
+
ctx.response._res.end();
|
|
1277
|
+
} else {
|
|
1278
|
+
await next();
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
};
|
|
1282
|
+
var cors_default = (config) => [handler(config)];
|
|
1283
|
+
var logger6 = getLogger("cors");
|
|
1284
|
+
function rejectRequest(response) {
|
|
1285
|
+
response.statusCode = 403;
|
|
1286
|
+
}
|
|
1287
|
+
function handleInternal(exchange, config, preFlightRequest) {
|
|
1288
|
+
const { request, response } = exchange;
|
|
1289
|
+
const responseHeaders = response.headers;
|
|
1290
|
+
const requestOrigin = request.headers.one("origin");
|
|
1291
|
+
const allowOrigin = checkOrigin(config, requestOrigin);
|
|
1292
|
+
if (allowOrigin === void 0) {
|
|
1293
|
+
if (logger6.enabledFor("debug")) {
|
|
1294
|
+
logger6.debug(`reject: '${requestOrigin}' origin is not allowed`);
|
|
1295
|
+
}
|
|
1296
|
+
rejectRequest(response);
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
const requestMethod = getMethodToUse(request, preFlightRequest);
|
|
1300
|
+
const allowMethods = checkMethods(config, requestMethod);
|
|
1301
|
+
if (allowMethods === void 0) {
|
|
1302
|
+
if (logger6.enabledFor("debug")) {
|
|
1303
|
+
logger6.debug(`reject: HTTP '${requestMethod}' is not allowed`);
|
|
1304
|
+
}
|
|
1305
|
+
rejectRequest(response);
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
const requestHeaders = getHeadersToUse(request, preFlightRequest);
|
|
1309
|
+
const allowHeaders = checkHeaders(config, requestHeaders);
|
|
1310
|
+
if (preFlightRequest && allowHeaders === void 0) {
|
|
1311
|
+
if (logger6.enabledFor("debug")) {
|
|
1312
|
+
logger6.debug(`reject: headers '${requestHeaders}' are not allowed`);
|
|
1313
|
+
}
|
|
1314
|
+
rejectRequest(response);
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
responseHeaders.set("access-control-allow-origin", allowOrigin);
|
|
1318
|
+
if (preFlightRequest) {
|
|
1319
|
+
responseHeaders.set("access-control-allow-methods", allowMethods.join(","));
|
|
1320
|
+
}
|
|
1321
|
+
if (preFlightRequest && allowHeaders !== void 0 && allowHeaders.length > 0) {
|
|
1322
|
+
responseHeaders.set("access-control-allow-headers", allowHeaders.join(", "));
|
|
1323
|
+
}
|
|
1324
|
+
const exposeHeaders = config.headers?.expose;
|
|
1325
|
+
if (exposeHeaders && exposeHeaders.length > 0) {
|
|
1326
|
+
responseHeaders.set("access-control-expose-headers", exposeHeaders.join(", "));
|
|
1327
|
+
}
|
|
1328
|
+
if (config.credentials?.allow) {
|
|
1329
|
+
responseHeaders.set("access-control-allow-credentials", "true");
|
|
1330
|
+
}
|
|
1331
|
+
if (config.privateNetwork?.allow && request.headers.one("access-control-request-private-network") === "true") {
|
|
1332
|
+
responseHeaders.set("access-control-allow-private-network", "true");
|
|
1333
|
+
}
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
var ALL = "*";
|
|
1337
|
+
var DEFAULT_METHODS = ["GET", "HEAD"];
|
|
1338
|
+
function validateAllowCredentials(config) {
|
|
1339
|
+
if (config.credentials?.allow === true && config.origins?.allow === ALL) {
|
|
1340
|
+
throw new Error(`when credentials.allow is true origins.allow cannot be "*"`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
function validateAllowPrivateNetwork(config) {
|
|
1344
|
+
if (config.privateNetwork?.allow === true && config.origins?.allow === ALL) {
|
|
1345
|
+
throw new Error(`when privateNetwork.allow is true origins.allow cannot be "*"`);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
function checkOrigin(config, origin) {
|
|
1349
|
+
if (origin) {
|
|
1350
|
+
const allowedOrigins = config.origins?.allow;
|
|
1351
|
+
if (allowedOrigins) {
|
|
1352
|
+
if (allowedOrigins === ALL) {
|
|
1353
|
+
validateAllowCredentials(config);
|
|
1354
|
+
validateAllowPrivateNetwork(config);
|
|
1355
|
+
return ALL;
|
|
1356
|
+
}
|
|
1357
|
+
const originToCheck = trimTrailingSlash(origin.toLowerCase());
|
|
1358
|
+
for (const allowedOrigin of allowedOrigins) {
|
|
1359
|
+
if (allowedOrigin === ALL || IOGateway6.Filtering.valueMatches(allowedOrigin, originToCheck)) {
|
|
1360
|
+
return origin;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
function checkMethods(config, requestMethod) {
|
|
1367
|
+
if (requestMethod) {
|
|
1368
|
+
const allowedMethods = config.methods?.allow ?? DEFAULT_METHODS;
|
|
1369
|
+
if (allowedMethods === ALL) {
|
|
1370
|
+
return [requestMethod];
|
|
1371
|
+
}
|
|
1372
|
+
if (IOGateway6.Filtering.valuesMatch(allowedMethods, requestMethod)) {
|
|
1373
|
+
return allowedMethods;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
function checkHeaders(config, requestHeaders) {
|
|
1378
|
+
if (requestHeaders === void 0) {
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
if (requestHeaders.length == 0) {
|
|
1382
|
+
return [];
|
|
1383
|
+
}
|
|
1384
|
+
const allowedHeaders = config.headers?.allow;
|
|
1385
|
+
if (allowedHeaders === void 0) {
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
const allowAnyHeader = allowedHeaders === ALL;
|
|
1389
|
+
const result = [];
|
|
1390
|
+
for (const requestHeader of requestHeaders) {
|
|
1391
|
+
const value = requestHeader?.trim();
|
|
1392
|
+
if (value) {
|
|
1393
|
+
if (allowAnyHeader) {
|
|
1394
|
+
result.push(value);
|
|
1395
|
+
} else {
|
|
1396
|
+
for (const allowedHeader of allowedHeaders) {
|
|
1397
|
+
if (value.toLowerCase() == allowedHeader) {
|
|
1398
|
+
result.push(value);
|
|
1399
|
+
break;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (result.length > 0) {
|
|
1406
|
+
return result;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
function trimTrailingSlash(origin) {
|
|
1410
|
+
return origin.endsWith("/") ? origin.slice(0, -1) : origin;
|
|
1411
|
+
}
|
|
1412
|
+
function getMethodToUse(request, isPreFlight) {
|
|
1413
|
+
return isPreFlight ? request.headers.one("access-control-request-method") : request.method;
|
|
1414
|
+
}
|
|
1415
|
+
function getHeadersToUse(request, isPreFlight) {
|
|
1416
|
+
const headers = request.headers;
|
|
1417
|
+
return isPreFlight ? headers.list("access-control-request-headers") : Array.from(headers.keys());
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// src/server.ts
|
|
1421
|
+
var logger7 = getLogger("app");
|
|
1422
|
+
function secureContextOptions(ssl) {
|
|
1423
|
+
const options = {};
|
|
1424
|
+
if (ssl.key) options.key = readFileSync(ssl.key);
|
|
1425
|
+
if (ssl.cert) options.cert = readFileSync(ssl.cert);
|
|
1426
|
+
if (ssl.ca) options.ca = readFileSync(ssl.ca);
|
|
1427
|
+
return options;
|
|
1428
|
+
}
|
|
1429
|
+
function createListener(middleware, routes3) {
|
|
1430
|
+
const storage = new AsyncLocalStorage();
|
|
1431
|
+
const listener = compose(
|
|
1432
|
+
async ({ response }, next) => {
|
|
1433
|
+
response.headers.set("server", "gateway-server");
|
|
1434
|
+
await next();
|
|
1435
|
+
},
|
|
1436
|
+
...cors_default({
|
|
1437
|
+
origins: { allow: [/http:\/\/localhost(:\d+)?/] },
|
|
1438
|
+
methods: { allow: ["GET", "HEAD", "POST", "DELETE"] },
|
|
1439
|
+
headers: { allow: "*" },
|
|
1440
|
+
credentials: { allow: true }
|
|
1441
|
+
}),
|
|
1442
|
+
...middleware,
|
|
1443
|
+
async ({ request, response }, next) => {
|
|
1444
|
+
if (request.method === "GET" && request.path === "/health") {
|
|
1445
|
+
response.statusCode = 200;
|
|
1446
|
+
response._res.end(http.STATUS_CODES[200]);
|
|
1447
|
+
} else {
|
|
1448
|
+
await next();
|
|
1449
|
+
}
|
|
1450
|
+
},
|
|
1451
|
+
async ({ request, response }, next) => {
|
|
1452
|
+
if (request.method === "GET" && request.path === "/") {
|
|
1453
|
+
response._res.end(`io.Gateway Server`);
|
|
1454
|
+
} else {
|
|
1455
|
+
await next();
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1458
|
+
async ({ request, response }, _next) => {
|
|
1459
|
+
const route = routes3.get(request.path);
|
|
1460
|
+
if (route) {
|
|
1461
|
+
response.statusCode = 426;
|
|
1462
|
+
response._res.appendHeader("Upgrade", "websocket").appendHeader("Connection", "Upgrade").appendHeader("Content-Type", "text/plain");
|
|
1463
|
+
response._res.end(`This service [${request.path}] requires use of the websocket protocol.`);
|
|
1464
|
+
} else {
|
|
1465
|
+
response.statusCode = 404;
|
|
1466
|
+
response._res.end(http.STATUS_CODES[404]);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
);
|
|
1470
|
+
return (request, response) => {
|
|
1471
|
+
const exchange = new DefaultWebExchange(new HttpServerRequest(request), new HttpServerResponse(response));
|
|
1472
|
+
return storage.run(exchange, async () => {
|
|
1473
|
+
if (logger7.enabledFor("debug")) {
|
|
1474
|
+
const socket = exchange.request._req.socket;
|
|
1475
|
+
if (logger7.enabledFor("debug")) {
|
|
1476
|
+
logger7.debug(`received ${exchange.method} request for ${exchange.path} from ${socket.remoteAddress}:${socket.remotePort}`);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return await listener(exchange);
|
|
1480
|
+
});
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
function promisify(fn) {
|
|
1484
|
+
return new Promise((resolve, reject) => {
|
|
1485
|
+
const r = fn((err) => {
|
|
1486
|
+
if (err) {
|
|
1487
|
+
reject(err);
|
|
1488
|
+
} else {
|
|
1489
|
+
resolve(r);
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
function memoryMonitor(config) {
|
|
1495
|
+
if (config) {
|
|
1496
|
+
return start({
|
|
1497
|
+
memoryLimit: config.memory_limit,
|
|
1498
|
+
dumpLocation: config.dump_location,
|
|
1499
|
+
dumpPrefix: config.dump_prefix,
|
|
1500
|
+
reportInterval: config.report_interval,
|
|
1501
|
+
maxBackups: config.max_backups
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
function regexAwareReplacer(_key, value) {
|
|
1506
|
+
return value instanceof RegExp ? value.toString() : value;
|
|
1507
|
+
}
|
|
1508
|
+
var Factory = async (options) => {
|
|
1509
|
+
const ssl = options.ssl;
|
|
1510
|
+
const createServer = ssl ? (options2, handler2) => https.createServer({ ...options2, ...secureContextOptions(ssl) }, handler2) : (options2, handler2) => http.createServer(options2, handler2);
|
|
1511
|
+
const monitor = memoryMonitor(options.memory);
|
|
1512
|
+
const middleware = [];
|
|
1513
|
+
const routes3 = /* @__PURE__ */ new Map();
|
|
1514
|
+
const gw = IOGateway7.Factory({ ...options.gateway });
|
|
1515
|
+
if (options.gateway) {
|
|
1516
|
+
const config = options.gateway;
|
|
1517
|
+
routes3.set(config.route ?? "/", {
|
|
1518
|
+
default: config.route === void 0,
|
|
1519
|
+
ping: options.gateway.ping,
|
|
1520
|
+
factory: core_default.bind(gw),
|
|
1521
|
+
maxConnections: config.limits?.max_connections,
|
|
1522
|
+
originFilters: regexifyOriginFilters(config.origins)
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
if (options.mesh) {
|
|
1526
|
+
const connections = new InMemoryNodeConnections(options.mesh.timeout ?? 6e4);
|
|
1527
|
+
middleware.push(...routes_default(connections));
|
|
1528
|
+
const ping = options.mesh.ping ?? 3e4;
|
|
1529
|
+
routes3.set("/broker", { factory: core_default2, ping });
|
|
1530
|
+
routes3.set("/cluster", { factory: core_default4, ping });
|
|
1531
|
+
routes3.set("/relays", { factory: core_default3, ping });
|
|
1532
|
+
}
|
|
1533
|
+
if (options.metrics) {
|
|
1534
|
+
middleware.push(...await routes_default2(options.metrics));
|
|
1535
|
+
}
|
|
1536
|
+
const ports = portRange(options.port ?? 0);
|
|
1537
|
+
const host = options.host;
|
|
1538
|
+
const serverP = new Promise((resolve, reject) => {
|
|
1539
|
+
const onSocketError = (err) => logger7.error(`socket error: ${err}`, err);
|
|
1540
|
+
const server2 = createServer(
|
|
1541
|
+
{},
|
|
1542
|
+
createListener(middleware, routes3)
|
|
1543
|
+
);
|
|
1544
|
+
server2.on("error", (e) => {
|
|
1545
|
+
if (e["code"] === "EADDRINUSE") {
|
|
1546
|
+
logger7.debug(`port ${e["port"]} already in use on address ${e["address"]}`);
|
|
1547
|
+
const { value: port } = ports.next();
|
|
1548
|
+
if (port) {
|
|
1549
|
+
logger7.info(`retry starting server on port ${port} and host ${host ?? "<unspecified>"}`);
|
|
1550
|
+
server2.close();
|
|
1551
|
+
server2.listen(port, host);
|
|
1552
|
+
} else {
|
|
1553
|
+
logger7.warn(`all configured port(s) ${options.port} are in use. closing...`);
|
|
1554
|
+
server2.close();
|
|
1555
|
+
reject(e);
|
|
1556
|
+
}
|
|
1557
|
+
} else {
|
|
1558
|
+
logger7.error(`server error: ${e.message}`, e);
|
|
1559
|
+
reject(e);
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
server2.on("listening", async () => {
|
|
1563
|
+
const info = server2.address();
|
|
1564
|
+
for (const [path, route] of routes3) {
|
|
1565
|
+
try {
|
|
1566
|
+
logger7.info(`creating ws server for [${path}]. max connections: ${route.maxConnections ?? "<unlimited>"}, origin filters: ${route.originFilters ? JSON.stringify(route.originFilters, regexAwareReplacer) : "<none>"}`);
|
|
1567
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
1568
|
+
const endpoint = `${ssl ? "wss" : "ws"}://${localIp}:${info.port}${path}`;
|
|
1569
|
+
const handler2 = await route.factory({ endpoint, wss });
|
|
1570
|
+
const pingInterval = route.ping;
|
|
1571
|
+
if (pingInterval) {
|
|
1572
|
+
const pingIntervalId = setInterval(() => {
|
|
1573
|
+
for (const client of wss.clients) {
|
|
1574
|
+
if (client["connected"] === false) {
|
|
1575
|
+
client.terminate();
|
|
1576
|
+
}
|
|
1577
|
+
client["connected"] = false;
|
|
1578
|
+
client.ping();
|
|
1579
|
+
}
|
|
1580
|
+
}, pingInterval);
|
|
1581
|
+
wss.on("close", () => {
|
|
1582
|
+
clearInterval(pingIntervalId);
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
route.wss = wss;
|
|
1586
|
+
route.close = handler2.close?.bind(handler2);
|
|
1587
|
+
} catch (e) {
|
|
1588
|
+
logger7.warn(`failed to init route ${path}`, e);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
logger7.info(`http server listening on ${info.address}:${info.port}`);
|
|
1592
|
+
resolve(server2);
|
|
1593
|
+
});
|
|
1594
|
+
server2.on("upgrade", (req, socket, head) => {
|
|
1595
|
+
socket.addListener("error", onSocketError);
|
|
1596
|
+
try {
|
|
1597
|
+
const request = new HttpServerRequest(req);
|
|
1598
|
+
const path = request.path ?? "/";
|
|
1599
|
+
const route = routes3.get(path) ?? Array.from(routes3.values()).find((route2) => {
|
|
1600
|
+
if (path === "/" && route2.default === true) {
|
|
1601
|
+
return true;
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
const host2 = request.host;
|
|
1605
|
+
const info = socketKey(request.socket);
|
|
1606
|
+
if (route?.wss) {
|
|
1607
|
+
socket.removeListener("error", onSocketError);
|
|
1608
|
+
const wss = route.wss;
|
|
1609
|
+
if (route.maxConnections !== void 0 && wss.clients?.size >= route.maxConnections) {
|
|
1610
|
+
logger7.warn(`${info} dropping ws connection request from ${host2} on ${path}. max connections exceeded.`);
|
|
1611
|
+
socket.destroy();
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
const origin = request.headers["origin"];
|
|
1615
|
+
if (!acceptsOrigin(origin, route.originFilters)) {
|
|
1616
|
+
logger7.info(`${info} dropping ws connection request from ${host2} on ${path}. origin ${origin ?? "<missing>"}`);
|
|
1617
|
+
socket.destroy();
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
if (logger7.enabledFor("debug")) {
|
|
1621
|
+
logger7.debug(`${info} accepted new ws connection request from ${host2} on ${path}`);
|
|
1622
|
+
}
|
|
1623
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
1624
|
+
ws.on("pong", () => ws["connected"] = true);
|
|
1625
|
+
ws.on("ping", () => {
|
|
1626
|
+
});
|
|
1627
|
+
wss.emit("connection", ws, req);
|
|
1628
|
+
});
|
|
1629
|
+
} else {
|
|
1630
|
+
logger7.warn(`${info} rejected upgrade request from ${host2} on ${path}`);
|
|
1631
|
+
socket.destroy();
|
|
1632
|
+
}
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
logger7.error(`upgrade error: ${err}`, err);
|
|
1635
|
+
}
|
|
1636
|
+
}).on("close", async () => {
|
|
1637
|
+
logger7.info(`http server closed.`);
|
|
1638
|
+
});
|
|
1639
|
+
try {
|
|
1640
|
+
const { value: port } = ports.next();
|
|
1641
|
+
server2.listen(port, host);
|
|
1642
|
+
} catch (e) {
|
|
1643
|
+
logger7.error(`error starting web socket server`, e);
|
|
1644
|
+
reject(e instanceof Error ? e : new Error(`listen failed: ${e}`));
|
|
1645
|
+
}
|
|
1646
|
+
});
|
|
1647
|
+
const server = await serverP;
|
|
1648
|
+
return new class {
|
|
1649
|
+
gateway = gw;
|
|
1650
|
+
async close() {
|
|
1651
|
+
for (const [path, route] of routes3) {
|
|
1652
|
+
try {
|
|
1653
|
+
if (route.close) {
|
|
1654
|
+
await route.close();
|
|
1655
|
+
}
|
|
1656
|
+
logger7.info(`stopping ws server for [${path}]. clients: ${route.wss?.clients?.size ?? 0}`);
|
|
1657
|
+
route.wss?.clients?.forEach((client) => {
|
|
1658
|
+
client.terminate();
|
|
1659
|
+
});
|
|
1660
|
+
route.wss?.close();
|
|
1661
|
+
} catch (e) {
|
|
1662
|
+
logger7.warn(`error closing route ${path}`, e);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
await promisify((cb) => {
|
|
1666
|
+
server.closeAllConnections();
|
|
1667
|
+
server.close(cb);
|
|
1668
|
+
});
|
|
1669
|
+
if (monitor) {
|
|
1670
|
+
await stop(monitor);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}();
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
// src/index.ts
|
|
1677
|
+
var index_default = Factory;
|
|
1678
|
+
export {
|
|
1679
|
+
server_exports as GatewayServer,
|
|
1680
|
+
index_default as default
|
|
1681
|
+
};
|
|
1682
|
+
//# sourceMappingURL=index.js.map
|