geonix 1.30.2 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/package.json +2 -2
- package/src/Codec.js +1 -1
- package/src/Connection.js +89 -19
- package/src/Crypto.js +23 -9
- package/src/Gateway.js +47 -32
- package/src/LocalBus.js +190 -0
- package/src/Logger.js +16 -7
- package/src/Registry.js +19 -13
- package/src/Remote.js +12 -8
- package/src/Request.js +35 -16
- package/src/Service.js +66 -39
- package/src/Stream.js +12 -6
- package/src/Util.js +70 -56
- package/src/WebServer.js +11 -12
- package/.vscode/settings.json +0 -11
package/README.md
CHANGED
|
@@ -287,7 +287,7 @@ Each returned part has:
|
|
|
287
287
|
|
|
288
288
|
| Env Variable | Default | Description |
|
|
289
289
|
|---|---|---|
|
|
290
|
-
| `GX_TRANSPORT` | `nats://localhost` | NATS server URL(s) |
|
|
290
|
+
| `GX_TRANSPORT` | `nats://localhost` | NATS server URL(s). Set to `local://` for single-process monolith mode — see "Local transport" below. |
|
|
291
291
|
| `GX_PORT` | `8080` | Gateway listen port |
|
|
292
292
|
| `GX_LOCAL_PORT` | random | Force service HTTP server to a specific port |
|
|
293
293
|
| `GX_VERSION` | `999.999.<seconds>` | Service version |
|
|
@@ -298,6 +298,14 @@ Each returned part has:
|
|
|
298
298
|
| `GX_SECRET` | — | Encryption key: AES-256-GCM payloads + HMAC-SHA256 subjects. Services without the same key cannot communicate. |
|
|
299
299
|
| `GX_DEBUG_ENDPOINT` | — | Mount path for the debug router (e.g. `/_debug`). Disabled when unset. |
|
|
300
300
|
|
|
301
|
+
## Local transport (`GX_TRANSPORT=local://`)
|
|
302
|
+
|
|
303
|
+
For single-process monolith deployments, set `GX_TRANSPORT=local://` to replace the NATS connection with an in-process pub/sub bus. Services running in the same Node process announce themselves, the registry populates, and `Remote<T>()` calls work end-to-end without a `nats-server` running anywhere.
|
|
304
|
+
|
|
305
|
+
RPC traffic still flows through HTTP-on-loopback (`127.0.0.1:<servicePort>/!!_gx/rpc/...`) — the bus only replaces the discovery and NATS-control plane. The wire contract is identical to a distributed deployment, so migrating from monolith to multi-host is a single env var change (`GX_TRANSPORT=nats://your-host:4222`); no code changes.
|
|
306
|
+
|
|
307
|
+
Semantics matched against real NATS: subject wildcards (`*`, `>`), queue groups, `{ max: N }`, FIFO per subscription, byte payloads with defensive copy on publish. Not supported in local mode: multi-process (the bus is per-process), JetStream, and any feature Geonix doesn't already use.
|
|
308
|
+
|
|
301
309
|
> **Deprecation notice:** The legacy unprefixed names `TRANSPORT`, `PORT`, `LOCAL_PORT`, `VERSION`, and `TRANSPORT_DEBUG` are still read as fallbacks but will be removed in the next major version. Migrate to the `GX_` prefixed names.
|
|
302
310
|
|
|
303
311
|
### Bus Encryption (`GX_SECRET`)
|
package/package.json
CHANGED
package/src/Codec.js
CHANGED
package/src/Connection.js
CHANGED
|
@@ -10,7 +10,7 @@ import { encryptPayload, decryptPayload, encryptSubject, wrapSubscription } from
|
|
|
10
10
|
const CONNECTION_TIMEOUT = 10000;
|
|
11
11
|
|
|
12
12
|
const defaultRequestOptions = {
|
|
13
|
-
timeout: 300000
|
|
13
|
+
timeout: 300000,
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
const DEFAULT_CONNECTION_COUNT = 1;
|
|
@@ -21,7 +21,16 @@ const defaultConnectionOptions = {
|
|
|
21
21
|
debug: (process.env.GX_TRANSPORT_DEBUG || process.env.TRANSPORT_DEBUG) === "true",
|
|
22
22
|
maxReconnectAttempts: 30,
|
|
23
23
|
pingInterval: 30000,
|
|
24
|
-
waitOnFirstConnect: true
|
|
24
|
+
waitOnFirstConnect: true,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const TRANSPORT_TIMEOUT = 60_000;
|
|
28
|
+
const WATCHDOG_INTERVAL = 1000;
|
|
29
|
+
|
|
30
|
+
// env read via a function so tests can set GX_TRANSPORT_TIMEOUT after this module is imported
|
|
31
|
+
const getTransportTimeout = () => {
|
|
32
|
+
const raw = parseInt(process.env.GX_TRANSPORT_TIMEOUT, 10);
|
|
33
|
+
return Number.isFinite(raw) ? raw : TRANSPORT_TIMEOUT;
|
|
25
34
|
};
|
|
26
35
|
// -------------------------------------------------------------------------------------------------
|
|
27
36
|
|
|
@@ -32,12 +41,13 @@ const defaultConnectionOptions = {
|
|
|
32
41
|
* encryption are activated by setting the `GX_SECRET` environment variable.
|
|
33
42
|
*/
|
|
34
43
|
class Connection {
|
|
35
|
-
|
|
36
44
|
#draining = false;
|
|
37
45
|
#closed = false;
|
|
38
46
|
#ready = false;
|
|
39
47
|
#connections = [];
|
|
40
48
|
#connectionRoundRobin = 0;
|
|
49
|
+
#disconnectedSince = Date.now();
|
|
50
|
+
#watchdogInterval = null;
|
|
41
51
|
|
|
42
52
|
#getConnection() {
|
|
43
53
|
return this.#connections[this.#connectionRoundRobin++ % this.#connections.length];
|
|
@@ -52,22 +62,67 @@ class Connection {
|
|
|
52
62
|
* @returns {Promise<void>}
|
|
53
63
|
*/
|
|
54
64
|
async start(transport = process.env.GX_TRANSPORT || process.env.TRANSPORT || "nats://localhost") {
|
|
65
|
+
// local:// — in-process pub/sub bus, no socket, no nats package call
|
|
66
|
+
if (transport === "local://" || transport === "local") {
|
|
67
|
+
const { createLocalBus } = await import("./LocalBus.js");
|
|
68
|
+
this.#startWatchdog();
|
|
69
|
+
this.#connections.push(createLocalBus());
|
|
70
|
+
this.#disconnectedSince = null;
|
|
71
|
+
logger.info("gx.connection.connected (local)");
|
|
72
|
+
this.#ready = true;
|
|
73
|
+
this.monitorStatus();
|
|
74
|
+
this.waitUntilClosed().catch((e) => logger.error("gx.connection.waitUntilClosed:", e));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
const { connections: _connectionCount, ...natsOptions } = {
|
|
56
79
|
...defaultConnectionOptions,
|
|
57
|
-
...parseURL(transport)
|
|
80
|
+
...parseURL(transport),
|
|
58
81
|
};
|
|
59
82
|
const connectionCount = parseInt(_connectionCount) || DEFAULT_CONNECTION_COUNT;
|
|
60
83
|
|
|
84
|
+
this.#startWatchdog();
|
|
85
|
+
|
|
61
86
|
for (let i = 0; i < connectionCount; i++) {
|
|
62
87
|
this.#connections.push(await connect(natsOptions));
|
|
63
88
|
}
|
|
64
89
|
|
|
90
|
+
this.#disconnectedSince = null;
|
|
91
|
+
|
|
65
92
|
logger.info("gx.connection.connected");
|
|
66
93
|
|
|
67
94
|
this.#ready = true;
|
|
68
95
|
|
|
69
96
|
this.monitorStatus();
|
|
70
|
-
this.waitUntilClosed().catch(e => logger.error("gx.connection.waitUntilClosed:", e));
|
|
97
|
+
this.waitUntilClosed().catch((e) => logger.error("gx.connection.waitUntilClosed:", e));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Polls the disconnect timestamp; exits the process if the transport has been
|
|
101
|
+
// unreachable longer than GX_TRANSPORT_TIMEOUT (default 60s, 0 disables).
|
|
102
|
+
#startWatchdog() {
|
|
103
|
+
if (this.#watchdogInterval) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.#watchdogInterval = setInterval(() => {
|
|
108
|
+
const limit = getTransportTimeout();
|
|
109
|
+
if (limit === 0) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (this.#disconnectedSince === null) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const elapsed = Date.now() - this.#disconnectedSince;
|
|
117
|
+
if (elapsed >= limit) {
|
|
118
|
+
logger.error(
|
|
119
|
+
`gx.connection: transport unreachable for ${Math.round(elapsed / 1000)}s (limit ${limit}ms), exiting`,
|
|
120
|
+
);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}, WATCHDOG_INTERVAL);
|
|
124
|
+
|
|
125
|
+
this.#watchdogInterval.unref?.();
|
|
71
126
|
}
|
|
72
127
|
|
|
73
128
|
/**
|
|
@@ -78,9 +133,17 @@ class Connection {
|
|
|
78
133
|
for (const conn of this.#connections) {
|
|
79
134
|
(async () => {
|
|
80
135
|
for await (const event of conn.status()) {
|
|
136
|
+
if (event.type === "disconnect") {
|
|
137
|
+
if (this.#disconnectedSince === null) {
|
|
138
|
+
this.#disconnectedSince = Date.now();
|
|
139
|
+
}
|
|
140
|
+
} else if (event.type === "reconnect") {
|
|
141
|
+
this.#disconnectedSince = null;
|
|
142
|
+
}
|
|
143
|
+
|
|
81
144
|
logger.debug("gx.connection.status", JSON.stringify(event));
|
|
82
145
|
}
|
|
83
|
-
})().catch(e => logger.error("gx.connection.status:", e));
|
|
146
|
+
})().catch((e) => logger.error("gx.connection.status:", e));
|
|
84
147
|
}
|
|
85
148
|
}
|
|
86
149
|
|
|
@@ -92,11 +155,16 @@ class Connection {
|
|
|
92
155
|
*/
|
|
93
156
|
async waitUntilClosed() {
|
|
94
157
|
// wait for all connections to be closed
|
|
95
|
-
await Promise.all(this.#connections.map(connection => connection.closed()));
|
|
158
|
+
await Promise.all(this.#connections.map((connection) => connection.closed()));
|
|
96
159
|
|
|
97
160
|
this.#closed = true;
|
|
98
161
|
logger.info("gx.connection.closed");
|
|
99
162
|
|
|
163
|
+
if (this.#watchdogInterval) {
|
|
164
|
+
clearInterval(this.#watchdogInterval);
|
|
165
|
+
this.#watchdogInterval = null;
|
|
166
|
+
}
|
|
167
|
+
|
|
100
168
|
webserver.stop();
|
|
101
169
|
|
|
102
170
|
await sleep(5000);
|
|
@@ -118,9 +186,9 @@ class Connection {
|
|
|
118
186
|
|
|
119
187
|
/**
|
|
120
188
|
* Publish JSON
|
|
121
|
-
*
|
|
122
|
-
* @param {string} subject
|
|
123
|
-
* @param {object} json
|
|
189
|
+
*
|
|
190
|
+
* @param {string} subject
|
|
191
|
+
* @param {object} json
|
|
124
192
|
* @returns void
|
|
125
193
|
*/
|
|
126
194
|
async publish(subject, json) {
|
|
@@ -150,21 +218,24 @@ class Connection {
|
|
|
150
218
|
return;
|
|
151
219
|
}
|
|
152
220
|
|
|
153
|
-
await this.#getConnection().publish(
|
|
221
|
+
await this.#getConnection().publish(
|
|
222
|
+
encryptSubject(subject),
|
|
223
|
+
encryptPayload(data != null ? Buffer.from(data) : Buffer.alloc(0)),
|
|
224
|
+
);
|
|
154
225
|
}
|
|
155
226
|
|
|
156
227
|
/**
|
|
157
228
|
* Request/Reply pattern on top of pub/sub
|
|
158
|
-
*
|
|
159
|
-
* @param {string} subject
|
|
160
|
-
* @param {object} json
|
|
161
|
-
* @param {object} options
|
|
229
|
+
*
|
|
230
|
+
* @param {string} subject
|
|
231
|
+
* @param {object} json
|
|
232
|
+
* @param {object} options
|
|
162
233
|
* @returns any
|
|
163
234
|
*/
|
|
164
235
|
async request(subject, json, opts = {}) {
|
|
165
236
|
const options = {
|
|
166
237
|
...defaultRequestOptions,
|
|
167
|
-
...opts
|
|
238
|
+
...opts,
|
|
168
239
|
};
|
|
169
240
|
|
|
170
241
|
const respondTo = `gx2.r.${picoid(16)}`;
|
|
@@ -235,9 +306,8 @@ class Connection {
|
|
|
235
306
|
async drain() {
|
|
236
307
|
this.#draining = true;
|
|
237
308
|
|
|
238
|
-
await Promise.all(this.#connections.map(connection => connection.drain()));
|
|
309
|
+
await Promise.all(this.#connections.map((connection) => connection.drain()));
|
|
239
310
|
}
|
|
240
|
-
|
|
241
311
|
}
|
|
242
312
|
|
|
243
313
|
/**
|
|
@@ -247,7 +317,7 @@ class Connection {
|
|
|
247
317
|
* @type {Connection}
|
|
248
318
|
*/
|
|
249
319
|
export const connection = new Connection();
|
|
250
|
-
connection.start().catch(e => {
|
|
320
|
+
connection.start().catch((e) => {
|
|
251
321
|
logger.error("gx.connection.start:", e);
|
|
252
322
|
process.exit(1);
|
|
253
323
|
});
|
package/src/Crypto.js
CHANGED
|
@@ -3,7 +3,11 @@ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes }
|
|
|
3
3
|
const _secret = process.env.GX_SECRET || null;
|
|
4
4
|
|
|
5
5
|
// Subject key is derived separately so HMAC-subject and AES-payload never share key material.
|
|
6
|
-
const _subjectKey = _secret
|
|
6
|
+
const _subjectKey = _secret
|
|
7
|
+
? createHash("sha256")
|
|
8
|
+
.update(_secret + "\x00subject")
|
|
9
|
+
.digest()
|
|
10
|
+
: null;
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* AES-256-GCM key derived from `GX_SECRET`, or `null` when encryption is disabled.
|
|
@@ -11,7 +15,11 @@ const _subjectKey = _secret ? createHash("sha256").update(_secret + "\x00subject
|
|
|
11
15
|
*
|
|
12
16
|
* @type {Buffer|null}
|
|
13
17
|
*/
|
|
14
|
-
export const _payloadKey = _secret
|
|
18
|
+
export const _payloadKey = _secret
|
|
19
|
+
? createHash("sha256")
|
|
20
|
+
.update(_secret + "\x00payload")
|
|
21
|
+
.digest()
|
|
22
|
+
: null;
|
|
15
23
|
|
|
16
24
|
/**
|
|
17
25
|
* Encrypts `data` with AES-256-GCM using {@link _payloadKey}. Returns `data` unchanged when
|
|
@@ -21,7 +29,9 @@ export const _payloadKey = _secret ? createHash("sha256").update(_secret + "\x00
|
|
|
21
29
|
* @returns {Buffer}
|
|
22
30
|
*/
|
|
23
31
|
export function encryptPayload(data) {
|
|
24
|
-
if (!_payloadKey) {
|
|
32
|
+
if (!_payloadKey) {
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
25
35
|
const buf = data != null ? Buffer.from(data) : Buffer.alloc(0);
|
|
26
36
|
const iv = randomBytes(12);
|
|
27
37
|
const cipher = createCipheriv("aes-256-gcm", _payloadKey, iv);
|
|
@@ -42,7 +52,9 @@ export function encryptPayload(data) {
|
|
|
42
52
|
* @returns {Buffer}
|
|
43
53
|
*/
|
|
44
54
|
export function decryptPayload(data) {
|
|
45
|
-
if (!_payloadKey) {
|
|
55
|
+
if (!_payloadKey) {
|
|
56
|
+
return data;
|
|
57
|
+
}
|
|
46
58
|
const buf = Buffer.from(data);
|
|
47
59
|
const decipher = createDecipheriv("aes-256-gcm", _payloadKey, buf.subarray(0, 12));
|
|
48
60
|
decipher.setAuthTag(buf.subarray(12, 28));
|
|
@@ -64,10 +76,12 @@ export function encryptSubject(subject) {
|
|
|
64
76
|
return subject;
|
|
65
77
|
}
|
|
66
78
|
|
|
67
|
-
return subject
|
|
68
|
-
(
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
return subject
|
|
80
|
+
.split(".")
|
|
81
|
+
.map((seg) =>
|
|
82
|
+
seg === "*" || seg === ">" ? seg : createHmac("sha256", _subjectKey).update(seg).digest("hex").slice(0, 32),
|
|
83
|
+
)
|
|
84
|
+
.join(".");
|
|
71
85
|
}
|
|
72
86
|
|
|
73
87
|
/**
|
|
@@ -94,7 +108,7 @@ export function wrapSubscription(sub) {
|
|
|
94
108
|
}
|
|
95
109
|
|
|
96
110
|
return { value: { ...value, data: decryptPayload(value.data) }, done: false };
|
|
97
|
-
}
|
|
111
|
+
},
|
|
98
112
|
};
|
|
99
113
|
},
|
|
100
114
|
drain: () => sub.drain(),
|
package/src/Gateway.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { connection } from "./Connection.js";
|
|
2
2
|
import { registry } from "./Registry.js";
|
|
3
|
-
import { cleanupWebsocketUrl, createTCPServer, GeonixVersion, hash, picoid, proxyHttp, sleep } from "./Util.js";
|
|
3
|
+
import { cleanupWebsocketUrl, createTCPServer, GeonixVersion, hash, isLoopbackAddress, picoid, proxyHttp, sleep } from "./Util.js";
|
|
4
4
|
import express, { Router } from "express";
|
|
5
5
|
import { Request } from "./Request.js";
|
|
6
6
|
import expressWs from "express-ws";
|
|
@@ -15,12 +15,14 @@ const MAX_SESSIONS = 16384;
|
|
|
15
15
|
// Real client IP — checks well-known CDN/proxy headers before falling back to socket address
|
|
16
16
|
function getClientIp(req) {
|
|
17
17
|
const header =
|
|
18
|
-
req.headers["cf-connecting-ip"] ||
|
|
19
|
-
req.headers["true-client-ip"] ||
|
|
20
|
-
req.headers["x-real-ip"] ||
|
|
21
|
-
req.headers["x-forwarded-for"];
|
|
18
|
+
req.headers["cf-connecting-ip"] || // Cloudflare
|
|
19
|
+
req.headers["true-client-ip"] || // Cloudflare Enterprise / Akamai
|
|
20
|
+
req.headers["x-real-ip"] || // nginx
|
|
21
|
+
req.headers["x-forwarded-for"]; // standard (may be comma-separated list)
|
|
22
22
|
|
|
23
|
-
if (header) {
|
|
23
|
+
if (header) {
|
|
24
|
+
return header.split(",")[0].trim();
|
|
25
|
+
}
|
|
24
26
|
return req.socket?.remoteAddress || "unknown";
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -39,12 +41,12 @@ const stats = {
|
|
|
39
41
|
requests: 0,
|
|
40
42
|
proxied: 0,
|
|
41
43
|
proxied_over_nats: 0,
|
|
42
|
-
debug_requests: 0
|
|
44
|
+
debug_requests: 0,
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
const defaultOpts = {
|
|
46
|
-
beforeRequest: (_req, _res) => {
|
|
47
|
-
afterRequest: (_req, _res) => {
|
|
48
|
+
beforeRequest: (_req, _res) => {},
|
|
49
|
+
afterRequest: (_req, _res) => {},
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
/**
|
|
@@ -53,7 +55,6 @@ const defaultOpts = {
|
|
|
53
55
|
* dynamically as services join and leave the bus.
|
|
54
56
|
*/
|
|
55
57
|
export class Gateway {
|
|
56
|
-
|
|
57
58
|
/**
|
|
58
59
|
* Creates and starts a new Gateway instance.
|
|
59
60
|
*
|
|
@@ -85,7 +86,7 @@ export class Gateway {
|
|
|
85
86
|
|
|
86
87
|
this.#opts = { ...this.#opts, ...opts };
|
|
87
88
|
|
|
88
|
-
this.#start().catch(e => logger.error("gx.gateway.start:", e));
|
|
89
|
+
this.#start().catch((e) => logger.error("gx.gateway.start:", e));
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
async #start(port = 8080) {
|
|
@@ -179,7 +180,7 @@ export class Gateway {
|
|
|
179
180
|
this.#api.all("*", (req, res) => {
|
|
180
181
|
res.status(404).send({
|
|
181
182
|
error: 404,
|
|
182
|
-
source: "gw"
|
|
183
|
+
source: "gw",
|
|
183
184
|
});
|
|
184
185
|
});
|
|
185
186
|
|
|
@@ -253,7 +254,7 @@ export class Gateway {
|
|
|
253
254
|
return true;
|
|
254
255
|
};
|
|
255
256
|
|
|
256
|
-
entries = (await Promise.all(entries.map(processEntry))).filter(result => result === true);
|
|
257
|
+
entries = (await Promise.all(entries.map(processEntry))).filter((result) => result === true);
|
|
257
258
|
|
|
258
259
|
if (entries.length > 0) {
|
|
259
260
|
this.#rebuildRouter = true;
|
|
@@ -285,7 +286,7 @@ export class Gateway {
|
|
|
285
286
|
});
|
|
286
287
|
|
|
287
288
|
router.get("/services", (req, res) => {
|
|
288
|
-
const services = Object.values(registry.getEntries()).map(e =>
|
|
289
|
+
const services = Object.values(registry.getEntries()).map((e) => `${e.n}@${e.v}`);
|
|
289
290
|
services.sort();
|
|
290
291
|
res.send(services);
|
|
291
292
|
});
|
|
@@ -312,11 +313,11 @@ export class Gateway {
|
|
|
312
313
|
node: {
|
|
313
314
|
version: process.version,
|
|
314
315
|
platform: process.platform,
|
|
315
|
-
arch: process.arch
|
|
316
|
+
arch: process.arch,
|
|
316
317
|
},
|
|
317
318
|
mem: process.memoryUsage(),
|
|
318
319
|
rss: process.memoryUsage.rss(),
|
|
319
|
-
cpu: process.cpuUsage()
|
|
320
|
+
cpu: process.cpuUsage(),
|
|
320
321
|
});
|
|
321
322
|
});
|
|
322
323
|
|
|
@@ -341,19 +342,21 @@ export class Gateway {
|
|
|
341
342
|
});
|
|
342
343
|
|
|
343
344
|
const dataLoop = async () => {
|
|
344
|
-
for await (const event of ingress) {
|
|
345
|
+
for await (const event of ingress) {
|
|
346
|
+
client.write(event.data);
|
|
347
|
+
}
|
|
345
348
|
};
|
|
346
349
|
|
|
347
|
-
dataLoop().catch(e => logger.error("nats.proxy.dataLoop:", e));
|
|
350
|
+
dataLoop().catch((e) => logger.error("nats.proxy.dataLoop:", e));
|
|
348
351
|
}
|
|
349
352
|
}
|
|
350
353
|
|
|
351
354
|
/**
|
|
352
355
|
* Proxies websocket connection
|
|
353
|
-
*
|
|
354
|
-
* @param {string} target
|
|
355
|
-
* @param {Readable} inbound
|
|
356
|
-
* @param {Request} req
|
|
356
|
+
*
|
|
357
|
+
* @param {string} target
|
|
358
|
+
* @param {Readable} inbound
|
|
359
|
+
* @param {Request} req
|
|
357
360
|
*/
|
|
358
361
|
#proxyWebsocket(target, inbound, req) {
|
|
359
362
|
try {
|
|
@@ -366,7 +369,7 @@ export class Gateway {
|
|
|
366
369
|
|
|
367
370
|
backend.on("open", () => {
|
|
368
371
|
backend.on("message", (data, isBinary) => inbound.send(isBinary ? data : data.toString()));
|
|
369
|
-
inbound.on("message", data => backend.send(data));
|
|
372
|
+
inbound.on("message", (data) => backend.send(data));
|
|
370
373
|
|
|
371
374
|
backend.on("close", () => inbound.close());
|
|
372
375
|
inbound.on("close", () => backend.close());
|
|
@@ -394,22 +397,32 @@ export class Gateway {
|
|
|
394
397
|
const endpoints = [];
|
|
395
398
|
|
|
396
399
|
for (let { entry, proxy } of entries) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
+
let backendAddr;
|
|
401
|
+
if (proxy) {
|
|
402
|
+
backendAddr = `127.0.0.1:${proxy.port}`;
|
|
403
|
+
} else {
|
|
404
|
+
const addrs = entry.a ?? [];
|
|
405
|
+
const loopback = addrs.filter(isLoopbackAddress);
|
|
406
|
+
const pool = loopback.length > 0 ? loopback : addrs;
|
|
407
|
+
backendAddr = pool.length > 0 ? pool[Math.floor(Math.random() * pool.length)] : undefined;
|
|
408
|
+
}
|
|
400
409
|
|
|
401
|
-
if (!backendAddr) {
|
|
410
|
+
if (!backendAddr) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
402
413
|
|
|
403
414
|
// generate global endpoint list
|
|
404
415
|
for (let e of entry.m) {
|
|
405
416
|
const endpointMatch = endpointMatcher.exec(e);
|
|
406
417
|
if (endpointMatch) {
|
|
407
418
|
const endpoint = endpointMatch.groups;
|
|
408
|
-
const parsed = endpoint.options
|
|
419
|
+
const parsed = endpoint.options
|
|
420
|
+
? Object.fromEntries(new URLSearchParams(endpoint.options))
|
|
421
|
+
: {};
|
|
409
422
|
const parsedOrder = parseInt(parsed.order, 10);
|
|
410
423
|
let options = {
|
|
411
424
|
...parsed,
|
|
412
|
-
order: Number.isNaN(parsedOrder) ? 100 : parsedOrder
|
|
425
|
+
order: Number.isNaN(parsedOrder) ? 100 : parsedOrder,
|
|
413
426
|
};
|
|
414
427
|
|
|
415
428
|
try {
|
|
@@ -418,7 +431,7 @@ export class Gateway {
|
|
|
418
431
|
version: semver.coerce(entry.v).version,
|
|
419
432
|
options,
|
|
420
433
|
endpoint,
|
|
421
|
-
backend: [backendAddr]
|
|
434
|
+
backend: [backendAddr],
|
|
422
435
|
});
|
|
423
436
|
} catch (e) {
|
|
424
437
|
logger.error("gateway.buildRouter.error:", entry);
|
|
@@ -435,7 +448,10 @@ export class Gateway {
|
|
|
435
448
|
const url = `${endpoints[index].endpoint.verb} ${endpoints[index].endpoint.url}`;
|
|
436
449
|
|
|
437
450
|
for (let n = 0; n < index; n++) {
|
|
438
|
-
if (
|
|
451
|
+
if (
|
|
452
|
+
`${endpoints[n].endpoint.verb} ${endpoints[n].endpoint.url}` === url &&
|
|
453
|
+
endpoints[n].version === version
|
|
454
|
+
) {
|
|
439
455
|
endpoints[n].backend = endpoints[n].backend.concat(endpoints[index].backend);
|
|
440
456
|
endpoints.splice(index, 1);
|
|
441
457
|
break;
|
|
@@ -509,5 +525,4 @@ export class Gateway {
|
|
|
509
525
|
this.#isActive = false;
|
|
510
526
|
clearInterval(this.#rebuildRouterInterval);
|
|
511
527
|
}
|
|
512
|
-
|
|
513
528
|
}
|